Tre richieste, piu' la causa vera della lentezza trovata misurando. PERCORSI. Le impostazioni e i dati vanno in Documenti\AutoBidder per chi installa da zero; sessione, riferimenti del sito e profilo del browser restano in %LocalAppData%, perche' la sessione e' protetta con DPAPI e legata all'utente Windows. Chi ha gia' un'installazione non viene spostato: se le impostazioni esistono in %LocalAppData% e non in Documenti si continua a usare quelle dove sono (scelta esplicita dell'utente, per non toccare mesi di raccolta). La radice si decide una volta sola all'avvio e non cambia piu' nel processo. La suite di test la dirotta in una cartella temporanea: senza, un test che tocca le impostazioni leggerebbe quelle vere. PULIZIA. Tre criteri nuovi: per prodotto (elenco con i conteggi, confronto senza maiuscole), solo osservate / con mie puntate (i due versi: storico compatto delle proprie partite, o storico "di mercato" senza le proprie mosse), prezzo finale oltre una percentuale del valore (le chiusure incaponite spostano medie e limiti consigliati verso l'alto per tutti; senza valore noto l'asta resta, non si indovina). Dopo ogni pulizia le statistiche per prodotto si ricostruiscono da zero dallo storico rimasto: sono un archivio a parte alimentato asta per asta, e avrebbero continuato a descrivere aste che non ci sono piu'. MODALITA'. Addestramento registra tutto; Gara toglie i poll grezzi nel dossier, le righe informative del registro dell'asta e il dettaglio esteso. Letta una volta all'avvio e immutabile fino alla chiusura, come chiesto: la scheda mostra la modalita' in vigore e avvisa del riavvio solo se la scelta differisce. Misurato prima di promettere: il lavoro che Gara evita costa 4,9 microsecondi per poll. A 57 aste per 17 poll al secondo sono 4,7 ms di CPU al secondo, mezzo punto di un core. La scrittura su disco era gia' asincrona. Gara fa bene al disco e ai dossier, non alla velocita'. LA LENTEZZA VERA. Ogni poll arrivava al thread dell'interfaccia: un BeginInvoke, una scansione lineare dei ViewModel e venti notifiche di proprieta'. Con trenta aste diecimila notifiche al secondo, con cinquantasette quasi ventimila. Adesso si manda all'interfaccia solo il poll in cui cambia qualcosa di visibile (prezzo, puntatore, stato, secondo intero, se e' mio), e comunque uno al secondo per asta perche' ping e contatori si rinfreschino. Il conto alla rovescia scorre gia' da solo con il battito da 200 ms. Il motore non e' toccato: la decisione di puntare vede ogni poll come prima. Misurato su 40 dossier veri, 146.138 poll: ne passano 16.375, l'88,8% viene scartato. Nove volte meno lavoro sul thread dell'interfaccia. Nella scheda Impostazioni entrano anche la fascia oraria e la pulizia dell'elenco, che il motore usava gia' ma che non si potevano ancora cambiare. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
372 lines
17 KiB
C#
372 lines
17 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();
|
|
|
|
// ? 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.
|
|
///
|
|
/// <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 una sola volta e su un thread di
|
|
/// lavoro: rileggere l'archivio all'avvio non deve ritardare la finestra.</para>
|
|
/// </summary>
|
|
private void EnsureProductCardsBuilt()
|
|
{
|
|
try
|
|
{
|
|
if (System.IO.File.Exists(AppPaths.ProductsFile)) return;
|
|
|
|
_ = System.Threading.Tasks.Task.Run(() =>
|
|
{
|
|
var count = ProductStatsStore.RebuildFromHistory();
|
|
|
|
Dispatcher.BeginInvoke(() =>
|
|
Log($"[PRODOTTI] Schede prodotto costruite 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();
|
|
});
|
|
}
|
|
}
|
|
}
|