Ottimizzazione RAM, UI e sistema di timing aste
- Ridotto consumo RAM: limiti log, pulizia e compattazione dati aste, timer periodico di cleanup - UI più fluida: cache locale aste, throttling aggiornamenti, refresh log solo se necessario - Nuovo sistema Ticker Loop: timing configurabile, strategie solo vicino alla scadenza, feedback puntate tardive - Migliorato layout e splitter, log visivo, gestione cache HTML - Aggiornata UI impostazioni e fix vari per performance e thread-safety
This commit is contained in:
@@ -13,8 +13,9 @@ namespace AutoBidder.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Numero massimo di righe di log da mantenere per ogni asta
|
||||
/// Ridotto per ottimizzare consumo RAM
|
||||
/// </summary>
|
||||
private const int MAX_LOG_LINES = 500;
|
||||
private const int MAX_LOG_LINES = 200;
|
||||
|
||||
public string AuctionId { get; set; } = "";
|
||||
public string Name { get; set; } = ""; // Opzionale, può essere lasciato vuoto
|
||||
@@ -176,6 +177,11 @@ namespace AutoBidder.Models
|
||||
{
|
||||
var timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
|
||||
|
||||
// DEBUG: Print per verificare che i log vengano aggiunti
|
||||
#if DEBUG
|
||||
System.Diagnostics.Debug.WriteLine($"[AddLog] {AuctionId}: {message}");
|
||||
#endif
|
||||
|
||||
// ?? DEDUPLICAZIONE: Se l'ultimo messaggio è uguale, incrementa contatore
|
||||
if (AuctionLog.Count > 0)
|
||||
{
|
||||
@@ -233,9 +239,9 @@ namespace AutoBidder.Models
|
||||
public List<int> LatencyHistory { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Numero massimo di latenze da memorizzare
|
||||
/// Numero massimo di latenze da memorizzare (ridotto per RAM)
|
||||
/// </summary>
|
||||
private const int MAX_LATENCY_HISTORY = 20;
|
||||
private const int MAX_LATENCY_HISTORY = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Aggiunge una misurazione di latenza allo storico
|
||||
@@ -390,6 +396,103 @@ namespace AutoBidder.Models
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public double DuelAdvantage { get; set; } = 0;
|
||||
|
||||
// ???????????????????????????????????????????????????????????????????
|
||||
// GESTIONE MEMORIA
|
||||
// ???????????????????????????????????????????????????????????????????
|
||||
|
||||
/// <summary>
|
||||
/// Pulisce tutti i dati in memoria dell'asta per liberare RAM.
|
||||
/// Chiamare prima di rimuovere l'asta dalla lista.
|
||||
/// </summary>
|
||||
public void ClearData()
|
||||
{
|
||||
// Pulisci liste storiche
|
||||
BidHistory?.Clear();
|
||||
BidHistory = null!;
|
||||
|
||||
RecentBids?.Clear();
|
||||
RecentBids = null!;
|
||||
|
||||
AuctionLog?.Clear();
|
||||
AuctionLog = null!;
|
||||
|
||||
BidderStats?.Clear();
|
||||
BidderStats = null!;
|
||||
|
||||
LatencyHistory?.Clear();
|
||||
LatencyHistory = null!;
|
||||
|
||||
AggressiveBidders?.Clear();
|
||||
AggressiveBidders = null!;
|
||||
|
||||
// Pulisci oggetti complessi
|
||||
LastState = null;
|
||||
CalculatedValue = null;
|
||||
DuelOpponent = null;
|
||||
WinLimitDescription = null;
|
||||
|
||||
// Reset flag
|
||||
IsTrackedFromStart = false;
|
||||
TrackingStartedAt = null;
|
||||
DeadlineUtc = null;
|
||||
LastDeadlineUpdateUtc = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compatta i dati mantenendo solo le informazioni recenti.
|
||||
/// Utile per ridurre la memoria senza eliminare completamente i dati.
|
||||
/// </summary>
|
||||
public void CompactData(int maxBidHistory = 50, int maxRecentBids = 30, int maxLogLines = 100)
|
||||
{
|
||||
// Compatta BidHistory
|
||||
if (BidHistory != null && BidHistory.Count > maxBidHistory)
|
||||
{
|
||||
var recent = BidHistory.TakeLast(maxBidHistory).ToList();
|
||||
BidHistory.Clear();
|
||||
BidHistory.AddRange(recent);
|
||||
BidHistory.TrimExcess();
|
||||
}
|
||||
|
||||
// Compatta RecentBids
|
||||
if (RecentBids != null && RecentBids.Count > maxRecentBids)
|
||||
{
|
||||
var recent = RecentBids.TakeLast(maxRecentBids).ToList();
|
||||
RecentBids.Clear();
|
||||
RecentBids.AddRange(recent);
|
||||
RecentBids.TrimExcess();
|
||||
}
|
||||
|
||||
// Compatta AuctionLog
|
||||
if (AuctionLog != null && AuctionLog.Count > maxLogLines)
|
||||
{
|
||||
var recent = AuctionLog.TakeLast(maxLogLines).ToList();
|
||||
AuctionLog.Clear();
|
||||
AuctionLog.AddRange(recent);
|
||||
AuctionLog.TrimExcess();
|
||||
}
|
||||
|
||||
// Compatta LatencyHistory
|
||||
if (LatencyHistory != null && LatencyHistory.Count > 10)
|
||||
{
|
||||
var recent = LatencyHistory.TakeLast(10).ToList();
|
||||
LatencyHistory.Clear();
|
||||
LatencyHistory.AddRange(recent);
|
||||
LatencyHistory.TrimExcess();
|
||||
}
|
||||
|
||||
// Compatta BidderStats - mantieni solo i top bidders
|
||||
if (BidderStats != null && BidderStats.Count > 20)
|
||||
{
|
||||
var topBidders = BidderStats
|
||||
.OrderByDescending(kv => kv.Value.BidCount)
|
||||
.Take(20)
|
||||
.ToDictionary(kv => kv.Key, kv => kv.Value, StringComparer.OrdinalIgnoreCase);
|
||||
BidderStats.Clear();
|
||||
foreach (var kv in topBidders)
|
||||
BidderStats[kv.Key] = kv.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<div class="status-indicators">
|
||||
<div class="status-pill total" title="Totale aste">
|
||||
<i class="bi bi-collection"></i>
|
||||
<span>@auctions.Count</span>
|
||||
<span>@(auctions?.Count ?? 0)</span>
|
||||
</div>
|
||||
<div class="status-pill active" title="Aste attive">
|
||||
<i class="bi bi-play-circle-fill"></i>
|
||||
@@ -76,7 +76,7 @@
|
||||
<i class="bi bi-x-circle"></i>
|
||||
</button>
|
||||
<div class="manage-separator"></div>
|
||||
<button class="manage-btn danger-fill" @onclick="RemoveAllAuctions" disabled="@(auctions.Count == 0)" title="Rimuovi TUTTE">
|
||||
<button class="manage-btn danger-fill" @onclick="RemoveAllAuctions" disabled="@((auctions?.Count ?? 0) == 0)" title="Rimuovi TUTTE">
|
||||
<i class="bi bi-trash-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -91,7 +91,7 @@
|
||||
<div class="panel-header">
|
||||
<span><i class="bi bi-list-check"></i> Aste Monitorate</span>
|
||||
</div>
|
||||
@if (auctions.Count == 0)
|
||||
@if ((auctions?.Count ?? 0) == 0)
|
||||
{
|
||||
<div class="alert alert-info animate-fade-in-up m-2">
|
||||
<i class="bi bi-info-circle"></i> Nessuna asta monitorata. Clicca su <i class="bi bi-plus-lg"></i> per iniziare.
|
||||
@@ -521,7 +521,7 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@* Script Splitter *@
|
||||
@* Script Splitter - Versione Fixed senza sovrapposizioni *@
|
||||
<script suppress-error="BL9992">
|
||||
(function() {
|
||||
function initSplitters() {
|
||||
@@ -541,12 +541,23 @@
|
||||
let startPos = 0;
|
||||
let startSizeA = 0;
|
||||
let startSizeB = 0;
|
||||
let containerSize = 0;
|
||||
|
||||
function onMouseDown(e, type, elA, elB) {
|
||||
active = { type, elA, elB };
|
||||
startPos = type === 'v' ? e.clientX : e.clientY;
|
||||
startSizeA = type === 'v' ? elA.offsetWidth : elA.offsetHeight;
|
||||
startSizeB = type === 'v' ? elB.offsetWidth : elB.offsetHeight;
|
||||
|
||||
// Calcola dimensioni attuali
|
||||
if (type === 'v') {
|
||||
startSizeA = elA.offsetWidth;
|
||||
startSizeB = elB.offsetWidth;
|
||||
containerSize = elA.parentElement.offsetWidth - gutterV.offsetWidth;
|
||||
} else {
|
||||
startSizeA = elA.offsetHeight;
|
||||
startSizeB = elB.offsetHeight;
|
||||
containerSize = elA.parentElement.offsetHeight - gutterH.offsetHeight;
|
||||
}
|
||||
|
||||
document.body.style.cursor = type === 'v' ? 'col-resize' : 'row-resize';
|
||||
document.body.style.userSelect = 'none';
|
||||
e.preventDefault();
|
||||
@@ -560,16 +571,34 @@
|
||||
const { type, elA, elB } = active;
|
||||
const pos = type === 'v' ? e.clientX : e.clientY;
|
||||
const diff = pos - startPos;
|
||||
const total = startSizeA + startSizeB;
|
||||
|
||||
let newA = startSizeA + diff;
|
||||
let newB = startSizeB - diff;
|
||||
|
||||
const minA = type === 'v' ? 200 : 150;
|
||||
const minB = type === 'v' ? 150 : 80;
|
||||
// Limiti minimi
|
||||
const minA = type === 'v' ? 300 : 200;
|
||||
const minB = type === 'v' ? 200 : 150;
|
||||
|
||||
if (newA < minA) { newA = minA; newB = total - newA; }
|
||||
if (newB < minB) { newB = minB; newA = total - newB; }
|
||||
// Applica limiti
|
||||
if (newA < minA) {
|
||||
newA = minA;
|
||||
newB = containerSize - newA;
|
||||
}
|
||||
if (newB < minB) {
|
||||
newB = minB;
|
||||
newA = containerSize - newB;
|
||||
}
|
||||
|
||||
// Assicura che la somma sia corretta (no sovrapposizioni/gap)
|
||||
const totalCheck = newA + newB;
|
||||
if (Math.abs(totalCheck - containerSize) > 1) {
|
||||
// Normalizza le dimensioni
|
||||
const ratio = containerSize / totalCheck;
|
||||
newA = Math.round(newA * ratio);
|
||||
newB = containerSize - newA;
|
||||
}
|
||||
|
||||
// Applica le nuove dimensioni con flex none per dimensioni fisse
|
||||
if (type === 'v') {
|
||||
elA.style.width = newA + 'px';
|
||||
elA.style.flex = 'none';
|
||||
|
||||
@@ -15,7 +15,44 @@ namespace AutoBidder.Pages
|
||||
[Inject] private HtmlCacheService HtmlCache { get; set; } = default!;
|
||||
[Inject] private StatsService StatsService { get; set; } = default!;
|
||||
|
||||
private List<AuctionInfo> auctions => AppState.Auctions.ToList();
|
||||
// Cache locale per evitare ricreare liste ad ogni render
|
||||
private List<AuctionInfo>? _cachedAuctions = new List<AuctionInfo>(); // Inizializzata per evitare NullRef
|
||||
private int _lastAuctionsHash;
|
||||
|
||||
private List<AuctionInfo> auctions
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
// Protezione null-safety
|
||||
if (AppState == null) return _cachedAuctions ?? new List<AuctionInfo>();
|
||||
|
||||
// Usa cache per evitare copie continue
|
||||
var current = AppState.GetAuctionsDirectRef();
|
||||
if (current == null) return _cachedAuctions ?? new List<AuctionInfo>();
|
||||
|
||||
var hash = current.Count;
|
||||
if (_cachedAuctions == null || hash != _lastAuctionsHash)
|
||||
{
|
||||
_cachedAuctions = current.ToList();
|
||||
_lastAuctionsHash = hash;
|
||||
}
|
||||
return _cachedAuctions;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fallback sicuro
|
||||
return _cachedAuctions ?? new List<AuctionInfo>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Invalida cache quando necessario
|
||||
private void InvalidateAuctionCache()
|
||||
{
|
||||
_cachedAuctions = null;
|
||||
}
|
||||
|
||||
private AuctionInfo? selectedAuction
|
||||
{
|
||||
@@ -37,7 +74,7 @@ namespace AutoBidder.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private List<LogEntry> globalLog => AppState.GlobalLog.ToList();
|
||||
private List<LogEntry> globalLog => AppState.GetLogDirectRef();
|
||||
private bool isMonitoringActive
|
||||
{
|
||||
get => AppState.IsMonitoringActive;
|
||||
@@ -45,8 +82,16 @@ namespace AutoBidder.Pages
|
||||
}
|
||||
|
||||
|
||||
private System.Threading.Timer? refreshTimer;
|
||||
private System.Threading.Timer? sessionTimer;
|
||||
private System.Threading.Timer? logRefreshTimer;
|
||||
private DateTime _lastUiUpdate = DateTime.MinValue;
|
||||
private DateTime _lastLogRefresh = DateTime.MinValue;
|
||||
private const int UI_UPDATE_THROTTLE_MS = 250; // Max 4 aggiornamenti al secondo
|
||||
private const int LOG_REFRESH_MS = 500; // Aggiorna log ogni 500ms se ci sono cambiamenti
|
||||
|
||||
// Tracking log asta selezionata
|
||||
private int _lastSelectedAuctionLogCount = 0;
|
||||
private string? _lastSelectedAuctionId = null;
|
||||
|
||||
// Dialog Aggiungi Asta
|
||||
private bool showAddDialog = false;
|
||||
@@ -92,20 +137,68 @@ namespace AutoBidder.Pages
|
||||
AuctionMonitor.OnLog += OnGlobalLog;
|
||||
AuctionMonitor.OnAuctionUpdated += OnAuctionUpdated;
|
||||
|
||||
refreshTimer = new System.Threading.Timer(async _ =>
|
||||
// RIMOSSO: Timer refresh ogni 1s causava lag estremo
|
||||
// Gli aggiornamenti UI ora avvengono SOLO quando necessario via eventi
|
||||
|
||||
// Timer dedicato per aggiornare i log (leggero, solo se ci sono cambiamenti)
|
||||
logRefreshTimer = new System.Threading.Timer(async _ =>
|
||||
{
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
|
||||
try
|
||||
{
|
||||
bool needsRefresh = false;
|
||||
|
||||
// Controlla se il log globale è cambiato
|
||||
var currentGlobalLogCount = globalLog.Count;
|
||||
if (currentGlobalLogCount != lastLogCount)
|
||||
{
|
||||
lastLogCount = currentGlobalLogCount;
|
||||
needsRefresh = true;
|
||||
}
|
||||
|
||||
// Controlla se l'asta selezionata ha nuovi log
|
||||
var selected = selectedAuction;
|
||||
if (selected != null)
|
||||
{
|
||||
var currentAuctionLogCount = selected.AuctionLog.Count;
|
||||
|
||||
// Se è cambiata l'asta selezionata, forza refresh
|
||||
if (_lastSelectedAuctionId != selected.AuctionId)
|
||||
{
|
||||
_lastSelectedAuctionId = selected.AuctionId;
|
||||
_lastSelectedAuctionLogCount = currentAuctionLogCount;
|
||||
needsRefresh = true;
|
||||
}
|
||||
// Se il count dei log è cambiato, refresh
|
||||
else if (currentAuctionLogCount != _lastSelectedAuctionLogCount)
|
||||
{
|
||||
_lastSelectedAuctionLogCount = currentAuctionLogCount;
|
||||
needsRefresh = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Nessuna asta selezionata, reset tracking
|
||||
_lastSelectedAuctionId = null;
|
||||
_lastSelectedAuctionLogCount = 0;
|
||||
}
|
||||
|
||||
if (needsRefresh)
|
||||
{
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
catch { /* Ignora errori del timer */ }
|
||||
}, null, TimeSpan.FromMilliseconds(300), TimeSpan.FromMilliseconds(300));
|
||||
|
||||
// Carica sessione all'avvio
|
||||
LoadSession();
|
||||
|
||||
// Timer per aggiornamento sessione ogni 30 secondi
|
||||
// Timer per aggiornamento sessione ogni 60 secondi (era 30)
|
||||
sessionTimer = new System.Threading.Timer(async _ =>
|
||||
{
|
||||
await RefreshSessionAsync();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}, null, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30));
|
||||
await ThrottledStateHasChanged();
|
||||
}, null, TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(60));
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
@@ -132,6 +225,20 @@ namespace AutoBidder.Pages
|
||||
// Handler async per eventi da background thread
|
||||
private async Task OnAppStateChangedAsync()
|
||||
{
|
||||
await ThrottledStateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggiorna UI con throttling per evitare troppi re-render
|
||||
/// </summary>
|
||||
private async Task ThrottledStateHasChanged()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
if ((now - _lastUiUpdate).TotalMilliseconds < UI_UPDATE_THROTTLE_MS)
|
||||
{
|
||||
return; // Skip, aggiornamento recente già fatto
|
||||
}
|
||||
_lastUiUpdate = now;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
@@ -171,12 +278,12 @@ namespace AutoBidder.Pages
|
||||
private void OnGlobalLog(string message)
|
||||
{
|
||||
AppState.AddLog($"[{DateTime.Now:HH:mm:ss}] {message}");
|
||||
InvokeAsync(StateHasChanged);
|
||||
// Non forziamo StateHasChanged qui - verrà aggiornato dal throttle
|
||||
}
|
||||
|
||||
private void OnAuctionUpdated(AuctionState state)
|
||||
{
|
||||
var auction = auctions.FirstOrDefault(a => a.AuctionId == state.AuctionId);
|
||||
var auction = AppState.GetAuctionById(state.AuctionId);
|
||||
if (auction != null)
|
||||
{
|
||||
// Salva l'ultimo stato ricevuto
|
||||
@@ -188,18 +295,21 @@ namespace AutoBidder.Pages
|
||||
auction.BidsUsedOnThisAuction = state.MyBidsCount.Value;
|
||||
}
|
||||
|
||||
// Notifica il cambiamento usando InvokeAsync per thread-safety
|
||||
_ = InvokeAsync(() =>
|
||||
{
|
||||
AppState.ForceUpdate();
|
||||
StateHasChanged();
|
||||
});
|
||||
// Invalida cache
|
||||
InvalidateAuctionCache();
|
||||
|
||||
// Notifica con throttling
|
||||
_ = ThrottledStateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectAuction(AuctionInfo auction)
|
||||
{
|
||||
selectedAuction = auction;
|
||||
// Imposta direttamente senza notifiche async per risposta immediata
|
||||
AppState.SetSelectedAuctionDirect(auction);
|
||||
|
||||
// Forza aggiornamento immediato per visualizzare i log dell'asta selezionata
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
// Gestione controlli globali
|
||||
@@ -1384,8 +1494,8 @@ namespace AutoBidder.Pages
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
refreshTimer?.Dispose();
|
||||
sessionTimer?.Dispose();
|
||||
logRefreshTimer?.Dispose();
|
||||
|
||||
// Rimuovi sottoscrizioni (ASYNC)
|
||||
if (AppState != null)
|
||||
@@ -1475,34 +1585,69 @@ namespace AutoBidder.Pages
|
||||
|
||||
private int GetActiveAuctionsCount()
|
||||
{
|
||||
return auctions.Count(a => a.IsActive && !a.IsPaused &&
|
||||
(a.LastState == null || a.LastState.Status == AuctionStatus.Running));
|
||||
try
|
||||
{
|
||||
return auctions?.Count(a => a.IsActive && !a.IsPaused &&
|
||||
(a.LastState == null || a.LastState.Status == AuctionStatus.Running)) ?? 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private int GetPausedAuctionsCount()
|
||||
{
|
||||
return auctions.Count(a => a.IsPaused ||
|
||||
(a.LastState != null && a.LastState.Status == AuctionStatus.Paused));
|
||||
try
|
||||
{
|
||||
return auctions?.Count(a => a.IsPaused ||
|
||||
(a.LastState != null && a.LastState.Status == AuctionStatus.Paused)) ?? 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private int GetWonAuctionsCount()
|
||||
{
|
||||
return auctions.Count(a => a.LastState != null &&
|
||||
a.LastState.Status == AuctionStatus.EndedWon);
|
||||
try
|
||||
{
|
||||
return auctions?.Count(a => a.LastState != null &&
|
||||
a.LastState.Status == AuctionStatus.EndedWon) ?? 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private int GetLostAuctionsCount()
|
||||
{
|
||||
return auctions.Count(a => a.LastState != null &&
|
||||
a.LastState.Status == AuctionStatus.EndedLost);
|
||||
try
|
||||
{
|
||||
return auctions?.Count(a => a.LastState != null &&
|
||||
a.LastState.Status == AuctionStatus.EndedLost) ?? 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private int GetStoppedAuctionsCount()
|
||||
{
|
||||
return auctions.Count(a => !a.IsActive &&
|
||||
(a.LastState == null ||
|
||||
(a.LastState.Status != AuctionStatus.EndedWon &&
|
||||
a.LastState.Status != AuctionStatus.EndedLost)));
|
||||
try
|
||||
{
|
||||
return auctions?.Count(a => !a.IsActive &&
|
||||
(a.LastState == null ||
|
||||
(a.LastState.Status != AuctionStatus.EndedWon &&
|
||||
a.LastState.Status != AuctionStatus.EndedLost))) ?? 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +138,15 @@
|
||||
Usa i pulsanti <i class="bi bi-arrow-repeat"></i> per applicare la singola impostazione a tutte le aste
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<!-- TICKER LOOP - TIMING -->
|
||||
<h6 class="fw-bold mb-3 text-primary"><i class="bi bi-clock-history"></i> Sistema di Timing (Ticker Loop)</h6>
|
||||
<div class="alert alert-warning border-0 shadow-sm mb-3">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||
<strong>Importante:</strong> Il sistema rispetta ESATTAMENTE i valori inseriti.
|
||||
Se la puntata arriva tardi, aumenta l'"Anticipo puntata". Non vengono applicate compensazioni automatiche.
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-bold"><i class="bi bi-speedometer2"></i> Anticipo puntata (ms)</label>
|
||||
<div class="input-group">
|
||||
@@ -149,7 +157,33 @@
|
||||
<i class="bi bi-arrow-repeat"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-text">Millisecondi prima della scadenza per tentare la puntata</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-bold"><i class="bi bi-hourglass-split"></i> Intervallo Ticker (ms)</label>
|
||||
<input type="number" class="form-control" @bind="settings.TickerIntervalMs" min="10" max="500" />
|
||||
<div class="form-text">Più basso = più preciso ma più CPU. Consigliato: 50-100ms</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-bold"><i class="bi bi-funnel"></i> Soglia controllo strategie (ms)</label>
|
||||
<input type="number" class="form-control" @bind="settings.StrategyCheckThresholdMs" />
|
||||
<div class="form-text">Inizia a verificare le strategie solo sotto questa soglia (ottimizza CPU)</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="form-check form-switch mt-4">
|
||||
<input type="checkbox" class="form-check-input" id="lateBidWarning" @bind="settings.ShowLateBidWarning" />
|
||||
<label class="form-check-label" for="lateBidWarning">
|
||||
<strong>Mostra avviso puntata tardiva</strong>
|
||||
<div class="form-text">Suggerisce di aumentare l'anticipo se la puntata arriva tardi</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4" />
|
||||
<h6 class="fw-bold mb-3"><i class="bi bi-sliders"></i> Limiti Default</h6>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-bold"><i class="bi bi-hand-index-thumb"></i> Click massimi</label>
|
||||
<div class="input-group">
|
||||
|
||||
@@ -551,4 +551,48 @@ app.MapRazorPages(); // ? AGGIUNTO: abilita Razor Pages (Login, Logout)
|
||||
app.MapBlazorHub();
|
||||
app.MapFallbackToPage("/_Host");
|
||||
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
// TIMER PULIZIA MEMORIA PERIODICA
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
|
||||
// Timer per pulizia periodica della memoria (ogni 5 minuti)
|
||||
var memoryCleanupTimer = new System.Threading.Timer(async _ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using var scope = app.Services.CreateScope();
|
||||
var appState = scope.ServiceProvider.GetRequiredService<ApplicationStateService>();
|
||||
var htmlCache = scope.ServiceProvider.GetRequiredService<HtmlCacheService>();
|
||||
|
||||
// Pulisci cache HTML scaduta
|
||||
htmlCache.CleanExpiredCache();
|
||||
|
||||
// Compatta dati aste completate
|
||||
appState.CleanupCompletedAuctions();
|
||||
|
||||
// Forza garbage collection leggera
|
||||
GC.Collect(1, GCCollectionMode.Optimized, false);
|
||||
|
||||
// Log statistiche memoria
|
||||
var stats = appState.GetMemoryStats();
|
||||
var memoryMB = GC.GetTotalMemory(false) / 1024.0 / 1024.0;
|
||||
Console.WriteLine($"[MEMORY] Cleanup: {stats.AuctionsCount} aste, " +
|
||||
$"{stats.TotalBidHistoryEntries} bid history, " +
|
||||
$"{stats.TotalRecentBidsEntries} recent bids, " +
|
||||
$"{stats.GlobalLogEntries} global log, " +
|
||||
$"RAM: {memoryMB:F1}MB");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[MEMORY ERROR] Cleanup failed: {ex.Message}");
|
||||
}
|
||||
}, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
|
||||
|
||||
// Assicura che il timer venga disposto quando l'app si chiude
|
||||
app.Lifetime.ApplicationStopping.Register(() =>
|
||||
{
|
||||
Console.WriteLine("[SHUTDOWN] Disposing memory cleanup timer...");
|
||||
memoryCleanupTimer.Dispose();
|
||||
});
|
||||
|
||||
app.Run();
|
||||
|
||||
@@ -53,6 +53,32 @@ namespace AutoBidder.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene riferimento diretto alla lista per lettura veloce (NO COPY).
|
||||
/// ATTENZIONE: Non modificare la lista, usare solo per lettura!
|
||||
/// </summary>
|
||||
public List<AuctionInfo> GetAuctionsDirectRef()
|
||||
{
|
||||
return _auctions; // Accesso diretto senza lock per velocità
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene riferimento diretto al log per lettura veloce (NO COPY).
|
||||
/// </summary>
|
||||
public List<LogEntry> GetLogDirectRef()
|
||||
{
|
||||
return _globalLog;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Imposta l'asta selezionata SENZA notificare eventi async.
|
||||
/// Usare per risposta UI immediata.
|
||||
/// </summary>
|
||||
public void SetSelectedAuctionDirect(AuctionInfo? auction)
|
||||
{
|
||||
_selectedAuction = auction;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene la lista originale delle aste per il salvataggio.
|
||||
/// ATTENZIONE: Usare solo per persistenza, non per iterazione durante modifiche!
|
||||
@@ -277,15 +303,16 @@ namespace AutoBidder.Services
|
||||
{
|
||||
_globalLog.Add(entry);
|
||||
|
||||
// Mantieni solo gli ultimi 1000 log
|
||||
if (_globalLog.Count > 1000)
|
||||
// Mantieni solo gli ultimi 500 log (ridotto da 1000 per RAM)
|
||||
if (_globalLog.Count > 500)
|
||||
{
|
||||
_globalLog.RemoveRange(0, _globalLog.Count - 1000);
|
||||
_globalLog.RemoveRange(0, _globalLog.Count - 500);
|
||||
_globalLog.TrimExcess();
|
||||
}
|
||||
}
|
||||
|
||||
_ = NotifyLogAddedAsync(message);
|
||||
_ = NotifyStateChangedAsync();
|
||||
// RIMOSSO: NotifyStateChangedAsync qui causava troppi re-render
|
||||
// I log vengono visualizzati al prossimo refresh naturale
|
||||
}
|
||||
|
||||
public void ClearLog()
|
||||
@@ -391,6 +418,80 @@ namespace AutoBidder.Services
|
||||
{
|
||||
_ = NotifyStateChangedAsync();
|
||||
}
|
||||
|
||||
// ???????????????????????????????????????????????????????????????????
|
||||
// GESTIONE MEMORIA
|
||||
// ???????????????????????????????????????????????????????????????????
|
||||
|
||||
/// <summary>
|
||||
/// Compatta i dati di tutte le aste per ridurre il consumo RAM
|
||||
/// </summary>
|
||||
public void CompactAllAuctions()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var auction in _auctions)
|
||||
{
|
||||
try
|
||||
{
|
||||
auction.CompactData();
|
||||
}
|
||||
catch { /* Ignora errori */ }
|
||||
}
|
||||
}
|
||||
Console.WriteLine($"[AppState] Compattati dati di {_auctions.Count} aste");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pulisce i dati delle aste terminate dalla memoria
|
||||
/// </summary>
|
||||
public void CleanupCompletedAuctions()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var auction in _auctions.Where(a => !a.IsActive))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Per le aste terminate, mantieni solo dati essenziali
|
||||
auction.CompactData(maxBidHistory: 20, maxRecentBids: 10, maxLogLines: 50);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ritorna statistiche sull'uso della memoria
|
||||
/// </summary>
|
||||
public MemoryStats GetMemoryStats()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return new MemoryStats
|
||||
{
|
||||
AuctionsCount = _auctions.Count,
|
||||
ActiveAuctionsCount = _auctions.Count(a => a.IsActive),
|
||||
TotalBidHistoryEntries = _auctions.Sum(a => a.BidHistory?.Count ?? 0),
|
||||
TotalRecentBidsEntries = _auctions.Sum(a => a.RecentBids?.Count ?? 0),
|
||||
TotalLogEntries = _auctions.Sum(a => a.AuctionLog?.Count ?? 0),
|
||||
GlobalLogEntries = _globalLog.Count
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Statistiche memoria per debug
|
||||
/// </summary>
|
||||
public class MemoryStats
|
||||
{
|
||||
public int AuctionsCount { get; set; }
|
||||
public int ActiveAuctionsCount { get; set; }
|
||||
public int TotalBidHistoryEntries { get; set; }
|
||||
public int TotalRecentBidsEntries { get; set; }
|
||||
public int TotalLogEntries { get; set; }
|
||||
public int GlobalLogEntries { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -137,6 +137,13 @@ namespace AutoBidder.Services
|
||||
OnLog?.Invoke($"[REMOVE] Rimozione asta non terminata: {auction.Name} (non salvata nelle statistiche)");
|
||||
}
|
||||
|
||||
// ?? IMPORTANTE: Pulisci tutti i dati in memoria per liberare RAM
|
||||
try
|
||||
{
|
||||
auction.ClearData();
|
||||
}
|
||||
catch { /* Ignora errori durante pulizia */ }
|
||||
|
||||
_auctions.Remove(auction);
|
||||
}
|
||||
}
|
||||
@@ -244,10 +251,18 @@ namespace AutoBidder.Services
|
||||
|
||||
private async Task MonitoringLoop(CancellationToken token)
|
||||
{
|
||||
var settings = SettingsManager.Load();
|
||||
int tickerIntervalMs = Math.Max(100, settings.TickerIntervalMs); // Minimo 100ms
|
||||
int pollingIntervalMs = 500; // Poll API ogni 500ms max
|
||||
DateTime lastPoll = DateTime.MinValue;
|
||||
|
||||
OnLog?.Invoke($"[TICKER] Avviato con intervallo={tickerIntervalMs}ms, polling={pollingIntervalMs}ms");
|
||||
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
// === FASE 1: Raccogli aste attive ===
|
||||
List<AuctionInfo> activeAuctions;
|
||||
lock (_auctions)
|
||||
{
|
||||
@@ -259,70 +274,48 @@ namespace AutoBidder.Services
|
||||
|
||||
if (activeAuctions.Count == 0)
|
||||
{
|
||||
await Task.Delay(1000, token);
|
||||
// Nessuna asta attiva - polling molto lento
|
||||
await Task.Delay(2000, token);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Poll tutte le aste in parallelo
|
||||
var pollTasks = activeAuctions.Select(a => PollAndProcessAuction(a, token));
|
||||
await Task.WhenAll(pollTasks);
|
||||
|
||||
// Ottimizzazione polling per aste in pausa
|
||||
bool anyPaused = false;
|
||||
DateTime now = DateTime.Now;
|
||||
int pauseDelayMs = 1000;
|
||||
foreach (var a in activeAuctions)
|
||||
{
|
||||
if (a.IsPaused)
|
||||
{
|
||||
anyPaused = true;
|
||||
if (now.Hour < 9 || (now.Hour == 9 && now.Minute < 55))
|
||||
pauseDelayMs = 60000;
|
||||
else if (now.Hour == 9 && now.Minute >= 55)
|
||||
pauseDelayMs = 5000;
|
||||
}
|
||||
}
|
||||
if (anyPaused)
|
||||
{
|
||||
await Task.Delay(pauseDelayMs, token);
|
||||
continue;
|
||||
}
|
||||
|
||||
// ?? Delay adattivo ULTRA-OTTIMIZZATO
|
||||
// Considera l'offset target più basso tra tutte le aste attive
|
||||
var settings = Utilities.SettingsManager.Load();
|
||||
// === FASE 2: Poll API solo ogni pollingIntervalMs ===
|
||||
var now = DateTime.UtcNow;
|
||||
bool shouldPoll = (now - lastPoll).TotalMilliseconds >= pollingIntervalMs;
|
||||
|
||||
// ?? Polling VELOCE quando vicino alla scadenza
|
||||
int minDelayMs = 500;
|
||||
// Poll più frequente se vicino alla scadenza
|
||||
bool anyNearDeadline = activeAuctions.Any(a =>
|
||||
GetEstimatedTimerMs(a) < settings.StrategyCheckThresholdMs);
|
||||
|
||||
foreach (var a in activeAuctions)
|
||||
if (shouldPoll || anyNearDeadline)
|
||||
{
|
||||
if (a.IsPaused || !a.LastDeadlineUpdateUtc.HasValue) continue;
|
||||
var pollTasks = activeAuctions.Select(a => PollAuctionState(a, token));
|
||||
await Task.WhenAll(pollTasks);
|
||||
lastPoll = now;
|
||||
}
|
||||
|
||||
// === FASE 3: TICKER CHECK - Verifica timing per ogni asta ===
|
||||
foreach (var auction in activeAuctions)
|
||||
{
|
||||
if (auction.IsPaused || auction.LastState == null) continue;
|
||||
|
||||
// Calcola tempo stimato rimanente
|
||||
var elapsed = (DateTime.UtcNow - a.LastDeadlineUpdateUtc.Value).TotalMilliseconds;
|
||||
double estimatedRemaining = a.LastRawTimer - elapsed;
|
||||
// Calcola timer stimato LOCALMENTE (più preciso del polling)
|
||||
double estimatedTimerMs = GetEstimatedTimerMs(auction);
|
||||
|
||||
int offsetMs = a.BidBeforeDeadlineMs > 0
|
||||
? a.BidBeforeDeadlineMs
|
||||
// Offset configurato dall'utente (SENZA compensazioni)
|
||||
int offsetMs = auction.BidBeforeDeadlineMs > 0
|
||||
? auction.BidBeforeDeadlineMs
|
||||
: settings.DefaultBidBeforeDeadlineMs;
|
||||
|
||||
double msToTarget = estimatedRemaining - offsetMs;
|
||||
|
||||
// Polling più veloce quando vicino al target
|
||||
int delay;
|
||||
if (msToTarget < 100) delay = 5;
|
||||
else if (msToTarget < 300) delay = 10;
|
||||
else if (msToTarget < 500) delay = 20;
|
||||
else if (msToTarget < 1000) delay = 50;
|
||||
else if (msToTarget < 2000) delay = 100;
|
||||
else if (msToTarget < 5000) delay = 200;
|
||||
else delay = 400;
|
||||
|
||||
minDelayMs = Math.Min(minDelayMs, delay);
|
||||
// TRIGGER: Timer <= Offset configurato dall'utente
|
||||
if (estimatedTimerMs <= offsetMs && estimatedTimerMs > 0)
|
||||
{
|
||||
await TryPlaceBidTicker(auction, estimatedTimerMs, offsetMs, token);
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(minDelayMs, token);
|
||||
|
||||
// === FASE 4: Delay fisso del ticker ===
|
||||
await Task.Delay(tickerIntervalMs, token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -330,10 +323,202 @@ namespace AutoBidder.Services
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnLog?.Invoke($"[ERRORE] Loop monitoraggio: {ex.Message}");
|
||||
await Task.Delay(1000, token);
|
||||
OnLog?.Invoke($"[ERRORE] Ticker loop: {ex.Message}");
|
||||
await Task.Delay(500, token);
|
||||
}
|
||||
}
|
||||
|
||||
OnLog?.Invoke("[TICKER] Fermato");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calcola il timer stimato localmente (interpolazione tra poll)
|
||||
/// </summary>
|
||||
private double GetEstimatedTimerMs(AuctionInfo auction)
|
||||
{
|
||||
if (!auction.LastDeadlineUpdateUtc.HasValue || auction.LastRawTimer <= 0)
|
||||
{
|
||||
return auction.LastState?.Timer * 1000 ?? 0;
|
||||
}
|
||||
|
||||
// Tempo trascorso dall'ultimo poll
|
||||
var elapsed = (DateTime.UtcNow - auction.LastDeadlineUpdateUtc.Value).TotalMilliseconds;
|
||||
|
||||
// Timer stimato = timer raw - tempo trascorso
|
||||
double estimated = auction.LastRawTimer - elapsed;
|
||||
|
||||
return Math.Max(0, estimated);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Poll dello stato dell'asta (SOLO aggiornamento dati, nessuna logica di puntata)
|
||||
/// </summary>
|
||||
private async Task PollAuctionState(AuctionInfo auction, CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
var state = await _apiClient.PollAuctionStateAsync(auction.AuctionId, auction.OriginalUrl, token);
|
||||
|
||||
if (state == null) return;
|
||||
|
||||
// Log primo poll per debug
|
||||
if (!auction.IsTrackedFromStart && auction.BidHistory.Count == 0)
|
||||
{
|
||||
auction.AddLog($"[START] Monitoraggio avviato - Timer: {state.Timer:F1}s, Prezzo: €{state.Price:F2}");
|
||||
}
|
||||
|
||||
// Aggiorna latenza
|
||||
auction.AddLatencyMeasurement(state.PollingLatencyMs);
|
||||
|
||||
// Tracking inizio
|
||||
if (!auction.IsTrackedFromStart && auction.BidHistory.Count == 0)
|
||||
{
|
||||
auction.IsTrackedFromStart = true;
|
||||
auction.TrackingStartedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
// Aggiorna storia puntate
|
||||
if (state.RecentBidsHistory != null && state.RecentBidsHistory.Count > 0)
|
||||
{
|
||||
MergeBidHistory(auction, state.RecentBidsHistory);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(state.LastBidder) && state.Price > 0)
|
||||
{
|
||||
EnsureCurrentBidInHistory(auction, state);
|
||||
}
|
||||
|
||||
// Gestione fine asta
|
||||
if (state.Status == AuctionStatus.EndedWon ||
|
||||
state.Status == AuctionStatus.EndedLost ||
|
||||
state.Status == AuctionStatus.Closed)
|
||||
{
|
||||
HandleAuctionEnded(auction, state);
|
||||
return;
|
||||
}
|
||||
|
||||
// Notifica SOLO se ci sono cambiamenti significativi
|
||||
bool shouldNotify = auction.LastState == null ||
|
||||
Math.Abs(auction.LastState.Price - state.Price) > 0.001 ||
|
||||
auction.LastState.LastBidder != state.LastBidder ||
|
||||
Math.Abs(auction.LastState.Timer - state.Timer) > 1;
|
||||
|
||||
auction.LastState = state;
|
||||
|
||||
if (shouldNotify)
|
||||
{
|
||||
OnAuctionUpdated?.Invoke(state);
|
||||
}
|
||||
|
||||
UpdateAuctionHistory(auction, state);
|
||||
|
||||
// Calcola valore prodotto
|
||||
UpdateProductValue(auction, state);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
auction.AddLog($"[POLL ERROR] {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gestisce la fine dell'asta
|
||||
/// </summary>
|
||||
private void HandleAuctionEnded(AuctionInfo auction, AuctionState state)
|
||||
{
|
||||
string statusMsg = state.Status == AuctionStatus.EndedWon ? "VINTA" :
|
||||
state.Status == AuctionStatus.EndedLost ? "Persa" : "Chiusa";
|
||||
bool won = state.Status == AuctionStatus.EndedWon;
|
||||
|
||||
// Fix: Aggiungi ultima puntata se mancante
|
||||
if (!string.IsNullOrEmpty(state.LastBidder) && state.Price > 0)
|
||||
{
|
||||
var statePrice = (decimal)state.Price;
|
||||
var alreadyExists = auction.RecentBids.Any(b =>
|
||||
Math.Abs(b.Price - statePrice) < 0.001m &&
|
||||
b.Username.Equals(state.LastBidder, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!alreadyExists)
|
||||
{
|
||||
auction.RecentBids.Insert(0, new BidHistoryEntry
|
||||
{
|
||||
Username = state.LastBidder,
|
||||
Price = statePrice,
|
||||
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
||||
BidType = "Auto"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
auction.IsActive = false;
|
||||
auction.LastState = state;
|
||||
auction.AddLog($"[ASTA TERMINATA] {statusMsg}");
|
||||
OnLog?.Invoke($"[FINE] [{auction.AuctionId}] Asta {statusMsg}");
|
||||
|
||||
// Feedback per aste perse
|
||||
var settings = SettingsManager.Load();
|
||||
if (!won && settings.ShowLateBidWarning)
|
||||
{
|
||||
// Se abbiamo provato a puntare ma fallito con errore timer
|
||||
var lastBidAttempt = auction.BidHistory
|
||||
.Where(b => b.EventType == BidEventType.MyBid && !b.Success)
|
||||
.OrderByDescending(b => b.Timestamp)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (lastBidAttempt != null &&
|
||||
(lastBidAttempt.Notes?.Contains("timer") == true ||
|
||||
lastBidAttempt.Notes?.Contains("scaduto") == true))
|
||||
{
|
||||
int currentOffset = auction.BidBeforeDeadlineMs > 0
|
||||
? auction.BidBeforeDeadlineMs
|
||||
: settings.DefaultBidBeforeDeadlineMs;
|
||||
|
||||
auction.AddLog($"[⚠️ SUGGERIMENTO] Puntata arrivata troppo tardi! " +
|
||||
$"Tempo attuale: {currentOffset}ms. " +
|
||||
$"Prova ad aumentarlo a {currentOffset + 500}ms o più.");
|
||||
OnLog?.Invoke($"[LATE] {auction.Name}: aumenta il tempo di puntata (attuale: {currentOffset}ms)");
|
||||
}
|
||||
}
|
||||
|
||||
auction.BidHistory.Add(new BidHistory
|
||||
{
|
||||
Timestamp = DateTime.UtcNow,
|
||||
EventType = BidEventType.Reset,
|
||||
Bidder = state.LastBidder,
|
||||
Price = state.Price,
|
||||
Timer = 0,
|
||||
Notes = $"Asta {statusMsg}"
|
||||
});
|
||||
|
||||
OnAuctionUpdated?.Invoke(state);
|
||||
|
||||
try
|
||||
{
|
||||
OnAuctionCompleted?.Invoke(auction, state, won);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnLog?.Invoke($"[STATS ERROR] {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggiorna il valore calcolato del prodotto
|
||||
/// </summary>
|
||||
private void UpdateProductValue(AuctionInfo auction, AuctionState state)
|
||||
{
|
||||
if (!auction.BuyNowPrice.HasValue && !auction.ShippingCost.HasValue) return;
|
||||
|
||||
try
|
||||
{
|
||||
var productValue = Utilities.ProductValueCalculator.Calculate(
|
||||
auction,
|
||||
state.Price,
|
||||
auction.RecentBids?.Count ?? 0
|
||||
);
|
||||
auction.CalculatedValue = productValue;
|
||||
}
|
||||
catch { /* Silenzioso */ }
|
||||
}
|
||||
|
||||
private bool IsAuctionTerminated(AuctionInfo auction)
|
||||
@@ -353,189 +538,91 @@ namespace AutoBidder.Services
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TICKER: Tenta la puntata quando il timer locale raggiunge l'offset configurato.
|
||||
/// NESSUNA compensazione automatica della latenza - l'utente decide il timing esatto.
|
||||
/// </summary>
|
||||
private async Task TryPlaceBidTicker(AuctionInfo auction, double estimatedTimerMs, int offsetMs, CancellationToken token)
|
||||
{
|
||||
var settings = SettingsManager.Load();
|
||||
var state = auction.LastState;
|
||||
|
||||
if (state == null || state.IsMyBid || estimatedTimerMs <= 0) return;
|
||||
|
||||
// Log timing se abilitato
|
||||
if (settings.LogTiming)
|
||||
{
|
||||
auction.AddLog($"[TICKER] Timer stimato={estimatedTimerMs:F0}ms <= Offset={offsetMs}ms");
|
||||
}
|
||||
|
||||
// === PROTEZIONE DOPPIA PUNTATA ===
|
||||
// Reset se timer è aumentato (qualcuno ha puntato = nuovo ciclo)
|
||||
if (estimatedTimerMs > auction.LastScheduledTimerMs + 500)
|
||||
{
|
||||
auction.BidScheduled = false;
|
||||
}
|
||||
|
||||
// Reset se passato troppo tempo dall'ultima puntata
|
||||
if (auction.LastClickAt.HasValue &&
|
||||
(DateTime.UtcNow - auction.LastClickAt.Value).TotalSeconds > 10)
|
||||
{
|
||||
auction.BidScheduled = false;
|
||||
}
|
||||
|
||||
// Skip se già schedulata per questo ciclo
|
||||
if (auction.BidScheduled && Math.Abs(auction.LastScheduledTimerMs - estimatedTimerMs) < 200)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Cooldown 1 secondo tra puntate
|
||||
if (auction.LastClickAt.HasValue &&
|
||||
(DateTime.UtcNow - auction.LastClickAt.Value).TotalMilliseconds < 1000)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// === OTTIMIZZAZIONE: Controlla strategie SOLO se vicino al target ===
|
||||
// Evita calcoli inutili quando siamo lontani
|
||||
if (estimatedTimerMs > settings.StrategyCheckThresholdMs)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// === CONTROLLI FONDAMENTALI ===
|
||||
if (!ShouldBid(auction, state))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// === STRATEGIE AVANZATE ===
|
||||
var session = _apiClient.GetSession();
|
||||
var decision = _bidStrategy.ShouldPlaceBid(auction, state, settings, session?.Username ?? "");
|
||||
|
||||
if (!decision.ShouldBid)
|
||||
{
|
||||
auction.AddLog($"[STRATEGY] {decision.Reason}");
|
||||
return;
|
||||
}
|
||||
|
||||
// === ESEGUI PUNTATA ===
|
||||
auction.BidScheduled = true;
|
||||
auction.LastScheduledTimerMs = estimatedTimerMs;
|
||||
|
||||
auction.AddLog($"[BID] Timer locale={estimatedTimerMs:F0}ms, Offset utente={offsetMs}ms");
|
||||
|
||||
await ExecuteBid(auction, state, token);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// METODO LEGACY - Mantenuto per compatibilità ma non più usato
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
[Obsolete("Usare TryPlaceBidTicker con il nuovo sistema Ticker Loop")]
|
||||
private async Task PollAndProcessAuction(AuctionInfo auction, CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
var state = await _apiClient.PollAuctionStateAsync(auction.AuctionId, auction.OriginalUrl, token);
|
||||
|
||||
if (state == null)
|
||||
{
|
||||
auction.AddLog("ERRORE: Nessun dato ricevuto da API");
|
||||
OnLog?.Invoke($"[ERRORE] [{auction.AuctionId}] API non ha risposto");
|
||||
return;
|
||||
}
|
||||
|
||||
// ?? Aggiorna latenza con storico
|
||||
auction.AddLatencyMeasurement(state.PollingLatencyMs);
|
||||
|
||||
// ?? Segna tracking dall'inizio se è la prima volta
|
||||
if (!auction.IsTrackedFromStart && auction.BidHistory.Count == 0)
|
||||
{
|
||||
auction.IsTrackedFromStart = true;
|
||||
auction.TrackingStartedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
// Aggiorna storia puntate mantenendo quelle vecchie
|
||||
if (state.RecentBidsHistory != null && state.RecentBidsHistory.Count > 0)
|
||||
{
|
||||
MergeBidHistory(auction, state.RecentBidsHistory);
|
||||
}
|
||||
|
||||
// ?? FIX: Aggiungi SEMPRE l'ultima puntata corrente allo storico (durante il monitoraggio)
|
||||
// Questo assicura che la puntata vincente sia sempre inclusa
|
||||
if (!string.IsNullOrEmpty(state.LastBidder) && state.Price > 0)
|
||||
{
|
||||
EnsureCurrentBidInHistory(auction, state);
|
||||
}
|
||||
|
||||
if (state.Status == AuctionStatus.EndedWon ||
|
||||
state.Status == AuctionStatus.EndedLost ||
|
||||
state.Status == AuctionStatus.Closed)
|
||||
{
|
||||
string statusMsg = state.Status == AuctionStatus.EndedWon ? "VINTA" :
|
||||
state.Status == AuctionStatus.EndedLost ? "Persa" : "Chiusa";
|
||||
|
||||
bool won = state.Status == AuctionStatus.EndedWon;
|
||||
|
||||
// ?? FIX: Aggiungi ultima puntata mancante a RecentBids
|
||||
// L'API spesso non include l'ultima puntata nella storia
|
||||
if (!string.IsNullOrEmpty(state.LastBidder) && state.Price > 0)
|
||||
{
|
||||
var lastBidTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
var statePrice = (decimal)state.Price;
|
||||
|
||||
// Verifica se questa puntata non è già presente
|
||||
var alreadyExists = auction.RecentBids.Any(b =>
|
||||
Math.Abs(b.Price - statePrice) < 0.001m &&
|
||||
b.Username.Equals(state.LastBidder, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!alreadyExists)
|
||||
{
|
||||
auction.RecentBids.Insert(0, new BidHistoryEntry
|
||||
{
|
||||
Username = state.LastBidder,
|
||||
Price = statePrice,
|
||||
Timestamp = lastBidTimestamp,
|
||||
BidType = "Auto"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
auction.IsActive = false;
|
||||
auction.LastState = state; // Salva stato finale per statistiche
|
||||
auction.AddLog($"[ASTA TERMINATA] {statusMsg}");
|
||||
OnLog?.Invoke($"[FINE] [{auction.AuctionId}] Asta {statusMsg} - Polling fermato");
|
||||
|
||||
// 💡 SUGGERIMENTO: Se persa e non abbiamo mai provato a puntare, potrebbe essere un problema di timing
|
||||
if (!won && auction.SessionBidCount == 0)
|
||||
{
|
||||
var settings = Utilities.SettingsManager.Load();
|
||||
int offsetMs = auction.BidBeforeDeadlineMs > 0
|
||||
? auction.BidBeforeDeadlineMs
|
||||
: settings.DefaultBidBeforeDeadlineMs;
|
||||
|
||||
// Se l'offset è <= 1000ms, il polling (~1s) potrebbe non catturare il momento giusto
|
||||
if (offsetMs <= 1000)
|
||||
{
|
||||
auction.AddLog($"[💡 SUGGERIMENTO] Asta persa senza mai puntare. Con offset={offsetMs}ms e polling~1s, " +
|
||||
$"potresti non vedere mai il timer scendere sotto {offsetMs}ms. Considera di aumentare l'offset a 1500-2000ms nelle impostazioni.");
|
||||
}
|
||||
}
|
||||
|
||||
auction.BidHistory.Add(new BidHistory
|
||||
{
|
||||
Timestamp = DateTime.UtcNow,
|
||||
EventType = BidEventType.Reset,
|
||||
Bidder = state.LastBidder,
|
||||
Price = state.Price,
|
||||
Timer = 0,
|
||||
Notes = $"Asta {statusMsg}"
|
||||
});
|
||||
|
||||
OnAuctionUpdated?.Invoke(state);
|
||||
|
||||
// ?? Emetti evento per salvare statistiche
|
||||
try
|
||||
{
|
||||
OnAuctionCompleted?.Invoke(auction, state, won);
|
||||
OnLog?.Invoke($"[STATS] Evento OnAuctionCompleted emesso per {auction.Name}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnLog?.Invoke($"[STATS ERROR] Errore in OnAuctionCompleted: {ex.Message}");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.Status == AuctionStatus.Running)
|
||||
{
|
||||
// Log RIMOSSO per ridurre verbosità - polling continuo non necessita log
|
||||
// Solo eventi importanti (bid, reset, errori) vengono loggati
|
||||
}
|
||||
else if (state.Status == AuctionStatus.Paused)
|
||||
{
|
||||
// Log solo primo cambio stato, non ad ogni polling
|
||||
var lastLog = auction.AuctionLog.LastOrDefault();
|
||||
if (lastLog == null || !lastLog.Contains("[PAUSA]"))
|
||||
{
|
||||
auction.AddLog($"[PAUSA] Asta in pausa - Timer: {state.Timer:F3}s, EUR{state.Price:F2}");
|
||||
}
|
||||
}
|
||||
|
||||
OnAuctionUpdated?.Invoke(state);
|
||||
UpdateAuctionHistory(auction, state);
|
||||
|
||||
|
||||
|
||||
// ?? NUOVO: Calcola e aggiorna valore prodotto se disponibili dati
|
||||
if (auction.BuyNowPrice.HasValue || auction.ShippingCost.HasValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
var settings = Utilities.SettingsManager.Load();
|
||||
|
||||
var productValue = Utilities.ProductValueCalculator.Calculate(
|
||||
auction,
|
||||
state.Price,
|
||||
auction.RecentBids?.Count ?? 0
|
||||
);
|
||||
|
||||
auction.CalculatedValue = productValue;
|
||||
|
||||
// Log valore solo se abilitato nelle impostazioni
|
||||
bool shouldLogValue = false;
|
||||
if (settings.LogValueCalculations && auction.PollingLatencyMs % 10 == 0) // Ogni ~10 poll
|
||||
{
|
||||
shouldLogValue = true;
|
||||
}
|
||||
|
||||
if (shouldLogValue && productValue.Savings.HasValue)
|
||||
{
|
||||
var valueMsg = Utilities.ProductValueCalculator.FormatValueMessage(productValue);
|
||||
auction.AddLog($"[VALUE] {valueMsg}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Silenzioso - non vogliamo bloccare il polling per errori di calcolo
|
||||
auction.AddLog($"[WARN] Errore calcolo valore: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// ?? NUOVA LOGICA: Controlla PRIMA il timing, POI le strategie
|
||||
if (state.Status == AuctionStatus.Running && !auction.IsPaused)
|
||||
{
|
||||
await TryPlaceBid(auction, state, token);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
auction.AddLog($"[EXCEPTION] {ex.Message}");
|
||||
OnLog?.Invoke($"[EXCEPTION] [{auction.AuctionId}] {ex.Message}");
|
||||
}
|
||||
// Ora gestito da PollAuctionState + TryPlaceBidTicker
|
||||
await PollAuctionState(auction, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -543,99 +630,16 @@ namespace AutoBidder.Services
|
||||
/// Punta esattamente a `offset` ms dalla scadenza.
|
||||
/// Le strategie decidono SE puntare, non QUANDO.
|
||||
/// </summary>
|
||||
[Obsolete("Usare TryPlaceBidTicker")]
|
||||
private async Task TryPlaceBid(AuctionInfo auction, AuctionState state, CancellationToken token)
|
||||
{
|
||||
// Reindirizza al nuovo sistema
|
||||
var settings = SettingsManager.Load();
|
||||
|
||||
// Offset: millisecondi prima della scadenza (configurato dall'utente)
|
||||
int offsetMs = auction.BidBeforeDeadlineMs > 0
|
||||
? auction.BidBeforeDeadlineMs
|
||||
: settings.DefaultBidBeforeDeadlineMs;
|
||||
|
||||
// Timer dall'API (in secondi, convertito in ms)
|
||||
double timerMs = state.Timer * 1000;
|
||||
|
||||
// Skip se già vincitore o timer scaduto
|
||||
if (state.IsMyBid || timerMs <= 0) return;
|
||||
|
||||
// Log timing solo se abilitato
|
||||
if (settings.LogTiming)
|
||||
{
|
||||
auction.AddLog($"[TIMING] API={timerMs:F0}ms, Offset={offsetMs}ms");
|
||||
}
|
||||
|
||||
// Punta quando il timer API è <= offset configurato dall'utente
|
||||
// NESSUNA modifica automatica - l'utente decide il timing
|
||||
if (timerMs > offsetMs)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Timer <= offset = È IL MOMENTO DI PUNTARE!
|
||||
auction.AddLog($"[BID WINDOW] Timer={timerMs:F0}ms <= Offset={offsetMs}ms - Verifica condizioni...");
|
||||
|
||||
// Resetta BidScheduled se il timer è AUMENTATO (qualcun altro ha puntato = nuovo ciclo)
|
||||
if (timerMs > auction.LastScheduledTimerMs + 500)
|
||||
{
|
||||
auction.BidScheduled = false;
|
||||
}
|
||||
|
||||
// Resetta anche se è passato troppo tempo dall'ultima puntata (nuovo ciclo)
|
||||
if (auction.LastClickAt.HasValue &&
|
||||
(DateTime.UtcNow - auction.LastClickAt.Value).TotalSeconds > 10)
|
||||
{
|
||||
auction.BidScheduled = false;
|
||||
}
|
||||
|
||||
// Protezione doppia puntata SOLO per lo stesso ciclo di timer
|
||||
if (auction.BidScheduled && Math.Abs(auction.LastScheduledTimerMs - timerMs) < 100)
|
||||
{
|
||||
auction.AddLog($"[SKIP] Puntata già schedulata per timer~={timerMs:F0}ms in questo ciclo");
|
||||
return;
|
||||
}
|
||||
|
||||
// Cooldown 1 secondo tra puntate
|
||||
if (auction.LastClickAt.HasValue && (DateTime.UtcNow - auction.LastClickAt.Value).TotalMilliseconds < 1000)
|
||||
{
|
||||
auction.AddLog($"[COOLDOWN] Attesa cooldown puntata precedente");
|
||||
return;
|
||||
}
|
||||
|
||||
// 🔴 CONTROLLI FONDAMENTALI (prezzo, reset, limiti, puntate residue)
|
||||
if (!ShouldBid(auction, state))
|
||||
{
|
||||
// I motivi vengono ora loggati sempre dentro ShouldBid
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ MOMENTO GIUSTO! Verifica strategie avanzate
|
||||
var session = _apiClient.GetSession();
|
||||
var decision = _bidStrategy.ShouldPlaceBid(auction, state, settings, session?.Username ?? "");
|
||||
|
||||
if (!decision.ShouldBid)
|
||||
{
|
||||
// 🔥 FIX: Logga SEMPRE il motivo del blocco strategia, non solo se LogStrategyDecisions è attivo
|
||||
// Questo aiuta a capire perché si perdono le aste
|
||||
auction.AddLog($"[STRATEGY] {decision.Reason}");
|
||||
|
||||
// Log aggiuntivo solo se debug strategie attivo
|
||||
if (settings.LogStrategyDecisions)
|
||||
{
|
||||
OnLog?.Invoke($"[{auction.Name}] STRATEGY blocked: {decision.Reason}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ?? PUNTA!
|
||||
auction.BidScheduled = true;
|
||||
auction.LastScheduledTimerMs = timerMs;
|
||||
|
||||
if (settings.LogBids)
|
||||
{
|
||||
auction.AddLog($"[BID] Puntata con timer API={timerMs:F0}ms");
|
||||
}
|
||||
|
||||
await ExecuteBid(auction, state, token);
|
||||
await TryPlaceBidTicker(auction, timerMs, offsetMs, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -690,8 +694,31 @@ namespace AutoBidder.Services
|
||||
else
|
||||
{
|
||||
var pollingPing = auction.PollingLatencyMs;
|
||||
var settings = SettingsManager.Load();
|
||||
|
||||
// Rileva errore "timer scaduto" per feedback utente
|
||||
bool isLateBid = result.Error?.Contains("timer") == true ||
|
||||
result.Error?.Contains("scaduto") == true ||
|
||||
result.Error?.Contains("closed") == true ||
|
||||
result.Error?.Contains("ended") == true;
|
||||
|
||||
auction.AddLog($"[BID FAIL] {result.Error} | Ping: {pollingPing}ms");
|
||||
OnLog?.Invoke($"[FAIL] Puntata fallita su {auction.Name} ({auction.AuctionId}): {result.Error}");
|
||||
|
||||
// Feedback per puntata tardiva
|
||||
if (isLateBid && settings.ShowLateBidWarning)
|
||||
{
|
||||
int currentOffset = auction.BidBeforeDeadlineMs > 0
|
||||
? auction.BidBeforeDeadlineMs
|
||||
: settings.DefaultBidBeforeDeadlineMs;
|
||||
|
||||
int suggestedOffset = currentOffset + 300 + pollingPing;
|
||||
|
||||
auction.AddLog($"[⚠️ TIMING] Puntata arrivata troppo tardi! " +
|
||||
$"Offset attuale: {currentOffset}ms. Latenza totale: ~{pollingPing + result.LatencyMs}ms. " +
|
||||
$"Suggerimento: aumenta a {suggestedOffset}ms");
|
||||
OnLog?.Invoke($"[LATE] {auction.Name}: puntata tardiva, aumenta offset a {suggestedOffset}ms");
|
||||
}
|
||||
}
|
||||
|
||||
auction.BidHistory.Add(new BidHistory
|
||||
@@ -926,6 +953,17 @@ namespace AutoBidder.Services
|
||||
if (isNewBid)
|
||||
{
|
||||
auction.ResetCount++;
|
||||
|
||||
// Log quando c'è una nuova puntata (se logging attivo)
|
||||
var settings = SettingsManager.Load();
|
||||
if (settings.LogAuctionStatus)
|
||||
{
|
||||
var session = _apiClient.GetSession();
|
||||
var isMyBid = state.LastBidder?.Equals(session?.Username, StringComparison.OrdinalIgnoreCase) == true;
|
||||
|
||||
auction.AddLog($"[RESET #{auction.ResetCount}] {state.LastBidder} → €{state.Price:F2} | Timer: {state.Timer:F1}s{(isMyBid ? " (TU)" : "")}");
|
||||
}
|
||||
|
||||
auction.BidHistory.Add(new BidHistory
|
||||
{
|
||||
Timestamp = DateTime.UtcNow,
|
||||
@@ -936,6 +974,12 @@ namespace AutoBidder.Services
|
||||
Notes = $"Puntata: EUR{state.Price:F2}"
|
||||
});
|
||||
|
||||
// ?? Limita dimensione BidHistory per evitare memory leak
|
||||
if (auction.BidHistory.Count > 100)
|
||||
{
|
||||
auction.BidHistory.RemoveRange(0, auction.BidHistory.Count - 100);
|
||||
}
|
||||
|
||||
// ? RIMOSSO: Non incrementare qui - è già gestito da UpdateBidderStatsFromRecentBids
|
||||
// L'incremento doppio causava conteggi gonfiati
|
||||
|
||||
@@ -1011,10 +1055,12 @@ namespace AutoBidder.Services
|
||||
.ToList();
|
||||
|
||||
// Limita al numero massimo di puntate (mantieni le più recenti = prime della lista)
|
||||
if (maxEntries > 0 && auction.RecentBids.Count > maxEntries)
|
||||
// ?? MEMORY: Limite ridotto per ottimizzazione RAM
|
||||
int limit = maxEntries > 0 ? Math.Min(maxEntries, 50) : 50;
|
||||
if (auction.RecentBids.Count > limit)
|
||||
{
|
||||
auction.RecentBids = auction.RecentBids
|
||||
.Take(maxEntries)
|
||||
.Take(limit)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace AutoBidder.Services
|
||||
private readonly int _maxConcurrentRequests;
|
||||
private readonly TimeSpan _cacheExpiration;
|
||||
private readonly int _maxRetries;
|
||||
private readonly int _maxCacheEntries;
|
||||
|
||||
// Logging callback
|
||||
public Action<string>? OnLog { get; set; }
|
||||
@@ -36,12 +37,14 @@ namespace AutoBidder.Services
|
||||
int maxConcurrentRequests = 3,
|
||||
int requestsPerSecond = 5,
|
||||
TimeSpan? cacheExpiration = null,
|
||||
int maxRetries = 2)
|
||||
int maxRetries = 2,
|
||||
int maxCacheEntries = 50)
|
||||
{
|
||||
_maxConcurrentRequests = maxConcurrentRequests;
|
||||
_minRequestDelay = TimeSpan.FromMilliseconds(1000.0 / requestsPerSecond);
|
||||
_cacheExpiration = cacheExpiration ?? TimeSpan.FromMinutes(5);
|
||||
_cacheExpiration = cacheExpiration ?? TimeSpan.FromMinutes(3); // Ridotto da 5 a 3 minuti
|
||||
_maxRetries = maxRetries;
|
||||
_maxCacheEntries = maxCacheEntries;
|
||||
_rateLimiter = new SemaphoreSlim(maxConcurrentRequests, maxConcurrentRequests);
|
||||
|
||||
_httpClient.Timeout = TimeSpan.FromSeconds(15);
|
||||
@@ -191,10 +194,26 @@ namespace AutoBidder.Services
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Salva HTML in cache
|
||||
/// Salva HTML in cache con limite dimensione
|
||||
/// </summary>
|
||||
private void SaveToCache(string url, string html)
|
||||
{
|
||||
// Limita dimensione cache per evitare memory leak
|
||||
if (_cache.Count >= _maxCacheEntries)
|
||||
{
|
||||
// Rimuovi le entry più vecchie
|
||||
var oldestEntries = _cache
|
||||
.OrderBy(kvp => kvp.Value.Timestamp)
|
||||
.Take(_cache.Count - _maxCacheEntries + 10) // Rimuovi 10 extra per evitare chiamate frequenti
|
||||
.Select(kvp => kvp.Key)
|
||||
.ToList();
|
||||
|
||||
foreach (var key in oldestEntries)
|
||||
{
|
||||
_cache.TryRemove(key, out _);
|
||||
}
|
||||
}
|
||||
|
||||
_cache[url] = new CachedHtml
|
||||
{
|
||||
Html = html,
|
||||
|
||||
@@ -15,6 +15,33 @@ namespace AutoBidder.Utilities
|
||||
public int DefaultMinResets { get; set; } = 0;
|
||||
public int DefaultMaxResets { get; set; } = 0;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// TICKER LOOP - SISTEMA DI TIMING SEMPLIFICATO
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
/// Intervallo del ticker in millisecondi.
|
||||
/// Più basso = più preciso ma più CPU.
|
||||
/// Valori consigliati: 50-100ms
|
||||
/// Default: 50ms
|
||||
/// </summary>
|
||||
public int TickerIntervalMs { get; set; } = 50;
|
||||
|
||||
/// <summary>
|
||||
/// Soglia in millisecondi per iniziare i controlli delle strategie.
|
||||
/// Se il timer è superiore a questo valore, non vengono eseguiti i controlli.
|
||||
/// Questo ottimizza le risorse evitando controlli inutili quando siamo lontani dal momento di puntare.
|
||||
/// Default: 5000ms (5 secondi)
|
||||
/// </summary>
|
||||
public int StrategyCheckThresholdMs { get; set; } = 5000;
|
||||
|
||||
/// <summary>
|
||||
/// Mostra avviso quando una puntata arriva troppo tardi (timer scaduto).
|
||||
/// Suggerisce all'utente di aumentare il tempo di puntata.
|
||||
/// Default: true
|
||||
/// </summary>
|
||||
public bool ShowLateBidWarning { get; set; } = true;
|
||||
|
||||
// LIMITI LOG
|
||||
/// <summary>
|
||||
/// Numero massimo di righe di log da mantenere per ogni singola asta (default: 500)
|
||||
|
||||
@@ -208,6 +208,68 @@
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
LOG BOX - SCROLL FISSO
|
||||
═══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
.log-box {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.4;
|
||||
padding: 0.5rem;
|
||||
height: 100%; /* Usa altezza dal pannello */
|
||||
overflow-y: auto; /* Scroll verticale */
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.log-box-compact {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.4;
|
||||
padding: 0.5rem;
|
||||
max-height: 100%; /* Non superare il contenitore */
|
||||
overflow-y: auto; /* Scroll verticale */
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
padding: 0.25rem 0.5rem;
|
||||
margin-bottom: 0.15rem;
|
||||
border-radius: 4px;
|
||||
word-wrap: break-word;
|
||||
transition: background 0.1s ease;
|
||||
}
|
||||
|
||||
.log-entry:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.log-entry-error {
|
||||
color: #f87171;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-left: 3px solid #ef4444;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.log-entry-warning {
|
||||
color: #fbbf24;
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
border-left: 3px solid #f59e0b;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.log-entry-success {
|
||||
color: #4ade80;
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
border-left: 3px solid #22c55e;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.log-entry-debug {
|
||||
color: #60a5fa;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
LAYOUT CON SPLITTER TRASCINABILI
|
||||
═══════════════════════════════════════════════════════════════════ */
|
||||
@@ -220,34 +282,38 @@
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
gap: 0; /* IMPORTANTE: nessun gap tra toolbar e content */
|
||||
}
|
||||
|
||||
/* Area contenuto principale */
|
||||
.main-content-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
flex: 1 1 auto; /* Cresce e si riduce */
|
||||
min-height: 0; /* IMPORTANTE per flex */
|
||||
overflow: hidden;
|
||||
gap: 0;
|
||||
gap: 0; /* IMPORTANTE: nessun gap, gli splitter gestiscono lo spazio */
|
||||
}
|
||||
|
||||
/* Riga superiore (Aste + Log) */
|
||||
.top-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
min-height: 150px;
|
||||
flex: 1 1 auto; /* Cresce e si riduce - NON percentuale fissa */
|
||||
min-height: 200px; /* Altezza minima */
|
||||
overflow: hidden;
|
||||
gap: 0;
|
||||
gap: 0; /* IMPORTANTE: nessun gap, gutter gestisce lo spazio */
|
||||
}
|
||||
|
||||
/* Riga inferiore (Dettagli) */
|
||||
.bottom-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 80px;
|
||||
flex: 0 0 auto; /* NON cresce automaticamente */
|
||||
height: 300px; /* Altezza iniziale fissa */
|
||||
min-height: 150px; /* Altezza minima */
|
||||
overflow: hidden;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
/* Pannello generico */
|
||||
@@ -259,33 +325,55 @@
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
height: 100%; /* IMPORTANTE: altezza fissa dal contenitore */
|
||||
position: relative; /* Per z-index */
|
||||
}
|
||||
|
||||
/* IMPORTANTE: Previeni collasso dei bordi */
|
||||
.panel::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* Pannello Aste */
|
||||
.panel-auctions {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
flex: 1 1 auto; /* Cresce per riempire lo spazio */
|
||||
min-width: 300px;
|
||||
height: 100%; /* Usa tutta l'altezza del contenitore */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Pannello Log */
|
||||
.panel-log {
|
||||
flex: 0 0 280px;
|
||||
min-width: 150px;
|
||||
max-width: 450px;
|
||||
flex: 0 0 auto; /* NON cresce/riduce automaticamente */
|
||||
width: 320px; /* Larghezza fissa iniziale */
|
||||
min-width: 200px;
|
||||
max-width: 500px;
|
||||
height: 100%; /* Usa tutta l'altezza del contenitore */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Pannello Dettagli */
|
||||
.panel-details {
|
||||
flex: 1;
|
||||
min-height: 80px;
|
||||
overflow: auto;
|
||||
flex: 1 1 auto; /* Cresce per riempire */
|
||||
min-height: 150px;
|
||||
height: 100%; /* Usa tutta l'altezza del contenitore */
|
||||
overflow: hidden; /* Il contenitore non scrolla */
|
||||
}
|
||||
|
||||
/* Gutter/Splitter */
|
||||
.gutter {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
flex-shrink: 0;
|
||||
flex-shrink: 0; /* NON si riduce mai */
|
||||
flex-grow: 0; /* NON cresce mai */
|
||||
transition: background 0.15s ease;
|
||||
z-index: 10; /* Sopra i pannelli */
|
||||
}
|
||||
|
||||
.gutter:hover {
|
||||
@@ -299,11 +387,15 @@
|
||||
.gutter-vertical {
|
||||
width: 6px;
|
||||
cursor: col-resize;
|
||||
min-width: 6px; /* Larghezza fissa */
|
||||
max-width: 6px;
|
||||
}
|
||||
|
||||
.gutter-horizontal {
|
||||
height: 6px;
|
||||
cursor: row-resize;
|
||||
min-height: 6px; /* Altezza fissa */
|
||||
max-height: 6px;
|
||||
}
|
||||
|
||||
/* Header pannello */
|
||||
@@ -327,15 +419,47 @@
|
||||
|
||||
.panel-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto; /* Scroll verticale */
|
||||
overflow-x: hidden;
|
||||
min-height: 0; /* Importante per flex */
|
||||
max-height: 100%; /* Non superare il pannello */
|
||||
}
|
||||
|
||||
/* Contenuto dettagli */
|
||||
.auction-details-content {
|
||||
padding: 0.5rem;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden; /* Il contenitore non scrolla */
|
||||
}
|
||||
|
||||
/* Tab content deve scrollare */
|
||||
.tab-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
display: flex; /* IMPORTANTE per i tab-pane */
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tab-pane {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: none; /* Bootstrap lo gestisce con show active */
|
||||
}
|
||||
|
||||
.tab-pane.show.active {
|
||||
display: flex; /* Quando attivo diventa flex */
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tab-panel-content {
|
||||
flex: 1; /* Riempie il tab-pane */
|
||||
overflow-y: auto; /* Scroll per il contenuto dei tab */
|
||||
overflow-x: hidden;
|
||||
padding: 0.5rem;
|
||||
min-height: 0; /* IMPORTANTE per flex */
|
||||
}
|
||||
|
||||
.details-header {
|
||||
|
||||
Reference in New Issue
Block a user