Compare commits
2 Commits
690f7e636a
...
docker
| Author | SHA1 | Date | |
|---|---|---|---|
| e18a09e1da | |||
| f3262a0497 |
@@ -40,10 +40,10 @@ namespace AutoBidder.Models
|
|||||||
public int MaxResets { get; set; } = 0; // Numero massimo reset (0 = illimitati)
|
public int MaxResets { get; set; } = 0; // Numero massimo reset (0 = illimitati)
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// [OBSOLETO] Numero massimo di puntate consentite - Non più utilizzato nell'UI
|
/// Numero massimo di puntate consentite per questa asta (0 = illimitato).
|
||||||
/// Mantenuto per retrocompatibilità con salvataggi JSON esistenti
|
/// Impostato dall'utente nella griglia statistiche o dai limiti prodotto.
|
||||||
|
/// Controllato in ShouldBid contro BidsUsedOnThisAuction.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("MaxClicks non è più utilizzato. Usa invece la logica di limiti per prodotto.")]
|
|
||||||
[JsonPropertyName("MaxClicks")]
|
[JsonPropertyName("MaxClicks")]
|
||||||
public int MaxClicks { get; set; } = 0;
|
public int MaxClicks { get; set; } = 0;
|
||||||
|
|
||||||
@@ -107,6 +107,13 @@ namespace AutoBidder.Models
|
|||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public double LastScheduledTimerMs { get; set; }
|
public double LastScheduledTimerMs { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stato di fine asta ricevuto dal poll ma non ancora processato.
|
||||||
|
/// Il ticker ha un'ultima occasione di puntare prima che venga gestito.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public AuctionState? PendingEndState { get; set; }
|
||||||
|
|
||||||
// Storico
|
// Storico
|
||||||
public List<BidHistory> BidHistory { get; set; } = new List<BidHistory>();
|
public List<BidHistory> BidHistory { get; set; } = new List<BidHistory>();
|
||||||
public Dictionary<string, BidderInfo> BidderStats { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
public Dictionary<string, BidderInfo> BidderStats { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
@@ -118,9 +125,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 +175,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
|
||||||
|
|
||||||
// ???????????????????????????????????????????????????????????????
|
// ???????????????????????????????????????????????????????????????
|
||||||
@@ -428,6 +512,7 @@ namespace AutoBidder.Models
|
|||||||
|
|
||||||
// Pulisci oggetti complessi
|
// Pulisci oggetti complessi
|
||||||
LastState = null;
|
LastState = null;
|
||||||
|
PendingEndState = null;
|
||||||
CalculatedValue = null;
|
CalculatedValue = null;
|
||||||
DuelOpponent = null;
|
DuelOpponent = null;
|
||||||
WinLimitDescription = null;
|
WinLimitDescription = null;
|
||||||
|
|||||||
126
Mimante/Models/AuctionLogEntry.cs
Normal file
126
Mimante/Models/AuctionLogEntry.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -102,7 +102,6 @@ namespace AutoBidder.Pages
|
|||||||
private string? sessionUsername;
|
private string? sessionUsername;
|
||||||
private int sessionRemainingBids;
|
private int sessionRemainingBids;
|
||||||
private double sessionShopCredit;
|
private double sessionShopCredit;
|
||||||
private int sessionAuctionsWon;
|
|
||||||
|
|
||||||
// Recommended limits
|
// Recommended limits
|
||||||
private bool isLoadingRecommendations = false;
|
private bool isLoadingRecommendations = false;
|
||||||
@@ -527,18 +526,56 @@ 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 maxClicks = settings.DefaultMaxClicks;
|
||||||
|
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.UserDefaultMaxBids.HasValue)
|
||||||
|
maxClicks = productStats.UserDefaultMaxBids.Value;
|
||||||
|
if (productStats.UserDefaultBidBeforeDeadlineMs.HasValue)
|
||||||
|
bidDeadlineMs = productStats.UserDefaultBidBeforeDeadlineMs.Value;
|
||||||
|
|
||||||
|
AddLog($"[STATS] Limiti prodotto (da URL): €{minPrice:F2}-€{maxPrice:F2}, MaxClicks={maxClicks}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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,
|
MaxClicks = maxClicks,
|
||||||
MaxResets = settings.DefaultMaxResets,
|
|
||||||
IsActive = isActive,
|
IsActive = isActive,
|
||||||
IsPaused = isPaused
|
IsPaused = isPaused
|
||||||
};
|
};
|
||||||
@@ -640,7 +677,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 (!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 +1302,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 +1477,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>
|
||||||
@@ -1444,7 +1546,6 @@ namespace AutoBidder.Pages
|
|||||||
sessionUsername = savedSession.Username;
|
sessionUsername = savedSession.Username;
|
||||||
sessionRemainingBids = savedSession.RemainingBids;
|
sessionRemainingBids = savedSession.RemainingBids;
|
||||||
sessionShopCredit = savedSession.ShopCredit;
|
sessionShopCredit = savedSession.ShopCredit;
|
||||||
sessionAuctionsWon = 0; // TODO: add to BidooSession model
|
|
||||||
|
|
||||||
// Inizializza AuctionMonitor con la sessione salvata
|
// Inizializza AuctionMonitor con la sessione salvata
|
||||||
if (!string.IsNullOrEmpty(savedSession.CookieString))
|
if (!string.IsNullOrEmpty(savedSession.CookieString))
|
||||||
@@ -1458,7 +1559,6 @@ namespace AutoBidder.Pages
|
|||||||
sessionUsername = session?.Username;
|
sessionUsername = session?.Username;
|
||||||
sessionRemainingBids = session?.RemainingBids ?? 0;
|
sessionRemainingBids = session?.RemainingBids ?? 0;
|
||||||
sessionShopCredit = session?.ShopCredit ?? 0;
|
sessionShopCredit = session?.ShopCredit ?? 0;
|
||||||
sessionAuctionsWon = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1475,7 +1575,6 @@ namespace AutoBidder.Pages
|
|||||||
sessionUsername = session.Username;
|
sessionUsername = session.Username;
|
||||||
sessionRemainingBids = session.RemainingBids;
|
sessionRemainingBids = session.RemainingBids;
|
||||||
sessionShopCredit = session.ShopCredit;
|
sessionShopCredit = session.ShopCredit;
|
||||||
sessionAuctionsWon = 0; // TODO: add to BidooSession model
|
|
||||||
|
|
||||||
// Salva sessione aggiornata
|
// Salva sessione aggiornata
|
||||||
AutoBidder.Services.SessionManager.SaveSession(session);
|
AutoBidder.Services.SessionManager.SaveSession(session);
|
||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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,42 +279,128 @@ namespace AutoBidder.Services
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// === FASE 2: Poll API solo ogni pollingIntervalMs ===
|
// === DIAGNOSTICA PERIODICA (ogni 30s) ===
|
||||||
var now = DateTime.UtcNow;
|
var nowDiag = DateTime.UtcNow;
|
||||||
bool shouldPoll = (now - lastPoll).TotalMilliseconds >= pollingIntervalMs;
|
if ((nowDiag - lastDiagnostic).TotalSeconds >= 30)
|
||||||
|
|
||||||
// Poll più frequente se vicino alla scadenza
|
|
||||||
bool anyNearDeadline = activeAuctions.Any(a =>
|
|
||||||
GetEstimatedTimerMs(a) < settings.StrategyCheckThresholdMs);
|
|
||||||
|
|
||||||
if (shouldPoll || anyNearDeadline)
|
|
||||||
{
|
{
|
||||||
var pollTasks = activeAuctions.Select(a => PollAuctionState(a, token));
|
lastDiagnostic = nowDiag;
|
||||||
await Task.WhenAll(pollTasks);
|
settings = SettingsManager.Load(); // Ricarica impostazioni
|
||||||
lastPoll = now;
|
|
||||||
|
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 3: TICKER CHECK - Verifica timing per ogni asta ===
|
// === FASE 2: PRE-BID CHECK (zero I/O, solo timer locale) ===
|
||||||
|
// CRITICO: Controlla il ticker PRIMA del poll per le aste nella zona di puntata.
|
||||||
|
// Il poll prende 40-100ms di rete. Con più aste near-deadline,
|
||||||
|
// Task.WhenAll aspetta il poll più lento → il tick effettivo è 100-200ms.
|
||||||
|
// Se l'offset è 200ms, la finestra di puntata è PIÙ STRETTA del tick:
|
||||||
|
// Tick N: estimated=300ms > offset → non trigghera
|
||||||
|
// [poll prende 150ms]
|
||||||
|
// Tick N+1: estimated=-50ms → fuori finestra → MANCATA!
|
||||||
|
// Il pre-bid check usa lo stato dal poll PRECEDENTE (< 100ms fa),
|
||||||
|
// che è sufficientemente fresco per IsMyBid, prezzo e ultimo puntatore.
|
||||||
foreach (var auction in activeAuctions)
|
foreach (var auction in activeAuctions)
|
||||||
{
|
{
|
||||||
if (auction.IsPaused || auction.LastState == null) continue;
|
if (auction.IsPaused || auction.LastState == null) continue;
|
||||||
|
|
||||||
// Calcola timer stimato LOCALMENTE (più preciso del polling)
|
|
||||||
double estimatedTimerMs = GetEstimatedTimerMs(auction);
|
double estimatedTimerMs = GetEstimatedTimerMs(auction);
|
||||||
|
|
||||||
// Offset configurato dall'utente (SENZA compensazioni)
|
|
||||||
int offsetMs = auction.BidBeforeDeadlineMs > 0
|
int offsetMs = auction.BidBeforeDeadlineMs > 0
|
||||||
? auction.BidBeforeDeadlineMs
|
? auction.BidBeforeDeadlineMs
|
||||||
: settings.DefaultBidBeforeDeadlineMs;
|
: settings.DefaultBidBeforeDeadlineMs;
|
||||||
|
|
||||||
// TRIGGER: Timer <= Offset configurato dall'utente
|
if (estimatedTimerMs <= offsetMs && estimatedTimerMs > -tickerIntervalMs)
|
||||||
if (estimatedTimerMs <= offsetMs && estimatedTimerMs > 0)
|
|
||||||
{
|
{
|
||||||
await TryPlaceBidTicker(auction, estimatedTimerMs, offsetMs, token);
|
await TryPlaceBidTicker(auction, estimatedTimerMs, offsetMs, settings, token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === FASE 4: Delay fisso del ticker ===
|
// === FASE 3: Poll API ===
|
||||||
|
// Il poll aggiorna dati (prezzo, ultimo puntatore, timer) per il prossimo tick.
|
||||||
|
// Se rileva la fine dell'asta, la salva in PendingEndState (non la processa subito).
|
||||||
|
// Le aste nella zona critica (< offset*2 ms) NON vengono pollate:
|
||||||
|
// il poll prenderebbe tempo prezioso e i dati locali sono sufficienti.
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
bool shouldPollAll = (now - lastPoll).TotalMilliseconds >= pollingIntervalMs;
|
||||||
|
|
||||||
|
if (shouldPollAll)
|
||||||
|
{
|
||||||
|
// Poll normale: tutte le aste attive
|
||||||
|
var pollTasks = activeAuctions.Select(a => PollAuctionState(a, token));
|
||||||
|
await Task.WhenAll(pollTasks);
|
||||||
|
lastPoll = now;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Poll rapido: SOLO le aste vicine alla scadenza MA non nella zona critica
|
||||||
|
// Se il timer è < offset*2, il poll ruberebbe tempo al prossimo pre-bid check
|
||||||
|
var nearDeadlineAuctions = activeAuctions.Where(a =>
|
||||||
|
{
|
||||||
|
double est = GetEstimatedTimerMs(a);
|
||||||
|
int off = a.BidBeforeDeadlineMs > 0
|
||||||
|
? a.BidBeforeDeadlineMs
|
||||||
|
: settings.DefaultBidBeforeDeadlineMs;
|
||||||
|
return est < settings.StrategyCheckThresholdMs && est > off * 2;
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
if (nearDeadlineAuctions.Count > 0)
|
||||||
|
{
|
||||||
|
var pollTasks = nearDeadlineAuctions.Select(a => PollAuctionState(a, token));
|
||||||
|
await Task.WhenAll(pollTasks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === FASE 4: POST-POLL TICKER CHECK ===
|
||||||
|
// Per aste che sono entrate nella zona di puntata DURANTE il poll.
|
||||||
|
// Le aste già puntate dal pre-bid check vengono skippate da BidScheduled.
|
||||||
|
foreach (var auction in activeAuctions)
|
||||||
|
{
|
||||||
|
if (auction.IsPaused || auction.LastState == null) continue;
|
||||||
|
|
||||||
|
double estimatedTimerMs = GetEstimatedTimerMs(auction);
|
||||||
|
int offsetMs = auction.BidBeforeDeadlineMs > 0
|
||||||
|
? auction.BidBeforeDeadlineMs
|
||||||
|
: settings.DefaultBidBeforeDeadlineMs;
|
||||||
|
|
||||||
|
if (estimatedTimerMs <= offsetMs && estimatedTimerMs > -tickerIntervalMs)
|
||||||
|
{
|
||||||
|
await TryPlaceBidTicker(auction, estimatedTimerMs, offsetMs, settings, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === FASE 4: Processa aste terminate (deferred) ===
|
||||||
|
// Solo ORA gestiamo le aste che il poll ha marcato come terminate.
|
||||||
|
// Il ticker ha avuto la sua ultima occasione nella Fase 3.
|
||||||
|
foreach (var auction in activeAuctions)
|
||||||
|
{
|
||||||
|
if (auction.PendingEndState != null)
|
||||||
|
{
|
||||||
|
HandleAuctionEnded(auction, auction.PendingEndState);
|
||||||
|
auction.PendingEndState = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === FASE 5: Delay fisso del ticker ===
|
||||||
await Task.Delay(tickerIntervalMs, token);
|
await Task.Delay(tickerIntervalMs, token);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
@@ -345,9 +431,11 @@ namespace AutoBidder.Services
|
|||||||
var elapsed = (DateTime.UtcNow - auction.LastDeadlineUpdateUtc.Value).TotalMilliseconds;
|
var elapsed = (DateTime.UtcNow - auction.LastDeadlineUpdateUtc.Value).TotalMilliseconds;
|
||||||
|
|
||||||
// Timer stimato = timer raw - tempo trascorso
|
// Timer stimato = timer raw - tempo trascorso
|
||||||
|
// NON clampare a 0: il ticker usa valori leggermente negativi
|
||||||
|
// per catturare la finestra quando il timer scade tra due tick
|
||||||
double estimated = auction.LastRawTimer - elapsed;
|
double estimated = auction.LastRawTimer - elapsed;
|
||||||
|
|
||||||
return Math.Max(0, estimated);
|
return estimated;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -388,12 +476,19 @@ namespace AutoBidder.Services
|
|||||||
EnsureCurrentBidInHistory(auction, state);
|
EnsureCurrentBidInHistory(auction, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gestione fine asta
|
// Gestione fine asta — DIFFERITA
|
||||||
|
// Non chiamare HandleAuctionEnded qui: il ticker deve avere
|
||||||
|
// un'ultima occasione di puntare con i dati freschi del poll.
|
||||||
|
// Lo stato di fine viene salvato in PendingEndState e processato
|
||||||
|
// dal loop principale DOPO il ticker check.
|
||||||
if (state.Status == AuctionStatus.EndedWon ||
|
if (state.Status == AuctionStatus.EndedWon ||
|
||||||
state.Status == AuctionStatus.EndedLost ||
|
state.Status == AuctionStatus.EndedLost ||
|
||||||
state.Status == AuctionStatus.Closed)
|
state.Status == AuctionStatus.Closed)
|
||||||
{
|
{
|
||||||
HandleAuctionEnded(auction, state);
|
// Aggiorna LastState con i dati più freschi (ultimo puntatore, prezzo)
|
||||||
|
// così il ticker può fare controlli accurati (es. IsMyBid, prezzo)
|
||||||
|
auction.LastState = state;
|
||||||
|
auction.PendingEndState = state;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,6 +500,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,31 +642,64 @@ 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task TryPlaceBidTicker(AuctionInfo auction, double estimatedTimerMs, int offsetMs, CancellationToken token)
|
private async Task TryPlaceBidTicker(AuctionInfo auction, double estimatedTimerMs, int offsetMs, AppSettings settings, CancellationToken token)
|
||||||
{
|
{
|
||||||
var settings = SettingsManager.Load();
|
|
||||||
var state = auction.LastState;
|
var state = auction.LastState;
|
||||||
|
|
||||||
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 +709,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,18 +718,23 @@ 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 ===
|
||||||
if (!ShouldBid(auction, state))
|
auction.AddLog($"→ Inizio controlli puntata (timer={estimatedTimerMs:F0}ms ≤ soglia={settings.StrategyCheckThresholdMs}ms)",
|
||||||
|
Models.AuctionLogLevel.Info, Models.AuctionLogCategory.BidAttempt);
|
||||||
|
|
||||||
|
if (!ShouldBid(auction, state, settings))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -601,17 +745,22 @@ 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;
|
||||||
|
|
||||||
auction.AddLog($"[BID] Timer locale={estimatedTimerMs:F0}ms, Offset utente={offsetMs}ms");
|
auction.AddLog($"[BID] Timer locale={estimatedTimerMs:F0}ms, Offset utente={offsetMs}ms");
|
||||||
|
|
||||||
await ExecuteBid(auction, state, token);
|
await ExecuteBid(auction, state, settings, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
@@ -639,13 +788,13 @@ namespace AutoBidder.Services
|
|||||||
? auction.BidBeforeDeadlineMs
|
? auction.BidBeforeDeadlineMs
|
||||||
: settings.DefaultBidBeforeDeadlineMs;
|
: settings.DefaultBidBeforeDeadlineMs;
|
||||||
double timerMs = state.Timer * 1000;
|
double timerMs = state.Timer * 1000;
|
||||||
await TryPlaceBidTicker(auction, timerMs, offsetMs, token);
|
await TryPlaceBidTicker(auction, timerMs, offsetMs, settings, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Esegue la puntata e registra metriche
|
/// Esegue la puntata e registra metriche
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task ExecuteBid(AuctionInfo auction, AuctionState state, CancellationToken token)
|
private async Task ExecuteBid(AuctionInfo auction, AuctionState state, AppSettings settings, CancellationToken token)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -694,7 +843,6 @@ namespace AutoBidder.Services
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
var pollingPing = auction.PollingLatencyMs;
|
var pollingPing = auction.PollingLatencyMs;
|
||||||
var settings = SettingsManager.Load();
|
|
||||||
|
|
||||||
// Rileva errore "timer scaduto" per feedback utente
|
// Rileva errore "timer scaduto" per feedback utente
|
||||||
bool isLateBid = result.Error?.Contains("timer") == true ||
|
bool isLateBid = result.Error?.Contains("timer") == true ||
|
||||||
@@ -740,16 +888,11 @@ namespace AutoBidder.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ShouldBid(AuctionInfo auction, AuctionState state)
|
private bool ShouldBid(AuctionInfo auction, AuctionState state, AppSettings? settings = null)
|
||||||
{
|
{
|
||||||
var settings = Utilities.SettingsManager.Load();
|
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 +900,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 +933,108 @@ 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: Limite puntate per questa asta
|
||||||
|
// Controlla MaxClicks (impostato dall'utente nella griglia o da product stats)
|
||||||
|
{
|
||||||
|
int maxBids = auction.MaxClicks; // 0 = illimitato
|
||||||
|
int usedBids = auction.BidsUsedOnThisAuction ?? 0;
|
||||||
|
|
||||||
|
if (maxBids > 0 && usedBids >= maxBids)
|
||||||
|
{
|
||||||
|
auction.AddLog($"⛔ Limite puntate raggiunto ({usedBids}/{maxBids})",
|
||||||
|
Models.AuctionLogLevel.Warning, Models.AuctionLogCategory.Limit);
|
||||||
|
LogBlockThrottled(auction, "MAXBIDS", $"[LIMIT] {auction.Name}: puntate {usedBids}/{maxBids} - STOP");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxBids > 0)
|
||||||
|
{
|
||||||
|
auction.AddLog($"✓ Puntate OK ({usedBids}/{maxBids})",
|
||||||
|
Models.AuctionLogLevel.Debug, Models.AuctionLogCategory.Limit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CONTROLLO 4: 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;
|
||||||
}
|
}
|
||||||
@@ -968,7 +1093,7 @@ namespace AutoBidder.Services
|
|||||||
{
|
{
|
||||||
Timestamp = DateTime.UtcNow,
|
Timestamp = DateTime.UtcNow,
|
||||||
EventType = BidEventType.Reset,
|
EventType = BidEventType.Reset,
|
||||||
Bidder = state.LastBidder,
|
Bidder = state.LastBidder ?? "",
|
||||||
Price = state.Price,
|
Price = state.Price,
|
||||||
Timer = state.Timer,
|
Timer = state.Timer,
|
||||||
Notes = $"Puntata: EUR{state.Price:F2}"
|
Notes = $"Puntata: EUR{state.Price:F2}"
|
||||||
|
|||||||
@@ -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"))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -428,17 +436,40 @@ namespace AutoBidder.Utilities
|
|||||||
private static readonly string _folder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "AutoBidder");
|
private static readonly string _folder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "AutoBidder");
|
||||||
private static readonly string _file = Path.Combine(_folder, "settings.json");
|
private static readonly string _file = Path.Combine(_folder, "settings.json");
|
||||||
|
|
||||||
|
// Cache per evitare letture disco ripetute nel hot path (ticker loop 50ms)
|
||||||
|
private static readonly object _cacheLock = new();
|
||||||
|
private static AppSettings? _cached;
|
||||||
|
private static DateTime _cacheExpiry = DateTime.MinValue;
|
||||||
|
private const int CACHE_TTL_MS = 2000; // Ricarica da disco al massimo ogni 2s
|
||||||
|
|
||||||
public static AppSettings Load()
|
public static AppSettings Load()
|
||||||
{
|
{
|
||||||
try
|
lock (_cacheLock)
|
||||||
{
|
{
|
||||||
if (!File.Exists(_file)) return new AppSettings();
|
var now = DateTime.UtcNow;
|
||||||
var txt = File.ReadAllText(_file);
|
if (_cached != null && now < _cacheExpiry)
|
||||||
var s = JsonSerializer.Deserialize<AppSettings>(txt);
|
return _cached;
|
||||||
if (s == null) return new AppSettings();
|
|
||||||
return s;
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(_file))
|
||||||
|
{
|
||||||
|
_cached = new AppSettings();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var txt = File.ReadAllText(_file);
|
||||||
|
_cached = JsonSerializer.Deserialize<AppSettings>(txt) ?? new AppSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_cached ??= new AppSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
_cacheExpiry = now.AddMilliseconds(CACHE_TTL_MS);
|
||||||
|
return _cached;
|
||||||
}
|
}
|
||||||
catch { return new AppSettings(); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Save(AppSettings settings)
|
public static void Save(AppSettings settings)
|
||||||
@@ -448,6 +479,13 @@ namespace AutoBidder.Utilities
|
|||||||
if (!Directory.Exists(_folder)) Directory.CreateDirectory(_folder);
|
if (!Directory.Exists(_folder)) Directory.CreateDirectory(_folder);
|
||||||
var txt = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true });
|
var txt = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true });
|
||||||
File.WriteAllText(_file, txt);
|
File.WriteAllText(_file, txt);
|
||||||
|
|
||||||
|
// Invalida cache così il prossimo Load() legge i nuovi valori
|
||||||
|
lock (_cacheLock)
|
||||||
|
{
|
||||||
|
_cached = settings;
|
||||||
|
_cacheExpiry = DateTime.UtcNow.AddMilliseconds(CACHE_TTL_MS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user