Log aste strutturato, limiti prodotto e UI statistiche

- 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
This commit is contained in:
2026-02-16 23:10:04 +01:00
parent 690f7e636a
commit f3262a0497
13 changed files with 1562 additions and 1162 deletions

View File

@@ -118,9 +118,9 @@ namespace AutoBidder.Models
[JsonPropertyName("RecentBids")]
public List<BidHistoryEntry> RecentBids { get; set; } = new List<BidHistoryEntry>();
// Log per-asta (non serializzato)
// Log per-asta strutturato (non serializzato)
[System.Text.Json.Serialization.JsonIgnore]
public List<string> AuctionLog { get; set; } = new();
public List<AuctionLogEntry> AuctionLog { get; set; } = new();
// Flag runtime: indica che è in corso un'operazione di final attack per questa asta
[System.Text.Json.Serialization.JsonIgnore]
@@ -168,64 +168,141 @@ namespace AutoBidder.Models
/// <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.
/// Aggiunge una voce strutturata al log dell'asta con deduplicazione e limite righe.
/// Parsifica automaticamente il tag [TAG] per determinare livello e categoria.
/// </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)
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})";
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);
}
}
/// <summary>
/// Aggiunge una voce tipizzata al log dell'asta (senza parsing del tag).
/// </summary>
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);
}
}
/// <summary>
/// Parsifica i tag [TAG] per determinare livello e categoria automaticamente.
/// </summary>
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
// ???????????????????????????????????????????????????????????????

View File

@@ -0,0 +1,126 @@
using System;
namespace AutoBidder.Models
{
/// <summary>
/// Entry strutturata per il log di una singola asta.
/// Contiene timestamp preciso, livello di gravità, categoria e messaggio.
/// </summary>
public class AuctionLogEntry
{
public DateTime Timestamp { get; set; }
public AuctionLogLevel Level { get; set; }
public AuctionLogCategory Category { get; set; }
public string Message { get; set; } = "";
/// <summary>
/// Contatore deduplicazione (se > 1, il messaggio è stato ripetuto)
/// </summary>
public int RepeatCount { get; set; } = 1;
/// <summary>
/// Formato compatto per display: solo ora con millisecondi
/// </summary>
public string TimeDisplay => Timestamp.ToString("HH:mm:ss.fff");
/// <summary>
/// Icona Bootstrap per il livello
/// </summary>
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"
};
/// <summary>
/// Classe CSS per il livello
/// </summary>
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"
};
/// <summary>
/// Label breve del livello
/// </summary>
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"
};
/// <summary>
/// Label della categoria
/// </summary>
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"
};
}
/// <summary>
/// Livello di gravità del log per-asta
/// </summary>
public enum AuctionLogLevel
{
Debug = 0,
Info = 1,
Timing = 2,
Strategy = 3,
Bid = 4,
Success = 5,
Warning = 6,
Error = 7
}
/// <summary>
/// Categoria del log per filtraggio e raggruppamento
/// </summary>
public enum AuctionLogCategory
{
General,
Ticker,
Price,
Reset,
BidAttempt,
BidResult,
Strategy,
Value,
Competition,
Limit,
Diagnostic,
Status,
Polling
}
}

View File

@@ -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; }
/// <summary>
/// Se true, usa i limiti personalizzati del prodotto. Se false, usa i globali.
/// </summary>
public bool UseCustomLimits { get; set; }
// JSON con statistiche per fascia oraria
public string? HourlyStatsJson { get; set; }

View File

@@ -457,24 +457,45 @@
<!-- TAB LOG -->
<div class="tab-pane fade" id="content-log" role="tabpanel">
<div class="tab-panel-content">
<div class="log-box-compact">
<div class="tab-panel-content p-0">
@if (selectedAuction.AuctionLog.Any())
{
@foreach (var logEntry in GetAuctionLog(selectedAuction))
<div class="auction-log-grid">
<div class="alog-header">
<span class="alog-col-time">Ora</span>
<span class="alog-col-level">Livello</span>
<span class="alog-col-cat">Categoria</span>
<span class="alog-col-msg">Messaggio</span>
</div>
<div class="alog-body">
@foreach (var entry in GetAuctionLog(selectedAuction))
{
<div class="log-entry">@logEntry</div>
<div class="alog-row @entry.LevelClass">
<span class="alog-col-time">@entry.TimeDisplay</span>
<span class="alog-col-level">
<i class="bi @entry.LevelIcon"></i> @entry.LevelLabel
</span>
<span class="alog-col-cat">@entry.CategoryLabel</span>
<span class="alog-col-msg">
@entry.Message
@if (entry.RepeatCount > 1)
{
<span class="alog-repeat">x@entry.RepeatCount</span>
}
</span>
</div>
}
</div>
</div>
}
else
{
<div class="text-muted"><i class="bi bi-inbox"></i> Nessun log disponibile.</div>
<div class="text-muted p-3"><i class="bi bi-inbox"></i> Nessun log disponibile.</div>
}
</div>
</div>
</div>
</div>
</div>
}
else
{

View File

@@ -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,6 +1299,12 @@ namespace AutoBidder.Pages
private IEnumerable<AuctionInfo> GetSortedAuctions()
{
try
{
// Protezione null-safety
if (auctions == null || auctions.Count == 0)
return Enumerable.Empty<AuctionInfo>();
var list = auctions.AsEnumerable();
list = auctionSortColumn switch
@@ -1239,6 +1332,12 @@ namespace AutoBidder.Pages
return list;
}
catch
{
// Fallback sicuro in caso di errore
return Enumerable.Empty<AuctionInfo>();
}
}
// ?? NUOVI METODI: Visualizzazione valori prodotto
@@ -1375,9 +1474,9 @@ namespace AutoBidder.Pages
}
}
private IEnumerable<string> GetAuctionLog(AuctionInfo auction)
private IEnumerable<AuctionLogEntry> GetAuctionLog(AuctionInfo auction)
{
return auction.AuctionLog.TakeLast(50);
return auction.AuctionLog.TakeLast(100);
}
/// <summary>
@@ -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();

View File

@@ -206,6 +206,7 @@
<i class="bi bi-arrow-repeat"></i>
</button>
</div>
<div class="form-text">0 = punta a qualsiasi prezzo. Il prezzo deve essere ? a questo valore per puntare.</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-currency-euro"></i> Prezzo massimo (€)</label>
@@ -217,27 +218,16 @@
<i class="bi bi-arrow-repeat"></i>
</button>
</div>
<div class="form-text">0 = nessun limite. Se il prezzo supera questo valore, SMETTE di puntare.</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-arrow-repeat"></i> Reset minimi</label>
<div class="input-group">
<input type="number" class="form-control" @bind="settings.DefaultMinResets" />
<button class="btn btn-outline-primary" @onclick="() => ApplySingleSettingToAll(nameof(settings.DefaultMinResets))"
disabled="@applyingSettings.Contains(nameof(settings.DefaultMinResets))"
title="Applica a tutte le aste">
<i class="bi bi-arrow-repeat"></i>
</button>
</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-arrow-repeat"></i> Reset massimi</label>
<div class="input-group">
<input type="number" class="form-control" @bind="settings.DefaultMaxResets" />
<button class="btn btn-outline-primary" @onclick="() => ApplySingleSettingToAll(nameof(settings.DefaultMaxResets))"
disabled="@applyingSettings.Contains(nameof(settings.DefaultMaxResets))"
title="Applica a tutte le aste">
<i class="bi bi-arrow-repeat"></i>
</button>
<div class="col-12">
<label class="form-label fw-bold"><i class="bi bi-database-gear"></i> Priorità limiti nuove aste</label>
<select class="form-select" @bind="settings.NewAuctionLimitsPriority">
<option value="ProductStats">Usa limiti salvati nelle statistiche prodotto</option>
<option value="GlobalDefaults">Usa sempre limiti globali</option>
</select>
<div class="form-text">
Se "Statistiche prodotto", quando aggiungi un'asta di un prodotto già salvato, verranno usati i limiti personalizzati delle statistiche invece di quelli globali.
</div>
</div>
<div class="col-12 col-md-6">
@@ -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++;
}

File diff suppressed because it is too large Load Diff

View File

@@ -26,6 +26,9 @@ namespace AutoBidder.Services
public event Action<string>? OnLog;
public event Action<string>? OnResetCountChanged;
// Throttling per log di blocco (evita spam nel log globale)
private readonly Dictionary<string, DateTime> _lastBlockLogTime = new();
/// <summary>
/// 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");
@@ -279,6 +279,37 @@ namespace AutoBidder.Services
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<string>();
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;
bool shouldPoll = (now - lastPoll).TotalMilliseconds >= pollingIntervalMs;
@@ -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;
}
/// <summary>
/// Logga un blocco nel log globale con throttling per evitare spam.
/// Ogni chiave (auctionId+reason) può loggare al massimo una volta ogni 10 secondi.
/// </summary>
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);
}
/// <summary>
/// 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;
}

View File

@@ -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"))
);
}

View File

@@ -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<double> 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];
}
}
}

View File

@@ -318,6 +318,25 @@ namespace AutoBidder.Services
return await _productStatsService.GetRecommendedLimitsAsync(productKey);
}
/// <summary>
/// Ottiene le statistiche di un singolo prodotto
/// </summary>
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;
}
}
/// <summary>
/// Ottiene tutte le statistiche prodotto
/// </summary>

View File

@@ -181,6 +181,14 @@ namespace AutoBidder.Utilities
/// </summary>
public bool AutoApplyProductDefaults { get; set; } = true;
/// <summary>
/// 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)
/// </summary>
public string NewAuctionLimitsPriority { get; set; } = "ProductStats";
/// <summary>
/// Log stato asta (terminata, reset, ecc.) [STATUS]
/// Default: true

View File

@@ -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);
}