From f3262a04972bd321aae9dbab42db29bab7de67a8 Mon Sep 17 00:00:00 2001 From: Alberto Balbo Date: Mon, 16 Feb 2026 23:10:04 +0100 Subject: [PATCH] Log aste strutturato, limiti prodotto e UI statistiche MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Log per-asta ora strutturato con livelli, categorie e deduplicazione; motivi di blocco puntata tracciati in modo dettagliato e throttled - Nuova visualizzazione log compatta e colorata nella UI - Migliorate statistiche prodotto: aggiunta mediana prezzo, flag UseCustomLimits e editing limiti inline - Impostazione prioritΓ  limiti nuove aste (globali vs personalizzati) - Refactoring: rimossi limiti reset, UI statistiche rinnovata, ordinamenti e filtri avanzati - Aggiornato schema DB (MedianFinalPrice, UseCustomLimits) - Diagnostica periodica e log dettagliato su ticker/controlli --- Mimante/Models/AuctionInfo.cs | 165 +- Mimante/Models/AuctionLogEntry.cs | 126 ++ Mimante/Models/ProductStatisticsRecord.cs | 6 + Mimante/Pages/Index.razor | 49 +- Mimante/Pages/Index.razor.cs | 169 +- Mimante/Pages/Settings.razor | 40 +- Mimante/Pages/Statistics.razor | 1599 ++++++++---------- Mimante/Services/AuctionMonitor.cs | 236 +-- Mimante/Services/DatabaseService.cs | 117 +- Mimante/Services/ProductStatisticsService.cs | 12 + Mimante/Services/StatsService.cs | 19 + Mimante/Utilities/SettingsManager.cs | 8 + Mimante/wwwroot/css/modern-pages.css | 178 ++ 13 files changed, 1562 insertions(+), 1162 deletions(-) create mode 100644 Mimante/Models/AuctionLogEntry.cs diff --git a/Mimante/Models/AuctionInfo.cs b/Mimante/Models/AuctionInfo.cs index 6f16573..a355e3f 100644 --- a/Mimante/Models/AuctionInfo.cs +++ b/Mimante/Models/AuctionInfo.cs @@ -118,9 +118,9 @@ namespace AutoBidder.Models [JsonPropertyName("RecentBids")] public List RecentBids { get; set; } = new List(); - // Log per-asta (non serializzato) + // Log per-asta strutturato (non serializzato) [System.Text.Json.Serialization.JsonIgnore] - public List AuctionLog { get; set; } = new(); + public List AuctionLog { get; set; } = new(); // Flag runtime: indica che θ in corso un'operazione di final attack per questa asta [System.Text.Json.Serialization.JsonIgnore] @@ -168,63 +168,140 @@ namespace AutoBidder.Models /// - /// 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. + /// Aggiunge una voce strutturata al log dell'asta con deduplicazione e limite righe. + /// Parsifica automaticamente il tag [TAG] per determinare livello e categoria. /// - /// Messaggio da aggiungere al log - /// Numero massimo di righe da mantenere (default: 500) - public void AddLog(string message, int maxLines = 500) + public void AddLog(string message, int maxLines = 200) { - var timestamp = DateTime.Now.ToString("HH:mm:ss.fff"); + // Protezione null-safety (dopo ClearData) + if (AuctionLog == null) AuctionLog = new(); - // DEBUG: Print per verificare che i log vengano aggiunti - #if DEBUG - System.Diagnostics.Debug.WriteLine($"[AddLog] {AuctionId}: {message}"); - #endif + var now = DateTime.Now; - // ?? DEDUPLICAZIONE: Se l'ultimo messaggio θ uguale, incrementa contatore + // Parsifica tag dal messaggio per determinare livello e categoria + var (level, category, cleanMessage) = ParseLogTag(message); + + // 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 last = AuctionLog[^1]; + if (last.Message == cleanMessage && last.Category == category) { - 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; - } + last.RepeatCount++; + last.Timestamp = now; + return; } } - // Nuovo messaggio diverso dall'ultimo - var entry = $"{timestamp} - {message}"; - AuctionLog.Add(entry); + AuctionLog.Add(new AuctionLogEntry + { + Timestamp = now, + Level = level, + Category = category, + Message = cleanMessage + }); - // Mantieni solo gli ultimi maxLines log if (AuctionLog.Count > maxLines) { - int excessCount = AuctionLog.Count - maxLines; - AuctionLog.RemoveRange(0, excessCount); + AuctionLog.RemoveRange(0, AuctionLog.Count - maxLines); } } + + /// + /// Aggiunge una voce tipizzata al log dell'asta (senza parsing del tag). + /// + public void AddLog(string message, AuctionLogLevel level, AuctionLogCategory category) + { + // Protezione null-safety (dopo ClearData) + if (AuctionLog == null) AuctionLog = new(); + + var now = DateTime.Now; + + if (AuctionLog.Count > 0) + { + var last = AuctionLog[^1]; + if (last.Message == message && last.Category == category) + { + last.RepeatCount++; + last.Timestamp = now; + return; + } + } + + AuctionLog.Add(new AuctionLogEntry + { + Timestamp = now, + Level = level, + Category = category, + Message = message + }); + + if (AuctionLog.Count > MAX_LOG_LINES) + { + AuctionLog.RemoveRange(0, AuctionLog.Count - MAX_LOG_LINES); + } + } + + /// + /// Parsifica i tag [TAG] per determinare livello e categoria automaticamente. + /// + private static (AuctionLogLevel level, AuctionLogCategory category, string cleanMessage) ParseLogTag(string message) + { + // Cerca pattern [TAG] all'inizio del messaggio + var tagMatch = System.Text.RegularExpressions.Regex.Match(message, @"^\[([A-Z_ ]+)\]\s*(.*)$"); + if (!tagMatch.Success) + return (AuctionLogLevel.Info, AuctionLogCategory.General, message); + + var tag = tagMatch.Groups[1].Value.Trim(); + var cleanMsg = tagMatch.Groups[2].Value; + + return tag switch + { + // Bid/puntata + "BID" => (AuctionLogLevel.Bid, AuctionLogCategory.BidAttempt, cleanMsg), + "BID OK" => (AuctionLogLevel.Success, AuctionLogCategory.BidResult, cleanMsg), + "BID FAIL" => (AuctionLogLevel.Error, AuctionLogCategory.BidResult, cleanMsg), + "BID EXCEPTION" => (AuctionLogLevel.Error, AuctionLogCategory.BidResult, cleanMsg), + "MANUAL BID" => (AuctionLogLevel.Bid, AuctionLogCategory.BidAttempt, cleanMsg), + "MANUAL BID OK" => (AuctionLogLevel.Success, AuctionLogCategory.BidResult, cleanMsg), + "MANUAL BID FAIL" => (AuctionLogLevel.Error, AuctionLogCategory.BidResult, cleanMsg), + + // Timing + "TICKER" => (AuctionLogLevel.Timing, AuctionLogCategory.Ticker, cleanMsg), + "TIMING" or "\u26a0\ufe0f TIMING" => (AuctionLogLevel.Warning, AuctionLogCategory.Ticker, cleanMsg), + + // Prezzi/limiti + "PRICE" => (AuctionLogLevel.Warning, AuctionLogCategory.Price, cleanMsg), + "VALUE" => (AuctionLogLevel.Warning, AuctionLogCategory.Value, cleanMsg), + "LIMIT" => (AuctionLogLevel.Warning, AuctionLogCategory.Limit, cleanMsg), + + // Reset + var r when r.StartsWith("RESET") => (AuctionLogLevel.Info, AuctionLogCategory.Reset, cleanMsg), + + // Strategie + "STRATEGY" => (AuctionLogLevel.Strategy, AuctionLogCategory.Strategy, cleanMsg), + "COMPETITION" => (AuctionLogLevel.Strategy, AuctionLogCategory.Competition, cleanMsg), + + // Diagnostica + "DIAG" => (AuctionLogLevel.Debug, AuctionLogCategory.Diagnostic, cleanMsg), + "DEBUG" => (AuctionLogLevel.Debug, AuctionLogCategory.General, cleanMsg), + + // Stato + "START" => (AuctionLogLevel.Info, AuctionLogCategory.Status, cleanMsg), + "ASTA TERMINATA" => (AuctionLogLevel.Warning, AuctionLogCategory.Status, cleanMsg), + "\u26a0\ufe0f SUGGERIMENTO" => (AuctionLogLevel.Warning, AuctionLogCategory.Ticker, cleanMsg), + + // Polling + "POLL ERROR" => (AuctionLogLevel.Error, AuctionLogCategory.Polling, cleanMsg), + + // Errori generici + "ERROR" or "ERRORE" => (AuctionLogLevel.Error, AuctionLogCategory.General, cleanMsg), + "WARN" => (AuctionLogLevel.Warning, AuctionLogCategory.General, cleanMsg), + "OK" => (AuctionLogLevel.Success, AuctionLogCategory.General, cleanMsg), + + _ => (AuctionLogLevel.Info, AuctionLogCategory.General, message) + }; + } public int PollingLatencyMs { get; set; } = 0; // Ultima latenza polling ms diff --git a/Mimante/Models/AuctionLogEntry.cs b/Mimante/Models/AuctionLogEntry.cs new file mode 100644 index 0000000..0313982 --- /dev/null +++ b/Mimante/Models/AuctionLogEntry.cs @@ -0,0 +1,126 @@ +using System; + +namespace AutoBidder.Models +{ + /// + /// Entry strutturata per il log di una singola asta. + /// Contiene timestamp preciso, livello di gravitΰ, categoria e messaggio. + /// + public class AuctionLogEntry + { + public DateTime Timestamp { get; set; } + public AuctionLogLevel Level { get; set; } + public AuctionLogCategory Category { get; set; } + public string Message { get; set; } = ""; + + /// + /// Contatore deduplicazione (se > 1, il messaggio θ stato ripetuto) + /// + public int RepeatCount { get; set; } = 1; + + /// + /// Formato compatto per display: solo ora con millisecondi + /// + public string TimeDisplay => Timestamp.ToString("HH:mm:ss.fff"); + + /// + /// Icona Bootstrap per il livello + /// + public string LevelIcon => Level switch + { + AuctionLogLevel.Error => "bi-x-circle-fill", + AuctionLogLevel.Warning => "bi-exclamation-triangle-fill", + AuctionLogLevel.Success => "bi-check-circle-fill", + AuctionLogLevel.Bid => "bi-hand-index-thumb-fill", + AuctionLogLevel.Strategy => "bi-shield-fill", + AuctionLogLevel.Timing => "bi-stopwatch-fill", + AuctionLogLevel.Debug => "bi-bug-fill", + _ => "bi-info-circle-fill" + }; + + /// + /// Classe CSS per il livello + /// + public string LevelClass => Level switch + { + AuctionLogLevel.Error => "alog-error", + AuctionLogLevel.Warning => "alog-warning", + AuctionLogLevel.Success => "alog-success", + AuctionLogLevel.Bid => "alog-bid", + AuctionLogLevel.Strategy => "alog-strategy", + AuctionLogLevel.Timing => "alog-timing", + AuctionLogLevel.Debug => "alog-debug", + _ => "alog-info" + }; + + /// + /// Label breve del livello + /// + public string LevelLabel => Level switch + { + AuctionLogLevel.Error => "ERR", + AuctionLogLevel.Warning => "WARN", + AuctionLogLevel.Success => "OK", + AuctionLogLevel.Bid => "BID", + AuctionLogLevel.Strategy => "STRAT", + AuctionLogLevel.Timing => "TIME", + AuctionLogLevel.Debug => "DBG", + _ => "INFO" + }; + + /// + /// Label della categoria + /// + public string CategoryLabel => Category switch + { + AuctionLogCategory.Ticker => "Ticker", + AuctionLogCategory.Price => "Prezzo", + AuctionLogCategory.Reset => "Reset", + AuctionLogCategory.BidAttempt => "Puntata", + AuctionLogCategory.BidResult => "Risultato", + AuctionLogCategory.Strategy => "Strategia", + AuctionLogCategory.Value => "Valore", + AuctionLogCategory.Competition => "Compet.", + AuctionLogCategory.Limit => "Limite", + AuctionLogCategory.Diagnostic => "Diagn.", + AuctionLogCategory.Status => "Stato", + AuctionLogCategory.Polling => "Poll", + _ => "Generale" + }; + } + + /// + /// Livello di gravitΰ del log per-asta + /// + public enum AuctionLogLevel + { + Debug = 0, + Info = 1, + Timing = 2, + Strategy = 3, + Bid = 4, + Success = 5, + Warning = 6, + Error = 7 + } + + /// + /// Categoria del log per filtraggio e raggruppamento + /// + public enum AuctionLogCategory + { + General, + Ticker, + Price, + Reset, + BidAttempt, + BidResult, + Strategy, + Value, + Competition, + Limit, + Diagnostic, + Status, + Polling + } +} diff --git a/Mimante/Models/ProductStatisticsRecord.cs b/Mimante/Models/ProductStatisticsRecord.cs index 1f5a69f..1d9ebeb 100644 --- a/Mimante/Models/ProductStatisticsRecord.cs +++ b/Mimante/Models/ProductStatisticsRecord.cs @@ -17,6 +17,7 @@ namespace AutoBidder.Models public double AvgFinalPrice { get; set; } public double? MinFinalPrice { get; set; } public double? MaxFinalPrice { get; set; } + public double? MedianFinalPrice { get; set; } // Statistiche puntate public double AvgBidsToWin { get; set; } @@ -43,6 +44,11 @@ namespace AutoBidder.Models public int? UserDefaultMaxBids { get; set; } public int? UserDefaultBidBeforeDeadlineMs { get; set; } + /// + /// Se true, usa i limiti personalizzati del prodotto. Se false, usa i globali. + /// + public bool UseCustomLimits { get; set; } + // JSON con statistiche per fascia oraria public string? HourlyStatsJson { get; set; } diff --git a/Mimante/Pages/Index.razor b/Mimante/Pages/Index.razor index a81ca3d..1c690da 100644 --- a/Mimante/Pages/Index.razor +++ b/Mimante/Pages/Index.razor @@ -457,20 +457,41 @@
-
-
- @if (selectedAuction.AuctionLog.Any()) - { - @foreach (var logEntry in GetAuctionLog(selectedAuction)) - { -
@logEntry
- } - } - else - { -
Nessun log disponibile.
- } -
+
+ @if (selectedAuction.AuctionLog.Any()) + { +
+
+ Ora + Livello + Categoria + Messaggio +
+
+ @foreach (var entry in GetAuctionLog(selectedAuction)) + { +
+ @entry.TimeDisplay + + @entry.LevelLabel + + @entry.CategoryLabel + + @entry.Message + @if (entry.RepeatCount > 1) + { + x@entry.RepeatCount + } + +
+ } +
+
+ } + else + { +
Nessun log disponibile.
+ }
diff --git a/Mimante/Pages/Index.razor.cs b/Mimante/Pages/Index.razor.cs index f8a952c..6d9037c 100644 --- a/Mimante/Pages/Index.razor.cs +++ b/Mimante/Pages/Index.razor.cs @@ -527,18 +527,52 @@ namespace AutoBidder.Pages var productName = ExtractProductNameFromUrl(addDialogUrl); var tempName = string.IsNullOrWhiteSpace(productName) ? $"Asta {auctionId}" : productName; + // Carica limiti dal database prodotti se disponibili + double minPrice = settings.DefaultMinPrice; + double maxPrice = settings.DefaultMaxPrice; + int bidDeadlineMs = settings.DefaultBidBeforeDeadlineMs; + + // Se abilitato, cerca limiti salvati per questo prodotto + // Nota: il nome estratto dall'URL θ approssimativo. I limiti verranno + // ri-applicati in FetchAuctionDetailsInBackgroundAsync con il nome REALE. + if (!string.IsNullOrWhiteSpace(productName) && + settings.NewAuctionLimitsPriority == "ProductStats" && + StatsService.IsAvailable) + { + try + { + var productKey = ProductStatisticsService.GenerateProductKey(productName); + var productStats = StatsService.GetProductStats(productKey); + + if (productStats != null && productStats.UseCustomLimits) + { + // Usa limiti personalizzati se abilitati per questo prodotto + if (productStats.UserDefaultMinPrice.HasValue) + minPrice = productStats.UserDefaultMinPrice.Value; + if (productStats.UserDefaultMaxPrice.HasValue) + maxPrice = productStats.UserDefaultMaxPrice.Value; + if (productStats.UserDefaultBidBeforeDeadlineMs.HasValue) + bidDeadlineMs = productStats.UserDefaultBidBeforeDeadlineMs.Value; + + AddLog($"[STATS] Limiti prodotto (da URL): €{minPrice:F2}-€{maxPrice:F2}"); + } + } + catch (Exception ex) + { + AddLog($"[STATS WARN] Impossibile caricare limiti prodotto: {ex.Message}"); + } + } + // Crea nuova asta var newAuction = new AuctionInfo { AuctionId = auctionId, Name = tempName, OriginalUrl = addDialogUrl, - BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs, + BidBeforeDeadlineMs = bidDeadlineMs, CheckAuctionOpenBeforeBid = settings.DefaultCheckAuctionOpenBeforeBid, - MinPrice = settings.DefaultMinPrice, - MaxPrice = settings.DefaultMaxPrice, - MinResets = settings.DefaultMinResets, - MaxResets = settings.DefaultMaxResets, + MinPrice = minPrice, + MaxPrice = maxPrice, IsActive = isActive, IsPaused = isPaused }; @@ -640,7 +674,60 @@ namespace AutoBidder.Pages AddLog($"[FETCH] Info prodotto: Valore={auction.BuyNowPrice:F2}€, Spedizione={auction.ShippingCost:F2}€"); } - // 3. Salva se qualcosa θ cambiato + // 3. Cerca limiti prodotto dal database con il nome REALE + if (updated && !string.IsNullOrWhiteSpace(auction.Name)) + { + var settings = AutoBidder.Utilities.SettingsManager.Load(); + if (settings.NewAuctionLimitsPriority == "ProductStats" && StatsService.IsAvailable) + { + try + { + var productKey = ProductStatisticsService.GenerateProductKey(auction.Name); + var productStats = StatsService.GetProductStats(productKey); + + if (productStats != null && productStats.UseCustomLimits) + { + bool limitsApplied = false; + + if (productStats.UserDefaultMinPrice.HasValue) + { + auction.MinPrice = productStats.UserDefaultMinPrice.Value; + limitsApplied = true; + } + if (productStats.UserDefaultMaxPrice.HasValue) + { + auction.MaxPrice = productStats.UserDefaultMaxPrice.Value; + limitsApplied = true; + } + if (productStats.UserDefaultMaxBids.HasValue) + { + auction.MaxClicks = productStats.UserDefaultMaxBids.Value; + limitsApplied = true; + } + if (productStats.UserDefaultBidBeforeDeadlineMs.HasValue) + { + auction.BidBeforeDeadlineMs = productStats.UserDefaultBidBeforeDeadlineMs.Value; + limitsApplied = true; + } + + if (limitsApplied) + { + AddLog($"[STATS] Limiti prodotto applicati dal DB: €{auction.MinPrice:F2}-€{auction.MaxPrice:F2}, Anticipo={auction.BidBeforeDeadlineMs}ms (key={productKey})"); + } + } + else + { + AddLog($"[STATS] Nessun limite trovato per productKey={productKey}"); + } + } + catch (Exception ex) + { + AddLog($"[STATS WARN] Errore ricerca limiti prodotto: {ex.Message}"); + } + } + } + + // 4. Salva se qualcosa θ cambiato if (updated) { SaveAuctions(); @@ -1212,32 +1299,44 @@ namespace AutoBidder.Pages private IEnumerable GetSortedAuctions() { - var list = auctions.AsEnumerable(); - - list = auctionSortColumn switch + try { - "stato" => auctionSortAscending - ? list.OrderBy(a => a.IsActive).ThenBy(a => a.IsPaused) - : list.OrderByDescending(a => a.IsActive).ThenByDescending(a => a.IsPaused), - "nome" => auctionSortAscending - ? list.OrderBy(a => a.Name) - : list.OrderByDescending(a => a.Name), - "prezzo" => auctionSortAscending - ? list.OrderBy(a => a.LastState?.Price ?? 0) - : list.OrderByDescending(a => a.LastState?.Price ?? 0), - "timer" => auctionSortAscending - ? list.OrderBy(a => a.LastState?.Timer ?? 999) - : list.OrderByDescending(a => a.LastState?.Timer ?? 999), - "puntate" => auctionSortAscending - ? list.OrderBy(a => a.BidsUsedOnThisAuction ?? 0) - : list.OrderByDescending(a => a.BidsUsedOnThisAuction ?? 0), - "ping" => auctionSortAscending - ? list.OrderBy(a => a.PollingLatencyMs) - : list.OrderByDescending(a => a.PollingLatencyMs), - _ => list - }; - - return list; + // Protezione null-safety + if (auctions == null || auctions.Count == 0) + return Enumerable.Empty(); + + var list = auctions.AsEnumerable(); + + list = auctionSortColumn switch + { + "stato" => auctionSortAscending + ? list.OrderBy(a => a.IsActive).ThenBy(a => a.IsPaused) + : list.OrderByDescending(a => a.IsActive).ThenByDescending(a => a.IsPaused), + "nome" => auctionSortAscending + ? list.OrderBy(a => a.Name) + : list.OrderByDescending(a => a.Name), + "prezzo" => auctionSortAscending + ? list.OrderBy(a => a.LastState?.Price ?? 0) + : list.OrderByDescending(a => a.LastState?.Price ?? 0), + "timer" => auctionSortAscending + ? list.OrderBy(a => a.LastState?.Timer ?? 999) + : list.OrderByDescending(a => a.LastState?.Timer ?? 999), + "puntate" => auctionSortAscending + ? list.OrderBy(a => a.BidsUsedOnThisAuction ?? 0) + : list.OrderByDescending(a => a.BidsUsedOnThisAuction ?? 0), + "ping" => auctionSortAscending + ? list.OrderBy(a => a.PollingLatencyMs) + : list.OrderByDescending(a => a.PollingLatencyMs), + _ => list + }; + + return list; + } + catch + { + // Fallback sicuro in caso di errore + return Enumerable.Empty(); + } } // ?? NUOVI METODI: Visualizzazione valori prodotto @@ -1375,9 +1474,9 @@ namespace AutoBidder.Pages } } - private IEnumerable GetAuctionLog(AuctionInfo auction) + private IEnumerable GetAuctionLog(AuctionInfo auction) { - return auction.AuctionLog.TakeLast(50); + return auction.AuctionLog.TakeLast(100); } /// @@ -1541,8 +1640,6 @@ namespace AutoBidder.Pages // Applica comunque ma con avviso selectedAuction.MinPrice = limits.MinPrice; selectedAuction.MaxPrice = limits.MaxPrice; - selectedAuction.MinResets = limits.MinResets; - selectedAuction.MaxResets = limits.MaxResets; SaveAuctions(); @@ -1554,8 +1651,6 @@ namespace AutoBidder.Pages // Applica limiti con buona confidenza selectedAuction.MinPrice = limits.MinPrice; selectedAuction.MaxPrice = limits.MaxPrice; - selectedAuction.MinResets = limits.MinResets; - selectedAuction.MaxResets = limits.MaxResets; SaveAuctions(); diff --git a/Mimante/Pages/Settings.razor b/Mimante/Pages/Settings.razor index 4b70329..fcd674d 100644 --- a/Mimante/Pages/Settings.razor +++ b/Mimante/Pages/Settings.razor @@ -206,6 +206,7 @@ +
0 = punta a qualsiasi prezzo. Il prezzo deve essere ? a questo valore per puntare.
@@ -217,27 +218,16 @@
+
0 = nessun limite. Se il prezzo supera questo valore, SMETTE di puntare.
-
- -
- - -
-
-
- -
- - +
+ + +
+ Se "Statistiche prodotto", quando aggiungi un'asta di un prodotto giΰ salvato, verranno usati i limiti personalizzati delle statistiche invece di quelli globali.
@@ -854,8 +844,6 @@ private System.Threading.Timer? updateTimer; auction.BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs; auction.MinPrice = settings.DefaultMinPrice; auction.MaxPrice = settings.DefaultMaxPrice; - auction.MinResets = settings.DefaultMinResets; - auction.MaxResets = settings.DefaultMaxResets; // Resetta override per usare impostazioni globali auction.AdvancedStrategiesEnabled = null; @@ -925,14 +913,6 @@ private System.Threading.Timer? updateTimer; auction.MaxPrice = settings.DefaultMaxPrice; settingLabel = $"Prezzo massimo (€{settings.DefaultMaxPrice:F2})"; break; - case nameof(settings.DefaultMinResets): - auction.MinResets = settings.DefaultMinResets; - settingLabel = $"Reset minimi ({settings.DefaultMinResets})"; - break; - case nameof(settings.DefaultMaxResets): - auction.MaxResets = settings.DefaultMaxResets; - settingLabel = $"Reset massimi ({settings.DefaultMaxResets})"; - break; } count++; } diff --git a/Mimante/Pages/Statistics.razor b/Mimante/Pages/Statistics.razor index 6b54e1d..3e3632a 100644 --- a/Mimante/Pages/Statistics.razor +++ b/Mimante/Pages/Statistics.razor @@ -11,742 +11,285 @@ Statistiche - AutoBidder -
-
-
- -

Statistiche

+
+ +
+
+ + Statistiche
@if (StatsService.IsAvailable) { - }
@if (!StatsService.IsAvailable) { -
-
- -
-
Statistiche non disponibili
-

Il database per le statistiche non θ stato configurato o non θ accessibile.

-
+
+ +
+ Statistiche non disponibili + Il database non θ stato configurato o non θ accessibile.
} else if (isLoading) { -
-
-

Caricamento statistiche...

+
+
+ Caricamento statistiche...
} else { -
- -
-
-
-
-
- - Aste Terminate (@(filteredAuctions?.Count ?? 0)) -
-
-
- - -
-
-
-
- - - @if (!string.IsNullOrEmpty(filterName)) - { - - } -
-
-
- -
-
- Clicca intestazioni per ordinare -
-
-
- -
- @if (filteredAuctions == null || !filteredAuctions.Any()) + +
+
+ Prodotti Salvati (@(filteredProducts?.Count ?? 0)) +
+
- - -
-
-
-
- - Prodotti Salvati (@(filteredProducts?.Count ?? 0)) -
+
+ @if (filteredProducts == null || !filteredProducts.Any()) + { +
+ + @(string.IsNullOrEmpty(filterProductName) ? "Nessun prodotto salvato" : $"Nessun prodotto per \"{filterProductName}\"")
- - -
-
-
-
- - - @if (!string.IsNullOrEmpty(filterProductName)) - { - - } -
-
-
- Clicca intestazioni per ordinare -
-
-
- -
- @if (filteredProducts == null || !filteredProducts.Any()) - { -
- -

- @if (!string.IsNullOrEmpty(filterProductName)) - { - Nessun prodotto trovato per "@filterProductName" - } - else - { - Nessun prodotto salvato - } -

-
- } - else - { -
- - - - - - - - - - - - - - @foreach (var product in filteredProducts) - { - var winRate = product.TotalAuctions > 0 - ? (product.WonAuctions * 100.0 / product.TotalAuctions) - : 0; - var isEditing = editingProductKey == product.ProductKey; - - - - - - - - - - - - - @if (isEditing) + } + else + { +
- Prodotto @GetProductSortIndicator("name") - - Aste @GetProductSortIndicator("auctions") - - Win% @GetProductSortIndicator("winrate") - - Prezzo Medio @GetProductSortIndicator("avgprice") - Range StoricoLimiti ConsigliatiAzioni
-
- -
- @product.ProductName -
- @product.TotalAuctions totali (@product.WonAuctions vinte) -
-
-
- @product.TotalAuctions - - - @winRate.ToString("F0")% - - - €@product.AvgFinalPrice.ToString("F2") - - @if (product.MinFinalPrice.HasValue && product.MaxFinalPrice.HasValue) - { - - €@product.MinFinalPrice.Value.ToString("F2") - €@product.MaxFinalPrice.Value.ToString("F2") - - } - else - { - - - } - - @if (product.RecommendedMinPrice.HasValue && product.RecommendedMaxPrice.HasValue) - { - - €@product.RecommendedMinPrice.Value.ToString("F2") - €@product.RecommendedMaxPrice.Value.ToString("F2") - - } - else - { - N/D - } - -
- @if (product.RecommendedMinPrice.HasValue && product.RecommendedMaxPrice.HasValue) - { - - } - -
-
+ + + + + + + + + + + + + + + + + + + @foreach (var product in filteredProducts) + { + var winRate = product.TotalAuctions > 0 + ? (product.WonAuctions * 100.0 / product.TotalAuctions) + : 0; + + + + + + + + + + + + + + + - - + } - } - -
+ Prodotto @GetProductSortIndicator("name") + + Aste @GetProductSortIndicator("auctions") + + @GetProductSortIndicator("wins") + + Win% @GetProductSortIndicator("winrate") + + € Min @GetProductSortIndicator("minprice") + + € Med @GetProductSortIndicator("avgprice") + + € Mdn @GetProductSortIndicator("medianprice") + + € Max @GetProductSortIndicator("maxprice") + OnMin €Max €Max Punt.Azioni
+ @product.ProductName + @product.TotalAuctions@product.WonAuctions + @winRate.ToString("F0")% + + €@(product.MinFinalPrice?.ToString("F2") ?? "-") + + €@product.AvgFinalPrice.ToString("F2") + + €@(product.MedianFinalPrice?.ToString("F2") ?? "-") + + €@(product.MaxFinalPrice?.ToString("F2") ?? "-") + + + + + + + + + +
+ @if (product.RecommendedMinPrice.HasValue) { -
-
-
- - Limiti Default per: @product.ProductName -
- -
- -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- -
- - Valori consigliati dall'algoritmo:
- €@product.RecommendedMinPrice?.ToString("F2") - €@product.RecommendedMaxPrice?.ToString("F2")
- Reset: @product.RecommendedMinResets - @product.RecommendedMaxResets -
-
-
- - -
- - - - -
-
-
-
- } -
-
+ + + +
+ + + } + + + }
- - @if (selectedAuctionDetail != null) - { -
-
-
-
-
- - Dettagli Asta: @selectedAuctionDetail.AuctionName -
- -
-
-
- -
-
Informazioni Base
- - - - - - - - - - @if (selectedAuctionDetail.BuyNowPrice.HasValue) - { - - - - - } - @if (selectedAuctionDetail.Savings.HasValue) - { - - - - - } - - - - - - - - - - - - -
ID Asta:@selectedAuctionDetail.AuctionId
Prezzo Finale:€@selectedAuctionDetail.FinalPrice.ToString("F2")
Valore Prodotto:€@selectedAuctionDetail.BuyNowPrice.Value.ToString("F2")
Risparmio:€@selectedAuctionDetail.Savings.Value.ToString("F2")
Risultato: - @if (selectedAuctionDetail.Won) - { - ? VINTA - } - else - { - ? Persa - } -
Vincitore:@(selectedAuctionDetail.WinnerUsername ?? "-")
Data Chiusura:@FormatTimestamp(selectedAuctionDetail.Timestamp)
-
- - -
-
Statistiche Puntate
- - - - - - @if (selectedAuctionDetail.WinnerBidsUsed.HasValue) - { - - - - - } - @if (selectedAuctionDetail.TotalResets.HasValue) - { - - - - - } - @if (selectedAuctionDetail.ClosedAtHour.HasValue) - { - - - - - } -
Le mie puntate:@selectedAuctionDetail.BidsUsed
Puntate vincitore:@selectedAuctionDetail.WinnerBidsUsed
Reset totali:@selectedAuctionDetail.TotalResets
Ora chiusura:@selectedAuctionDetail.ClosedAtHour:00
-
- - -
-
Analisi Costi
- - @if (selectedAuctionDetail.ShippingCost.HasValue) - { - - - - - } - @if (selectedAuctionDetail.TotalCost.HasValue) - { - - - - - } - @{ - var bidCost = selectedAuctionDetail.BidsUsed * 0.15; - } - - - - - @if (selectedAuctionDetail.ProductKey != null) - { - - - - - } -
Spedizione:€@selectedAuctionDetail.ShippingCost.Value.ToString("F2")
Costo totale:€@selectedAuctionDetail.TotalCost.Value.ToString("F2")
Costo puntate (~):€@bidCost.ToString("F2")
Chiave prodotto:@selectedAuctionDetail.ProductKey
-
-
-
-
-
-
- } - - - @if (selectedProduct != null) - { -
-
-
-
-
- - Aste di: @selectedProduct.ProductName - @(selectedProductAuctions?.Count ?? 0) aste -
- -
- - @if (isLoadingProductAuctions) + +
+
+ Aste Recenti (@(filteredAuctions?.Count ?? 0)) +
+ +
- } +
+ @if (filteredAuctions == null || !filteredAuctions.Any()) + { +
+ + @(string.IsNullOrEmpty(filterName) && string.IsNullOrEmpty(filterWon) ? "Nessuna asta terminata" : "Nessun risultato per i filtri") +
+ } + else + { + + + + + + + + + + + + + @foreach (var auction in filteredAuctions) + { + + + + + + + + + } + +
+ Nome @GetSortIndicator("name") + + Prezzo @GetSortIndicator("price") + + Puntate @GetSortIndicator("bids") + Vincitore + Stato @GetSortIndicator("won") + + Data @GetSortIndicator("date") +
@TruncateName(auction.AuctionName, 40)€@auction.FinalPrice.ToString("F2") + @auction.BidsUsed + @if (auction.WinnerBidsUsed.HasValue && auction.WinnerBidsUsed != auction.BidsUsed) + { + /@auction.WinnerBidsUsed + } + @(auction.WinnerUsername ?? "-") + @if (auction.Won) + { + + } + else + { + + } + @FormatTimestamp(auction.Timestamp)
+ } +
+
}
@@ -761,27 +304,14 @@ private List? filteredProducts; // Filtri e ordinamento aste private string filterName = ""; private string filterWon = ""; -private AuctionResultExtended? selectedAuctionDetail; // Filtri e ordinamento prodotti private string filterProductName = ""; private string productSortColumn = "name"; private bool productSortDescending = false; -// Prodotto selezionato e sue aste -private ProductStatisticsRecord? selectedProduct = null; -private List? selectedProductAuctions = null; -private bool isLoadingProductAuctions = false; - // Editing limiti default prodotto -private string? editingProductKey = null; private bool isSavingDefaults = false; -private double? tempUserDefaultMinPrice; -private double? tempUserDefaultMaxPrice; -private int? tempUserDefaultMinResets; -private int? tempUserDefaultMaxResets; -private int? tempUserDefaultMaxBids; -private int? tempUserDefaultBidDeadline; protected override async Task OnInitializedAsync() { @@ -850,7 +380,6 @@ private int? tempUserDefaultBidDeadline; "bids" => sortDescending ? filtered.OrderByDescending(a => a.BidsUsed) : filtered.OrderBy(a => a.BidsUsed), "name" => sortDescending ? filtered.OrderByDescending(a => a.AuctionName) : filtered.OrderBy(a => a.AuctionName), "won" => sortDescending ? filtered.OrderByDescending(a => a.Won) : filtered.OrderBy(a => a.Won), - "resets" => sortDescending ? filtered.OrderByDescending(a => a.TotalResets ?? 0) : filtered.OrderBy(a => a.TotalResets ?? 0), _ => filtered.OrderByDescending(a => a.Timestamp) // date_desc default }; @@ -892,49 +421,6 @@ private int? tempUserDefaultBidDeadline; : new MarkupString(""); } - private void SelectAuction(AuctionResultExtended auction) - { - selectedAuctionDetail = auction; - } - - private async Task SelectProduct(ProductStatisticsRecord product) - { - selectedProduct = product; - selectedProductAuctions = null; - isLoadingProductAuctions = true; - StateHasChanged(); - - try - { - // Carica tutte le aste per questo prodotto - selectedProductAuctions = await DatabaseService.GetAuctionResultsByProductAsync(product.ProductKey, 1000); - } - catch (Exception ex) - { - Console.WriteLine($"[Statistics] Error loading product auctions: {ex.Message}"); - await JSRuntime.InvokeVoidAsync("alert", $"Errore caricamento aste: {ex.Message}"); - } - finally - { - isLoadingProductAuctions = false; - StateHasChanged(); - } - } - - private string GetHeatBadgeClass(int heat) - { - if (heat < 30) return "bg-success"; - if (heat < 60) return "bg-warning text-dark"; - return "bg-danger"; - } - - private string GetResetBadgeClass(int resets) - { - if (resets < 10) return "bg-success"; - if (resets < 30) return "bg-warning text-dark"; - return "bg-danger"; - } - private string TruncateName(string name, int maxLength) { if (string.IsNullOrEmpty(name)) return "-"; @@ -951,46 +437,6 @@ private int? tempUserDefaultBidDeadline; return timestamp; } - private async Task ApplyLimitsToProduct(ProductStatisticsRecord product) - { - try - { - // Trova tutte le aste con questo ProductKey nel monitor - var matchingAuctions = AppState.Auctions - .Where(a => ProductStatisticsService.GenerateProductKey(a.Name) == product.ProductKey) - .ToList(); - - if (!matchingAuctions.Any()) - { - await JSRuntime.InvokeVoidAsync("alert", $"Nessuna asta trovata per '{product.ProductName}'"); - return; - } - - // Applica i limiti - foreach (var auction in matchingAuctions) - { - auction.MinPrice = product.RecommendedMinPrice ?? 0; - auction.MaxPrice = product.RecommendedMaxPrice ?? 0; - auction.MinResets = product.RecommendedMinResets ?? 0; - auction.MaxResets = product.RecommendedMaxResets ?? 0; - } - - // Salva - AutoBidder.Utilities.PersistenceManager.SaveAuctions(AppState.Auctions.ToList()); - - await JSRuntime.InvokeVoidAsync("alert", - $"? Limiti applicati a {matchingAuctions.Count} aste di '{product.ProductName}'\n\n" + - $"Min: €{product.RecommendedMinPrice:F2}\n" + - $"Max: €{product.RecommendedMaxPrice:F2}\n" + - $"Min Reset: {product.RecommendedMinResets}\n" + - $"Max Reset: {product.RecommendedMaxResets}"); - } - catch (Exception ex) - { - await JSRuntime.InvokeVoidAsync("alert", $"Errore: {ex.Message}"); - } - } - private async Task DeleteProduct(ProductStatisticsRecord product) { try @@ -1067,12 +513,24 @@ private int? tempUserDefaultBidDeadline; "auctions" => productSortDescending ? filtered.OrderByDescending(p => p.TotalAuctions) : filtered.OrderBy(p => p.TotalAuctions), + "wins" => productSortDescending + ? filtered.OrderByDescending(p => p.WonAuctions) + : filtered.OrderBy(p => p.WonAuctions), "winrate" => productSortDescending ? filtered.OrderByDescending(p => p.TotalAuctions > 0 ? (p.WonAuctions * 100.0 / p.TotalAuctions) : 0) : filtered.OrderBy(p => p.TotalAuctions > 0 ? (p.WonAuctions * 100.0 / p.TotalAuctions) : 0), + "minprice" => productSortDescending + ? filtered.OrderByDescending(p => p.MinFinalPrice ?? 0) + : filtered.OrderBy(p => p.MinFinalPrice ?? 0), "avgprice" => productSortDescending ? filtered.OrderByDescending(p => p.AvgFinalPrice) : filtered.OrderBy(p => p.AvgFinalPrice), + "medianprice" => productSortDescending + ? filtered.OrderByDescending(p => p.MedianFinalPrice ?? 0) + : filtered.OrderBy(p => p.MedianFinalPrice ?? 0), + "maxprice" => productSortDescending + ? filtered.OrderByDescending(p => p.MaxFinalPrice ?? 0) + : filtered.OrderBy(p => p.MaxFinalPrice ?? 0), _ => filtered.OrderBy(p => p.ProductName) // Default alfabetico ascendente }; @@ -1112,94 +570,77 @@ private int? tempUserDefaultBidDeadline; : new MarkupString(""); } - // ??????????????????????????????????????????????????????????????????? - // METODI EDITING LIMITI DEFAULT PRODOTTO - // ??????????????????????????????????????????????????????????????????? + // ??????????????????????????????????????????????????????????????? + // METODI EDITING INLINE LIMITI DEFAULT PRODOTTO + // ??????????????????????????????????????????????????????????????? - private void ToggleEditProduct(ProductStatisticsRecord product) + private void OnInlineFieldChanged(ProductStatisticsRecord product, string field, ChangeEventArgs e) { - if (editingProductKey == product.ProductKey) - { - // Chiudi editor - CancelEditProduct(); - } - else - { - // Apri editor - editingProductKey = product.ProductKey; - LoadCurrentDefaults(product); - } - } - - private void LoadCurrentDefaults(ProductStatisticsRecord product) - { - tempUserDefaultMinPrice = product.UserDefaultMinPrice; - tempUserDefaultMaxPrice = product.UserDefaultMaxPrice; - tempUserDefaultMinResets = product.UserDefaultMinResets; - tempUserDefaultMaxResets = product.UserDefaultMaxResets; - tempUserDefaultMaxBids = product.UserDefaultMaxBids; - tempUserDefaultBidDeadline = product.UserDefaultBidBeforeDeadlineMs; - } - - private void CopyRecommendedToTemp(ProductStatisticsRecord product) - { - tempUserDefaultMinPrice = product.RecommendedMinPrice; - tempUserDefaultMaxPrice = product.RecommendedMaxPrice; - tempUserDefaultMinResets = product.RecommendedMinResets; - tempUserDefaultMaxResets = product.RecommendedMaxResets; - tempUserDefaultMaxBids = product.RecommendedMaxBids; - // Bid deadline rimane quello dell'utente o default 200ms - if (!tempUserDefaultBidDeadline.HasValue) - tempUserDefaultBidDeadline = 200; + var rawValue = e.Value?.ToString(); - StateHasChanged(); + switch (field) + { + case nameof(ProductStatisticsRecord.UserDefaultMinPrice): + product.UserDefaultMinPrice = double.TryParse(rawValue, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var minP) ? minP : null; + break; + case nameof(ProductStatisticsRecord.UserDefaultMaxPrice): + product.UserDefaultMaxPrice = double.TryParse(rawValue, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var maxP) ? maxP : null; + break; + case nameof(ProductStatisticsRecord.UserDefaultMaxBids): + product.UserDefaultMaxBids = int.TryParse(rawValue, out var maxB) ? maxB : null; + break; + } } - private void CancelEditProduct() + private async Task CopyRecommendedInline(ProductStatisticsRecord product) { - editingProductKey = null; - tempUserDefaultMinPrice = null; - tempUserDefaultMaxPrice = null; - tempUserDefaultMinResets = null; - tempUserDefaultMaxResets = null; - tempUserDefaultMaxBids = null; - tempUserDefaultBidDeadline = null; + product.UserDefaultMinPrice = product.RecommendedMinPrice; + product.UserDefaultMaxPrice = product.RecommendedMaxPrice; + product.UserDefaultMaxBids = product.RecommendedMaxBids; + product.UseCustomLimits = true; + StateHasChanged(); + + try + { + await DatabaseService.UpdateProductUserDefaultsAsync( + product.ProductKey, + product.UserDefaultMinPrice, + product.UserDefaultMaxPrice, + null, + null, + product.UserDefaultMaxBids, + null, + true + ); + } + catch (Exception ex) + { + Console.WriteLine($"[Statistics] Error saving recommended defaults: {ex.Message}"); + } } - private async Task SaveProductDefaults(ProductStatisticsRecord product) + private async Task SaveInlineDefaults(ProductStatisticsRecord product) { try { isSavingDefaults = true; StateHasChanged(); - // Aggiorna nel database await DatabaseService.UpdateProductUserDefaultsAsync( product.ProductKey, - tempUserDefaultMinPrice, - tempUserDefaultMaxPrice, - tempUserDefaultMinResets, - tempUserDefaultMaxResets, - tempUserDefaultMaxBids, - tempUserDefaultBidDeadline + product.UserDefaultMinPrice, + product.UserDefaultMaxPrice, + null, + null, + product.UserDefaultMaxBids, + null, + product.UseCustomLimits ); - // Aggiorna l'oggetto locale - product.UserDefaultMinPrice = tempUserDefaultMinPrice; - product.UserDefaultMaxPrice = tempUserDefaultMaxPrice; - product.UserDefaultMinResets = tempUserDefaultMinResets; - product.UserDefaultMaxResets = tempUserDefaultMaxResets; - product.UserDefaultMaxBids = tempUserDefaultMaxBids; - product.UserDefaultBidBeforeDeadlineMs = tempUserDefaultBidDeadline; - await JSRuntime.InvokeVoidAsync("alert", - $"? Limiti default salvati per '{product.ProductName}'!\n\n" + - $"Min: €{tempUserDefaultMinPrice:F2} - Max: €{tempUserDefaultMaxPrice:F2}\n" + - $"Reset: {tempUserDefaultMinResets} - {tempUserDefaultMaxResets}\n" + - $"Max Puntate: {tempUserDefaultMaxBids}\n" + - $"Anticipo: {tempUserDefaultBidDeadline}ms"); - - CancelEditProduct(); + $"? Limiti salvati per '{product.ProductName}'!\n" + + $"Min: €{product.UserDefaultMinPrice:F2} - Max: €{product.UserDefaultMaxPrice:F2}\n" + + $"Max Puntate: {product.UserDefaultMaxBids ?? 0}"); } catch (Exception ex) { @@ -1229,9 +670,7 @@ private int? tempUserDefaultBidDeadline; var confirmed = await JSRuntime.InvokeAsync("confirm", $"Applicare i limiti default a {matchingAuctions.Count} aste di '{product.ProductName}'?\n\n" + $"Min: €{product.UserDefaultMinPrice:F2} - Max: €{product.UserDefaultMaxPrice:F2}\n" + - $"Reset: {product.UserDefaultMinResets} - {product.UserDefaultMaxResets}\n" + - $"Max Puntate: {product.UserDefaultMaxBids}\n" + - $"Anticipo: {product.UserDefaultBidBeforeDeadlineMs}ms"); + $"Max Puntate: {product.UserDefaultMaxBids}"); if (!confirmed) return; @@ -1242,14 +681,8 @@ private int? tempUserDefaultBidDeadline; auction.MinPrice = product.UserDefaultMinPrice.Value; if (product.UserDefaultMaxPrice.HasValue) auction.MaxPrice = product.UserDefaultMaxPrice.Value; - if (product.UserDefaultMinResets.HasValue) - auction.MinResets = product.UserDefaultMinResets.Value; - if (product.UserDefaultMaxResets.HasValue) - auction.MaxResets = product.UserDefaultMaxResets.Value; if (product.UserDefaultMaxBids.HasValue) auction.MaxClicks = product.UserDefaultMaxBids.Value; - if (product.UserDefaultBidBeforeDeadlineMs.HasValue) - auction.BidBeforeDeadlineMs = product.UserDefaultBidBeforeDeadlineMs.Value; } // Salva @@ -1266,49 +699,425 @@ private int? tempUserDefaultBidDeadline; private bool HasUserDefaults(ProductStatisticsRecord product) { - return product.UserDefaultMinPrice.HasValue + return product.UseCustomLimits + && product.UserDefaultMinPrice.HasValue && product.UserDefaultMaxPrice.HasValue; } + + private async Task ToggleCustomLimits(ProductStatisticsRecord product, bool enabled) + { + product.UseCustomLimits = enabled; + + try + { + await DatabaseService.UpdateProductUserDefaultsAsync( + product.ProductKey, + product.UserDefaultMinPrice, + product.UserDefaultMaxPrice, + null, + null, + product.UserDefaultMaxBids, + null, + enabled + ); + } + catch (Exception ex) + { + Console.WriteLine($"[Statistics] Error toggling custom limits: {ex.Message}"); + } + + StateHasChanged(); + } } diff --git a/Mimante/Services/AuctionMonitor.cs b/Mimante/Services/AuctionMonitor.cs index 2c2d29e..4ca993a 100644 --- a/Mimante/Services/AuctionMonitor.cs +++ b/Mimante/Services/AuctionMonitor.cs @@ -26,6 +26,9 @@ namespace AutoBidder.Services public event Action? OnLog; public event Action? OnResetCountChanged; + // Throttling per log di blocco (evita spam nel log globale) + private readonly Dictionary _lastBlockLogTime = new(); + /// /// Evento fired quando un'asta termina (vinta, persa o chiusa). /// Parametri: AuctionInfo, AuctionState finale, bool won (true se vinta dall'utente) @@ -185,10 +188,8 @@ namespace AutoBidder.Services auction.MinPrice = minPrice; auction.MaxPrice = maxPrice; - auction.MinResets = minResets; - auction.MaxResets = maxResets; - OnLog?.Invoke($"[LIMITS] Aggiornati limiti per {auction.Name}: MinPrice={minPrice:F2}, MaxPrice={maxPrice:F2}, MinResets={minResets}, MaxResets={maxResets}"); + OnLog?.Invoke($"[LIMITS] Aggiornati limiti per {auction.Name}: MinPrice={minPrice:F2}, MaxPrice={maxPrice:F2}"); return true; } } @@ -210,8 +211,6 @@ namespace AutoBidder.Services { auction.MinPrice = minPrice; auction.MaxPrice = maxPrice; - auction.MinResets = minResets; - auction.MaxResets = maxResets; count++; OnLog?.Invoke($"[LIMITS] Aggiornati limiti per {auction.Name}"); @@ -255,6 +254,7 @@ namespace AutoBidder.Services int tickerIntervalMs = Math.Max(100, settings.TickerIntervalMs); // Minimo 100ms int pollingIntervalMs = 500; // Poll API ogni 500ms max DateTime lastPoll = DateTime.MinValue; + DateTime lastDiagnostic = DateTime.MinValue; OnLog?.Invoke($"[TICKER] Avviato con intervallo={tickerIntervalMs}ms, polling={pollingIntervalMs}ms"); @@ -278,6 +278,37 @@ namespace AutoBidder.Services await Task.Delay(2000, token); continue; } + + // === DIAGNOSTICA PERIODICA (ogni 30s) === + var nowDiag = DateTime.UtcNow; + if ((nowDiag - lastDiagnostic).TotalSeconds >= 30) + { + lastDiagnostic = nowDiag; + settings = SettingsManager.Load(); // Ricarica impostazioni + + foreach (var a in activeAuctions.Where(x => !x.IsPaused)) + { + var timer = a.LastState?.Timer ?? 0; + var price = a.LastState?.Price ?? 0; + int offset = a.BidBeforeDeadlineMs > 0 ? a.BidBeforeDeadlineMs : settings.DefaultBidBeforeDeadlineMs; + double estimatedMs = GetEstimatedTimerMs(a); + + var statusParts = new List(); + statusParts.Add($"Timer={timer:F1}s"); + statusParts.Add($"Stima={estimatedMs:F0}ms"); + statusParts.Add($"€{price:F2}"); + statusParts.Add($"Offset={offset}ms"); + statusParts.Add($"RawMs={a.LastRawTimer:F0}"); + + // Indica perchΓ© potrebbe non puntare + if (a.MaxPrice > 0 && price > a.MaxPrice) + statusParts.Add($"β›”MaxPrice={a.MaxPrice:F2}"); + if (a.MinPrice > 0 && price < a.MinPrice) + statusParts.Add($"β›”MinPrice={a.MinPrice:F2}"); + + a.AddLog($"[DIAG] {string.Join(" | ", statusParts)}"); + } + } // === FASE 2: Poll API solo ogni pollingIntervalMs === var now = DateTime.UtcNow; @@ -405,6 +436,15 @@ namespace AutoBidder.Services auction.LastState = state; + // ?? FIX CRITICO: Aggiorna timer locale per interpolazione tra poll + // Senza questo, GetEstimatedTimerMs restituisce sempre il valore + // statico dell'ultimo poll e il countdown non funziona + if (state.Timer > 0) + { + auction.LastRawTimer = state.Timer * 1000; // Converti secondi β†’ millisecondi + auction.LastDeadlineUpdateUtc = DateTime.UtcNow; + } + if (shouldNotify) { OnAuctionUpdated?.Invoke(state); @@ -538,6 +578,39 @@ namespace AutoBidder.Services return false; } + /// + /// Logga un blocco nel log globale con throttling per evitare spam. + /// Ogni chiave (auctionId+reason) puΓ² loggare al massimo una volta ogni 10 secondi. + /// + private void LogBlockThrottled(AuctionInfo auction, string reason, string message) + { + var key = $"{auction.AuctionId}_{reason}"; + var now = DateTime.UtcNow; + + lock (_lastBlockLogTime) + { + if (_lastBlockLogTime.TryGetValue(key, out var lastTime)) + { + if ((now - lastTime).TotalSeconds < 10) + return; // Throttle: giΓ  loggato di recente + } + _lastBlockLogTime[key] = now; + + // Pulizia periodica entries vecchie (max 100) + if (_lastBlockLogTime.Count > 100) + { + var oldKeys = _lastBlockLogTime + .Where(kv => (now - kv.Value).TotalMinutes > 5) + .Select(kv => kv.Key) + .ToList(); + foreach (var k in oldKeys) + _lastBlockLogTime.Remove(k); + } + } + + OnLog?.Invoke(message); + } + /// /// TICKER: Tenta la puntata quando il timer locale raggiunge l'offset configurato. /// NESSUNA compensazione automatica della latenza - l'utente decide il timing esatto. @@ -549,20 +622,21 @@ namespace AutoBidder.Services if (state == null || state.IsMyBid || estimatedTimerMs <= 0) return; - // Log timing se abilitato - if (settings.LogTiming) - { - auction.AddLog($"[TICKER] Timer stimato={estimatedTimerMs:F0}ms <= Offset={offsetMs}ms"); - } + // Log timing dettagliato per ogni check del ticker + auction.AddLog($"Timer={estimatedTimerMs:F0}ms | Offset={offsetMs}ms | Prezzo=€{state.Price:F2} | Ultimo={state.LastBidder ?? "-"}", + Models.AuctionLogLevel.Timing, Models.AuctionLogCategory.Ticker); // === PROTEZIONE DOPPIA PUNTATA === - // Reset se timer Γ¨ aumentato (qualcuno ha puntato = nuovo ciclo) if (estimatedTimerMs > auction.LastScheduledTimerMs + 500) { + if (auction.BidScheduled) + { + auction.AddLog($"Reset ciclo: timer salito {auction.LastScheduledTimerMs:F0}β†’{estimatedTimerMs:F0}ms (qualcuno ha puntato)", + Models.AuctionLogLevel.Timing, Models.AuctionLogCategory.Ticker); + } auction.BidScheduled = false; } - // Reset se passato troppo tempo dall'ultima puntata if (auction.LastClickAt.HasValue && (DateTime.UtcNow - auction.LastClickAt.Value).TotalSeconds > 10) { @@ -572,6 +646,8 @@ namespace AutoBidder.Services // Skip se giΓ  schedulata per questo ciclo if (auction.BidScheduled && Math.Abs(auction.LastScheduledTimerMs - estimatedTimerMs) < 200) { + auction.AddLog($"Skip: giΓ  schedulata per questo ciclo (Ξ”={Math.Abs(auction.LastScheduledTimerMs - estimatedTimerMs):F0}ms)", + Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Ticker); return; } @@ -579,17 +655,22 @@ namespace AutoBidder.Services if (auction.LastClickAt.HasValue && (DateTime.UtcNow - auction.LastClickAt.Value).TotalMilliseconds < 1000) { + var cooldownRemaining = 1000 - (DateTime.UtcNow - auction.LastClickAt.Value).TotalMilliseconds; + auction.AddLog($"Cooldown attivo: {cooldownRemaining:F0}ms rimanenti", + Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Ticker); return; } // === OTTIMIZZAZIONE: Controlla strategie SOLO se vicino al target === - // Evita calcoli inutili quando siamo lontani if (estimatedTimerMs > settings.StrategyCheckThresholdMs) { return; } // === CONTROLLI FONDAMENTALI === + auction.AddLog($"β†’ Inizio controlli puntata (timer={estimatedTimerMs:F0}ms ≀ soglia={settings.StrategyCheckThresholdMs}ms)", + Models.AuctionLogLevel.Info, Models.AuctionLogCategory.BidAttempt); + if (!ShouldBid(auction, state)) { return; @@ -601,10 +682,15 @@ namespace AutoBidder.Services if (!decision.ShouldBid) { - auction.AddLog($"[STRATEGY] {decision.Reason}"); + auction.AddLog($"β›” Strategia blocca: {decision.Reason}", + Models.AuctionLogLevel.Strategy, Models.AuctionLogCategory.Strategy); + LogBlockThrottled(auction, "STRATEGY", $"[STRATEGY] {auction.Name}: {decision.Reason}"); return; } + auction.AddLog($"βœ“ Tutti i controlli superati β†’ PUNTATA!", + Models.AuctionLogLevel.Bid, Models.AuctionLogCategory.BidAttempt); + // === ESEGUI PUNTATA === auction.BidScheduled = true; auction.LastScheduledTimerMs = estimatedTimerMs; @@ -744,12 +830,7 @@ namespace AutoBidder.Services { var settings = Utilities.SettingsManager.Load(); - if (settings.LogTiming) - { - auction.AddLog($"[DEBUG] === INIZIO CONTROLLI PUNTATA ==="); - } - - // ?? CONTROLLO 0: Verifica convenienza (se abilitato e dati disponibili) + // CONTROLLO 0: Verifica convenienza (se abilitato e dati disponibili) if (settings.ValueCheckEnabled && auction.BuyNowPrice.HasValue && auction.BuyNowPrice.Value > 0 && @@ -757,27 +838,26 @@ namespace AutoBidder.Services auction.CalculatedValue.Savings.HasValue && !auction.CalculatedValue.IsWorthIt) { - // Usa la percentuale configurabile dall'utente if (auction.CalculatedValue.SavingsPercentage.HasValue && auction.CalculatedValue.SavingsPercentage.Value < settings.MinSavingsPercentage) { - // πŸ”₯ Logga SEMPRE - Γ¨ un blocco frequente e importante - auction.AddLog($"[VALUE] Puntata bloccata: risparmio {auction.CalculatedValue.SavingsPercentage.Value:F1}% < {settings.MinSavingsPercentage:F1}% richiesto"); + auction.AddLog($"β›” Risparmio {auction.CalculatedValue.SavingsPercentage.Value:F1}% < {settings.MinSavingsPercentage:F1}% richiesto", + Models.AuctionLogLevel.Warning, Models.AuctionLogCategory.Value); + LogBlockThrottled(auction, "VALUE", $"[VALUE] {auction.Name}: risparmio {auction.CalculatedValue.SavingsPercentage.Value:F1}% insufficiente"); return false; } } - - if (settings.LogTiming && settings.ValueCheckEnabled) + else { - auction.AddLog($"[DEBUG] βœ“ Controllo convenienza OK"); + auction.AddLog($"βœ“ Convenienza OK (check={settings.ValueCheckEnabled}, buyNow={auction.BuyNowPrice?.ToString("F2") ?? "N/D"})", + Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Value); } - // ?? CONTROLLO ANTI-COLLISIONE (OPZIONALE): Rileva aste troppo "affollate" - // DISABILITATO DI DEFAULT - puΓ² far perdere aste competitive! + // CONTROLLO ANTI-COLLISIONE (OPZIONALE) if (settings.HardcodedAntiCollisionEnabled) { - var recentBidsThreshold = 10; // secondi - var maxActiveBidders = 3; // se 3+ bidder attivi, potrebbe essere troppo affollata + var recentBidsThreshold = 10; + var maxActiveBidders = 3; try { @@ -791,125 +871,87 @@ namespace AutoBidder.Services .Distinct(StringComparer.OrdinalIgnoreCase) .Count(); - if (settings.LogTiming) - { - auction.AddLog($"[DEBUG] Bidder attivi ultimi {recentBidsThreshold}s: {activeBidders}/{maxActiveBidders}"); - } + auction.AddLog($"Competizione: {activeBidders} bidder attivi (soglia={maxActiveBidders})", + Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Competition); if (activeBidders >= maxActiveBidders) { - // Controlla se l'ultimo bidder sono io - se sΓ¬, posso continuare var session = _apiClient.GetSession(); var lastBid = recentBids.OrderByDescending(b => b.Timestamp).FirstOrDefault(); if (lastBid != null && !lastBid.Username.Equals(session?.Username, StringComparison.OrdinalIgnoreCase)) { - auction.AddLog($"[COMPETITION] Asta affollata: {activeBidders} bidder attivi negli ultimi {recentBidsThreshold}s - SKIP"); + auction.AddLog($"β›” Asta affollata: {activeBidders} bidder attivi", + Models.AuctionLogLevel.Strategy, Models.AuctionLogCategory.Competition); return false; } } } - catch { /* Ignora errori nel controllo competizione */ } + catch { } } - if (settings.LogTiming) - { - auction.AddLog($"[DEBUG] ? Controllo competizione OK"); - } - - // ?? CONTROLLO 1: Limite minimo puntate residue + // CONTROLLO 1: Limite minimo puntate residue if (settings.MinimumRemainingBids > 0) { var session = _apiClient.GetSession(); if (session != null && session.RemainingBids <= settings.MinimumRemainingBids) { - // πŸ”₯ Logga SEMPRE - Γ¨ un blocco importante - auction.AddLog($"[LIMIT] Puntata bloccata: puntate residue ({session.RemainingBids}) <= limite ({settings.MinimumRemainingBids})"); + auction.AddLog($"β›” Puntate residue ({session.RemainingBids}) ≀ limite ({settings.MinimumRemainingBids})", + Models.AuctionLogLevel.Warning, Models.AuctionLogCategory.Limit); + LogBlockThrottled(auction, "LIMIT", $"[LIMIT] {auction.Name}: puntate residue ({session.RemainingBids}) <= limite ({settings.MinimumRemainingBids})"); return false; } - - if (settings.LogTiming && session != null) + else if (session != null) { - auction.AddLog($"[DEBUG] βœ“ Puntate residue OK ({session.RemainingBids} > {settings.MinimumRemainingBids})"); + auction.AddLog($"βœ“ Puntate residue OK ({session.RemainingBids} > {settings.MinimumRemainingBids})", + Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Limit); } } - // ? CONTROLLO 2: Non puntare se sono giΓ  il vincitore corrente + // CONTROLLO 2: Non puntare se sono giΓ  il vincitore corrente if (state.IsMyBid) { - if (settings.LogTiming) - { - auction.AddLog($"[DEBUG] Sono giΓ  vincitore"); - } + auction.AddLog($"βœ“ Sono giΓ  vincitore corrente - skip", + Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.BidAttempt); return false; } - // ?? CONTROLLO 3: MinPrice/MaxPrice + // CONTROLLO 3: MinPrice/MaxPrice if (auction.MinPrice > 0 && state.Price < auction.MinPrice) { - // πŸ”₯ Logga SEMPRE questo blocco - Γ¨ critico per capire perchΓ© non punta - auction.AddLog($"[PRICE] Prezzo troppo basso: €{state.Price:F2} < Min €{auction.MinPrice:F2}"); + auction.AddLog($"β›” Prezzo €{state.Price:F2} < Min €{auction.MinPrice:F2}", + Models.AuctionLogLevel.Warning, Models.AuctionLogCategory.Price); + LogBlockThrottled(auction, "PRICE_LOW", $"[PRICE] {auction.Name}: €{state.Price:F2} < Min €{auction.MinPrice:F2} - NON PUNTA"); return false; } if (auction.MaxPrice > 0 && state.Price > auction.MaxPrice) { - // πŸ”₯ Logga SEMPRE questo blocco - Γ¨ critico - auction.AddLog($"[PRICE] Prezzo troppo alto: €{state.Price:F2} > Max €{auction.MaxPrice:F2}"); + auction.AddLog($"β›” Prezzo €{state.Price:F2} > Max €{auction.MaxPrice:F2}", + Models.AuctionLogLevel.Warning, Models.AuctionLogCategory.Price); + LogBlockThrottled(auction, "PRICE_HIGH", $"[PRICE] {auction.Name}: €{state.Price:F2} > Max €{auction.MaxPrice:F2} - NON PUNTA"); return false; } - if (settings.LogTiming) - { - if (auction.MinPrice > 0 || auction.MaxPrice > 0) - { - auction.AddLog($"[DEBUG] ? Range prezzo OK (€{state.Price:F2} in [{auction.MinPrice:F2}, {auction.MaxPrice:F2}])"); - } - } - - // ?? CONTROLLO 4: MinResets/MaxResets - if (auction.MinResets > 0 && auction.ResetCount < auction.MinResets) - { - // πŸ”₯ Logga SEMPRE - Γ¨ un motivo comune di aste perse - auction.AddLog($"[RESET] Reset troppo bassi: {auction.ResetCount} < Min {auction.MinResets}"); - return false; - } - - if (auction.MaxResets > 0 && auction.ResetCount >= auction.MaxResets) - { - // πŸ”₯ Logga SEMPRE - Γ¨ un motivo comune di aste perse - auction.AddLog($"[RESET] Reset massimi raggiunti: {auction.ResetCount} >= Max {auction.MaxResets}"); - return false; - } - - if (settings.LogTiming) - { - if (auction.MinResets > 0 || auction.MaxResets > 0) - { - auction.AddLog($"[DEBUG] ? Range reset OK ({auction.ResetCount} in [{auction.MinResets}, {auction.MaxResets}])"); - } - } + auction.AddLog($"βœ“ Prezzo OK (€{state.Price:F2} in [{auction.MinPrice:F2}, {(auction.MaxPrice > 0 ? auction.MaxPrice.ToString("F2") : "∞")}])", + Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Price); - // ?? CONTROLLO 6: Cooldown (evita puntate multiple ravvicinate) + // CONTROLLO 6: Cooldown if (auction.LastClickAt.HasValue) { var timeSinceLastClick = DateTime.UtcNow - auction.LastClickAt.Value; if (timeSinceLastClick.TotalMilliseconds < 800) { - if (settings.LogTiming) - { - auction.AddLog($"[DEBUG] Cooldown attivo ({timeSinceLastClick.TotalMilliseconds:F0}ms < 800ms)"); - } + auction.AddLog($"Cooldown: {timeSinceLastClick.TotalMilliseconds:F0}ms < 800ms", + Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Ticker); return false; } } - if (settings.LogTiming) - { - auction.AddLog($"[DEBUG] === TUTTI I CONTROLLI SUPERATI ==="); - } + auction.AddLog($"βœ“ Tutti i controlli ShouldBid superati", + Models.AuctionLogLevel.Info, Models.AuctionLogCategory.BidAttempt); return true; } diff --git a/Mimante/Services/DatabaseService.cs b/Mimante/Services/DatabaseService.cs index 1e9d0ff..43ad7b3 100644 --- a/Mimante/Services/DatabaseService.cs +++ b/Mimante/Services/DatabaseService.cs @@ -747,6 +747,24 @@ namespace AutoBidder.Services await using var cmd = conn.CreateCommand(); cmd.CommandText = sql; await cmd.ExecuteNonQueryAsync(); + }), + + new Migration(16, "Add UseCustomLimits flag to ProductStatistics", async (conn) => { + var sql = @" + ALTER TABLE ProductStatistics ADD COLUMN UseCustomLimits INTEGER NOT NULL DEFAULT 0; + "; + await using var cmd = conn.CreateCommand(); + cmd.CommandText = sql; + await cmd.ExecuteNonQueryAsync(); + }), + + new Migration(17, "Add MedianFinalPrice to ProductStatistics", async (conn) => { + var sql = @" + ALTER TABLE ProductStatistics ADD COLUMN MedianFinalPrice REAL; + "; + await using var cmd = conn.CreateCommand(); + cmd.CommandText = sql; + await cmd.ExecuteNonQueryAsync(); }) }; @@ -1418,14 +1436,14 @@ namespace AutoBidder.Services var sql = @" INSERT INTO ProductStatistics (ProductKey, ProductName, TotalAuctions, WonAuctions, LostAuctions, - AvgFinalPrice, MinFinalPrice, MaxFinalPrice, + AvgFinalPrice, MinFinalPrice, MaxFinalPrice, MedianFinalPrice, AvgBidsToWin, MinBidsToWin, MaxBidsToWin, AvgResets, MinResets, MaxResets, RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids, UserDefaultMinPrice, UserDefaultMaxPrice, UserDefaultMinResets, UserDefaultMaxResets, UserDefaultMaxBids, UserDefaultBidBeforeDeadlineMs, HourlyStatsJson, LastUpdated) VALUES (@productKey, @productName, @totalAuctions, @wonAuctions, @lostAuctions, - @avgFinalPrice, @minFinalPrice, @maxFinalPrice, + @avgFinalPrice, @minFinalPrice, @maxFinalPrice, @medianFinalPrice, @avgBidsToWin, @minBidsToWin, @maxBidsToWin, @avgResets, @minResets, @maxResets, @recMinPrice, @recMaxPrice, @recMinResets, @recMaxResets, @recMaxBids, @@ -1439,6 +1457,7 @@ namespace AutoBidder.Services AvgFinalPrice = @avgFinalPrice, MinFinalPrice = @minFinalPrice, MaxFinalPrice = @maxFinalPrice, + MedianFinalPrice = @medianFinalPrice, AvgBidsToWin = @avgBidsToWin, MinBidsToWin = @minBidsToWin, MaxBidsToWin = @maxBidsToWin, @@ -1469,6 +1488,7 @@ namespace AutoBidder.Services new SqliteParameter("@avgFinalPrice", stats.AvgFinalPrice), new SqliteParameter("@minFinalPrice", (object?)stats.MinFinalPrice ?? DBNull.Value), new SqliteParameter("@maxFinalPrice", (object?)stats.MaxFinalPrice ?? DBNull.Value), + new SqliteParameter("@medianFinalPrice", (object?)stats.MedianFinalPrice ?? DBNull.Value), new SqliteParameter("@avgBidsToWin", stats.AvgBidsToWin), new SqliteParameter("@minBidsToWin", (object?)stats.MinBidsToWin ?? DBNull.Value), new SqliteParameter("@maxBidsToWin", (object?)stats.MaxBidsToWin ?? DBNull.Value), @@ -1498,12 +1518,12 @@ namespace AutoBidder.Services { var sql = @" SELECT ProductKey, ProductName, TotalAuctions, WonAuctions, LostAuctions, - AvgFinalPrice, MinFinalPrice, MaxFinalPrice, + AvgFinalPrice, MinFinalPrice, MaxFinalPrice, MedianFinalPrice, AvgBidsToWin, MinBidsToWin, MaxBidsToWin, AvgResets, MinResets, MaxResets, RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids, UserDefaultMinPrice, UserDefaultMaxPrice, UserDefaultMinResets, UserDefaultMaxResets, UserDefaultMaxBids, UserDefaultBidBeforeDeadlineMs, - HourlyStatsJson, LastUpdated + HourlyStatsJson, LastUpdated, UseCustomLimits FROM ProductStatistics WHERE ProductKey = @productKey; "; @@ -1526,25 +1546,27 @@ namespace AutoBidder.Services AvgFinalPrice = reader.GetDouble(5), MinFinalPrice = reader.IsDBNull(6) ? null : reader.GetDouble(6), MaxFinalPrice = reader.IsDBNull(7) ? null : reader.GetDouble(7), - AvgBidsToWin = reader.GetDouble(8), - MinBidsToWin = reader.IsDBNull(9) ? null : reader.GetInt32(9), - MaxBidsToWin = reader.IsDBNull(10) ? null : reader.GetInt32(10), - AvgResets = reader.GetDouble(11), - MinResets = reader.IsDBNull(12) ? null : reader.GetInt32(12), - MaxResets = reader.IsDBNull(13) ? null : reader.GetInt32(13), - RecommendedMinPrice = reader.IsDBNull(14) ? null : reader.GetDouble(14), - RecommendedMaxPrice = reader.IsDBNull(15) ? null : reader.GetDouble(15), - RecommendedMinResets = reader.IsDBNull(16) ? null : reader.GetInt32(16), - RecommendedMaxResets = reader.IsDBNull(17) ? null : reader.GetInt32(17), - RecommendedMaxBids = reader.IsDBNull(18) ? null : reader.GetInt32(18), - UserDefaultMinPrice = reader.IsDBNull(19) ? null : reader.GetDouble(19), - UserDefaultMaxPrice = reader.IsDBNull(20) ? null : reader.GetDouble(20), - UserDefaultMinResets = reader.IsDBNull(21) ? null : reader.GetInt32(21), - UserDefaultMaxResets = reader.IsDBNull(22) ? null : reader.GetInt32(22), - UserDefaultMaxBids = reader.IsDBNull(23) ? null : reader.GetInt32(23), - UserDefaultBidBeforeDeadlineMs = reader.IsDBNull(24) ? null : reader.GetInt32(24), - HourlyStatsJson = reader.IsDBNull(25) ? null : reader.GetString(25), - LastUpdated = reader.GetString(26) + MedianFinalPrice = reader.IsDBNull(8) ? null : reader.GetDouble(8), + AvgBidsToWin = reader.GetDouble(9), + MinBidsToWin = reader.IsDBNull(10) ? null : reader.GetInt32(10), + MaxBidsToWin = reader.IsDBNull(11) ? null : reader.GetInt32(11), + AvgResets = reader.GetDouble(12), + MinResets = reader.IsDBNull(13) ? null : reader.GetInt32(13), + MaxResets = reader.IsDBNull(14) ? null : reader.GetInt32(14), + RecommendedMinPrice = reader.IsDBNull(15) ? null : reader.GetDouble(15), + RecommendedMaxPrice = reader.IsDBNull(16) ? null : reader.GetDouble(16), + RecommendedMinResets = reader.IsDBNull(17) ? null : reader.GetInt32(17), + RecommendedMaxResets = reader.IsDBNull(18) ? null : reader.GetInt32(18), + RecommendedMaxBids = reader.IsDBNull(19) ? null : reader.GetInt32(19), + UserDefaultMinPrice = reader.IsDBNull(20) ? null : reader.GetDouble(20), + UserDefaultMaxPrice = reader.IsDBNull(21) ? null : reader.GetDouble(21), + UserDefaultMinResets = reader.IsDBNull(22) ? null : reader.GetInt32(22), + UserDefaultMaxResets = reader.IsDBNull(23) ? null : reader.GetInt32(23), + UserDefaultMaxBids = reader.IsDBNull(24) ? null : reader.GetInt32(24), + UserDefaultBidBeforeDeadlineMs = reader.IsDBNull(25) ? null : reader.GetInt32(25), + HourlyStatsJson = reader.IsDBNull(26) ? null : reader.GetString(26), + LastUpdated = reader.GetString(27), + UseCustomLimits = !reader.IsDBNull(28) && reader.GetInt32(28) != 0 }; } @@ -1590,12 +1612,12 @@ namespace AutoBidder.Services { var sql = @" SELECT ProductKey, ProductName, TotalAuctions, WonAuctions, LostAuctions, - AvgFinalPrice, MinFinalPrice, MaxFinalPrice, + AvgFinalPrice, MinFinalPrice, MaxFinalPrice, MedianFinalPrice, AvgBidsToWin, MinBidsToWin, MaxBidsToWin, AvgResets, MinResets, MaxResets, RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids, UserDefaultMinPrice, UserDefaultMaxPrice, UserDefaultMinResets, UserDefaultMaxResets, UserDefaultMaxBids, UserDefaultBidBeforeDeadlineMs, - HourlyStatsJson, LastUpdated + HourlyStatsJson, LastUpdated, UseCustomLimits FROM ProductStatistics ORDER BY TotalAuctions DESC; "; @@ -1619,25 +1641,27 @@ namespace AutoBidder.Services AvgFinalPrice = reader.GetDouble(5), MinFinalPrice = reader.IsDBNull(6) ? null : reader.GetDouble(6), MaxFinalPrice = reader.IsDBNull(7) ? null : reader.GetDouble(7), - AvgBidsToWin = reader.GetDouble(8), - MinBidsToWin = reader.IsDBNull(9) ? null : reader.GetInt32(9), - MaxBidsToWin = reader.IsDBNull(10) ? null : reader.GetInt32(10), - AvgResets = reader.GetDouble(11), - MinResets = reader.IsDBNull(12) ? null : reader.GetInt32(12), - MaxResets = reader.IsDBNull(13) ? null : reader.GetInt32(13), - RecommendedMinPrice = reader.IsDBNull(14) ? null : reader.GetDouble(14), - RecommendedMaxPrice = reader.IsDBNull(15) ? null : reader.GetDouble(15), - RecommendedMinResets = reader.IsDBNull(16) ? null : reader.GetInt32(16), - RecommendedMaxResets = reader.IsDBNull(17) ? null : reader.GetInt32(17), - RecommendedMaxBids = reader.IsDBNull(18) ? null : reader.GetInt32(18), - UserDefaultMinPrice = reader.IsDBNull(19) ? null : reader.GetDouble(19), - UserDefaultMaxPrice = reader.IsDBNull(20) ? null : reader.GetDouble(20), - UserDefaultMinResets = reader.IsDBNull(21) ? null : reader.GetInt32(21), - UserDefaultMaxResets = reader.IsDBNull(22) ? null : reader.GetInt32(22), - UserDefaultMaxBids = reader.IsDBNull(23) ? null : reader.GetInt32(23), - UserDefaultBidBeforeDeadlineMs = reader.IsDBNull(24) ? null : reader.GetInt32(24), - HourlyStatsJson = reader.IsDBNull(25) ? null : reader.GetString(25), - LastUpdated = reader.GetString(26) + MedianFinalPrice = reader.IsDBNull(8) ? null : reader.GetDouble(8), + AvgBidsToWin = reader.GetDouble(9), + MinBidsToWin = reader.IsDBNull(10) ? null : reader.GetInt32(10), + MaxBidsToWin = reader.IsDBNull(11) ? null : reader.GetInt32(11), + AvgResets = reader.GetDouble(12), + MinResets = reader.IsDBNull(13) ? null : reader.GetInt32(13), + MaxResets = reader.IsDBNull(14) ? null : reader.GetInt32(14), + RecommendedMinPrice = reader.IsDBNull(15) ? null : reader.GetDouble(15), + RecommendedMaxPrice = reader.IsDBNull(16) ? null : reader.GetDouble(16), + RecommendedMinResets = reader.IsDBNull(17) ? null : reader.GetInt32(17), + RecommendedMaxResets = reader.IsDBNull(18) ? null : reader.GetInt32(18), + RecommendedMaxBids = reader.IsDBNull(19) ? null : reader.GetInt32(19), + UserDefaultMinPrice = reader.IsDBNull(20) ? null : reader.GetDouble(20), + UserDefaultMaxPrice = reader.IsDBNull(21) ? null : reader.GetDouble(21), + UserDefaultMinResets = reader.IsDBNull(22) ? null : reader.GetInt32(22), + UserDefaultMaxResets = reader.IsDBNull(23) ? null : reader.GetInt32(23), + UserDefaultMaxBids = reader.IsDBNull(24) ? null : reader.GetInt32(24), + UserDefaultBidBeforeDeadlineMs = reader.IsDBNull(25) ? null : reader.GetInt32(25), + HourlyStatsJson = reader.IsDBNull(26) ? null : reader.GetString(26), + LastUpdated = reader.GetString(27), + UseCustomLimits = !reader.IsDBNull(28) && reader.GetInt32(28) != 0 }); } @@ -1665,7 +1689,8 @@ namespace AutoBidder.Services public async Task UpdateProductUserDefaultsAsync(string productKey, double? minPrice, double? maxPrice, int? minResets, int? maxResets, - int? maxBids, int? bidBeforeDeadlineMs) + int? maxBids, int? bidBeforeDeadlineMs, + bool? useCustomLimits = null) { var sql = @" UPDATE ProductStatistics @@ -1675,6 +1700,7 @@ namespace AutoBidder.Services UserDefaultMaxResets = @maxResets, UserDefaultMaxBids = @maxBids, UserDefaultBidBeforeDeadlineMs = @bidDeadline, + UseCustomLimits = CASE WHEN @useCustomLimits IS NOT NULL THEN @useCustomLimits ELSE UseCustomLimits END, LastUpdated = @lastUpdated WHERE ProductKey = @productKey; "; @@ -1687,6 +1713,7 @@ namespace AutoBidder.Services new SqliteParameter("@maxResets", (object?)maxResets ?? DBNull.Value), new SqliteParameter("@maxBids", (object?)maxBids ?? DBNull.Value), new SqliteParameter("@bidDeadline", (object?)bidBeforeDeadlineMs ?? DBNull.Value), + new SqliteParameter("@useCustomLimits", useCustomLimits.HasValue ? (object)(useCustomLimits.Value ? 1 : 0) : DBNull.Value), new SqliteParameter("@lastUpdated", DateTime.UtcNow.ToString("O")) ); } diff --git a/Mimante/Services/ProductStatisticsService.cs b/Mimante/Services/ProductStatisticsService.cs index 55f62cb..821f326 100644 --- a/Mimante/Services/ProductStatisticsService.cs +++ b/Mimante/Services/ProductStatisticsService.cs @@ -98,12 +98,14 @@ namespace AutoBidder.Services stats.AvgFinalPrice = wonResults.Average(r => r.FinalPrice); stats.MinFinalPrice = wonResults.Min(r => r.FinalPrice); stats.MaxFinalPrice = wonResults.Max(r => r.FinalPrice); + stats.MedianFinalPrice = CalculateMedian(wonResults.Select(r => r.FinalPrice).ToList()); } else if (results.Any()) { stats.AvgFinalPrice = results.Average(r => r.FinalPrice); stats.MinFinalPrice = results.Min(r => r.FinalPrice); stats.MaxFinalPrice = results.Max(r => r.FinalPrice); + stats.MedianFinalPrice = CalculateMedian(results.Select(r => r.FinalPrice).ToList()); } // Statistiche puntate (usa WinnerBidsUsed se disponibile, altrimenti BidsUsed) @@ -336,5 +338,15 @@ namespace AutoBidder.Services double sumSquares = data.Sum(d => Math.Pow(d - avg, 2)); return Math.Sqrt(sumSquares / (data.Count - 1)); } + + private static double CalculateMedian(List data) + { + if (data.Count == 0) return 0; + var sorted = data.OrderBy(x => x).ToList(); + int mid = sorted.Count / 2; + return sorted.Count % 2 == 0 + ? (sorted[mid - 1] + sorted[mid]) / 2.0 + : sorted[mid]; + } } } diff --git a/Mimante/Services/StatsService.cs b/Mimante/Services/StatsService.cs index 22d2081..d64537a 100644 --- a/Mimante/Services/StatsService.cs +++ b/Mimante/Services/StatsService.cs @@ -318,6 +318,25 @@ namespace AutoBidder.Services return await _productStatsService.GetRecommendedLimitsAsync(productKey); } + /// + /// Ottiene le statistiche di un singolo prodotto + /// + public ProductStatisticsRecord? GetProductStats(string productKey) + { + if (_productStatsService == null || !IsAvailable) return null; + + try + { + // Carica statistiche dal database in modo sincrono + var allStats = _productStatsService.GetAllProductStatisticsAsync().GetAwaiter().GetResult(); + return allStats.FirstOrDefault(p => p.ProductKey == productKey); + } + catch + { + return null; + } + } + /// /// Ottiene tutte le statistiche prodotto /// diff --git a/Mimante/Utilities/SettingsManager.cs b/Mimante/Utilities/SettingsManager.cs index ded5a64..6fef9fd 100644 --- a/Mimante/Utilities/SettingsManager.cs +++ b/Mimante/Utilities/SettingsManager.cs @@ -181,6 +181,14 @@ namespace AutoBidder.Utilities /// public bool AutoApplyProductDefaults { get; set; } = true; + /// + /// Scelta prioritΓ  limiti quando si aggiunge un'asta per un prodotto giΓ  salvato: + /// - "ProductStats": Usa i limiti personalizzati salvati nelle statistiche prodotto (UserDefaultMinPrice, ecc.) + /// - "GlobalDefaults": Usa sempre i limiti globali (DefaultMinPrice, DefaultMaxPrice, ecc.) + /// Default: "ProductStats" (consigliato per usare limiti specifici per prodotto) + /// + public string NewAuctionLimitsPriority { get; set; } = "ProductStats"; + /// /// Log stato asta (terminata, reset, ecc.) [STATUS] /// Default: true diff --git a/Mimante/wwwroot/css/modern-pages.css b/Mimante/wwwroot/css/modern-pages.css index a165eb9..bce0269 100644 --- a/Mimante/wwwroot/css/modern-pages.css +++ b/Mimante/wwwroot/css/modern-pages.css @@ -1303,3 +1303,181 @@ .btn-xs i { font-size: 0.75rem; } + +/* ═══════════════════════════════════════════════════════════════════ + LOG ASTA STRUTTURATO - GRIGLIA A COLONNE COMPATTA + ═══════════════════════════════════════════════════════════════════ */ + +.auction-log-grid { + display: flex; + flex-direction: column; + height: 100%; + font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace; + font-size: 0.72rem; + line-height: 1.2; +} + +.alog-header { + display: flex; + gap: 0; + padding: 4px 6px; + background: rgba(255, 255, 255, 0.06); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + font-weight: 600; + font-size: 0.68rem; + text-transform: uppercase; + letter-spacing: 0.5px; + color: rgba(255, 255, 255, 0.5); + position: sticky; + top: 0; + z-index: 1; +} + +.alog-body { + flex: 1; + overflow-y: auto; + overflow-x: hidden; +} + +.alog-row { + display: flex; + gap: 0; + padding: 1px 6px; + border-bottom: 1px solid rgba(255, 255, 255, 0.025); + align-items: baseline; + transition: background 0.1s; +} + +.alog-row:hover { + background: rgba(255, 255, 255, 0.04); +} + +/* Colonne */ +.alog-col-time { + flex: 0 0 85px; + color: rgba(255, 255, 255, 0.35); + font-variant-numeric: tabular-nums; + padding-right: 6px; +} + +.alog-col-level { + flex: 0 0 62px; + font-weight: 600; + font-size: 0.65rem; + padding-right: 4px; + white-space: nowrap; +} + +.alog-col-level i { + font-size: 0.6rem; + margin-right: 2px; +} + +.alog-col-cat { + flex: 0 0 65px; + color: rgba(255, 255, 255, 0.4); + font-size: 0.65rem; + padding-right: 6px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.alog-col-msg { + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: rgba(255, 255, 255, 0.8); +} + +/* Badge ripetizione */ +.alog-repeat { + display: inline-block; + background: rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.5); + font-size: 0.6rem; + padding: 0 4px; + border-radius: 3px; + margin-left: 4px; + font-weight: 600; +} + +/* === COLORI PER LIVELLO === */ + +/* Error */ +.alog-error { + background: rgba(220, 53, 69, 0.08); +} +.alog-error .alog-col-level { + color: #ff4d5e; +} +.alog-error .alog-col-msg { + color: #ff7a85; +} + +/* Warning */ +.alog-warning { + background: rgba(255, 193, 7, 0.05); +} +.alog-warning .alog-col-level { + color: #ffc107; +} +.alog-warning .alog-col-msg { + color: rgba(255, 220, 130, 0.9); +} + +/* Success */ +.alog-success { + background: rgba(40, 167, 69, 0.08); +} +.alog-success .alog-col-level { + color: #4cff8e; +} +.alog-success .alog-col-msg { + color: #7dffb0; +} + +/* Bid */ +.alog-bid { + background: rgba(0, 123, 255, 0.08); +} +.alog-bid .alog-col-level { + color: #5eadff; +} +.alog-bid .alog-col-msg { + color: #8ec8ff; +} + +/* Strategy */ +.alog-strategy { + background: rgba(160, 80, 220, 0.08); +} +.alog-strategy .alog-col-level { + color: #c77dff; +} +.alog-strategy .alog-col-msg { + color: #dda0ff; +} + +/* Timing */ +.alog-timing .alog-col-level { + color: #17a2b8; +} +.alog-timing .alog-col-msg { + color: rgba(100, 200, 220, 0.85); +} + +/* Debug */ +.alog-debug { + opacity: 0.55; +} +.alog-debug .alog-col-level { + color: rgba(255, 255, 255, 0.3); +} + +/* Info (default) */ +.alog-info .alog-col-level { + color: rgba(255, 255, 255, 0.5); +}