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")] [JsonPropertyName("RecentBids")]
public List<BidHistoryEntry> RecentBids { get; set; } = new List<BidHistoryEntry>(); 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] [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 // Flag runtime: indica che è in corso un'operazione di final attack per questa asta
[System.Text.Json.Serialization.JsonIgnore] [System.Text.Json.Serialization.JsonIgnore]
@@ -168,64 +168,141 @@ namespace AutoBidder.Models
/// <summary> /// <summary>
/// Aggiunge una voce al log dell'asta con deduplicazione e limite automatico di righe. /// Aggiunge una voce strutturata al log dell'asta con deduplicazione e limite righe.
/// Se il messaggio è identico all'ultimo, incrementa un contatore invece di duplicare. /// Parsifica automaticamente il tag [TAG] per determinare livello e categoria.
/// </summary> /// </summary>
/// <param name="message">Messaggio da aggiungere al log</param> public void AddLog(string message, int maxLines = 200)
/// <param name="maxLines">Numero massimo di righe da mantenere (default: 500)</param>
public void AddLog(string message, int maxLines = 500)
{ {
var timestamp = DateTime.Now.ToString("HH:mm:ss.fff"); // Protezione null-safety (dopo ClearData)
if (AuctionLog == null) AuctionLog = new();
// DEBUG: Print per verificare che i log vengano aggiunti var now = DateTime.Now;
#if DEBUG
System.Diagnostics.Debug.WriteLine($"[AddLog] {AuctionId}: {message}");
#endif
// ?? 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) if (AuctionLog.Count > 0)
{ {
var lastEntry = AuctionLog[^1]; // Ultimo elemento var last = AuctionLog[^1];
if (last.Message == cleanMessage && last.Category == category)
// Estrai il messaggio senza timestamp e contatore
var lastMessageStart = lastEntry.IndexOf(" - ");
if (lastMessageStart > 0)
{ {
var lastMessage = lastEntry.Substring(lastMessageStart + 3); last.RepeatCount++;
last.Timestamp = now;
// Rimuovi eventuale contatore esistente (es: " (x5)") return;
var counterMatch = System.Text.RegularExpressions.Regex.Match(lastMessage, @" \(x(\d+)\)$");
if (counterMatch.Success)
{
lastMessage = lastMessage.Substring(0, lastMessage.Length - counterMatch.Length);
}
// Se il messaggio è identico, aggiorna contatore
if (lastMessage == message)
{
int newCount = counterMatch.Success
? int.Parse(counterMatch.Groups[1].Value) + 1
: 2;
// Aggiorna l'ultimo entry con il nuovo contatore
AuctionLog[^1] = $"{timestamp} - {message} (x{newCount})";
return;
}
} }
} }
// Nuovo messaggio diverso dall'ultimo AuctionLog.Add(new AuctionLogEntry
var entry = $"{timestamp} - {message}"; {
AuctionLog.Add(entry); Timestamp = now,
Level = level,
Category = category,
Message = cleanMessage
});
// Mantieni solo gli ultimi maxLines log
if (AuctionLog.Count > maxLines) if (AuctionLog.Count > maxLines)
{ {
int excessCount = AuctionLog.Count - maxLines; AuctionLog.RemoveRange(0, AuctionLog.Count - maxLines);
AuctionLog.RemoveRange(0, excessCount);
} }
} }
/// <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 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 AvgFinalPrice { get; set; }
public double? MinFinalPrice { get; set; } public double? MinFinalPrice { get; set; }
public double? MaxFinalPrice { get; set; } public double? MaxFinalPrice { get; set; }
public double? MedianFinalPrice { get; set; }
// Statistiche puntate // Statistiche puntate
public double AvgBidsToWin { get; set; } public double AvgBidsToWin { get; set; }
@@ -43,6 +44,11 @@ namespace AutoBidder.Models
public int? UserDefaultMaxBids { get; set; } public int? UserDefaultMaxBids { get; set; }
public int? UserDefaultBidBeforeDeadlineMs { 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 // JSON con statistiche per fascia oraria
public string? HourlyStatsJson { get; set; } public string? HourlyStatsJson { get; set; }

View File

@@ -457,20 +457,41 @@
<!-- TAB LOG --> <!-- TAB LOG -->
<div class="tab-pane fade" id="content-log" role="tabpanel"> <div class="tab-pane fade" id="content-log" role="tabpanel">
<div class="tab-panel-content"> <div class="tab-panel-content p-0">
<div class="log-box-compact"> @if (selectedAuction.AuctionLog.Any())
@if (selectedAuction.AuctionLog.Any()) {
{ <div class="auction-log-grid">
@foreach (var logEntry in GetAuctionLog(selectedAuction)) <div class="alog-header">
{ <span class="alog-col-time">Ora</span>
<div class="log-entry">@logEntry</div> <span class="alog-col-level">Livello</span>
} <span class="alog-col-cat">Categoria</span>
} <span class="alog-col-msg">Messaggio</span>
else </div>
{ <div class="alog-body">
<div class="text-muted"><i class="bi bi-inbox"></i> Nessun log disponibile.</div> @foreach (var entry in GetAuctionLog(selectedAuction))
} {
</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 p-3"><i class="bi bi-inbox"></i> Nessun log disponibile.</div>
}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -527,18 +527,52 @@ namespace AutoBidder.Pages
var productName = ExtractProductNameFromUrl(addDialogUrl); var productName = ExtractProductNameFromUrl(addDialogUrl);
var tempName = string.IsNullOrWhiteSpace(productName) ? $"Asta {auctionId}" : productName; 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 // Crea nuova asta
var newAuction = new AuctionInfo var newAuction = new AuctionInfo
{ {
AuctionId = auctionId, AuctionId = auctionId,
Name = tempName, Name = tempName,
OriginalUrl = addDialogUrl, OriginalUrl = addDialogUrl,
BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs, BidBeforeDeadlineMs = bidDeadlineMs,
CheckAuctionOpenBeforeBid = settings.DefaultCheckAuctionOpenBeforeBid, CheckAuctionOpenBeforeBid = settings.DefaultCheckAuctionOpenBeforeBid,
MinPrice = settings.DefaultMinPrice, MinPrice = minPrice,
MaxPrice = settings.DefaultMaxPrice, MaxPrice = maxPrice,
MinResets = settings.DefaultMinResets,
MaxResets = settings.DefaultMaxResets,
IsActive = isActive, IsActive = isActive,
IsPaused = isPaused IsPaused = isPaused
}; };
@@ -640,7 +674,60 @@ namespace AutoBidder.Pages
AddLog($"[FETCH] Info prodotto: Valore={auction.BuyNowPrice:F2}€, Spedizione={auction.ShippingCost:F2}€"); 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) if (updated)
{ {
SaveAuctions(); SaveAuctions();
@@ -1212,32 +1299,44 @@ namespace AutoBidder.Pages
private IEnumerable<AuctionInfo> GetSortedAuctions() private IEnumerable<AuctionInfo> GetSortedAuctions()
{ {
var list = auctions.AsEnumerable(); try
list = auctionSortColumn switch
{ {
"stato" => auctionSortAscending // Protezione null-safety
? list.OrderBy(a => a.IsActive).ThenBy(a => a.IsPaused) if (auctions == null || auctions.Count == 0)
: list.OrderByDescending(a => a.IsActive).ThenByDescending(a => a.IsPaused), return Enumerable.Empty<AuctionInfo>();
"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; 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<AuctionInfo>();
}
} }
// ?? NUOVI METODI: Visualizzazione valori prodotto // ?? 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> /// <summary>
@@ -1541,8 +1640,6 @@ namespace AutoBidder.Pages
// Applica comunque ma con avviso // Applica comunque ma con avviso
selectedAuction.MinPrice = limits.MinPrice; selectedAuction.MinPrice = limits.MinPrice;
selectedAuction.MaxPrice = limits.MaxPrice; selectedAuction.MaxPrice = limits.MaxPrice;
selectedAuction.MinResets = limits.MinResets;
selectedAuction.MaxResets = limits.MaxResets;
SaveAuctions(); SaveAuctions();
@@ -1554,8 +1651,6 @@ namespace AutoBidder.Pages
// Applica limiti con buona confidenza // Applica limiti con buona confidenza
selectedAuction.MinPrice = limits.MinPrice; selectedAuction.MinPrice = limits.MinPrice;
selectedAuction.MaxPrice = limits.MaxPrice; selectedAuction.MaxPrice = limits.MaxPrice;
selectedAuction.MinResets = limits.MinResets;
selectedAuction.MaxResets = limits.MaxResets;
SaveAuctions(); SaveAuctions();

View File

@@ -206,6 +206,7 @@
<i class="bi bi-arrow-repeat"></i> <i class="bi bi-arrow-repeat"></i>
</button> </button>
</div> </div>
<div class="form-text">0 = punta a qualsiasi prezzo. Il prezzo deve essere ? a questo valore per puntare.</div>
</div> </div>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-currency-euro"></i> Prezzo massimo (€)</label> <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> <i class="bi bi-arrow-repeat"></i>
</button> </button>
</div> </div>
<div class="form-text">0 = nessun limite. Se il prezzo supera questo valore, SMETTE di puntare.</div>
</div> </div>
<div class="col-12 col-md-6"> <div class="col-12">
<label class="form-label fw-bold"><i class="bi bi-arrow-repeat"></i> Reset minimi</label> <label class="form-label fw-bold"><i class="bi bi-database-gear"></i> Priorità limiti nuove aste</label>
<div class="input-group"> <select class="form-select" @bind="settings.NewAuctionLimitsPriority">
<input type="number" class="form-control" @bind="settings.DefaultMinResets" /> <option value="ProductStats">Usa limiti salvati nelle statistiche prodotto</option>
<button class="btn btn-outline-primary" @onclick="() => ApplySingleSettingToAll(nameof(settings.DefaultMinResets))" <option value="GlobalDefaults">Usa sempre limiti globali</option>
disabled="@applyingSettings.Contains(nameof(settings.DefaultMinResets))" </select>
title="Applica a tutte le aste"> <div class="form-text">
<i class="bi bi-arrow-repeat"></i> Se "Statistiche prodotto", quando aggiungi un'asta di un prodotto già salvato, verranno usati i limiti personalizzati delle statistiche invece di quelli globali.
</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> </div>
</div> </div>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
@@ -854,8 +844,6 @@ private System.Threading.Timer? updateTimer;
auction.BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs; auction.BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs;
auction.MinPrice = settings.DefaultMinPrice; auction.MinPrice = settings.DefaultMinPrice;
auction.MaxPrice = settings.DefaultMaxPrice; auction.MaxPrice = settings.DefaultMaxPrice;
auction.MinResets = settings.DefaultMinResets;
auction.MaxResets = settings.DefaultMaxResets;
// Resetta override per usare impostazioni globali // Resetta override per usare impostazioni globali
auction.AdvancedStrategiesEnabled = null; auction.AdvancedStrategiesEnabled = null;
@@ -925,14 +913,6 @@ private System.Threading.Timer? updateTimer;
auction.MaxPrice = settings.DefaultMaxPrice; auction.MaxPrice = settings.DefaultMaxPrice;
settingLabel = $"Prezzo massimo (€{settings.DefaultMaxPrice:F2})"; settingLabel = $"Prezzo massimo (€{settings.DefaultMaxPrice:F2})";
break; 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++; 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>? OnLog;
public event Action<string>? OnResetCountChanged; public event Action<string>? OnResetCountChanged;
// Throttling per log di blocco (evita spam nel log globale)
private readonly Dictionary<string, DateTime> _lastBlockLogTime = new();
/// <summary> /// <summary>
/// Evento fired quando un'asta termina (vinta, persa o chiusa). /// Evento fired quando un'asta termina (vinta, persa o chiusa).
/// Parametri: AuctionInfo, AuctionState finale, bool won (true se vinta dall'utente) /// Parametri: AuctionInfo, AuctionState finale, bool won (true se vinta dall'utente)
@@ -185,10 +188,8 @@ namespace AutoBidder.Services
auction.MinPrice = minPrice; auction.MinPrice = minPrice;
auction.MaxPrice = maxPrice; 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; return true;
} }
} }
@@ -210,8 +211,6 @@ namespace AutoBidder.Services
{ {
auction.MinPrice = minPrice; auction.MinPrice = minPrice;
auction.MaxPrice = maxPrice; auction.MaxPrice = maxPrice;
auction.MinResets = minResets;
auction.MaxResets = maxResets;
count++; count++;
OnLog?.Invoke($"[LIMITS] Aggiornati limiti per {auction.Name}"); 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 tickerIntervalMs = Math.Max(100, settings.TickerIntervalMs); // Minimo 100ms
int pollingIntervalMs = 500; // Poll API ogni 500ms max int pollingIntervalMs = 500; // Poll API ogni 500ms max
DateTime lastPoll = DateTime.MinValue; DateTime lastPoll = DateTime.MinValue;
DateTime lastDiagnostic = DateTime.MinValue;
OnLog?.Invoke($"[TICKER] Avviato con intervallo={tickerIntervalMs}ms, polling={pollingIntervalMs}ms"); OnLog?.Invoke($"[TICKER] Avviato con intervallo={tickerIntervalMs}ms, polling={pollingIntervalMs}ms");
@@ -279,6 +279,37 @@ namespace AutoBidder.Services
continue; 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 === // === FASE 2: Poll API solo ogni pollingIntervalMs ===
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
bool shouldPoll = (now - lastPoll).TotalMilliseconds >= pollingIntervalMs; bool shouldPoll = (now - lastPoll).TotalMilliseconds >= pollingIntervalMs;
@@ -405,6 +436,15 @@ namespace AutoBidder.Services
auction.LastState = state; 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) if (shouldNotify)
{ {
OnAuctionUpdated?.Invoke(state); OnAuctionUpdated?.Invoke(state);
@@ -538,6 +578,39 @@ namespace AutoBidder.Services
return false; 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> /// <summary>
/// TICKER: Tenta la puntata quando il timer locale raggiunge l'offset configurato. /// TICKER: Tenta la puntata quando il timer locale raggiunge l'offset configurato.
/// NESSUNA compensazione automatica della latenza - l'utente decide il timing esatto. /// 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; if (state == null || state.IsMyBid || estimatedTimerMs <= 0) return;
// Log timing se abilitato // Log timing dettagliato per ogni check del ticker
if (settings.LogTiming) auction.AddLog($"Timer={estimatedTimerMs:F0}ms | Offset={offsetMs}ms | Prezzo=€{state.Price:F2} | Ultimo={state.LastBidder ?? "-"}",
{ Models.AuctionLogLevel.Timing, Models.AuctionLogCategory.Ticker);
auction.AddLog($"[TICKER] Timer stimato={estimatedTimerMs:F0}ms <= Offset={offsetMs}ms");
}
// === PROTEZIONE DOPPIA PUNTATA === // === PROTEZIONE DOPPIA PUNTATA ===
// Reset se timer è aumentato (qualcuno ha puntato = nuovo ciclo)
if (estimatedTimerMs > auction.LastScheduledTimerMs + 500) 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; auction.BidScheduled = false;
} }
// Reset se passato troppo tempo dall'ultima puntata
if (auction.LastClickAt.HasValue && if (auction.LastClickAt.HasValue &&
(DateTime.UtcNow - auction.LastClickAt.Value).TotalSeconds > 10) (DateTime.UtcNow - auction.LastClickAt.Value).TotalSeconds > 10)
{ {
@@ -572,6 +646,8 @@ namespace AutoBidder.Services
// Skip se già schedulata per questo ciclo // Skip se già schedulata per questo ciclo
if (auction.BidScheduled && Math.Abs(auction.LastScheduledTimerMs - estimatedTimerMs) < 200) 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; return;
} }
@@ -579,17 +655,22 @@ namespace AutoBidder.Services
if (auction.LastClickAt.HasValue && if (auction.LastClickAt.HasValue &&
(DateTime.UtcNow - auction.LastClickAt.Value).TotalMilliseconds < 1000) (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; return;
} }
// === OTTIMIZZAZIONE: Controlla strategie SOLO se vicino al target === // === OTTIMIZZAZIONE: Controlla strategie SOLO se vicino al target ===
// Evita calcoli inutili quando siamo lontani
if (estimatedTimerMs > settings.StrategyCheckThresholdMs) if (estimatedTimerMs > settings.StrategyCheckThresholdMs)
{ {
return; return;
} }
// === CONTROLLI FONDAMENTALI === // === 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)) if (!ShouldBid(auction, state))
{ {
return; return;
@@ -601,10 +682,15 @@ namespace AutoBidder.Services
if (!decision.ShouldBid) 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; return;
} }
auction.AddLog($"✓ Tutti i controlli superati → PUNTATA!",
Models.AuctionLogLevel.Bid, Models.AuctionLogCategory.BidAttempt);
// === ESEGUI PUNTATA === // === ESEGUI PUNTATA ===
auction.BidScheduled = true; auction.BidScheduled = true;
auction.LastScheduledTimerMs = estimatedTimerMs; auction.LastScheduledTimerMs = estimatedTimerMs;
@@ -744,12 +830,7 @@ namespace AutoBidder.Services
{ {
var settings = Utilities.SettingsManager.Load(); var settings = Utilities.SettingsManager.Load();
if (settings.LogTiming) // CONTROLLO 0: Verifica convenienza (se abilitato e dati disponibili)
{
auction.AddLog($"[DEBUG] === INIZIO CONTROLLI PUNTATA ===");
}
// ?? CONTROLLO 0: Verifica convenienza (se abilitato e dati disponibili)
if (settings.ValueCheckEnabled && if (settings.ValueCheckEnabled &&
auction.BuyNowPrice.HasValue && auction.BuyNowPrice.HasValue &&
auction.BuyNowPrice.Value > 0 && auction.BuyNowPrice.Value > 0 &&
@@ -757,27 +838,26 @@ namespace AutoBidder.Services
auction.CalculatedValue.Savings.HasValue && auction.CalculatedValue.Savings.HasValue &&
!auction.CalculatedValue.IsWorthIt) !auction.CalculatedValue.IsWorthIt)
{ {
// Usa la percentuale configurabile dall'utente
if (auction.CalculatedValue.SavingsPercentage.HasValue && if (auction.CalculatedValue.SavingsPercentage.HasValue &&
auction.CalculatedValue.SavingsPercentage.Value < settings.MinSavingsPercentage) auction.CalculatedValue.SavingsPercentage.Value < settings.MinSavingsPercentage)
{ {
// 🔥 Logga SEMPRE - è un blocco frequente e importante auction.AddLog($"⛔ Risparmio {auction.CalculatedValue.SavingsPercentage.Value:F1}% < {settings.MinSavingsPercentage:F1}% richiesto",
auction.AddLog($"[VALUE] Puntata bloccata: 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; return false;
} }
} }
else
if (settings.LogTiming && settings.ValueCheckEnabled)
{ {
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" // CONTROLLO ANTI-COLLISIONE (OPZIONALE)
// DISABILITATO DI DEFAULT - può far perdere aste competitive!
if (settings.HardcodedAntiCollisionEnabled) if (settings.HardcodedAntiCollisionEnabled)
{ {
var recentBidsThreshold = 10; // secondi var recentBidsThreshold = 10;
var maxActiveBidders = 3; // se 3+ bidder attivi, potrebbe essere troppo affollata var maxActiveBidders = 3;
try try
{ {
@@ -791,125 +871,87 @@ namespace AutoBidder.Services
.Distinct(StringComparer.OrdinalIgnoreCase) .Distinct(StringComparer.OrdinalIgnoreCase)
.Count(); .Count();
if (settings.LogTiming) auction.AddLog($"Competizione: {activeBidders} bidder attivi (soglia={maxActiveBidders})",
{ Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Competition);
auction.AddLog($"[DEBUG] Bidder attivi ultimi {recentBidsThreshold}s: {activeBidders}/{maxActiveBidders}");
}
if (activeBidders >= maxActiveBidders) if (activeBidders >= maxActiveBidders)
{ {
// Controlla se l'ultimo bidder sono io - se sì, posso continuare
var session = _apiClient.GetSession(); var session = _apiClient.GetSession();
var lastBid = recentBids.OrderByDescending(b => b.Timestamp).FirstOrDefault(); var lastBid = recentBids.OrderByDescending(b => b.Timestamp).FirstOrDefault();
if (lastBid != null && if (lastBid != null &&
!lastBid.Username.Equals(session?.Username, StringComparison.OrdinalIgnoreCase)) !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; return false;
} }
} }
} }
catch { /* Ignora errori nel controllo competizione */ } catch { }
} }
if (settings.LogTiming) // CONTROLLO 1: Limite minimo puntate residue
{
auction.AddLog($"[DEBUG] ? Controllo competizione OK");
}
// ?? CONTROLLO 1: Limite minimo puntate residue
if (settings.MinimumRemainingBids > 0) if (settings.MinimumRemainingBids > 0)
{ {
var session = _apiClient.GetSession(); var session = _apiClient.GetSession();
if (session != null && session.RemainingBids <= settings.MinimumRemainingBids) if (session != null && session.RemainingBids <= settings.MinimumRemainingBids)
{ {
// 🔥 Logga SEMPRE - è un blocco importante auction.AddLog($"⛔ Puntate residue ({session.RemainingBids}) ≤ limite ({settings.MinimumRemainingBids})",
auction.AddLog($"[LIMIT] Puntata bloccata: 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; return false;
} }
else if (session != null)
if (settings.LogTiming && 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 (state.IsMyBid)
{ {
if (settings.LogTiming) auction.AddLog($"✓ Sono già vincitore corrente - skip",
{ Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.BidAttempt);
auction.AddLog($"[DEBUG] Sono già vincitore");
}
return false; return false;
} }
// ?? CONTROLLO 3: MinPrice/MaxPrice // CONTROLLO 3: MinPrice/MaxPrice
if (auction.MinPrice > 0 && state.Price < auction.MinPrice) if (auction.MinPrice > 0 && state.Price < auction.MinPrice)
{ {
// 🔥 Logga SEMPRE questo blocco - è critico per capire perché non punta auction.AddLog($"⛔ Prezzo €{state.Price:F2} < Min €{auction.MinPrice:F2}",
auction.AddLog($"[PRICE] Prezzo troppo basso: €{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; return false;
} }
if (auction.MaxPrice > 0 && state.Price > auction.MaxPrice) if (auction.MaxPrice > 0 && state.Price > auction.MaxPrice)
{ {
// 🔥 Logga SEMPRE questo blocco - è critico auction.AddLog($"⛔ Prezzo €{state.Price:F2} > Max €{auction.MaxPrice:F2}",
auction.AddLog($"[PRICE] Prezzo troppo alto: €{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; return false;
} }
if (settings.LogTiming) auction.AddLog($"✓ Prezzo OK (€{state.Price:F2} in [{auction.MinPrice:F2}, {(auction.MaxPrice > 0 ? auction.MaxPrice.ToString("F2") : "")}])",
{ Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Price);
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}])");
}
}
// ?? CONTROLLO 6: Cooldown (evita puntate multiple ravvicinate) // CONTROLLO 6: Cooldown
if (auction.LastClickAt.HasValue) if (auction.LastClickAt.HasValue)
{ {
var timeSinceLastClick = DateTime.UtcNow - auction.LastClickAt.Value; var timeSinceLastClick = DateTime.UtcNow - auction.LastClickAt.Value;
if (timeSinceLastClick.TotalMilliseconds < 800) if (timeSinceLastClick.TotalMilliseconds < 800)
{ {
if (settings.LogTiming) auction.AddLog($"Cooldown: {timeSinceLastClick.TotalMilliseconds:F0}ms < 800ms",
{ Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Ticker);
auction.AddLog($"[DEBUG] Cooldown attivo ({timeSinceLastClick.TotalMilliseconds:F0}ms < 800ms)");
}
return false; return false;
} }
} }
if (settings.LogTiming) auction.AddLog($"✓ Tutti i controlli ShouldBid superati",
{ Models.AuctionLogLevel.Info, Models.AuctionLogCategory.BidAttempt);
auction.AddLog($"[DEBUG] === TUTTI I CONTROLLI SUPERATI ===");
}
return true; return true;
} }

View File

@@ -747,6 +747,24 @@ namespace AutoBidder.Services
await using var cmd = conn.CreateCommand(); await using var cmd = conn.CreateCommand();
cmd.CommandText = sql; cmd.CommandText = sql;
await cmd.ExecuteNonQueryAsync(); 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 = @" var sql = @"
INSERT INTO ProductStatistics INSERT INTO ProductStatistics
(ProductKey, ProductName, TotalAuctions, WonAuctions, LostAuctions, (ProductKey, ProductName, TotalAuctions, WonAuctions, LostAuctions,
AvgFinalPrice, MinFinalPrice, MaxFinalPrice, AvgFinalPrice, MinFinalPrice, MaxFinalPrice, MedianFinalPrice,
AvgBidsToWin, MinBidsToWin, MaxBidsToWin, AvgBidsToWin, MinBidsToWin, MaxBidsToWin,
AvgResets, MinResets, MaxResets, AvgResets, MinResets, MaxResets,
RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids, RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids,
UserDefaultMinPrice, UserDefaultMaxPrice, UserDefaultMinResets, UserDefaultMaxResets, UserDefaultMaxBids, UserDefaultBidBeforeDeadlineMs, UserDefaultMinPrice, UserDefaultMaxPrice, UserDefaultMinResets, UserDefaultMaxResets, UserDefaultMaxBids, UserDefaultBidBeforeDeadlineMs,
HourlyStatsJson, LastUpdated) HourlyStatsJson, LastUpdated)
VALUES (@productKey, @productName, @totalAuctions, @wonAuctions, @lostAuctions, VALUES (@productKey, @productName, @totalAuctions, @wonAuctions, @lostAuctions,
@avgFinalPrice, @minFinalPrice, @maxFinalPrice, @avgFinalPrice, @minFinalPrice, @maxFinalPrice, @medianFinalPrice,
@avgBidsToWin, @minBidsToWin, @maxBidsToWin, @avgBidsToWin, @minBidsToWin, @maxBidsToWin,
@avgResets, @minResets, @maxResets, @avgResets, @minResets, @maxResets,
@recMinPrice, @recMaxPrice, @recMinResets, @recMaxResets, @recMaxBids, @recMinPrice, @recMaxPrice, @recMinResets, @recMaxResets, @recMaxBids,
@@ -1439,6 +1457,7 @@ namespace AutoBidder.Services
AvgFinalPrice = @avgFinalPrice, AvgFinalPrice = @avgFinalPrice,
MinFinalPrice = @minFinalPrice, MinFinalPrice = @minFinalPrice,
MaxFinalPrice = @maxFinalPrice, MaxFinalPrice = @maxFinalPrice,
MedianFinalPrice = @medianFinalPrice,
AvgBidsToWin = @avgBidsToWin, AvgBidsToWin = @avgBidsToWin,
MinBidsToWin = @minBidsToWin, MinBidsToWin = @minBidsToWin,
MaxBidsToWin = @maxBidsToWin, MaxBidsToWin = @maxBidsToWin,
@@ -1469,6 +1488,7 @@ namespace AutoBidder.Services
new SqliteParameter("@avgFinalPrice", stats.AvgFinalPrice), new SqliteParameter("@avgFinalPrice", stats.AvgFinalPrice),
new SqliteParameter("@minFinalPrice", (object?)stats.MinFinalPrice ?? DBNull.Value), new SqliteParameter("@minFinalPrice", (object?)stats.MinFinalPrice ?? DBNull.Value),
new SqliteParameter("@maxFinalPrice", (object?)stats.MaxFinalPrice ?? 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("@avgBidsToWin", stats.AvgBidsToWin),
new SqliteParameter("@minBidsToWin", (object?)stats.MinBidsToWin ?? DBNull.Value), new SqliteParameter("@minBidsToWin", (object?)stats.MinBidsToWin ?? DBNull.Value),
new SqliteParameter("@maxBidsToWin", (object?)stats.MaxBidsToWin ?? DBNull.Value), new SqliteParameter("@maxBidsToWin", (object?)stats.MaxBidsToWin ?? DBNull.Value),
@@ -1498,12 +1518,12 @@ namespace AutoBidder.Services
{ {
var sql = @" var sql = @"
SELECT ProductKey, ProductName, TotalAuctions, WonAuctions, LostAuctions, SELECT ProductKey, ProductName, TotalAuctions, WonAuctions, LostAuctions,
AvgFinalPrice, MinFinalPrice, MaxFinalPrice, AvgFinalPrice, MinFinalPrice, MaxFinalPrice, MedianFinalPrice,
AvgBidsToWin, MinBidsToWin, MaxBidsToWin, AvgBidsToWin, MinBidsToWin, MaxBidsToWin,
AvgResets, MinResets, MaxResets, AvgResets, MinResets, MaxResets,
RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids, RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids,
UserDefaultMinPrice, UserDefaultMaxPrice, UserDefaultMinResets, UserDefaultMaxResets, UserDefaultMaxBids, UserDefaultBidBeforeDeadlineMs, UserDefaultMinPrice, UserDefaultMaxPrice, UserDefaultMinResets, UserDefaultMaxResets, UserDefaultMaxBids, UserDefaultBidBeforeDeadlineMs,
HourlyStatsJson, LastUpdated HourlyStatsJson, LastUpdated, UseCustomLimits
FROM ProductStatistics FROM ProductStatistics
WHERE ProductKey = @productKey; WHERE ProductKey = @productKey;
"; ";
@@ -1526,25 +1546,27 @@ namespace AutoBidder.Services
AvgFinalPrice = reader.GetDouble(5), AvgFinalPrice = reader.GetDouble(5),
MinFinalPrice = reader.IsDBNull(6) ? null : reader.GetDouble(6), MinFinalPrice = reader.IsDBNull(6) ? null : reader.GetDouble(6),
MaxFinalPrice = reader.IsDBNull(7) ? null : reader.GetDouble(7), MaxFinalPrice = reader.IsDBNull(7) ? null : reader.GetDouble(7),
AvgBidsToWin = reader.GetDouble(8), MedianFinalPrice = reader.IsDBNull(8) ? null : reader.GetDouble(8),
MinBidsToWin = reader.IsDBNull(9) ? null : reader.GetInt32(9), AvgBidsToWin = reader.GetDouble(9),
MaxBidsToWin = reader.IsDBNull(10) ? null : reader.GetInt32(10), MinBidsToWin = reader.IsDBNull(10) ? null : reader.GetInt32(10),
AvgResets = reader.GetDouble(11), MaxBidsToWin = reader.IsDBNull(11) ? null : reader.GetInt32(11),
MinResets = reader.IsDBNull(12) ? null : reader.GetInt32(12), AvgResets = reader.GetDouble(12),
MaxResets = reader.IsDBNull(13) ? null : reader.GetInt32(13), MinResets = reader.IsDBNull(13) ? null : reader.GetInt32(13),
RecommendedMinPrice = reader.IsDBNull(14) ? null : reader.GetDouble(14), MaxResets = reader.IsDBNull(14) ? null : reader.GetInt32(14),
RecommendedMaxPrice = reader.IsDBNull(15) ? null : reader.GetDouble(15), RecommendedMinPrice = reader.IsDBNull(15) ? null : reader.GetDouble(15),
RecommendedMinResets = reader.IsDBNull(16) ? null : reader.GetInt32(16), RecommendedMaxPrice = reader.IsDBNull(16) ? null : reader.GetDouble(16),
RecommendedMaxResets = reader.IsDBNull(17) ? null : reader.GetInt32(17), RecommendedMinResets = reader.IsDBNull(17) ? null : reader.GetInt32(17),
RecommendedMaxBids = reader.IsDBNull(18) ? null : reader.GetInt32(18), RecommendedMaxResets = reader.IsDBNull(18) ? null : reader.GetInt32(18),
UserDefaultMinPrice = reader.IsDBNull(19) ? null : reader.GetDouble(19), RecommendedMaxBids = reader.IsDBNull(19) ? null : reader.GetInt32(19),
UserDefaultMaxPrice = reader.IsDBNull(20) ? null : reader.GetDouble(20), UserDefaultMinPrice = reader.IsDBNull(20) ? null : reader.GetDouble(20),
UserDefaultMinResets = reader.IsDBNull(21) ? null : reader.GetInt32(21), UserDefaultMaxPrice = reader.IsDBNull(21) ? null : reader.GetDouble(21),
UserDefaultMaxResets = reader.IsDBNull(22) ? null : reader.GetInt32(22), UserDefaultMinResets = reader.IsDBNull(22) ? null : reader.GetInt32(22),
UserDefaultMaxBids = reader.IsDBNull(23) ? null : reader.GetInt32(23), UserDefaultMaxResets = reader.IsDBNull(23) ? null : reader.GetInt32(23),
UserDefaultBidBeforeDeadlineMs = reader.IsDBNull(24) ? null : reader.GetInt32(24), UserDefaultMaxBids = reader.IsDBNull(24) ? null : reader.GetInt32(24),
HourlyStatsJson = reader.IsDBNull(25) ? null : reader.GetString(25), UserDefaultBidBeforeDeadlineMs = reader.IsDBNull(25) ? null : reader.GetInt32(25),
LastUpdated = reader.GetString(26) 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 = @" var sql = @"
SELECT ProductKey, ProductName, TotalAuctions, WonAuctions, LostAuctions, SELECT ProductKey, ProductName, TotalAuctions, WonAuctions, LostAuctions,
AvgFinalPrice, MinFinalPrice, MaxFinalPrice, AvgFinalPrice, MinFinalPrice, MaxFinalPrice, MedianFinalPrice,
AvgBidsToWin, MinBidsToWin, MaxBidsToWin, AvgBidsToWin, MinBidsToWin, MaxBidsToWin,
AvgResets, MinResets, MaxResets, AvgResets, MinResets, MaxResets,
RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids, RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids,
UserDefaultMinPrice, UserDefaultMaxPrice, UserDefaultMinResets, UserDefaultMaxResets, UserDefaultMaxBids, UserDefaultBidBeforeDeadlineMs, UserDefaultMinPrice, UserDefaultMaxPrice, UserDefaultMinResets, UserDefaultMaxResets, UserDefaultMaxBids, UserDefaultBidBeforeDeadlineMs,
HourlyStatsJson, LastUpdated HourlyStatsJson, LastUpdated, UseCustomLimits
FROM ProductStatistics FROM ProductStatistics
ORDER BY TotalAuctions DESC; ORDER BY TotalAuctions DESC;
"; ";
@@ -1619,25 +1641,27 @@ namespace AutoBidder.Services
AvgFinalPrice = reader.GetDouble(5), AvgFinalPrice = reader.GetDouble(5),
MinFinalPrice = reader.IsDBNull(6) ? null : reader.GetDouble(6), MinFinalPrice = reader.IsDBNull(6) ? null : reader.GetDouble(6),
MaxFinalPrice = reader.IsDBNull(7) ? null : reader.GetDouble(7), MaxFinalPrice = reader.IsDBNull(7) ? null : reader.GetDouble(7),
AvgBidsToWin = reader.GetDouble(8), MedianFinalPrice = reader.IsDBNull(8) ? null : reader.GetDouble(8),
MinBidsToWin = reader.IsDBNull(9) ? null : reader.GetInt32(9), AvgBidsToWin = reader.GetDouble(9),
MaxBidsToWin = reader.IsDBNull(10) ? null : reader.GetInt32(10), MinBidsToWin = reader.IsDBNull(10) ? null : reader.GetInt32(10),
AvgResets = reader.GetDouble(11), MaxBidsToWin = reader.IsDBNull(11) ? null : reader.GetInt32(11),
MinResets = reader.IsDBNull(12) ? null : reader.GetInt32(12), AvgResets = reader.GetDouble(12),
MaxResets = reader.IsDBNull(13) ? null : reader.GetInt32(13), MinResets = reader.IsDBNull(13) ? null : reader.GetInt32(13),
RecommendedMinPrice = reader.IsDBNull(14) ? null : reader.GetDouble(14), MaxResets = reader.IsDBNull(14) ? null : reader.GetInt32(14),
RecommendedMaxPrice = reader.IsDBNull(15) ? null : reader.GetDouble(15), RecommendedMinPrice = reader.IsDBNull(15) ? null : reader.GetDouble(15),
RecommendedMinResets = reader.IsDBNull(16) ? null : reader.GetInt32(16), RecommendedMaxPrice = reader.IsDBNull(16) ? null : reader.GetDouble(16),
RecommendedMaxResets = reader.IsDBNull(17) ? null : reader.GetInt32(17), RecommendedMinResets = reader.IsDBNull(17) ? null : reader.GetInt32(17),
RecommendedMaxBids = reader.IsDBNull(18) ? null : reader.GetInt32(18), RecommendedMaxResets = reader.IsDBNull(18) ? null : reader.GetInt32(18),
UserDefaultMinPrice = reader.IsDBNull(19) ? null : reader.GetDouble(19), RecommendedMaxBids = reader.IsDBNull(19) ? null : reader.GetInt32(19),
UserDefaultMaxPrice = reader.IsDBNull(20) ? null : reader.GetDouble(20), UserDefaultMinPrice = reader.IsDBNull(20) ? null : reader.GetDouble(20),
UserDefaultMinResets = reader.IsDBNull(21) ? null : reader.GetInt32(21), UserDefaultMaxPrice = reader.IsDBNull(21) ? null : reader.GetDouble(21),
UserDefaultMaxResets = reader.IsDBNull(22) ? null : reader.GetInt32(22), UserDefaultMinResets = reader.IsDBNull(22) ? null : reader.GetInt32(22),
UserDefaultMaxBids = reader.IsDBNull(23) ? null : reader.GetInt32(23), UserDefaultMaxResets = reader.IsDBNull(23) ? null : reader.GetInt32(23),
UserDefaultBidBeforeDeadlineMs = reader.IsDBNull(24) ? null : reader.GetInt32(24), UserDefaultMaxBids = reader.IsDBNull(24) ? null : reader.GetInt32(24),
HourlyStatsJson = reader.IsDBNull(25) ? null : reader.GetString(25), UserDefaultBidBeforeDeadlineMs = reader.IsDBNull(25) ? null : reader.GetInt32(25),
LastUpdated = reader.GetString(26) 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, public async Task UpdateProductUserDefaultsAsync(string productKey,
double? minPrice, double? maxPrice, double? minPrice, double? maxPrice,
int? minResets, int? maxResets, int? minResets, int? maxResets,
int? maxBids, int? bidBeforeDeadlineMs) int? maxBids, int? bidBeforeDeadlineMs,
bool? useCustomLimits = null)
{ {
var sql = @" var sql = @"
UPDATE ProductStatistics UPDATE ProductStatistics
@@ -1675,6 +1700,7 @@ namespace AutoBidder.Services
UserDefaultMaxResets = @maxResets, UserDefaultMaxResets = @maxResets,
UserDefaultMaxBids = @maxBids, UserDefaultMaxBids = @maxBids,
UserDefaultBidBeforeDeadlineMs = @bidDeadline, UserDefaultBidBeforeDeadlineMs = @bidDeadline,
UseCustomLimits = CASE WHEN @useCustomLimits IS NOT NULL THEN @useCustomLimits ELSE UseCustomLimits END,
LastUpdated = @lastUpdated LastUpdated = @lastUpdated
WHERE ProductKey = @productKey; WHERE ProductKey = @productKey;
"; ";
@@ -1687,6 +1713,7 @@ namespace AutoBidder.Services
new SqliteParameter("@maxResets", (object?)maxResets ?? DBNull.Value), new SqliteParameter("@maxResets", (object?)maxResets ?? DBNull.Value),
new SqliteParameter("@maxBids", (object?)maxBids ?? DBNull.Value), new SqliteParameter("@maxBids", (object?)maxBids ?? DBNull.Value),
new SqliteParameter("@bidDeadline", (object?)bidBeforeDeadlineMs ?? 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")) new SqliteParameter("@lastUpdated", DateTime.UtcNow.ToString("O"))
); );
} }

View File

@@ -98,12 +98,14 @@ namespace AutoBidder.Services
stats.AvgFinalPrice = wonResults.Average(r => r.FinalPrice); stats.AvgFinalPrice = wonResults.Average(r => r.FinalPrice);
stats.MinFinalPrice = wonResults.Min(r => r.FinalPrice); stats.MinFinalPrice = wonResults.Min(r => r.FinalPrice);
stats.MaxFinalPrice = wonResults.Max(r => r.FinalPrice); stats.MaxFinalPrice = wonResults.Max(r => r.FinalPrice);
stats.MedianFinalPrice = CalculateMedian(wonResults.Select(r => r.FinalPrice).ToList());
} }
else if (results.Any()) else if (results.Any())
{ {
stats.AvgFinalPrice = results.Average(r => r.FinalPrice); stats.AvgFinalPrice = results.Average(r => r.FinalPrice);
stats.MinFinalPrice = results.Min(r => r.FinalPrice); stats.MinFinalPrice = results.Min(r => r.FinalPrice);
stats.MaxFinalPrice = results.Max(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) // 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)); double sumSquares = data.Sum(d => Math.Pow(d - avg, 2));
return Math.Sqrt(sumSquares / (data.Count - 1)); 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); 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> /// <summary>
/// Ottiene tutte le statistiche prodotto /// Ottiene tutte le statistiche prodotto
/// </summary> /// </summary>

View File

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

View File

@@ -1303,3 +1303,181 @@
.btn-xs i { .btn-xs i {
font-size: 0.75rem; 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);
}