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>
|
/// <summary>
|
||||||
/// Numero massimo di righe di log da mantenere per ogni asta
|
/// Numero massimo di righe di log da mantenere per ogni asta
|
||||||
|
/// Ridotto per ottimizzare consumo RAM
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const int MAX_LOG_LINES = 500;
|
private const int MAX_LOG_LINES = 200;
|
||||||
|
|
||||||
public string AuctionId { get; set; } = "";
|
public string AuctionId { get; set; } = "";
|
||||||
public string Name { get; set; } = ""; // Opzionale, può essere lasciato vuoto
|
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");
|
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
|
// ?? DEDUPLICAZIONE: Se l'ultimo messaggio è uguale, incrementa contatore
|
||||||
if (AuctionLog.Count > 0)
|
if (AuctionLog.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -233,9 +239,9 @@ namespace AutoBidder.Models
|
|||||||
public List<int> LatencyHistory { get; set; } = new();
|
public List<int> LatencyHistory { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Numero massimo di latenze da memorizzare
|
/// Numero massimo di latenze da memorizzare (ridotto per RAM)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const int MAX_LATENCY_HISTORY = 20;
|
private const int MAX_LATENCY_HISTORY = 10;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Aggiunge una misurazione di latenza allo storico
|
/// Aggiunge una misurazione di latenza allo storico
|
||||||
@@ -390,6 +396,103 @@ namespace AutoBidder.Models
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public double DuelAdvantage { get; set; } = 0;
|
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>
|
/// <summary>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
<div class="status-indicators">
|
<div class="status-indicators">
|
||||||
<div class="status-pill total" title="Totale aste">
|
<div class="status-pill total" title="Totale aste">
|
||||||
<i class="bi bi-collection"></i>
|
<i class="bi bi-collection"></i>
|
||||||
<span>@auctions.Count</span>
|
<span>@(auctions?.Count ?? 0)</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-pill active" title="Aste attive">
|
<div class="status-pill active" title="Aste attive">
|
||||||
<i class="bi bi-play-circle-fill"></i>
|
<i class="bi bi-play-circle-fill"></i>
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
<i class="bi bi-x-circle"></i>
|
<i class="bi bi-x-circle"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="manage-separator"></div>
|
<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>
|
<i class="bi bi-trash-fill"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<span><i class="bi bi-list-check"></i> Aste Monitorate</span>
|
<span><i class="bi bi-list-check"></i> Aste Monitorate</span>
|
||||||
</div>
|
</div>
|
||||||
@if (auctions.Count == 0)
|
@if ((auctions?.Count ?? 0) == 0)
|
||||||
{
|
{
|
||||||
<div class="alert alert-info animate-fade-in-up m-2">
|
<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.
|
<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>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@* Script Splitter *@
|
@* Script Splitter - Versione Fixed senza sovrapposizioni *@
|
||||||
<script suppress-error="BL9992">
|
<script suppress-error="BL9992">
|
||||||
(function() {
|
(function() {
|
||||||
function initSplitters() {
|
function initSplitters() {
|
||||||
@@ -541,12 +541,23 @@
|
|||||||
let startPos = 0;
|
let startPos = 0;
|
||||||
let startSizeA = 0;
|
let startSizeA = 0;
|
||||||
let startSizeB = 0;
|
let startSizeB = 0;
|
||||||
|
let containerSize = 0;
|
||||||
|
|
||||||
function onMouseDown(e, type, elA, elB) {
|
function onMouseDown(e, type, elA, elB) {
|
||||||
active = { type, elA, elB };
|
active = { type, elA, elB };
|
||||||
startPos = type === 'v' ? e.clientX : e.clientY;
|
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.cursor = type === 'v' ? 'col-resize' : 'row-resize';
|
||||||
document.body.style.userSelect = 'none';
|
document.body.style.userSelect = 'none';
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -560,16 +571,34 @@
|
|||||||
const { type, elA, elB } = active;
|
const { type, elA, elB } = active;
|
||||||
const pos = type === 'v' ? e.clientX : e.clientY;
|
const pos = type === 'v' ? e.clientX : e.clientY;
|
||||||
const diff = pos - startPos;
|
const diff = pos - startPos;
|
||||||
const total = startSizeA + startSizeB;
|
|
||||||
let newA = startSizeA + diff;
|
let newA = startSizeA + diff;
|
||||||
let newB = startSizeB - diff;
|
let newB = startSizeB - diff;
|
||||||
|
|
||||||
const minA = type === 'v' ? 200 : 150;
|
// Limiti minimi
|
||||||
const minB = type === 'v' ? 150 : 80;
|
const minA = type === 'v' ? 300 : 200;
|
||||||
|
const minB = type === 'v' ? 200 : 150;
|
||||||
|
|
||||||
if (newA < minA) { newA = minA; newB = total - newA; }
|
// Applica limiti
|
||||||
if (newB < minB) { newB = minB; newA = total - newB; }
|
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') {
|
if (type === 'v') {
|
||||||
elA.style.width = newA + 'px';
|
elA.style.width = newA + 'px';
|
||||||
elA.style.flex = 'none';
|
elA.style.flex = 'none';
|
||||||
|
|||||||
@@ -15,7 +15,44 @@ namespace AutoBidder.Pages
|
|||||||
[Inject] private HtmlCacheService HtmlCache { get; set; } = default!;
|
[Inject] private HtmlCacheService HtmlCache { get; set; } = default!;
|
||||||
[Inject] private StatsService StatsService { 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
|
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
|
private bool isMonitoringActive
|
||||||
{
|
{
|
||||||
get => AppState.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? 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
|
// Dialog Aggiungi Asta
|
||||||
private bool showAddDialog = false;
|
private bool showAddDialog = false;
|
||||||
@@ -92,20 +137,68 @@ namespace AutoBidder.Pages
|
|||||||
AuctionMonitor.OnLog += OnGlobalLog;
|
AuctionMonitor.OnLog += OnGlobalLog;
|
||||||
AuctionMonitor.OnAuctionUpdated += OnAuctionUpdated;
|
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 _ =>
|
||||||
|
{
|
||||||
|
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);
|
await InvokeAsync(StateHasChanged);
|
||||||
}, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
|
}
|
||||||
|
}
|
||||||
|
catch { /* Ignora errori del timer */ }
|
||||||
|
}, null, TimeSpan.FromMilliseconds(300), TimeSpan.FromMilliseconds(300));
|
||||||
|
|
||||||
// Carica sessione all'avvio
|
// Carica sessione all'avvio
|
||||||
LoadSession();
|
LoadSession();
|
||||||
|
|
||||||
// Timer per aggiornamento sessione ogni 30 secondi
|
// Timer per aggiornamento sessione ogni 60 secondi (era 30)
|
||||||
sessionTimer = new System.Threading.Timer(async _ =>
|
sessionTimer = new System.Threading.Timer(async _ =>
|
||||||
{
|
{
|
||||||
await RefreshSessionAsync();
|
await RefreshSessionAsync();
|
||||||
await InvokeAsync(StateHasChanged);
|
await ThrottledStateHasChanged();
|
||||||
}, null, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30));
|
}, null, TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(60));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
@@ -132,6 +225,20 @@ namespace AutoBidder.Pages
|
|||||||
// Handler async per eventi da background thread
|
// Handler async per eventi da background thread
|
||||||
private async Task OnAppStateChangedAsync()
|
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);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,12 +278,12 @@ namespace AutoBidder.Pages
|
|||||||
private void OnGlobalLog(string message)
|
private void OnGlobalLog(string message)
|
||||||
{
|
{
|
||||||
AppState.AddLog($"[{DateTime.Now:HH:mm:ss}] {message}");
|
AppState.AddLog($"[{DateTime.Now:HH:mm:ss}] {message}");
|
||||||
InvokeAsync(StateHasChanged);
|
// Non forziamo StateHasChanged qui - verrà aggiornato dal throttle
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnAuctionUpdated(AuctionState state)
|
private void OnAuctionUpdated(AuctionState state)
|
||||||
{
|
{
|
||||||
var auction = auctions.FirstOrDefault(a => a.AuctionId == state.AuctionId);
|
var auction = AppState.GetAuctionById(state.AuctionId);
|
||||||
if (auction != null)
|
if (auction != null)
|
||||||
{
|
{
|
||||||
// Salva l'ultimo stato ricevuto
|
// Salva l'ultimo stato ricevuto
|
||||||
@@ -188,18 +295,21 @@ namespace AutoBidder.Pages
|
|||||||
auction.BidsUsedOnThisAuction = state.MyBidsCount.Value;
|
auction.BidsUsedOnThisAuction = state.MyBidsCount.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notifica il cambiamento usando InvokeAsync per thread-safety
|
// Invalida cache
|
||||||
_ = InvokeAsync(() =>
|
InvalidateAuctionCache();
|
||||||
{
|
|
||||||
AppState.ForceUpdate();
|
// Notifica con throttling
|
||||||
StateHasChanged();
|
_ = ThrottledStateHasChanged();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SelectAuction(AuctionInfo auction)
|
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
|
// Gestione controlli globali
|
||||||
@@ -1384,8 +1494,8 @@ namespace AutoBidder.Pages
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
refreshTimer?.Dispose();
|
|
||||||
sessionTimer?.Dispose();
|
sessionTimer?.Dispose();
|
||||||
|
logRefreshTimer?.Dispose();
|
||||||
|
|
||||||
// Rimuovi sottoscrizioni (ASYNC)
|
// Rimuovi sottoscrizioni (ASYNC)
|
||||||
if (AppState != null)
|
if (AppState != null)
|
||||||
@@ -1475,34 +1585,69 @@ namespace AutoBidder.Pages
|
|||||||
|
|
||||||
private int GetActiveAuctionsCount()
|
private int GetActiveAuctionsCount()
|
||||||
{
|
{
|
||||||
return auctions.Count(a => a.IsActive && !a.IsPaused &&
|
try
|
||||||
(a.LastState == null || a.LastState.Status == AuctionStatus.Running));
|
{
|
||||||
|
return auctions?.Count(a => a.IsActive && !a.IsPaused &&
|
||||||
|
(a.LastState == null || a.LastState.Status == AuctionStatus.Running)) ?? 0;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int GetPausedAuctionsCount()
|
private int GetPausedAuctionsCount()
|
||||||
{
|
{
|
||||||
return auctions.Count(a => a.IsPaused ||
|
try
|
||||||
(a.LastState != null && a.LastState.Status == AuctionStatus.Paused));
|
{
|
||||||
|
return auctions?.Count(a => a.IsPaused ||
|
||||||
|
(a.LastState != null && a.LastState.Status == AuctionStatus.Paused)) ?? 0;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int GetWonAuctionsCount()
|
private int GetWonAuctionsCount()
|
||||||
{
|
{
|
||||||
return auctions.Count(a => a.LastState != null &&
|
try
|
||||||
a.LastState.Status == AuctionStatus.EndedWon);
|
{
|
||||||
|
return auctions?.Count(a => a.LastState != null &&
|
||||||
|
a.LastState.Status == AuctionStatus.EndedWon) ?? 0;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int GetLostAuctionsCount()
|
private int GetLostAuctionsCount()
|
||||||
{
|
{
|
||||||
return auctions.Count(a => a.LastState != null &&
|
try
|
||||||
a.LastState.Status == AuctionStatus.EndedLost);
|
{
|
||||||
|
return auctions?.Count(a => a.LastState != null &&
|
||||||
|
a.LastState.Status == AuctionStatus.EndedLost) ?? 0;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int GetStoppedAuctionsCount()
|
private int GetStoppedAuctionsCount()
|
||||||
{
|
{
|
||||||
return auctions.Count(a => !a.IsActive &&
|
try
|
||||||
|
{
|
||||||
|
return auctions?.Count(a => !a.IsActive &&
|
||||||
(a.LastState == null ||
|
(a.LastState == null ||
|
||||||
(a.LastState.Status != AuctionStatus.EndedWon &&
|
(a.LastState.Status != AuctionStatus.EndedWon &&
|
||||||
a.LastState.Status != AuctionStatus.EndedLost)));
|
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
|
Usa i pulsanti <i class="bi bi-arrow-repeat"></i> per applicare la singola impostazione a tutte le aste
|
||||||
</div>
|
</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">
|
<div class="col-12 col-md-6">
|
||||||
<label class="form-label fw-bold"><i class="bi bi-speedometer2"></i> Anticipo puntata (ms)</label>
|
<label class="form-label fw-bold"><i class="bi bi-speedometer2"></i> Anticipo puntata (ms)</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
@@ -149,7 +157,33 @@
|
|||||||
<i class="bi bi-arrow-repeat"></i>
|
<i class="bi bi-arrow-repeat"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-text">Millisecondi prima della scadenza per tentare la puntata</div>
|
||||||
</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">
|
<div class="col-12 col-md-6">
|
||||||
<label class="form-label fw-bold"><i class="bi bi-hand-index-thumb"></i> Click massimi</label>
|
<label class="form-label fw-bold"><i class="bi bi-hand-index-thumb"></i> Click massimi</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
|
|||||||
@@ -551,4 +551,48 @@ app.MapRazorPages(); // ? AGGIUNTO: abilita Razor Pages (Login, Logout)
|
|||||||
app.MapBlazorHub();
|
app.MapBlazorHub();
|
||||||
app.MapFallbackToPage("/_Host");
|
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();
|
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>
|
/// <summary>
|
||||||
/// Ottiene la lista originale delle aste per il salvataggio.
|
/// Ottiene la lista originale delle aste per il salvataggio.
|
||||||
/// ATTENZIONE: Usare solo per persistenza, non per iterazione durante modifiche!
|
/// ATTENZIONE: Usare solo per persistenza, non per iterazione durante modifiche!
|
||||||
@@ -277,15 +303,16 @@ namespace AutoBidder.Services
|
|||||||
{
|
{
|
||||||
_globalLog.Add(entry);
|
_globalLog.Add(entry);
|
||||||
|
|
||||||
// Mantieni solo gli ultimi 1000 log
|
// Mantieni solo gli ultimi 500 log (ridotto da 1000 per RAM)
|
||||||
if (_globalLog.Count > 1000)
|
if (_globalLog.Count > 500)
|
||||||
{
|
{
|
||||||
_globalLog.RemoveRange(0, _globalLog.Count - 1000);
|
_globalLog.RemoveRange(0, _globalLog.Count - 500);
|
||||||
|
_globalLog.TrimExcess();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = NotifyLogAddedAsync(message);
|
// RIMOSSO: NotifyStateChangedAsync qui causava troppi re-render
|
||||||
_ = NotifyStateChangedAsync();
|
// I log vengono visualizzati al prossimo refresh naturale
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearLog()
|
public void ClearLog()
|
||||||
@@ -391,6 +418,80 @@ namespace AutoBidder.Services
|
|||||||
{
|
{
|
||||||
_ = NotifyStateChangedAsync();
|
_ = 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>
|
/// <summary>
|
||||||
|
|||||||
@@ -137,6 +137,13 @@ namespace AutoBidder.Services
|
|||||||
OnLog?.Invoke($"[REMOVE] Rimozione asta non terminata: {auction.Name} (non salvata nelle statistiche)");
|
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);
|
_auctions.Remove(auction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -244,10 +251,18 @@ namespace AutoBidder.Services
|
|||||||
|
|
||||||
private async Task MonitoringLoop(CancellationToken token)
|
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)
|
while (!token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// === FASE 1: Raccogli aste attive ===
|
||||||
List<AuctionInfo> activeAuctions;
|
List<AuctionInfo> activeAuctions;
|
||||||
lock (_auctions)
|
lock (_auctions)
|
||||||
{
|
{
|
||||||
@@ -259,70 +274,48 @@ namespace AutoBidder.Services
|
|||||||
|
|
||||||
if (activeAuctions.Count == 0)
|
if (activeAuctions.Count == 0)
|
||||||
{
|
{
|
||||||
await Task.Delay(1000, token);
|
// Nessuna asta attiva - polling molto lento
|
||||||
|
await Task.Delay(2000, token);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Poll tutte le aste in parallelo
|
// === FASE 2: Poll API solo ogni pollingIntervalMs ===
|
||||||
var pollTasks = activeAuctions.Select(a => PollAndProcessAuction(a, token));
|
var now = DateTime.UtcNow;
|
||||||
|
bool shouldPoll = (now - lastPoll).TotalMilliseconds >= pollingIntervalMs;
|
||||||
|
|
||||||
|
// 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));
|
||||||
await Task.WhenAll(pollTasks);
|
await Task.WhenAll(pollTasks);
|
||||||
|
lastPoll = now;
|
||||||
// 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
|
// === FASE 3: TICKER CHECK - Verifica timing per ogni asta ===
|
||||||
// Considera l'offset target più basso tra tutte le aste attive
|
foreach (var auction in activeAuctions)
|
||||||
var settings = Utilities.SettingsManager.Load();
|
|
||||||
|
|
||||||
// ?? Polling VELOCE quando vicino alla scadenza
|
|
||||||
int minDelayMs = 500;
|
|
||||||
|
|
||||||
foreach (var a in activeAuctions)
|
|
||||||
{
|
{
|
||||||
if (a.IsPaused || !a.LastDeadlineUpdateUtc.HasValue) continue;
|
if (auction.IsPaused || auction.LastState == null) continue;
|
||||||
|
|
||||||
// Calcola tempo stimato rimanente
|
// Calcola timer stimato LOCALMENTE (più preciso del polling)
|
||||||
var elapsed = (DateTime.UtcNow - a.LastDeadlineUpdateUtc.Value).TotalMilliseconds;
|
double estimatedTimerMs = GetEstimatedTimerMs(auction);
|
||||||
double estimatedRemaining = a.LastRawTimer - elapsed;
|
|
||||||
|
|
||||||
int offsetMs = a.BidBeforeDeadlineMs > 0
|
// Offset configurato dall'utente (SENZA compensazioni)
|
||||||
? a.BidBeforeDeadlineMs
|
int offsetMs = auction.BidBeforeDeadlineMs > 0
|
||||||
|
? auction.BidBeforeDeadlineMs
|
||||||
: settings.DefaultBidBeforeDeadlineMs;
|
: settings.DefaultBidBeforeDeadlineMs;
|
||||||
|
|
||||||
double msToTarget = estimatedRemaining - offsetMs;
|
// TRIGGER: Timer <= Offset configurato dall'utente
|
||||||
|
if (estimatedTimerMs <= offsetMs && estimatedTimerMs > 0)
|
||||||
// Polling più veloce quando vicino al target
|
{
|
||||||
int delay;
|
await TryPlaceBidTicker(auction, estimatedTimerMs, offsetMs, token);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.Delay(minDelayMs, token);
|
// === FASE 4: Delay fisso del ticker ===
|
||||||
|
await Task.Delay(tickerIntervalMs, token);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
@@ -330,10 +323,202 @@ namespace AutoBidder.Services
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
OnLog?.Invoke($"[ERRORE] Loop monitoraggio: {ex.Message}");
|
OnLog?.Invoke($"[ERRORE] Ticker loop: {ex.Message}");
|
||||||
await Task.Delay(1000, token);
|
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)
|
private bool IsAuctionTerminated(AuctionInfo auction)
|
||||||
@@ -353,189 +538,91 @@ namespace AutoBidder.Services
|
|||||||
return false;
|
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)
|
private async Task PollAndProcessAuction(AuctionInfo auction, CancellationToken token)
|
||||||
{
|
{
|
||||||
try
|
// Ora gestito da PollAuctionState + TryPlaceBidTicker
|
||||||
{
|
await PollAuctionState(auction, token);
|
||||||
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}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -543,99 +630,16 @@ namespace AutoBidder.Services
|
|||||||
/// Punta esattamente a `offset` ms dalla scadenza.
|
/// Punta esattamente a `offset` ms dalla scadenza.
|
||||||
/// Le strategie decidono SE puntare, non QUANDO.
|
/// Le strategie decidono SE puntare, non QUANDO.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Obsolete("Usare TryPlaceBidTicker")]
|
||||||
private async Task TryPlaceBid(AuctionInfo auction, AuctionState state, CancellationToken token)
|
private async Task TryPlaceBid(AuctionInfo auction, AuctionState state, CancellationToken token)
|
||||||
{
|
{
|
||||||
|
// Reindirizza al nuovo sistema
|
||||||
var settings = SettingsManager.Load();
|
var settings = SettingsManager.Load();
|
||||||
|
|
||||||
// Offset: millisecondi prima della scadenza (configurato dall'utente)
|
|
||||||
int offsetMs = auction.BidBeforeDeadlineMs > 0
|
int offsetMs = auction.BidBeforeDeadlineMs > 0
|
||||||
? auction.BidBeforeDeadlineMs
|
? auction.BidBeforeDeadlineMs
|
||||||
: settings.DefaultBidBeforeDeadlineMs;
|
: settings.DefaultBidBeforeDeadlineMs;
|
||||||
|
|
||||||
// Timer dall'API (in secondi, convertito in ms)
|
|
||||||
double timerMs = state.Timer * 1000;
|
double timerMs = state.Timer * 1000;
|
||||||
|
await TryPlaceBidTicker(auction, timerMs, offsetMs, token);
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -690,8 +694,31 @@ namespace AutoBidder.Services
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
var pollingPing = auction.PollingLatencyMs;
|
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");
|
auction.AddLog($"[BID FAIL] {result.Error} | Ping: {pollingPing}ms");
|
||||||
OnLog?.Invoke($"[FAIL] Puntata fallita su {auction.Name} ({auction.AuctionId}): {result.Error}");
|
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
|
auction.BidHistory.Add(new BidHistory
|
||||||
@@ -926,6 +953,17 @@ namespace AutoBidder.Services
|
|||||||
if (isNewBid)
|
if (isNewBid)
|
||||||
{
|
{
|
||||||
auction.ResetCount++;
|
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
|
auction.BidHistory.Add(new BidHistory
|
||||||
{
|
{
|
||||||
Timestamp = DateTime.UtcNow,
|
Timestamp = DateTime.UtcNow,
|
||||||
@@ -936,6 +974,12 @@ namespace AutoBidder.Services
|
|||||||
Notes = $"Puntata: EUR{state.Price:F2}"
|
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
|
// ? RIMOSSO: Non incrementare qui - è già gestito da UpdateBidderStatsFromRecentBids
|
||||||
// L'incremento doppio causava conteggi gonfiati
|
// L'incremento doppio causava conteggi gonfiati
|
||||||
|
|
||||||
@@ -1011,10 +1055,12 @@ namespace AutoBidder.Services
|
|||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
// Limita al numero massimo di puntate (mantieni le più recenti = prime della lista)
|
// 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
|
auction.RecentBids = auction.RecentBids
|
||||||
.Take(maxEntries)
|
.Take(limit)
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ namespace AutoBidder.Services
|
|||||||
private readonly int _maxConcurrentRequests;
|
private readonly int _maxConcurrentRequests;
|
||||||
private readonly TimeSpan _cacheExpiration;
|
private readonly TimeSpan _cacheExpiration;
|
||||||
private readonly int _maxRetries;
|
private readonly int _maxRetries;
|
||||||
|
private readonly int _maxCacheEntries;
|
||||||
|
|
||||||
// Logging callback
|
// Logging callback
|
||||||
public Action<string>? OnLog { get; set; }
|
public Action<string>? OnLog { get; set; }
|
||||||
@@ -36,12 +37,14 @@ namespace AutoBidder.Services
|
|||||||
int maxConcurrentRequests = 3,
|
int maxConcurrentRequests = 3,
|
||||||
int requestsPerSecond = 5,
|
int requestsPerSecond = 5,
|
||||||
TimeSpan? cacheExpiration = null,
|
TimeSpan? cacheExpiration = null,
|
||||||
int maxRetries = 2)
|
int maxRetries = 2,
|
||||||
|
int maxCacheEntries = 50)
|
||||||
{
|
{
|
||||||
_maxConcurrentRequests = maxConcurrentRequests;
|
_maxConcurrentRequests = maxConcurrentRequests;
|
||||||
_minRequestDelay = TimeSpan.FromMilliseconds(1000.0 / requestsPerSecond);
|
_minRequestDelay = TimeSpan.FromMilliseconds(1000.0 / requestsPerSecond);
|
||||||
_cacheExpiration = cacheExpiration ?? TimeSpan.FromMinutes(5);
|
_cacheExpiration = cacheExpiration ?? TimeSpan.FromMinutes(3); // Ridotto da 5 a 3 minuti
|
||||||
_maxRetries = maxRetries;
|
_maxRetries = maxRetries;
|
||||||
|
_maxCacheEntries = maxCacheEntries;
|
||||||
_rateLimiter = new SemaphoreSlim(maxConcurrentRequests, maxConcurrentRequests);
|
_rateLimiter = new SemaphoreSlim(maxConcurrentRequests, maxConcurrentRequests);
|
||||||
|
|
||||||
_httpClient.Timeout = TimeSpan.FromSeconds(15);
|
_httpClient.Timeout = TimeSpan.FromSeconds(15);
|
||||||
@@ -191,10 +194,26 @@ namespace AutoBidder.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Salva HTML in cache
|
/// Salva HTML in cache con limite dimensione
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void SaveToCache(string url, string html)
|
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
|
_cache[url] = new CachedHtml
|
||||||
{
|
{
|
||||||
Html = html,
|
Html = html,
|
||||||
|
|||||||
@@ -15,6 +15,33 @@ namespace AutoBidder.Utilities
|
|||||||
public int DefaultMinResets { get; set; } = 0;
|
public int DefaultMinResets { get; set; } = 0;
|
||||||
public int DefaultMaxResets { 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
|
// LIMITI LOG
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Numero massimo di righe di log da mantenere per ogni singola asta (default: 500)
|
/// 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);
|
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
|
LAYOUT CON SPLITTER TRASCINABILI
|
||||||
═══════════════════════════════════════════════════════════════════ */
|
═══════════════════════════════════════════════════════════════════ */
|
||||||
@@ -220,34 +282,38 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
gap: 0; /* IMPORTANTE: nessun gap tra toolbar e content */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Area contenuto principale */
|
/* Area contenuto principale */
|
||||||
.main-content-area {
|
.main-content-area {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1 1 auto; /* Cresce e si riduce */
|
||||||
min-height: 0;
|
min-height: 0; /* IMPORTANTE per flex */
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
gap: 0;
|
gap: 0; /* IMPORTANTE: nessun gap, gli splitter gestiscono lo spazio */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Riga superiore (Aste + Log) */
|
/* Riga superiore (Aste + Log) */
|
||||||
.top-row {
|
.top-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex: 1;
|
flex: 1 1 auto; /* Cresce e si riduce - NON percentuale fissa */
|
||||||
min-height: 150px;
|
min-height: 200px; /* Altezza minima */
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
gap: 0;
|
gap: 0; /* IMPORTANTE: nessun gap, gutter gestisce lo spazio */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Riga inferiore (Dettagli) */
|
/* Riga inferiore (Dettagli) */
|
||||||
.bottom-row {
|
.bottom-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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;
|
overflow: hidden;
|
||||||
|
gap: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Pannello generico */
|
/* Pannello generico */
|
||||||
@@ -259,33 +325,55 @@
|
|||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-sizing: border-box;
|
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 */
|
/* Pannello Aste */
|
||||||
.panel-auctions {
|
.panel-auctions {
|
||||||
flex: 1;
|
flex: 1 1 auto; /* Cresce per riempire lo spazio */
|
||||||
min-width: 200px;
|
min-width: 300px;
|
||||||
|
height: 100%; /* Usa tutta l'altezza del contenitore */
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Pannello Log */
|
/* Pannello Log */
|
||||||
.panel-log {
|
.panel-log {
|
||||||
flex: 0 0 280px;
|
flex: 0 0 auto; /* NON cresce/riduce automaticamente */
|
||||||
min-width: 150px;
|
width: 320px; /* Larghezza fissa iniziale */
|
||||||
max-width: 450px;
|
min-width: 200px;
|
||||||
|
max-width: 500px;
|
||||||
|
height: 100%; /* Usa tutta l'altezza del contenitore */
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Pannello Dettagli */
|
/* Pannello Dettagli */
|
||||||
.panel-details {
|
.panel-details {
|
||||||
flex: 1;
|
flex: 1 1 auto; /* Cresce per riempire */
|
||||||
min-height: 80px;
|
min-height: 150px;
|
||||||
overflow: auto;
|
height: 100%; /* Usa tutta l'altezza del contenitore */
|
||||||
|
overflow: hidden; /* Il contenitore non scrolla */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Gutter/Splitter */
|
/* Gutter/Splitter */
|
||||||
.gutter {
|
.gutter {
|
||||||
background: rgba(255, 255, 255, 0.03);
|
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;
|
transition: background 0.15s ease;
|
||||||
|
z-index: 10; /* Sopra i pannelli */
|
||||||
}
|
}
|
||||||
|
|
||||||
.gutter:hover {
|
.gutter:hover {
|
||||||
@@ -299,11 +387,15 @@
|
|||||||
.gutter-vertical {
|
.gutter-vertical {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
cursor: col-resize;
|
cursor: col-resize;
|
||||||
|
min-width: 6px; /* Larghezza fissa */
|
||||||
|
max-width: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gutter-horizontal {
|
.gutter-horizontal {
|
||||||
height: 6px;
|
height: 6px;
|
||||||
cursor: row-resize;
|
cursor: row-resize;
|
||||||
|
min-height: 6px; /* Altezza fissa */
|
||||||
|
max-height: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Header pannello */
|
/* Header pannello */
|
||||||
@@ -327,15 +419,47 @@
|
|||||||
|
|
||||||
.panel-content {
|
.panel-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow-y: auto; /* Scroll verticale */
|
||||||
min-height: 0;
|
overflow-x: hidden;
|
||||||
|
min-height: 0; /* Importante per flex */
|
||||||
|
max-height: 100%; /* Non superare il pannello */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Contenuto dettagli */
|
/* Contenuto dettagli */
|
||||||
.auction-details-content {
|
.auction-details-content {
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
height: 100%;
|
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 {
|
.details-header {
|
||||||
|
|||||||
Reference in New Issue
Block a user