- Nuova toolbar compatta con azioni rapide e indicatori stato aste - Layout a pannelli ridimensionabili con splitter drag&drop - Tabella aste compatta, ping colorato, azioni XS - Pulsanti per rimozione aste per stato (attive, vinte, ecc.) - Dettagli asta sempre visibili in pannello inferiore - Statistiche prodotti: filtro, ordinamento, editing limiti default - Limiti default prodotto salvati in DB, applicabili a tutte le aste - Migliorata sidebar utente con info sessione sempre visibili - Log motivi blocco puntata sempre visibili, suggerimenti timing - Miglioramenti filtri, UX responsive, fix minori e feedback visivi
461 lines
17 KiB
C#
461 lines
17 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Text.Json.Serialization;
|
||
|
||
namespace AutoBidder.Models
|
||
{
|
||
/// <summary>
|
||
/// Informazioni base di un'asta monitorata
|
||
/// Solo HTTP, nessuna modalità, browser o multi-click
|
||
/// </summary>
|
||
public class AuctionInfo
|
||
{
|
||
/// <summary>
|
||
/// Numero massimo di righe di log da mantenere per ogni asta
|
||
/// </summary>
|
||
private const int MAX_LOG_LINES = 500;
|
||
|
||
public string AuctionId { get; set; } = "";
|
||
public string Name { get; set; } = ""; // Opzionale, può essere lasciato vuoto
|
||
public string OriginalUrl { get; set; } = ""; // URL completo dell'asta (per referer)
|
||
|
||
// Configurazione asta
|
||
/// <summary>
|
||
/// Millisecondi prima della scadenza (deadline) per inviare la puntata.
|
||
/// Es: 200ms = punta 200ms prima che il timer raggiunga 0.
|
||
/// </summary>
|
||
public int BidBeforeDeadlineMs { get; set; } = 200;
|
||
|
||
/// <summary>
|
||
/// Se true, verifica che l'asta sia ancora aperta prima di piazzare la puntata.
|
||
/// Aggiunge una chiamata API extra ma aumenta la sicurezza.
|
||
/// </summary>
|
||
public bool CheckAuctionOpenBeforeBid { get; set; } = false;
|
||
|
||
public double MinPrice { get; set; } = 0;
|
||
public double MaxPrice { get; set; } = 0;
|
||
public int MinResets { get; set; } = 0; // Numero minimo reset prima di puntare
|
||
public int MaxResets { get; set; } = 0; // Numero massimo reset (0 = illimitati)
|
||
|
||
/// <summary>
|
||
/// [OBSOLETO] Numero massimo di puntate consentite - Non più utilizzato nell'UI
|
||
/// Mantenuto per retrocompatibilità con salvataggi JSON esistenti
|
||
/// </summary>
|
||
[Obsolete("MaxClicks non è più utilizzato. Usa invece la logica di limiti per prodotto.")]
|
||
[JsonPropertyName("MaxClicks")]
|
||
public int MaxClicks { get; set; } = 0;
|
||
|
||
// Stato asta
|
||
public bool IsActive { get; set; } = true;
|
||
public bool IsPaused { get; set; } = false;
|
||
|
||
// Contatori
|
||
public int ResetCount { get; set; } = 0;
|
||
|
||
/// <summary>
|
||
/// Puntate residue totali dell'utente (aggiornate dopo ogni puntata su questa asta)
|
||
/// </summary>
|
||
[JsonPropertyName("RemainingBids")]
|
||
public int? RemainingBids { get; set; }
|
||
|
||
/// <summary>
|
||
/// Puntate usate specificamente su questa asta (da risposta server)
|
||
/// </summary>
|
||
[JsonPropertyName("BidsUsedOnThisAuction")]
|
||
public int? BidsUsedOnThisAuction { get; set; }
|
||
|
||
|
||
// Timestamp
|
||
public DateTime AddedAt { get; set; } = DateTime.UtcNow;
|
||
public DateTime? LastClickAt { get; set; }
|
||
|
||
// ?? NUOVO: Sistema timing basato su deadline
|
||
/// <summary>
|
||
/// Timestamp UTC preciso della scadenza dell'asta.
|
||
/// Calcolato come: DateTime.UtcNow + Timer (quando riceviamo lo stato)
|
||
/// </summary>
|
||
[JsonIgnore]
|
||
public DateTime? DeadlineUtc { get; set; }
|
||
|
||
/// <summary>
|
||
/// Timestamp UTC dell'ultimo aggiornamento della deadline.
|
||
/// Usato per rilevare reset del timer.
|
||
/// </summary>
|
||
[JsonIgnore]
|
||
public DateTime? LastDeadlineUpdateUtc { get; set; }
|
||
|
||
/// <summary>
|
||
/// Timer raw dell'ultimo stato ricevuto (in secondi).
|
||
/// Usato per rilevare cambiamenti nel timer.
|
||
/// </summary>
|
||
[JsonIgnore]
|
||
public double LastRawTimer { get; set; }
|
||
|
||
/// <summary>
|
||
/// True se la puntata è già stata schedulata per questo ciclo.
|
||
/// Resettato quando il timer si resetta.
|
||
/// </summary>
|
||
[JsonIgnore]
|
||
public bool BidScheduled { get; set; }
|
||
|
||
/// <summary>
|
||
/// Timer per cui è stata schedulata l'ultima puntata.
|
||
/// Usato per evitare doppie puntate sullo stesso ciclo.
|
||
/// </summary>
|
||
[JsonIgnore]
|
||
public double LastScheduledTimerMs { get; set; }
|
||
|
||
// Storico
|
||
public List<BidHistory> BidHistory { get; set; } = new List<BidHistory>();
|
||
public Dictionary<string, BidderInfo> BidderStats { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||
|
||
/// <summary>
|
||
/// Storia delle ultime puntate effettuate sull'asta (da API)
|
||
/// Questa è la fonte UFFICIALE per il conteggio puntate per utente
|
||
/// </summary>
|
||
[JsonPropertyName("RecentBids")]
|
||
public List<BidHistoryEntry> RecentBids { get; set; } = new List<BidHistoryEntry>();
|
||
|
||
// Log per-asta (non serializzato)
|
||
[System.Text.Json.Serialization.JsonIgnore]
|
||
public List<string> AuctionLog { get; set; } = new();
|
||
|
||
// Flag runtime: indica che è in corso un'operazione di final attack per questa asta
|
||
[System.Text.Json.Serialization.JsonIgnore]
|
||
public bool IsAttackInProgress { get; set; } = false;
|
||
|
||
// === INFORMAZIONI PRODOTTO PER CALCOLO VALORE ===
|
||
|
||
/// <summary>
|
||
/// Prezzo "Compra Subito" del prodotto (valore nominale)
|
||
/// </summary>
|
||
public double? BuyNowPrice { get; set; }
|
||
|
||
/// <summary>
|
||
/// Spese di spedizione per questo prodotto
|
||
/// </summary>
|
||
public double? ShippingCost { get; set; }
|
||
|
||
/// <summary>
|
||
/// Indica se c'è un limite di vincita per questo prodotto (1 volta ogni X giorni)
|
||
/// </summary>
|
||
public bool HasWinLimit { get; set; } = false;
|
||
|
||
/// <summary>
|
||
/// Descrizione del limite di vincita (es: "1 volta ogni 30 giorni")
|
||
/// </summary>
|
||
public string? WinLimitDescription { get; set; }
|
||
|
||
/// <summary>
|
||
/// Costo medio per puntata da utilizzare nei calcoli (default: 0.20€)
|
||
/// </summary>
|
||
public double BidCost { get; set; } = 0.20;
|
||
|
||
/// <summary>
|
||
/// Ultimo valore calcolato del prodotto (costo reale considerando puntate)
|
||
/// Null se non ancora calcolato
|
||
/// </summary>
|
||
[JsonIgnore]
|
||
public ProductValue? CalculatedValue { get; set; }
|
||
|
||
/// <summary>
|
||
/// Ultimo stato ricevuto dal monitor per questa asta
|
||
/// </summary>
|
||
[JsonIgnore]
|
||
public AuctionState? LastState { get; set; }
|
||
|
||
|
||
/// <summary>
|
||
/// Aggiunge una voce al log dell'asta con deduplicazione e limite automatico di righe.
|
||
/// Se il messaggio è identico all'ultimo, incrementa un contatore invece di duplicare.
|
||
/// </summary>
|
||
/// <param name="message">Messaggio da aggiungere al log</param>
|
||
/// <param name="maxLines">Numero massimo di righe da mantenere (default: 500)</param>
|
||
public void AddLog(string message, int maxLines = 500)
|
||
{
|
||
var timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
|
||
|
||
// ?? DEDUPLICAZIONE: Se l'ultimo messaggio è uguale, incrementa contatore
|
||
if (AuctionLog.Count > 0)
|
||
{
|
||
var lastEntry = AuctionLog[^1]; // Ultimo elemento
|
||
|
||
// Estrai il messaggio senza timestamp e contatore
|
||
var lastMessageStart = lastEntry.IndexOf(" - ");
|
||
if (lastMessageStart > 0)
|
||
{
|
||
var lastMessage = lastEntry.Substring(lastMessageStart + 3);
|
||
|
||
// Rimuovi eventuale contatore esistente (es: " (x5)")
|
||
var counterMatch = System.Text.RegularExpressions.Regex.Match(lastMessage, @" \(x(\d+)\)$");
|
||
if (counterMatch.Success)
|
||
{
|
||
lastMessage = lastMessage.Substring(0, lastMessage.Length - counterMatch.Length);
|
||
}
|
||
|
||
// Se il messaggio è identico, aggiorna contatore
|
||
if (lastMessage == message)
|
||
{
|
||
int newCount = counterMatch.Success
|
||
? int.Parse(counterMatch.Groups[1].Value) + 1
|
||
: 2;
|
||
|
||
// Aggiorna l'ultimo entry con il nuovo contatore
|
||
AuctionLog[^1] = $"{timestamp} - {message} (x{newCount})";
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Nuovo messaggio diverso dall'ultimo
|
||
var entry = $"{timestamp} - {message}";
|
||
AuctionLog.Add(entry);
|
||
|
||
// Mantieni solo gli ultimi maxLines log
|
||
if (AuctionLog.Count > maxLines)
|
||
{
|
||
int excessCount = AuctionLog.Count - maxLines;
|
||
AuctionLog.RemoveRange(0, excessCount);
|
||
}
|
||
}
|
||
|
||
public int PollingLatencyMs { get; set; } = 0; // Ultima latenza polling ms
|
||
|
||
// ???????????????????????????????????????????????????????????????
|
||
// TRACKING AVANZATO PER STRATEGIE
|
||
// ???????????????????????????????????????????????????????????????
|
||
|
||
/// <summary>
|
||
/// Storico latenze ultime N misurazioni (per media mobile)
|
||
/// </summary>
|
||
[JsonIgnore]
|
||
public List<int> LatencyHistory { get; set; } = new();
|
||
|
||
/// <summary>
|
||
/// Numero massimo di latenze da memorizzare
|
||
/// </summary>
|
||
private const int MAX_LATENCY_HISTORY = 20;
|
||
|
||
/// <summary>
|
||
/// Aggiunge una misurazione di latenza allo storico
|
||
/// </summary>
|
||
public void AddLatencyMeasurement(int latencyMs)
|
||
{
|
||
LatencyHistory.Add(latencyMs);
|
||
if (LatencyHistory.Count > MAX_LATENCY_HISTORY)
|
||
LatencyHistory.RemoveAt(0);
|
||
PollingLatencyMs = latencyMs;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Latenza media calcolata sullo storico
|
||
/// </summary>
|
||
[JsonIgnore]
|
||
public double AverageLatencyMs => LatencyHistory.Count > 0
|
||
? LatencyHistory.Average()
|
||
: PollingLatencyMs > 0 ? PollingLatencyMs : 60;
|
||
|
||
/// <summary>
|
||
/// Heat metric (0-100) che indica quanto è "calda" l'asta
|
||
/// Calcolato in base a: bidder attivi, frequenza puntate, collisioni
|
||
/// </summary>
|
||
[JsonIgnore]
|
||
public int HeatMetric { get; set; } = 0;
|
||
|
||
/// <summary>
|
||
/// Numero di bidder unici attivi negli ultimi N secondi
|
||
/// </summary>
|
||
[JsonIgnore]
|
||
public int ActiveBiddersCount { get; set; } = 0;
|
||
|
||
/// <summary>
|
||
/// Numero di collisioni rilevate (puntate nello stesso secondo)
|
||
/// </summary>
|
||
[JsonIgnore]
|
||
public int CollisionCount { get; set; } = 0;
|
||
|
||
/// <summary>
|
||
/// Collisioni consecutive senza puntata vincente
|
||
/// </summary>
|
||
[JsonIgnore]
|
||
public int ConsecutiveCollisions { get; set; } = 0;
|
||
|
||
/// <summary>
|
||
/// Timestamp dell'ultimo soft retreat
|
||
/// </summary>
|
||
[JsonIgnore]
|
||
public DateTime? LastSoftRetreatAt { get; set; }
|
||
|
||
/// <summary>
|
||
/// Se true, l'asta è in soft retreat temporaneo
|
||
/// </summary>
|
||
[JsonIgnore]
|
||
public bool IsInSoftRetreat { get; set; } = false;
|
||
|
||
/// <summary>
|
||
/// Contatore puntate effettuate in questa sessione su questa asta
|
||
/// </summary>
|
||
[JsonIgnore]
|
||
public int SessionBidCount { get; set; } = 0;
|
||
|
||
/// <summary>
|
||
/// Numero di volte che il timer è scaduto prima della puntata
|
||
/// </summary>
|
||
[JsonIgnore]
|
||
public int TimerExpiredCount { get; set; } = 0;
|
||
|
||
/// <summary>
|
||
/// Numero di puntate riuscite
|
||
/// </summary>
|
||
[JsonIgnore]
|
||
public int SuccessfulBidCount { get; set; } = 0;
|
||
|
||
/// <summary>
|
||
/// Numero di puntate fallite
|
||
/// </summary>
|
||
[JsonIgnore]
|
||
public int FailedBidCount { get; set; } = 0;
|
||
|
||
/// <summary>
|
||
/// Lista utenti identificati come aggressivi in questa asta
|
||
/// </summary>
|
||
[JsonIgnore]
|
||
public HashSet<string> AggressiveBidders { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||
|
||
/// <summary>
|
||
/// Offset dinamico calcolato per questa asta (ms)
|
||
/// </summary>
|
||
[JsonIgnore]
|
||
public int DynamicOffsetMs { get; set; } = 150;
|
||
|
||
/// <summary>
|
||
/// Offset effettivo usato nell'ultima puntata (include jitter)
|
||
/// </summary>
|
||
[JsonIgnore]
|
||
public int LastUsedOffsetMs { get; set; } = 0;
|
||
|
||
/// <summary>
|
||
/// Indica se questa asta è stata seguita dall'inizio (per salvare storia completa)
|
||
/// </summary>
|
||
public bool IsTrackedFromStart { get; set; } = false;
|
||
|
||
/// <summary>
|
||
/// Timestamp di inizio tracking
|
||
/// </summary>
|
||
public DateTime? TrackingStartedAt { get; set; }
|
||
|
||
// ???????????????????????????????????????????????????????????????
|
||
// IMPOSTAZIONI PER-ASTA (override globali)
|
||
// ???????????????????????????????????????????????????????????????
|
||
|
||
/// <summary>
|
||
/// Override: abilita/disabilita strategie avanzate per questa asta
|
||
/// null = usa impostazione globale
|
||
/// </summary>
|
||
public bool? AdvancedStrategiesEnabled { get; set; }
|
||
|
||
/// <summary>
|
||
/// Override: abilita/disabilita jitter per questa asta
|
||
/// </summary>
|
||
public bool? JitterEnabledOverride { get; set; }
|
||
|
||
/// <summary>
|
||
/// Override: abilita/disabilita soft retreat per questa asta
|
||
/// </summary>
|
||
public bool? SoftRetreatEnabledOverride { get; set; }
|
||
|
||
/// <summary>
|
||
/// Override: limite puntate per questa asta
|
||
/// </summary>
|
||
public int? MaxBidsOverride { get; set; }
|
||
|
||
// ?? NUOVO: Rilevamento situazione di duello
|
||
|
||
/// <summary>
|
||
/// True se rilevata situazione di duello (solo 2 bidder dominanti)
|
||
/// </summary>
|
||
[JsonIgnore]
|
||
public bool IsDuelSituation { get; set; } = false;
|
||
|
||
/// <summary>
|
||
/// Username dell'avversario in caso di duello
|
||
/// </summary>
|
||
[JsonIgnore]
|
||
public string? DuelOpponent { get; set; }
|
||
|
||
/// <summary>
|
||
/// Vantaggio/svantaggio nel duello (% puntate mie - % puntate avversario)
|
||
/// Positivo = sto dominando, Negativo = sto perdendo
|
||
/// </summary>
|
||
[JsonIgnore]
|
||
public double DuelAdvantage { get; set; } = 0;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Rappresenta il valore calcolato di un prodotto all'asta
|
||
/// </summary>
|
||
public class ProductValue
|
||
{
|
||
/// <summary>
|
||
/// Prezzo attuale dell'asta in euro
|
||
/// </summary>
|
||
public double CurrentPrice { get; set; }
|
||
|
||
/// <summary>
|
||
/// Numero totale di puntate effettuate sull'asta
|
||
/// </summary>
|
||
public int TotalBids { get; set; }
|
||
|
||
/// <summary>
|
||
/// Numero di puntate effettuate dall'utente
|
||
/// </summary>
|
||
public int MyBids { get; set; }
|
||
|
||
/// <summary>
|
||
/// Costo delle puntate dell'utente (MyBids × BidCost)
|
||
/// </summary>
|
||
public double MyBidsCost { get; set; }
|
||
|
||
/// <summary>
|
||
/// Costo totale per l'utente se vince (CurrentPrice + MyBidsCost + ShippingCost)
|
||
/// </summary>
|
||
public double TotalCostIfWin { get; set; }
|
||
|
||
/// <summary>
|
||
/// Prezzo "Compra Subito" del prodotto
|
||
/// </summary>
|
||
public double? BuyNowPrice { get; set; }
|
||
|
||
/// <summary>
|
||
/// Spese di spedizione
|
||
/// </summary>
|
||
public double? ShippingCost { get; set; }
|
||
|
||
/// <summary>
|
||
/// Risparmio rispetto al prezzo "Compra Subito" (può essere negativo)
|
||
/// </summary>
|
||
public double? Savings { get; set; }
|
||
|
||
/// <summary>
|
||
/// Percentuale di risparmio rispetto al prezzo "Compra Subito"
|
||
/// </summary>
|
||
public double? SavingsPercentage { get; set; }
|
||
|
||
/// <summary>
|
||
/// Indica se conviene continuare (risparmio positivo)
|
||
/// </summary>
|
||
public bool IsWorthIt { get; set; }
|
||
|
||
/// <summary>
|
||
/// Timestamp del calcolo
|
||
/// </summary>
|
||
public DateTime CalculatedAt { get; set; }
|
||
|
||
/// <summary>
|
||
/// Messaggio riassuntivo del valore
|
||
/// </summary>
|
||
public string Summary { get; set; } = "";
|
||
}
|
||
}
|