Nella scheda Prodotti c'è il pulsante che rilegge tutto lo storico e rifà da capo i numeri di ogni articolo: aste viste, prezzo minimo, massimo e medio, puntate del vincitore, vittorie, osservazioni. I limiti scritti a mano e la stellina non cambiano. Il ricalcolo si costruisce da parte e sostituisce le schede solo alla fine: un annullamento a metà lascia quelle di prima. Ogni operazione massiva passa da una finestra di avanzamento (ProgressDialog): titolo, a che punto si è, barra, conto fatto/totale, tempo passato e Annulla. Modale quando è questione di secondi, altrimenti si continua a usare l'applicazione: il recupero delle puntate dei vincitori e lo studio delle aste vanno piano di proposito. La scheda Apprendimento ha un menu di operazioni: studiare le aste chiuse non ancora apprese (lo studio all'avvio si ferma dopo qualche minuto, questo va in fondo), ricostruire il profilo per prodotto e fascia oraria, rifare il bandit dalle decisioni registrate, azzerare lo sfidante o la calibrazione, ricominciare da capo. Lo studio delle aste è una sola routine con budget di tempo facoltativo, avanzamento e annullamento; quello che è già studiato resta studiato. Le barre in alto di tutte le schede hanno la stessa altezza (stile TabToolbar): passando da una scheda all'altra i pulsanti restano allo stesso posto. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
380 lines
18 KiB
C#
380 lines
18 KiB
C#
using System;
|
|
using System.Linq;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using AutoBidder.Models;
|
|
using AutoBidder.Services;
|
|
using AutoBidder.ViewModels;
|
|
using AutoBidder.Utilities;
|
|
|
|
namespace AutoBidder
|
|
{
|
|
/// <summary>
|
|
/// AutoBidder v4.0 - HTTP Only Multi-Auction Monitor
|
|
/// Architettura modulare con partial classes e UserControls
|
|
/// </summary>
|
|
public partial class MainWindow : Window
|
|
{
|
|
// SERVIZI CORE
|
|
private readonly AuctionMonitor _auctionMonitor;
|
|
private readonly System.Collections.ObjectModel.ObservableCollection<AuctionViewModel> _auctionViewModels = new System.Collections.ObjectModel.ObservableCollection<AuctionViewModel>();
|
|
|
|
// ? NUOVO: Servizio centralizzato per HTTP requests con cache e rate limiting
|
|
private readonly HtmlCacheService _htmlCacheService;
|
|
|
|
// UI State
|
|
private AuctionViewModel? _selectedAuction;
|
|
private bool _isAutomationActive = false;
|
|
private bool _isUpdatingSelection = false; // Previene loop di salvataggio
|
|
|
|
// Commands (initialized in MainWindow.Commands.cs)
|
|
public RelayCommand? StartAllCommand { get; private set; }
|
|
public RelayCommand? StopAllCommand { get; private set; }
|
|
public RelayCommand? PauseAllCommand { get; private set; }
|
|
public RelayCommand? GridStartCommand { get; private set; }
|
|
public RelayCommand? GridPauseCommand { get; private set; }
|
|
public RelayCommand? GridStopCommand { get; private set; }
|
|
public RelayCommand? GridBidCommand { get; private set; }
|
|
|
|
// Properties to access UserControl elements - AuctionMonitor
|
|
public DataGrid MultiAuctionsGrid => AuctionMonitor.MultiAuctionsGrid;
|
|
public RichTextBox LogBox => AuctionMonitor.LogBox;
|
|
public Button StartButton => AuctionMonitor.StartButton;
|
|
public Button PauseAllButton => AuctionMonitor.PauseAllButton;
|
|
public Button StopButton => AuctionMonitor.StopButton;
|
|
public Button AddUrlButton => AuctionMonitor.AddUrlButton;
|
|
public Button RemoveUrlButton => AuctionMonitor.RemoveUrlButton;
|
|
public TextBlock SelectedAuctionName => AuctionMonitor.SelectedAuctionName;
|
|
public TextBox SelectedAuctionUrl => AuctionMonitor.SelectedAuctionUrl;
|
|
public Button CopyAuctionUrlButton => AuctionMonitor.CopyAuctionUrlButton;
|
|
public TextBlock SelectedAuctionBiddersCount => AuctionMonitor.SelectedAuctionBiddersCount;
|
|
public DataGrid SelectedAuctionBiddersGrid => AuctionMonitor.SelectedAuctionBiddersGrid;
|
|
public Button ClearBiddersButton => AuctionMonitor.ClearBiddersButton;
|
|
public TextBlock MonitorateTitle => AuctionMonitor.MonitorateTitle;
|
|
public TextBox SelectedBidBeforeDeadlineMs => AuctionMonitor.SelectedBidBeforeDeadlineMs;
|
|
public TextBox SelectedMinPrice => AuctionMonitor.SelectedMinPrice;
|
|
public TextBox SelectedMaxPrice => AuctionMonitor.SelectedMaxPrice;
|
|
public TextBox SelectedMaxClicks => AuctionMonitor.SelectedMaxClicks;
|
|
public Button ResetSettingsButton => AuctionMonitor.ResetSettingsButton;
|
|
public Button ClearGlobalLogButton => AuctionMonitor.ClearGlobalLogButton;
|
|
public Button ClearLogButton => AuctionMonitor.ClearLogButton;
|
|
public RichTextBox SelectedAuctionLog => AuctionMonitor.SelectedAuctionLog;
|
|
// Puntate, credito e aste da confermare passano da UpdateAccountStatus: scriverli
|
|
// dall'esterno era il modo in cui la barra finiva per raccontare due storie diverse.
|
|
public TextBlock MinBidsLimitIndicator => AuctionMonitor.MinBidsLimitIndicator;
|
|
|
|
// Properties to access UserControl elements - Browser
|
|
public Microsoft.Web.WebView2.Wpf.WebView2 EmbeddedWebView => Browser.EmbeddedWebView;
|
|
public TextBox BrowserAddress => Browser.BrowserAddress;
|
|
|
|
// Properties to access UserControl elements - Settings
|
|
public TextBox DefaultBidBeforeDeadlineMs => Settings.DefaultBidBeforeDeadlineMsTextBox;
|
|
public TextBox DefaultMinPrice => Settings.DefaultMinPriceTextBox;
|
|
public TextBox DefaultMaxPrice => Settings.DefaultMaxPriceTextBox;
|
|
public TextBox DefaultMaxClicks => Settings.DefaultMaxClicksTextBox;
|
|
public TextBox MinimumRemainingBidsTextBox => Settings.MinimumRemainingBidsTextBox;
|
|
public TextBox MaxBidHistoryEntriesTextBox => Settings.MaxBidHistoryEntriesTextBox;
|
|
|
|
public MainWindow()
|
|
{
|
|
InitializeComponent();
|
|
|
|
// Titolo con la versione reale dell'assembly: un unico numero da cambiare nel
|
|
// .csproj a ogni release, senza rincorrerlo nelle stringhe.
|
|
Title = AppInfo.Title;
|
|
|
|
// Barra del titolo coerente col tema (scura/chiara)
|
|
SourceInitialized += (_, _) => ThemeManager.ApplyTitleBar(this);
|
|
|
|
// ? Inizializza HtmlCacheService con:
|
|
// - Max 3 richieste concorrenti
|
|
// - Max 5 richieste al secondo
|
|
// - Cache di 5 minuti
|
|
// - Max 2 retry per richiesta
|
|
_htmlCacheService = new HtmlCacheService(
|
|
maxConcurrentRequests: 3,
|
|
requestsPerSecond: 5,
|
|
cacheExpiration: TimeSpan.FromMinutes(5),
|
|
maxRetries: 2
|
|
);
|
|
_htmlCacheService.OnLog += (msg) => Log(msg, LogLevel.Info);
|
|
|
|
// Inizializza servizi
|
|
_auctionMonitor = new AuctionMonitor();
|
|
Learning.AuctionsProvider = () => _auctionMonitor.GetAuctions();
|
|
RefreshLearningToggle();
|
|
|
|
// ? NUOVO: Inizializza SessionService
|
|
InitializeSessionService();
|
|
|
|
// Initialize commands (from MainWindow.Commands.cs)
|
|
InitializeCommands();
|
|
|
|
// ? NUOVO: Inizializza validazione campi numerici
|
|
InitializeNumericInputValidation();
|
|
|
|
this.DataContext = this;
|
|
|
|
// Event handlers for auction monitor
|
|
_auctionMonitor.OnAuctionUpdated += AuctionMonitor_OnAuctionUpdated;
|
|
_auctionMonitor.OnBidExecuted += AuctionMonitor_OnBidExecuted;
|
|
_auctionMonitor.OnLog += AuctionMonitor_OnLog;
|
|
_auctionMonitor.OnResetCountChanged += AuctionMonitor_OnResetCountChanged;
|
|
_auctionMonitor.OnAuctionCompleted += AuctionMonitor_OnAuctionCompleted;
|
|
|
|
// ? NUOVO: Registra evento stato connessione
|
|
AuctionMonitor.ConnectionStatusClicked += AuctionMonitor_ConnectionStatusClicked;
|
|
|
|
// Bind griglia
|
|
MultiAuctionsGrid.ItemsSource = _auctionViewModels;
|
|
|
|
// Carica aste salvate (from MainWindow.AuctionManagement.cs)
|
|
LoadSavedAuctions();
|
|
|
|
// CARICA IMPOSTAZIONI PREDEFINITE ASTE
|
|
LoadDefaultSettings();
|
|
|
|
// Update initial button states
|
|
UpdateGlobalControlButtons();
|
|
|
|
// === LOG AVVIO APPLICAZIONE ===
|
|
Log("???????????????????????????????????????????????", LogLevel.Info);
|
|
Log($" {AppInfo.Title} - Multi-Auction Monitor", LogLevel.Info);
|
|
Log("???????????????????????????????????????????????", LogLevel.Info);
|
|
|
|
var settings = SettingsManager.Load();
|
|
Log($"Configurazione caricata:", LogLevel.Info);
|
|
Log($" � Livello log: {settings.MinLogLevel}", LogLevel.Info);
|
|
Log($" � Limite log globale: {settings.MaxGlobalLogLines} righe", LogLevel.Info);
|
|
Log($" � Limite log per asta: {settings.MaxLogLinesPerAuction} righe", LogLevel.Info);
|
|
Log($" � Puntate minime da mantenere: {(settings.MinimumRemainingBids > 0 ? settings.MinimumRemainingBids.ToString() : "nessun limite")}", LogLevel.Info);
|
|
Log($" � Anticipo puntata default: {settings.DefaultBidBeforeDeadlineMs}ms", LogLevel.Info);
|
|
|
|
var auctionCount = _auctionViewModels.Count;
|
|
if (auctionCount > 0)
|
|
{
|
|
Log($"Aste caricate: {auctionCount}", LogLevel.Success);
|
|
var activeCount = _auctionViewModels.Count(a => a.IsActive && !a.IsPaused);
|
|
var pausedCount = _auctionViewModels.Count(a => a.IsPaused);
|
|
var stoppedCount = _auctionViewModels.Count(a => !a.IsActive && !a.IsPaused);
|
|
if (activeCount > 0) Log($" � Attive: {activeCount}", LogLevel.Info);
|
|
if (pausedCount > 0) Log($" � In pausa: {pausedCount}", LogLevel.Info);
|
|
if (stoppedCount > 0) Log($" � Fermate: {stoppedCount}", LogLevel.Info);
|
|
}
|
|
else
|
|
{
|
|
Log("Nessuna asta caricata", LogLevel.Info);
|
|
}
|
|
|
|
Log("[OK] Applicazione pronta", LogLevel.Success);
|
|
Log("???????????????????????????????????????????????", LogLevel.Info);
|
|
|
|
// Initialize user info timers (from MainWindow.UserInfo.cs)
|
|
InitializeUserInfoTimers();
|
|
|
|
// ? NUOVO: Carica sessione salvata
|
|
LoadSavedSession();
|
|
|
|
// ? NUOVO: Pre-carica WebView2 in background per renderla subito disponibile
|
|
InitializeWebView2();
|
|
|
|
// Attach WebView2 context menu handler
|
|
try
|
|
{
|
|
if (EmbeddedWebView != null)
|
|
{
|
|
EmbeddedWebView.CoreWebView2InitializationCompleted += (s, e) =>
|
|
{
|
|
try
|
|
{
|
|
if (e.IsSuccess && EmbeddedWebView?.CoreWebView2 != null)
|
|
{
|
|
EmbeddedWebView.CoreWebView2.ContextMenuRequested += CoreWebView2_ContextMenuRequested;
|
|
}
|
|
}
|
|
catch { }
|
|
};
|
|
}
|
|
}
|
|
catch { }
|
|
|
|
// ? Timer per pulizia cache periodica (ogni 10 minuti)
|
|
var cacheCleanupTimer = new System.Windows.Threading.DispatcherTimer
|
|
{
|
|
Interval = TimeSpan.FromMinutes(10)
|
|
};
|
|
cacheCleanupTimer.Tick += (s, e) => _htmlCacheService.CleanExpiredCache();
|
|
cacheCleanupTimer.Start();
|
|
|
|
// Contatori e stato motore nella barra del Monitor
|
|
StartMonitorHeaderTimer();
|
|
|
|
// Sorveglianza dei prodotti seguiti (aggiunta automatica delle aste nuove)
|
|
StartProductWatcher();
|
|
|
|
// Riscatto automatico delle ricompense rimaste da prendere
|
|
StartFreeBidsService();
|
|
|
|
EnsureProductCardsBuilt();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Costruisce le schede prodotto la prima volta, ricavandole dallo storico già
|
|
/// raccolto, e le ricostruisce una volta sola quando cambia il modo di contare.
|
|
///
|
|
/// <para>Senza, chi ha già mesi di aste alle spalle aprirebbe un file vuoto e
|
|
/// dovrebbe aspettare le aste future per vedere qualcosa — proprio i numeri che
|
|
/// servono adesso per decidere i limiti. Gira su un thread di lavoro: rileggere
|
|
/// l'archivio all'avvio non deve ritardare la finestra.</para>
|
|
///
|
|
/// <para>Il contrassegno nel database d'esercizio dice con quale «generazione» le
|
|
/// schede sono state contate. La 2 è quella in cui la chiusura di un'asta arriva
|
|
/// una volta sola: prima ogni asta conclusa veniva contata due volte, e le schede
|
|
/// costruite allora vanno rifatte dallo storico, che è sempre stato giusto.</para>
|
|
/// </summary>
|
|
private void EnsureProductCardsBuilt()
|
|
{
|
|
try
|
|
{
|
|
var op = Data.OperationalDatabase.Instance;
|
|
if (ProductStatsStore.GetAll().Count > 0 && op.GetDocument(ProductStatsStore.GenerationDocument) == ProductStatsStore.CurrentGeneration) return;
|
|
|
|
_ = System.Threading.Tasks.Task.Run(() =>
|
|
{
|
|
var count = ProductStatsStore.RebuildFromHistory();
|
|
|
|
Dispatcher.BeginInvoke(() =>
|
|
Log($"[PRODOTTI] Schede prodotto ricostruite dallo storico ({count} aste)", LogLevel.Info));
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log($"[PRODOTTI] Costruzione schede non riuscita: {ex.Message}", LogLevel.Warning);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inizializza la validazione per tutti i campi numerici dell'applicazione
|
|
/// </summary>
|
|
private void InitializeNumericInputValidation()
|
|
{
|
|
// === AUCTION MONITOR - Campi Impostazioni Asta Selezionata ===
|
|
NumericTextBoxHelper.SetupIntegerInput(SelectedBidBeforeDeadlineMs, 200);
|
|
NumericTextBoxHelper.SetupDecimalInput(SelectedMinPrice, 0.00);
|
|
NumericTextBoxHelper.SetupDecimalInput(SelectedMaxPrice, 0.00);
|
|
NumericTextBoxHelper.SetupIntegerInput(SelectedMaxClicks, 0);
|
|
|
|
// === SETTINGS - Impostazioni Predefinite Aste ===
|
|
NumericTextBoxHelper.SetupIntegerInput(DefaultBidBeforeDeadlineMs, 200);
|
|
NumericTextBoxHelper.SetupDecimalInput(DefaultMinPrice, 0.00);
|
|
NumericTextBoxHelper.SetupDecimalInput(DefaultMaxPrice, 0.00);
|
|
NumericTextBoxHelper.SetupIntegerInput(DefaultMaxClicks, 0);
|
|
|
|
// === SETTINGS - Protezione Account ===
|
|
NumericTextBoxHelper.SetupIntegerInput(MinimumRemainingBidsTextBox, 0);
|
|
|
|
// === SETTINGS - Limiti Log ===
|
|
NumericTextBoxHelper.SetupIntegerInput(Settings.MaxLogLinesPerAuction, 500);
|
|
NumericTextBoxHelper.SetupIntegerInput(Settings.MaxGlobalLogLines, 1000);
|
|
NumericTextBoxHelper.SetupIntegerInput(Settings.MaxBidHistoryEntries, 20);
|
|
}
|
|
|
|
// ===== AUCTION MONITOR EVENT HANDLERS =====
|
|
|
|
// ── Coalescenza degli aggiornamenti verso l'interfaccia ──────────
|
|
//
|
|
// Con il tetto delle richieste tolto ogni asta produce ~17 interrogazioni al
|
|
// secondo, e ognuna arrivava qui: un BeginInvoke, una scansione lineare dei
|
|
// ViewModel, e venti notifiche di proprietà. Con trenta aste sono diecimila
|
|
// notifiche al secondo sul thread dell'interfaccia, con cinquantasette quasi
|
|
// ventimila. È questo che rende l'applicazione lenta — non il disco, la cui
|
|
// scrittura era già asincrona e costa mezzo punto di CPU.
|
|
//
|
|
// La stragrande maggioranza di quei poll non porta niente di nuovo da mostrare:
|
|
// stesso prezzo, stesso puntatore, stesso stato, stesso secondo sul cronometro.
|
|
// Il conto alla rovescia scorre da solo grazie al battito da 200 ms, che legge la
|
|
// scadenza ancorata e non ha bisogno del poll. Quindi si manda all'interfaccia
|
|
// solo il poll in cui cambia qualcosa di visibile, e comunque uno al secondo per
|
|
// asta perché ping e contatori si rinfreschino.
|
|
//
|
|
// Il motore non è toccato: la decisione di puntare vede ogni poll come prima.
|
|
private readonly System.Collections.Concurrent.ConcurrentDictionary<string, (string Firma, long Tick)> _ultimoInviato = new();
|
|
|
|
private static string FirmaVisibile(AuctionState s) =>
|
|
string.Concat(s.Price.ToString("F2"), '|', s.LastBidder, '|', s.Status, '|', (int)s.Timer, '|', s.IsMyBid ? '1' : '0');
|
|
|
|
private void AuctionMonitor_OnAuctionUpdated(AuctionState state)
|
|
{
|
|
var firma = FirmaVisibile(state);
|
|
var adesso = Environment.TickCount64;
|
|
|
|
if (_ultimoInviato.TryGetValue(state.AuctionId, out var prec) &&
|
|
prec.Firma == firma && adesso - prec.Tick < 1000)
|
|
{
|
|
return; // niente di nuovo da mostrare, e l'ultimo rinfresco è recente
|
|
}
|
|
|
|
_ultimoInviato[state.AuctionId] = (firma, adesso);
|
|
|
|
Dispatcher.BeginInvoke(() =>
|
|
{
|
|
var vm = _auctionViewModels.FirstOrDefault(a => a.AuctionId == state.AuctionId);
|
|
if (vm == null) return;
|
|
|
|
vm.UpdateState(state);
|
|
|
|
// I pannelli di dettaglio mostrano SOLO l'asta selezionata: ricostruirli
|
|
// anche per le altre significava rifare griglie e documenti di log invisibili,
|
|
// a ogni risposta di ogni asta.
|
|
var isSelected = _selectedAuction != null && _selectedAuction.AuctionId == state.AuctionId;
|
|
if (!isSelected) return;
|
|
|
|
vm.RefreshBidHistory();
|
|
UpdateAuctionLog(_selectedAuction!);
|
|
RefreshBiddersGrid(_selectedAuction!);
|
|
});
|
|
}
|
|
|
|
private void AuctionMonitor_OnBidExecuted(AuctionInfo auction, BidResult result)
|
|
{
|
|
Dispatcher.BeginInvoke(() =>
|
|
{
|
|
var vm = _auctionViewModels.FirstOrDefault(a => a.AuctionId == auction.AuctionId);
|
|
if (vm != null)
|
|
{
|
|
vm.RefreshCounters();
|
|
|
|
if (result.Success)
|
|
{
|
|
// Aggiorna il banner delle puntate residue dopo puntata automatica
|
|
if (result.RemainingBids.HasValue)
|
|
{
|
|
UpdateRemainingBidsDisplay();
|
|
}
|
|
|
|
Log($"[OK] Click su {auction.Name}: {result.LatencyMs}ms {result.Response}", LogLevel.Success);
|
|
}
|
|
else
|
|
{
|
|
Log($"[FAIL] Click fallito su {auction.Name}: {result.Error}", LogLevel.Error);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
private void AuctionMonitor_OnLog(string message)
|
|
{
|
|
Log(message);
|
|
}
|
|
|
|
private void AuctionMonitor_OnResetCountChanged(string auctionId)
|
|
{
|
|
Dispatcher.BeginInvoke(() =>
|
|
{
|
|
var vm = _auctionViewModels.FirstOrDefault(a => a.AuctionId == auctionId);
|
|
vm?.RefreshCounters();
|
|
});
|
|
}
|
|
}
|
|
}
|