Files
Mimante/Mimante/MainWindow.xaml.cs
T
Alby96andClaude Fable 5.1 3985935958 Ricalcolo dei prodotti dallo storico, finestra di avanzamento, operazioni sull'apprendimento, barre uguali
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>
2026-09-08 14:47:47 +02:00

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();
});
}
}
}