Files
Mimante/Mimante/Pages/Index.razor.cs
T
Alby96 ae861e78d2 Implementate strategie avanzate e tracking aste v1.3.0
- Aggiunto BidStrategyService: adaptive latency, jitter, offset dinamico, heat metric, soft retreat, probabilistic bidding, profiling avversari, bankroll manager.
- Esteso AuctionInfo con metriche avanzate: latenze, collisioni, heat, duello, tracking sessione, override strategie.
- Nuova sezione "Strategie Avanzate" in Settings (UI) con opzioni dettagliate e bulk update.
- Miglioramenti UX: auto-scroll log, filtri e dettagli avanzati in Statistics, gestione nomi prodotti, pulsanti sempre attivi.
- Fix bug Blazor (layout, redirect, log, conteggio puntate, entità HTML).
- Aggiornata documentazione, changelog, guide Docker/Gitea.
- Versione incrementata a 1.3.0. Migrazione database per nuove metriche e tracking completo.
2026-01-28 11:37:40 +01:00

1238 lines
43 KiB
C#

using AutoBidder.Models;
using AutoBidder.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace AutoBidder.Pages
{
public partial class Index : IDisposable
{
[Inject] private ApplicationStateService AppState { get; set; } = default!;
[Inject] private HtmlCacheService HtmlCache { get; set; } = default!;
[Inject] private StatsService StatsService { get; set; } = default!;
private List<AuctionInfo> auctions => AppState.Auctions.ToList();
private AuctionInfo? selectedAuction
{
get => AppState.SelectedAuction;
set => AppState.SelectedAuction = value;
}
private List<LogEntry> globalLog => AppState.GlobalLog.ToList();
private bool isMonitoringActive
{
get => AppState.IsMonitoringActive;
set => AppState.IsMonitoringActive = value;
}
private System.Threading.Timer? refreshTimer;
private System.Threading.Timer? sessionTimer;
// Dialog Aggiungi Asta
private bool showAddDialog = false;
private string addDialogUrl = "";
private string? addDialogError = null;
// Session info
private string? sessionUsername;
private int sessionRemainingBids;
private double sessionShopCredit;
private int sessionAuctionsWon;
// Recommended limits
private bool isLoadingRecommendations = false;
private string? recommendationMessage = null;
private bool recommendationSuccess = false;
// Auto-scroll log
private ElementReference globalLogRef;
private int lastLogCount = 0;
protected override void OnInitialized()
{
// Sottoscrivi agli eventi del servizio di stato (ASYNC)
AppState.OnStateChangedAsync += OnAppStateChangedAsync;
// Le aste vengono caricate in Program.cs all'avvio
// Qui carichiamo SOLO se non sono già state caricate (es. primo accesso dopo avvio)
if (auctions.Count == 0)
{
Console.WriteLine("[Index] No auctions in ApplicationStateService, loading from disk...");
LoadAuctionsFromDisk();
}
else
{
Console.WriteLine($"[Index] Auctions already loaded in ApplicationStateService: {auctions.Count}");
}
AuctionMonitor.OnLog += OnGlobalLog;
AuctionMonitor.OnAuctionUpdated += OnAuctionUpdated;
refreshTimer = new System.Threading.Timer(async _ =>
{
await InvokeAsync(StateHasChanged);
}, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
// Carica sessione all'avvio
LoadSession();
// Timer per aggiornamento sessione ogni 30 secondi
sessionTimer = new System.Threading.Timer(async _ =>
{
await RefreshSessionAsync();
await InvokeAsync(StateHasChanged);
}, null, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30));
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Aggiungi listener per tasto Canc
await JSRuntime.InvokeVoidAsync("addDeleteKeyListener",
DotNetObjectReference.Create(this));
}
// Auto-scroll log globale quando ci sono nuovi messaggi
if (globalLog.Count != lastLogCount)
{
lastLogCount = globalLog.Count;
try
{
await JSRuntime.InvokeVoidAsync("scrollToBottom", "globalLogContainer");
}
catch { /* Ignora errori JS */ }
}
}
// Handler async per eventi da background thread
private async Task OnAppStateChangedAsync()
{
await InvokeAsync(StateHasChanged);
}
private void LoadAuctionsFromDisk()
{
var loadedAuctions = AutoBidder.Utilities.PersistenceManager.LoadAuctions();
AppState.SetAuctions(loadedAuctions);
foreach (var auction in loadedAuctions)
{
AuctionMonitor.AddAuction(auction);
}
if (loadedAuctions.Count > 0)
{
AddLog($"Caricate {loadedAuctions.Count} aste salvate");
}
}
// Nota: ApplyAutoStartLogic è stato rimosso perché la logica di avvio
// è ora gestita centralmente in Program.cs durante l'inizializzazione dell'app.
// Questo evita duplicazioni e garantisce che lo stato sia ripristinato correttamente.
private void SaveAuctions()
{
AutoBidder.Utilities.PersistenceManager.SaveAuctions(auctions);
AddLog("Aste salvate");
}
private void AddLog(string message)
{
AppState.AddLog($"[{DateTime.Now:HH:mm:ss}] {message}");
}
private void OnGlobalLog(string message)
{
AppState.AddLog($"[{DateTime.Now:HH:mm:ss}] {message}");
InvokeAsync(StateHasChanged);
}
private void OnAuctionUpdated(AuctionState state)
{
var auction = auctions.FirstOrDefault(a => a.AuctionId == state.AuctionId);
if (auction != null)
{
// Salva l'ultimo stato ricevuto
auction.LastState = state;
// Aggiorna contatore puntate se disponibile nello stato
if (state.MyBidsCount.HasValue)
{
auction.BidsUsedOnThisAuction = state.MyBidsCount.Value;
}
// Notifica il cambiamento usando InvokeAsync per thread-safety
_ = InvokeAsync(() =>
{
AppState.ForceUpdate();
StateHasChanged();
});
}
}
private void SelectAuction(AuctionInfo auction)
{
selectedAuction = auction;
}
// Gestione controlli globali
private void StartAll()
{
foreach (var auction in auctions)
{
auction.IsActive = true;
auction.IsPaused = false;
}
AuctionMonitor.Start();
isMonitoringActive = true;
SaveAuctions();
AddLog("Avviate tutte le aste");
}
private void PauseAll()
{
foreach (var auction in auctions)
{
auction.IsPaused = true;
}
SaveAuctions();
AddLog("Messe in pausa tutte le aste");
}
private void StopAll()
{
foreach (var auction in auctions)
{
auction.IsActive = false;
auction.IsPaused = false;
}
AuctionMonitor.Stop();
isMonitoringActive = false;
SaveAuctions();
AddLog("Fermate tutte le aste");
}
// Gestione singola asta
private void StartAuction(AuctionInfo auction)
{
auction.IsActive = true;
auction.IsPaused = false;
// Auto-start monitor se non attivo
if (!isMonitoringActive)
{
AuctionMonitor.Start();
isMonitoringActive = true;
AddLog("[AUTO-START] Monitoraggio avviato");
}
SaveAuctions();
AddLog($"Avviata asta: {auction.Name}");
}
private void PauseAuction(AuctionInfo auction)
{
auction.IsPaused = true;
SaveAuctions();
AddLog($"In pausa asta: {auction.Name}");
}
private void ResumeAuction(AuctionInfo auction)
{
auction.IsPaused = false;
SaveAuctions();
AddLog($"Ripresa asta: {auction.Name}");
}
private void StopAuction(AuctionInfo auction)
{
auction.IsActive = false;
auction.IsPaused = false;
SaveAuctions();
AddLog($"Fermata asta: {auction.Name}");
// Auto-stop monitor se nessuna asta è attiva
if (!auctions.Any(a => a.IsActive))
{
AuctionMonitor.Stop();
isMonitoringActive = false;
AddLog("[AUTO-STOP] Monitoraggio fermato");
}
}
// Puntata manuale
private async Task ManualBidAuction(AuctionInfo auction)
{
if (AppState.IsManualBidding(auction.AuctionId))
{
// Già in corso una puntata manuale per questa asta
return;
}
try
{
// Segna come in corso
AppState.StartManualBidding(auction.AuctionId);
AddLog($"[MANUAL BID] Puntata manuale su: {auction.Name}");
// Esegui la puntata tramite AuctionMonitor
var result = await AuctionMonitor.PlaceManualBidAsync(auction);
if (result.Success)
{
AddLog($"[MANUAL BID OK] Puntata riuscita! Latenza: {result.LatencyMs}ms, Nuovo prezzo: €{result.NewPrice:F2}");
// Aggiorna i dati se disponibili
if (result.RemainingBids.HasValue)
{
auction.RemainingBids = result.RemainingBids.Value;
AddLog($"[BIDS] Puntate rimanenti: {result.RemainingBids.Value}");
}
if (result.BidsUsedOnThisAuction.HasValue)
{
auction.BidsUsedOnThisAuction = result.BidsUsedOnThisAuction.Value;
}
SaveAuctions();
}
else
{
AddLog($"[MANUAL BID FAIL] Puntata fallita: {result.Error}");
}
}
catch (Exception ex)
{
AddLog($"[ERROR] Errore puntata manuale: {ex.Message}");
}
finally
{
// Rimuovi dal set delle puntate in corso
AppState.StopManualBidding(auction.AuctionId);
}
}
private bool IsManualBidding(AuctionInfo auction)
{
return AppState.IsManualBidding(auction.AuctionId);
}
// Dialog Aggiungi Asta
private void ShowAddAuctionDialog()
{
showAddDialog = true;
addDialogUrl = "";
addDialogError = null;
}
private void CloseAddDialog()
{
showAddDialog = false;
}
private void AddAuction()
{
addDialogError = null;
// Valida URL
if (!addDialogUrl.Contains("bidoo.com"))
{
addDialogError = "URL non valido. Deve essere un link di Bidoo.com";
return;
}
// Estrai ID asta dall'URL
var auctionId = ExtractAuctionId(addDialogUrl);
if (string.IsNullOrEmpty(auctionId))
{
addDialogError = "Impossibile estrarre l'ID dell'asta dall'URL";
return;
}
// Verifica se già esiste
if (auctions.Any(a => a.AuctionId == auctionId))
{
addDialogError = "Questa asta è già monitorata";
return;
}
// Carica impostazioni default
var settings = AutoBidder.Utilities.SettingsManager.Load();
// Determina stato iniziale in base a DefaultNewAuctionState
bool isActive = false;
bool isPaused = false;
switch (settings.DefaultNewAuctionState)
{
case "Active":
isActive = true;
isPaused = false;
break;
case "Paused":
isActive = true;
isPaused = true;
break;
case "Stopped":
default:
isActive = false;
isPaused = false;
break;
}
// Estrai nome prodotto dall'URL se possibile, altrimenti usa nome temporaneo
var productName = ExtractProductNameFromUrl(addDialogUrl);
var tempName = string.IsNullOrWhiteSpace(productName) ? $"Asta {auctionId}" : productName;
// Crea nuova asta
var newAuction = new AuctionInfo
{
AuctionId = auctionId,
Name = tempName,
OriginalUrl = addDialogUrl,
BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs,
CheckAuctionOpenBeforeBid = settings.DefaultCheckAuctionOpenBeforeBid,
MinPrice = settings.DefaultMinPrice,
MaxPrice = settings.DefaultMaxPrice,
MinResets = settings.DefaultMinResets,
MaxResets = settings.DefaultMaxResets,
IsActive = isActive,
IsPaused = isPaused
};
auctions.Add(newAuction);
AppState.AddAuction(newAuction);
AuctionMonitor.AddAuction(newAuction);
SaveAuctions();
// Log stato iniziale
string stateLabel = settings.DefaultNewAuctionState switch
{
"Active" => "Attiva",
"Paused" => "In Pausa",
_ => "Fermata"
};
AddLog($"Aggiunta asta: {newAuction.Name} (ID: {auctionId}) - Stato: {stateLabel}");
// Auto-start monitor se necessario
if (isActive && !isMonitoringActive)
{
AuctionMonitor.Start();
isMonitoringActive = true;
AddLog("[AUTO-START] Monitoraggio avviato per nuova asta attiva");
}
CloseAddDialog();
selectedAuction = newAuction;
// Recupera nome reale e info prodotto in background
_ = FetchAuctionDetailsInBackgroundAsync(newAuction);
}
/// <summary>
/// Recupera il nome reale dell'asta e le informazioni prodotto in background
/// </summary>
private async Task FetchAuctionDetailsInBackgroundAsync(AuctionInfo auction)
{
try
{
AddLog($"[FETCH] Caricamento dettagli asta {auction.AuctionId}...");
// Usa HtmlCacheService per recuperare l'HTML
var response = await HtmlCache.GetHtmlAsync(
auction.OriginalUrl,
Services.RequestPriority.Normal,
bypassCache: false
);
if (!response.Success)
{
AddLog($"[FETCH] Errore: {response.Error}");
return;
}
bool updated = false;
// 1. Estrai nome dal <title>
var titleMatch = System.Text.RegularExpressions.Regex.Match(
response.Html,
@"<title>([^<]+)</title>",
System.Text.RegularExpressions.RegexOptions.IgnoreCase
);
if (titleMatch.Success)
{
var productName = titleMatch.Groups[1].Value
.Trim()
.Replace(" - Bidoo", "")
.Replace(" | Bidoo", "");
// Decodifica HTML entities
productName = System.Net.WebUtility.HtmlDecode(productName);
// ?? FIX: Sostituisci entità HTML non standard
productName = productName
.Replace("&plus;", "+")
.Replace("&amp;plus;", "+")
.Replace(" + ", " & "); // Normalizza separatori
if (!string.IsNullOrWhiteSpace(productName) && productName != auction.Name)
{
auction.Name = productName;
updated = true;
AddLog($"[FETCH] Nome aggiornato: {productName}");
}
}
// 2. Estrai informazioni prodotto (prezzo, spedizione, limiti)
var productInfoExtracted = AutoBidder.Utilities.ProductValueCalculator.ExtractProductInfo(
response.Html,
auction
);
if (productInfoExtracted)
{
updated = true;
AddLog($"[FETCH] Info prodotto: Valore={auction.BuyNowPrice:F2}€, Spedizione={auction.ShippingCost:F2}€");
}
// 3. Salva se qualcosa è cambiato
if (updated)
{
SaveAuctions();
AppState.ForceUpdate();
await InvokeAsync(StateHasChanged);
}
}
catch (Exception ex)
{
AddLog($"[FETCH ERROR] {ex.Message}");
}
}
private string ExtractProductNameFromUrl(string url)
{
try
{
// Pattern: /asta/nome-prodotto-123456
var match = System.Text.RegularExpressions.Regex.Match(
url,
@"/asta/([a-zA-Z0-9\-]+)-\d{5,}",
System.Text.RegularExpressions.RegexOptions.IgnoreCase
);
if (match.Success)
{
var slug = match.Groups[1].Value;
// Converte trattini in spazi e capitalizza
var name = slug.Replace("-", " ");
return System.Globalization.CultureInfo.CurrentCulture.TextInfo.ToTitleCase(name);
}
}
catch { }
return string.Empty;
}
/// <summary>
/// Estrae l'ID dell'asta dall'URL
/// Supporta formati:
/// - https://it.bidoo.com/asta/prodotto-123456
/// - https://it.bidoo.com/auction.php?a=asta_123456
/// </summary>
private string ExtractAuctionId(string url)
{
try
{
// Pattern 1: /asta/nome-prodotto-123456
var match1 = System.Text.RegularExpressions.Regex.Match(url, @"/asta/[a-zA-Z0-9\-]+-(\d{5,})");
if (match1.Success)
return match1.Groups[1].Value;
// Pattern 2: auction.php?a=asta_123456
var match2 = System.Text.RegularExpressions.Regex.Match(url, @"[?&]a=asta_(\d{5,})");
if (match2.Success)
return match2.Groups[1].Value;
// Pattern 3: Solo numeri
var match3 = System.Text.RegularExpressions.Regex.Match(url, @"(\d{5,})");
if (match3.Success)
return match3.Groups[1].Value;
}
catch { }
return string.Empty;
}
private void RemoveSelectedAuction()
{
if (selectedAuction != null)
{
var name = selectedAuction.Name;
AuctionMonitor.RemoveAuction(selectedAuction.AuctionId);
AppState.RemoveAuction(selectedAuction);
SaveAuctions();
AddLog($"Rimossa asta: {name}");
}
}
private async Task RemoveAllAuctions()
{
if (auctions.Count == 0) return;
var count = auctions.Count;
var confirmed = await JSRuntime.InvokeAsync<bool>("confirm",
$"Rimuovere TUTTE le {count} aste?\n\n" +
"?? Le aste terminate verranno salvate automaticamente nelle statistiche.\n" +
"Le aste non terminate andranno perse.");
if (!confirmed) return;
try
{
// Copia la lista per iterare in modo sicuro
var auctionsToRemove = auctions.ToList();
foreach (var auction in auctionsToRemove)
{
AuctionMonitor.RemoveAuction(auction.AuctionId);
AppState.RemoveAuction(auction);
}
SaveAuctions();
selectedAuction = null;
AddLog($"[BULK] Rimosse {count} aste");
await JSRuntime.InvokeVoidAsync("alert", $"? Rimosse {count} aste con successo");
}
catch (Exception ex)
{
AddLog($"Errore rimozione bulk: {ex.Message}");
await JSRuntime.InvokeVoidAsync("alert", $"Errore durante la rimozione:\n{ex.Message}");
}
}
private async Task RemoveSelectedAuctionWithConfirm()
{
if (selectedAuction == null) return;
var auctionName = selectedAuction.Name;
var currentIndex = auctions.IndexOf(selectedAuction);
// Chiedi conferma
var confirmed = await JSRuntime.InvokeAsync<bool>("confirm",
$"Rimuovere l'asta '{auctionName}'?\n\nL'asta verrà eliminata dalla lista e non sarà più monitorata.");
if (!confirmed) return;
try
{
var auctionId = selectedAuction.AuctionId;
// Rimuove dal monitor
AuctionMonitor.RemoveAuction(auctionId);
AppState.RemoveAuction(selectedAuction);
SaveAuctions();
AddLog($"Rimossa asta: {auctionName}");
// Sposta focus sulla riga vicina
if (auctions.Count > 0)
{
int newIndex;
if (currentIndex >= auctions.Count)
{
// Era l'ultima, seleziona la nuova ultima
newIndex = auctions.Count - 1;
}
else
{
// Seleziona quella che ora è nella stessa posizione
newIndex = currentIndex;
}
selectedAuction = auctions[newIndex];
AddLog($"Focus spostato su: {selectedAuction.Name}");
}
else
{
selectedAuction = null;
AddLog("Nessuna asta rimasta nella lista");
}
}
catch (Exception ex)
{
AddLog($"Errore rimozione asta: {ex.Message}");
await JSRuntime.InvokeVoidAsync("alert", $"Errore durante la rimozione:\n{ex.Message}");
}
}
private void ClearGlobalLog()
{
AppState.ClearLog();
AddLog("Log pulito");
}
private async Task CopyToClipboard(string text)
{
try
{
await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", text);
AddLog("URL copiato negli appunti");
}
catch
{
AddLog("Impossibile copiare negli appunti");
}
}
private async Task OpenAuctionInNewTab(string url)
{
try
{
await JSRuntime.InvokeVoidAsync("window.open", url, "_blank");
AddLog("Asta aperta in nuova scheda");
}
catch (Exception ex)
{
AddLog($"Errore apertura: {ex.Message}");
}
}
// Helper methods per stili e classi
private string GetRowClass(AuctionInfo auction)
{
return auction == selectedAuction ? "table-active" : "";
}
private string GetStatusBadgeClass(AuctionInfo auction)
{
// Prima controlla lo stato real-time dell'asta (da LastState)
if (auction.LastState != null)
{
return auction.LastState.Status switch
{
AuctionStatus.EndedWon => "status-won",
AuctionStatus.EndedLost => "status-lost",
AuctionStatus.Closed => "status-closed",
AuctionStatus.Paused => "status-system-paused",
AuctionStatus.Pending => "status-pending",
AuctionStatus.Scheduled => "status-scheduled",
AuctionStatus.NotStarted => "status-scheduled",
_ => GetUserControlStatusClass(auction)
};
}
return GetUserControlStatusClass(auction);
}
private string GetUserControlStatusClass(AuctionInfo auction)
{
// Stati controllati dall'utente
if (!auction.IsActive) return "status-stopped";
if (auction.IsPaused) return "status-paused";
if (auction.IsAttackInProgress) return "status-attacking";
return "status-active";
}
private string GetStatusText(AuctionInfo auction)
{
// Prima controlla lo stato real-time dell'asta
if (auction.LastState != null)
{
switch (auction.LastState.Status)
{
case AuctionStatus.EndedWon:
return "Vinta!";
case AuctionStatus.EndedLost:
return "Persa";
case AuctionStatus.Closed:
return "Chiusa";
case AuctionStatus.Paused:
return "Sospesa";
case AuctionStatus.Pending:
return "In Attesa";
case AuctionStatus.Scheduled:
case AuctionStatus.NotStarted:
return "Programmata";
}
}
// Stati controllati dall'utente
if (!auction.IsActive) return "Fermata";
if (auction.IsPaused) return "Pausa";
return "Attiva";
}
private string GetStatusIcon(AuctionInfo auction)
{
// Prima controlla lo stato real-time dell'asta
if (auction.LastState != null)
{
switch (auction.LastState.Status)
{
case AuctionStatus.EndedWon:
return "<i class='bi bi-trophy-fill'></i>";
case AuctionStatus.EndedLost:
return "<i class='bi bi-x-circle-fill'></i>";
case AuctionStatus.Closed:
return "<i class='bi bi-lock-fill'></i>";
case AuctionStatus.Paused:
return "<i class='bi bi-moon-fill'></i>";
case AuctionStatus.Pending:
return "<i class='bi bi-hourglass-split'></i>";
case AuctionStatus.Scheduled:
case AuctionStatus.NotStarted:
return "<i class='bi bi-calendar-event'></i>";
}
}
// Stati controllati dall'utente
if (!auction.IsActive) return "<i class='bi bi-stop-circle'></i>";
if (auction.IsPaused) return "<i class='bi bi-pause-circle'></i>";
return "<i class='bi bi-play-circle-fill'></i>";
}
private string GetStatusAnimationClass(AuctionInfo auction)
{
// Animazioni disabilitate - i colori sono sufficienti per identificare lo stato
return "";
}
private string GetPriceDisplay(AuctionInfo? auction)
{
try
{
if (auction == null) return "-";
// Prova a leggere da LastState prima
if (auction.LastState != null && auction.LastState.Price > 0)
return $"€{auction.LastState.Price:F2}";
// Fallback a CalculatedValue
if (auction.CalculatedValue?.CurrentPrice > 0)
return $"€{auction.CalculatedValue.CurrentPrice:F2}";
}
catch (Exception ex)
{
Console.WriteLine($"[ERROR] GetPriceDisplay: {ex.Message}");
}
return "-";
}
private string GetPriceClass(AuctionInfo? auction)
{
try
{
if (auction == null) return "text-muted";
double price = auction.LastState?.Price ?? auction.CalculatedValue?.CurrentPrice ?? 0;
if (price > 0)
return "fw-bold text-success";
}
catch (Exception ex)
{
Console.WriteLine($"[ERROR] GetPriceClass: {ex.Message}");
}
return "text-muted";
}
private string GetTimerDisplay(AuctionInfo? auction)
{
try
{
if (auction == null) return "-";
if (auction.LastState == null || auction.LastState.Timer <= 0)
return "-";
var ts = TimeSpan.FromSeconds(auction.LastState.Timer);
if (ts.TotalHours >= 1)
return $"{(int)ts.TotalHours}h {ts.Minutes}m";
if (ts.TotalMinutes >= 1)
return $"{ts.Minutes}m {ts.Seconds}s";
return $"{ts.Seconds}s";
}
catch (Exception ex)
{
Console.WriteLine($"[ERROR] GetTimerDisplay: {ex.Message}");
}
return "-";
}
private string GetLastBidder(AuctionInfo? auction)
{
try
{
if (auction == null) return "-";
return auction.RecentBids?.FirstOrDefault()?.Username ?? "-";
}
catch
{
return "-";
}
}
private int GetMyBidsCount(AuctionInfo? auction)
{
try
{
if (auction == null) return 0;
// Usa BidsUsedOnThisAuction se disponibile (più accurato)
if (auction.BidsUsedOnThisAuction.HasValue)
return auction.BidsUsedOnThisAuction.Value;
// Fallback: conta da BidHistory
return auction.BidHistory?.Count(b => b?.EventType == BidEventType.MyBid) ?? 0;
}
catch
{
return 0;
}
}
private string GetCurrentUsername()
{
return sessionUsername ?? "";
}
// ?? NUOVI METODI: Visualizzazione valori prodotto
private string GetTotalCostDisplay(AuctionInfo? auction)
{
try
{
if (auction?.CalculatedValue != null)
{
return $"€{auction.CalculatedValue.TotalCostIfWin:F2}";
}
}
catch { }
return "-";
}
private string GetSavingsDisplay(AuctionInfo? auction)
{
try
{
if (auction?.CalculatedValue?.Savings.HasValue == true)
{
var savings = auction.CalculatedValue.Savings.Value;
var percentage = auction.CalculatedValue.SavingsPercentage ?? 0;
if (savings > 0)
return $"+€{savings:F2} ({percentage:F0}%)";
else
return $"-€{Math.Abs(savings):F2} ({Math.Abs(percentage):F0}%)";
}
}
catch { }
return "-";
}
private string GetSavingsClass(AuctionInfo? auction)
{
try
{
if (auction?.CalculatedValue?.Savings.HasValue == true)
{
return auction.CalculatedValue.Savings.Value > 0
? "text-success fw-bold" // Verde per risparmio
: "text-danger fw-bold"; // Rosso per perdita
}
}
catch { }
return "text-muted";
}
private string GetBuyNowPriceDisplay(AuctionInfo? auction)
{
try
{
if (auction?.BuyNowPrice.HasValue == true)
{
return $"€{auction.BuyNowPrice.Value:F2}";
}
}
catch { }
return "-";
}
private string GetIsWorthItIcon(AuctionInfo? auction)
{
try
{
if (auction?.CalculatedValue != null)
{
// Usa icone Bootstrap Icons invece di emoji
return auction.CalculatedValue.IsWorthIt
? "<i class='bi bi-check-circle-fill text-success'></i>"
: "<i class='bi bi-x-circle-fill text-danger'></i>";
}
}
catch { }
return "-";
}
private string GetIsWorthItClass(AuctionInfo? auction)
{
try
{
if (auction?.CalculatedValue != null)
{
return auction.CalculatedValue.IsWorthIt
? "badge bg-success"
: "badge bg-danger";
}
}
catch { }
return "badge bg-secondary";
}
private string GetPingDisplay(AuctionInfo? auction)
{
try
{
if (auction == null) return "-";
var latency = auction.PollingLatencyMs;
if (latency <= 0) return "-";
// Colora in base al ping
var cssClass = latency < 100 ? "text-success" :
latency < 300 ? "text-warning" :
"text-danger";
return $"{latency}ms";
}
catch
{
return "-";
}
}
private IEnumerable<string> GetAuctionLog(AuctionInfo auction)
{
return auction.AuctionLog.TakeLast(50);
}
/// <summary>
/// Ottiene una copia thread-safe della lista RecentBids
/// </summary>
private List<BidHistoryEntry> GetRecentBidsSafe(AuctionInfo? auction)
{
if (auction?.RecentBids == null)
return new List<BidHistoryEntry>();
try
{
// Lock per evitare modifiche durante la copia
lock (auction.RecentBids)
{
return auction.RecentBids.ToList();
}
}
catch
{
// Fallback in caso di errore
return new List<BidHistoryEntry>();
}
}
private string GetLogEntryClass(LogEntry logEntry)
{
try
{
// Prima controlla il livello di log
switch (logEntry.Level)
{
case Services.LogLevel.Error:
return "log-entry-error";
case Services.LogLevel.Warning:
return "log-entry-warning";
case Services.LogLevel.Success:
return "log-entry-success";
case Services.LogLevel.Debug:
return "log-entry-debug";
default:
break;
}
// Poi controlla il messaggio per compatibilità
var msg = logEntry.Message;
if (msg.Contains("[ERROR]") || msg.Contains("Errore") || msg.Contains("errore") || msg.Contains("FAIL"))
return "log-entry-error";
if (msg.Contains("[WARN]") || msg.Contains("Warning") || msg.Contains("warning"))
return "log-entry-warning";
if (msg.Contains("[OK]") || msg.Contains("SUCCESS") || msg.Contains("Vinta"))
return "log-entry-success";
}
catch { }
return "log-entry-info";
}
private void LoadSession()
{
var savedSession = AutoBidder.Services.SessionManager.LoadSession();
if (savedSession != null && savedSession.IsValid)
{
sessionUsername = savedSession.Username;
sessionRemainingBids = savedSession.RemainingBids;
sessionShopCredit = savedSession.ShopCredit;
sessionAuctionsWon = 0; // TODO: add to BidooSession model
// Inizializza AuctionMonitor con la sessione salvata
if (!string.IsNullOrEmpty(savedSession.CookieString))
{
AuctionMonitor.InitializeSessionWithCookie(savedSession.CookieString, savedSession.Username ?? "");
}
}
else
{
var session = AuctionMonitor.GetSession();
sessionUsername = session?.Username;
sessionRemainingBids = session?.RemainingBids ?? 0;
sessionShopCredit = session?.ShopCredit ?? 0;
sessionAuctionsWon = 0;
}
}
private async Task RefreshSessionAsync()
{
try
{
var success = await AuctionMonitor.UpdateUserInfoAsync();
if (success)
{
var session = AuctionMonitor.GetSession();
if (session != null)
{
sessionUsername = session.Username;
sessionRemainingBids = session.RemainingBids;
sessionShopCredit = session.ShopCredit;
sessionAuctionsWon = 0; // TODO: add to BidooSession model
// Salva sessione aggiornata
AutoBidder.Services.SessionManager.SaveSession(session);
}
}
}
catch { }
}
private string GetBidsClass()
{
if (sessionRemainingBids < 50) return "bids-low";
if (sessionRemainingBids < 150) return "bids-medium";
return "bids-high";
}
public void Dispose()
{
refreshTimer?.Dispose();
sessionTimer?.Dispose();
// Rimuovi sottoscrizioni (ASYNC)
if (AppState != null)
{
AppState.OnStateChangedAsync -= OnAppStateChangedAsync;
}
if (AuctionMonitor != null)
{
AuctionMonitor.OnLog -= OnGlobalLog;
AuctionMonitor.OnAuctionUpdated -= OnAuctionUpdated;
}
}
[JSInvokable]
public async Task OnDeleteKeyPressed()
{
if (selectedAuction != null)
{
await RemoveSelectedAuctionWithConfirm();
}
}
private async Task ApplyRecommendedLimitsToSelected()
{
if (selectedAuction == null) return;
isLoadingRecommendations = true;
recommendationMessage = null;
StateHasChanged();
try
{
var limits = await StatsService.GetRecommendedLimitsAsync(selectedAuction.Name);
if (limits == null || limits.SampleSize == 0)
{
recommendationMessage = "Nessun dato storico disponibile per questo prodotto. Completa alcune aste per generare raccomandazioni.";
recommendationSuccess = false;
}
else if (limits.ConfidenceScore < 30)
{
// Applica comunque ma con avviso
selectedAuction.MinPrice = limits.MinPrice;
selectedAuction.MaxPrice = limits.MaxPrice;
selectedAuction.MinResets = limits.MinResets;
selectedAuction.MaxResets = limits.MaxResets;
SaveAuctions();
recommendationMessage = $"Limiti applicati con bassa confidenza ({limits.ConfidenceScore}%) - basati su {limits.SampleSize} aste";
recommendationSuccess = true;
}
else
{
// Applica limiti con buona confidenza
selectedAuction.MinPrice = limits.MinPrice;
selectedAuction.MaxPrice = limits.MaxPrice;
selectedAuction.MinResets = limits.MinResets;
selectedAuction.MaxResets = limits.MaxResets;
SaveAuctions();
var hourInfo = limits.BestHourToPlay.HasValue
? $" | Ora migliore: {limits.BestHourToPlay}:00"
: "";
recommendationMessage = $"? Limiti applicati (confidenza {limits.ConfidenceScore}%, {limits.SampleSize} aste){hourInfo}";
recommendationSuccess = true;
}
}
catch (Exception ex)
{
recommendationMessage = $"Errore: {ex.Message}";
recommendationSuccess = false;
}
finally
{
isLoadingRecommendations = false;
StateHasChanged();
}
}
}
}