ae861e78d2
- 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.
1238 lines
43 KiB
C#
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("+", "+")
|
|
.Replace("&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();
|
|
}
|
|
}
|
|
}
|
|
}
|