26 Commits
Author SHA1 Message Date
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
Alby96andClaude Fable 5.1 9177b31bd5 Due database, interruttore dell'apprendimento, barre a icone; via kill-switch, esportazioni e voci deprecate
Tutto ciò che l'applicazione registra sta ora in due file SQLite in
%LocalAppData%\AutoBidder\Database: autobidder.sqlite per le osservazioni e
esercizio.sqlite per i documenti d'esercizio (aste nel monitor, prodotti, promozioni,
modelli appresi con prefisso ml/) e per i registri applicativo e del riscatto puntate.
Il motore comune è SqliteDatabase; i file JSON e la cartella Apprendimento delle
versioni precedenti vengono importati la prima volta e lasciati dove sono. La cartella
si cambia dalle Impostazioni: al salvataggio si chiede se spostare i file, e si riavvia.
Le cartelle Dati/Statistiche/Registri, i registri su file, le misure dell'anticipo e
le impostazioni morte (LogBids, AutoApplyProductDefaults, NewAuctionLimitsPriority…)
non esistono più.

Il kill-switch se ne va: al suo posto un solo interruttore in barra, accanto ad
Avvia/Osserva/Ferma, che spegne l'apprendimento. Spento, il motore punta solo entro i
limiti dell'utente (prezzo, puntate, budget, fascia oraria, rischio) con l'anticipo
fisso: niente valore atteso, regime, duello, bandit, anticipo adattivo. Le aste vengono
comunque registrate e studiate.

Tre difetti visti osservando il motore dal vivo per un'ora, in Osserva:
- la copertura «Compralo Ora» con V lasciato al listino azzerava la perdita coperta e
  il motore diceva BID a ogni scadenza con P dell'1%: ora vale solo per i prodotti con
  «Valore reale €» scritto, altrimenti la puntata si conta persa e P decide;
- la chiusura di un'asta arrivava due volte (fine, poi rimozione dal monitor) e di
  nuovo al riavvio: modello, profilo, bandit e statistiche per prodotto contavano ogni
  asta due volte. Il monitor la comunica una volta sola, l'apprendimento salta le aste
  già apprese, e le schede prodotto vengono ricostruite una volta dallo storico;
- la pagina delle ricompense veniva scambiata per la pagina di accesso perché porta un
  collegamento a /login.php: prima si guarda se è la pagina delle ricompense.

Interfaccia: la scheda Esporta e tutto il suo codice sono tolti; le esportazioni
chiedono sempre dove salvare. Prodotti, Storico e Apprendimento hanno barre a sole
icone con tooltip che dicono esattamente cosa succede, colorate come il monitor, con
i nuovi pulsanti di pulizia (spegni tutte le stelline, togli i non seguiti, pulizia
completa; seleziona tutte / elimina le selezionate / svuota lo storico). Impostazioni:
sezione Database, Manutenzione con «Elimina tutti i dati (tranne la login)» e
«Impostazioni di fabbrica», esportazione dei registri in testo. Modifiche.txt, il
prompt ormai realizzato, esce dal repository.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 13:58:08 +02:00
Alby96andClaude Fable 5.1 66a1d23d87 Nel motivo della decisione stampa la probabilita, non il valore atteso
La riga «valore atteso +11,99 € (P 1.199,10%)» nel registro e nelle decisioni
mostrava il valore atteso al posto di P(senza risposta).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 11:02:02 +02:00
Alby96andClaude Fable 5.1 7993ca44a0 Database SQLite al posto dei dossier, gestore del rischio, decisioni in shadow, rete neurale, bandit e simulatore
Le aste non si salvano più in un file JSON per asta più tre archivi da tenere allineati:
un solo database SQLite (winsqlite3.dll di Windows via P/Invoke, nessun pacchetto) con
prodotti, aste, puntate, interrogazioni coalescenti, reset con profondità del ciclo,
nostre puntate, decisioni del motore, misure di rete, puntatori, sessioni e contabilità.
Scrittura in coda su un thread dedicato, letture in WAL. Per scelta dell'utente si parte
da zero: il database si riempie man mano che si osservano le aste; i vecchi dossier
restano leggibili per un'importazione futura ma non si scrivono più. Storico, schede,
apprendimento, rigiocata ed esportazioni leggono tutti da lì.

Il documento di progetto (Modifiche.txt) tradotto in C# senza librerie:
- RiskManager: kill-switch (file KILL_SWITCH, anche dalla barra), HALT persistente per
  stop-loss e drawdown, tetto del giorno, aste in gioco insieme, contabilità in euro.
- Theory: valore atteso con fee, spedizione, valore reale per prodotto (nuova colonna
  nella scheda Prodotti) e copertura «Compralo Ora»; null-model; sopravvivenza empirica e
  Kaplan-Meier dai reset; arrivi di Poisson.
- Shadow per asta: in Osserva il cecchino arriva allo stesso istante, registra cosa
  avrebbe fatto e non punta; alla chiusura ogni decisione riceve l'esito, e ShadowReport
  confronta le policy sugli stessi istanti con intervallo di confidenza sul ROI.
- NeuralNet (MLP, Adam) come sfidante del logistico, scelto dal Brier prequenziale;
  ThompsonBandit come seconda policy che impara dagli esiti delle decisioni;
  PennyAuctionEnv con avversari tarati sul database e QLearningAgent conservativo;
  SimulationLab per il confronto fra policy; esportazione CSV delle decisioni con
  reason_detail. Tutto nella scheda Apprendimento.

Ml/LEGGIMI.md riscritto con le avvertenze su termini d'uso, quadro legale e realtà
economica, lo schema del database e lo stato delle milestone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 11:00:49 +02:00
Alby96andClaude Fable 5.1 8b7c39c9e3 Sostituisci le euristiche a soglia con regime e anticipo adattivi; un solo archivio per asta
Le strategie a numero fisso (calore, ritiro morbido, anti-bot, avversari aggressivi,
puntata probabilistica, velocità del prezzo, esaurimento, concorrenza) e le impostazioni
morte (cadenze di polling, finestra critica, database, log avanzati, suggerimenti
sull'anticipo, schede dettagliate) non ci sono più: rigiocate sui dossier non fermavano
una puntata sbagliata senza fermarne anche di giuste, e i loro numeri andavano tarati a
mano. Restano i paletti dell'utente (tetti, budget, fascia oraria) e il duello.

Al loro posto due componenti che imparano dalla sessione in corso, dentro i paletti:
- CompetitionRegime per asta (Calmo / Sfogo / Sondaggio) guidato dal valore atteso
  appreso: tre negativi di fila e si lascia sfogare gli altri, si rientra con una
  puntata di prova dopo abbastanza cicli buoni, e se viene coperta subito la pazienza
  raddoppia. È il "capire da soli quando tornare a puntare".
- LatencyModel per l'anticipo: margine + p99 della latenza misurata adesso, tenuto fra
  LeadMinMs e LeadMaxMs; una puntata tardiva alza il margine subito, venti in tempo lo
  abbassano piano. Un anticipo scritto a mano su un'asta vince sempre (BidLeadIsManual).

Archiviazione: l'archivio mensile aste-AAAA-MM.jsonl (95 MB) duplicava il riepilogo che
ogni dossier ha già in coda. AuctionDetailStore ora legge testa e coda dei dossier, con
cache: 6349 file in 6 s la prima volta. Pulsanti per svuotare le esportazioni e per
azzerare le statistiche voce per voce (lo storico passa dalla copia di sicurezza).

Scheda Apprendimento: sezione "Autonomia sul momento" con il modello di latenza e il
regime di ogni asta seguita. Ml/LEGGIMI.md descrive l'algoritmo per intero.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 00:11:49 +02:00
Alby96andClaude Fable 5.1 afb778ea26 Scheda Apprendimento: cosa sa il modello, cosa ha deciso, e la valutazione dall'app
Una scheda nuova nella barra laterale, fra Esporta e Impostazioni, che legge
direttamente dal servizio e si aggiorna da sola ogni cinque secondi mentre e'
visibile: nessun altro pezzo dell'applicazione sa che esiste.

Mostra: lo stato (aste apprese, esempi visti, aste nel profilo, fattore di
calibrazione, soglia per decidere, se lo studio dei dossier e' in corso); i pesi
dal piu' forte con il verso dell'effetto, perche' le variabili sono a fasce e il
modello e' lineare proprio per poterli leggere; le ultime decisioni date al
motore, con probabilita', valore atteso ed esito (fermata, passa, o solo parere
se il modello non aveva ancora appreso abbastanza); il profilo per prodotto e
fascia oraria; e l'ultima valutazione.

La valutazione si lancia dalla scheda, in sottofondo e annullabile. La logica e'
uscita dal test ed e' entrata in LearningEvaluator, condivisa fra il target da
riga di comando e la scheda: i numeri che vede l'utente sono gli stessi che vede
chi sviluppa. "Ricomincia da capo" butta modello, profilo ed elenco dei dossier
letti e ristudia tutto l'archivio, con una conferma prima.

Le decisioni si tengono in una coda corta; lo stesso giro di un'asta ne
produrrebbe molte uguali di fila, quindi si registra solo il cambiamento.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 23:09:03 +02:00
Alby96andClaude Fable 5.1 46134c822d Apprendimento sempre acceso: impara da ogni asta chiusa e decide col valore atteso
Via le due modalita' Addestramento/Gara, per scelta dell'utente: la misura aveva
detto che il lavoro di registrazione costa mezzo punto di un core, e allora tanto
vale registrare sempre. Resta la coalescenza verso l'interfaccia, che era la
correzione vera.

COSA IMPARA. Per ogni puntata di ogni asta chiusa: e' rimasta senza risposta?
E' l'unica etichetta abbondante — 1,8 milioni di esempi sui dossier locali —
contro le diciannove aste vinte dall'utente, che non farebbero un modello. Il
comportamento avversario non dipende da chi ha puntato, e questo rende
utilizzabili le puntate altrui. Regressione logistica online scritta a mano,
senza librerie, in Ml/: un passo di gradiente per esempio, aggiornata a ogni
chiusura, salvata subito. Variabili a fasce: durata del ciclo, quota di
autopuntate recenti, puntatori distinti, prezzo sul valore, fascia oraria (le
fasce ricalcano la curva misurata sulle chiusure), feriale/festivo, ora di
aggancio, tipo di puntata, profondita' dell'asta, stesso puntatore di due fa,
prodotto in 32 cassetti con hash stabile. In parallelo un profilo per prodotto x
fascia oraria x tipo di giorno, con restringimento verso il prodotto e poi verso
il globale dove i dati scarseggiano.

COME DECIDE. In ShouldPlaceBid: probabilita' appresa per il margine residuo meno
il costo della puntata; sotto zero non si punta, e il registro riporta i numeri.
Il modello parla sempre ma decide solo dopo 300 aste apprese. Le previsioni sono
calibrate in linea con il rapporto osservato/previsto sugli ultimi 20.000 esempi.

QUANTO VALE, MISURATO. Valutazione prequenziale sui 3000 dossier piu' recenti
(ogni asta prima prevista poi appresa, come nell'uso): sollevamento 10,8x nella
cima dell'1%, 5,8x al 5%; calibrazione entro il 10-30% in ogni fascia. Sulle 116
puntate vere dell'utente di agosto: nessuna era finale, e il cancello ne avrebbe
fermate 45 senza perderne una vincente. La valutazione congelata (addestra sui
vecchi, giudica sui nuovi) sovrastima di 1,5-1,8x per deriva del mercato, non per
passo o regolarizzazione: provati entrambi. Griglia e prequenziale stanno nel
test MlModelBacktest, target Apprendimento, attivita' VS Code "valuta
apprendimento".

PRIMO ADDESTRAMENTO. In sottofondo, dal piu' recente, con un tempo massimo per
avvio. Il lettore salta le righe di poll prima del JSON: 6289 dossier per 8,8 GB
in 64 secondi, verificato dal vivo, archivio completo al primo avvio.

ACQUISIZIONE. I poll nel dossier si scrivono solo quando cambia qualcosa di
visibile, e comunque uno al secondo: -88,8% misurato, senza perdere la
profondita' di nessun ciclo. Il reset porta il minimo del cronometro e la durata
del ciclo chiuso. Via le righe di log che copiavano i reset parola per parola.
Corretto un difetto vecchio: nessun dossier marcava le puntate nostre, perche' il
parser lo faceva solo se riceveva il nome utente e nel percorso dei poll non gli
arrivava. Ora si marcano in MergeBidHistory e l'intestazione porta il nome
utente; per i dossier vecchi il lettore accetta il nome da fuori.

Trovato dal vivo e corretto: salvataggi concorrenti all'avvio quando piu' aste
chiudono insieme. Il modello salvato non rilegge piu' i parametri (passo,
regolarizzazione): sono scelte del codice, e un cambio vale dal riavvio dopo.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 22:45:49 +02:00
Alby96andClaude Fable 5.1 08c5023f77 Documenti per chi installa da zero, pulizia per prodotto ed esborso, modalita' Gara
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>
2026-09-07 18:39:19 +02:00
Alby96andClaude Opus 5 4e835e220a Togli da sole le aste concluse, ma trattieni quelle costate qualcosa
Con l'aggiunta automatica accesa le aste concluse si accumulano a centinaia e
seppelliscono quelle vive, che e' il contrario di quello che serve guardando un
cruscotto. Il pulsante manuale "rimuovi le aste concluse" c'era gia', ma toglieva
tutto senza distinguere.

La rimozione automatica e' accesa di serie e toglie solo le aste che non hanno
piu' niente da dire. Restano invece, ciascuna con la sua opzione:

- quelle su cui si e' puntato, perche' sono le uniche che vale la pena riguardare
  e i soldi erano veri;
- quelle vinte, perche' c'e' da confermare l'acquisto su Bidoo e una vittoria
  sparita dall'elenco e' il modo piu' semplice di dimenticarsene;
- quelle di cui non si e' vista la fine, perche' sono esattamente quelle da
  controllare.

L'ordine dei controlli conta: prima le ragioni per trattenere, cosi' il motivo
scritto nel registro e' il piu' importante e non l'ultimo incontrato. Una vinta su
cui si e' anche puntato resta "vinta", che e' quella che richiede un'azione. Il
motivo finisce nel registro anche quando l'asta resta: senza, un'asta che sparisce
e una che non sparisce sarebbero entrambe inspiegabili.

La rimozione avviene come ultimo passo, dopo che storico, dossier e statistiche
del prodotto sono gia' scritti: a quel punto togliere l'asta dall'elenco non perde
niente.

Corretto anche un difetto introdotto ieri con la fascia oraria: la rigiocata sui
dossier usava le impostazioni vere, fascia compresa, e la fascia guarda l'orologio
di parete. Una rigiocata ripercorre aste gia' concluse e l'ora in cui la si lancia
non c'entra nulla con l'ora in cui quelle aste correvano: lanciata di notte
avrebbe rifiutato ogni puntata e riportato zero, che e' il tipo di risultato che
sembra vero e non lo e'. Adesso la rigiocata lavora su una copia delle
impostazioni con la fascia spenta, senza mutare quelle dell'utente.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 00:39:00 +02:00
Alby96andClaude Opus 5 480e423124 Togli la pubblicazione dalla obj/ condivisa: rompeva la compilazione di debug
Sintomo: una raffica di CS2001 "file di origine MainWindow.g.cs non trovato" su
otto file .g.cs, lanciando la normale compilazione di debug da VS Code, senza che
il codice fosse cambiato. Il classico guasto intermittente della compilazione WPF.

Causa: il target Pubblica usava la sola opzione -o, che sposta il risultato ma
lascia gli INTERMEDI nella obj/ del progetto, cioe' la stessa cartella che usa la
compilazione di debug. La compilazione WPF genera un progetto temporaneo
_wpftmp.csproj a ogni giro, e quando ci trova dentro lo stato di un'altra
configurazione smette di produrre le classi parziali dello XAML. Bastava
pubblicare mentre si compilava in VS Code, o subito prima, per rompere tutto.

Era gia' il rimedio adottato per i test, dove artifacts-path era stato messo
proprio per questo; il target Pubblica era rimasto indietro. Le due opzioni
convivono: artifacts-path sposta gli intermedi, -o continua a decidere dove
finisce l'eseguibile.

Verificato ripetendo tre volte la sequenza che rompeva - pubblica, poi compila in
debug - con la obj/ svuotata all'inizio: zero errori tutte e tre le volte, e la
pubblicazione non crea piu' obj/Release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:27:40 +02:00
Alby96andClaude Opus 5 48096c7fc7 Porta avanti le impostazioni vecchie, o le correzioni di oggi non arrivano
Trovato provando davvero l'applicazione, non dai test. Il settings.json esistente
conteneva MaxRequestsPerSecond 40 e DefaultBidBeforeDeadlineMs 1000, e il valore
su disco vince sempre sui predefiniti: installando la versione nuova, le due
correzioni che pesano di piu' - tetto delle richieste tolto e anticipo a mezzo
secondo - sarebbero state annullate dal file dell'utente, che avrebbe concluso
che non funzionano.

Che il file vinca e' giusto: sono scelte di chi usa il programma. Ma quei due
valori non erano scelte, erano conseguenze di misure sbagliate. Erano stati
tarati quando il tetto stesso accodava le chiamate e gonfiava il ping a 444 ms al
p99,9; senza tetto il ping misurato sta fra 47 e 78 ms.

Migrazione una tantum, con un numero di schema nel file perche' giri una volta
sola. Tocca solo chi ha ancora esattamente il vecchio valore: chi lo aveva gia'
cambiato di suo se lo tiene, perche' quella e' una scelta e non spetta a un
aggiornamento ribaltarla. Il salvataggio avviene dentro Load, che di norma non
tocca il disco, ma una migrazione capita una volta nella vita del file e
lasciarla non salvata la farebbe ripetere a ogni avvio.

Verificato dal vivo sull'eseguibile pubblicato: 40 diventa 0, 1000 diventa 500,
lo schema passa da assente a 1, e le impostazioni nuove (fascia oraria, ritiro
dai duelli) compaiono coi valori giusti.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:16:54 +02:00
Alby96andClaude Opus 5 8b9691d692 Sospendi le puntate nella fascia oraria in cui costano il doppio
Su 6060 aste concluse con prezzo e valore noti, la stessa asta non costa uguale a
tutte le ore. Prezzo di chiusura mediano in percentuale del valore del prodotto:
4,6% alle 12 e 5,3% fra le 10 e le 13, contro 8,6% a mezzanotte e 9,7% alle 9. Le
puntate necessarie per vincere seguono la stessa curva: 21-24 nelle ore buone,
26-29 in quelle cattive. Quasi il doppio del costo per lo stesso oggetto.

Il motivo e' che di notte Bidoo sospende le puntate: restano poche aste in corso e
la concorrenza si concentra li'. Le ore di confine, mezzanotte e le nove, sono le
peggiori, perche' c'e' ancora qualcosa da giocare e ci sono tutti addosso.

La fascia predefinita e' 00:00-10:00. Sospende le puntate, NON il monitoraggio:
l'asta resta in stato Attiva, continua a essere seguita e riprende a puntare da
sola alla fine della fascia. E' voluto: le aste lunghe attraversano la fascia, e
fermarle davvero significherebbe perderle invece che risparmiare. Ogni puntata
saltata scrive nel registro il motivo e l'orario di ripresa.

Tre livelli di override, dal piu' specifico al piu' generale: la singola asta
scavalca il prodotto, il prodotto scavalca le impostazioni generali. Un null non
vuol dire "no" ma "non ho un'opinione", ed e' la differenza che permette di
spegnere il filtro su una sola asta senza toccare il resto.

Una fascia a cavallo di mezzanotte (22-6) funziona; inizio uguale a fine vuol dire
filtro spento, che e' l'unica lettura sensata visto che l'alternativa sarebbe non
puntare mai.

Backtest della regola sul duello, completato su tutte le soglie. Sui dossier veri
in cui il bot ha puntato: 13 aste, 366 puntate, zero vittorie. Con soglia 5 le
puntate sarebbero state 49, risparmiate 317 (86,6%), nessuna vittoria persa. Il
risultato tiene da soglia 2 a soglia 10, che e' quanto ci si puo' aspettare da un
campione in cui non si era vinto niente.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 15:50:06 +02:00
Alby96andClaude Opus 5 5bac29b0af Densita' della griglia e suggerimento dell'anticipo allineato ai dati nuovi
Il pannello della singola asta consigliava di tenere l'anticipo fra 600 e 2000 ms
e proponeva 800: numeri nati da misure fatte quando il tetto di 40 richieste al
secondo accodava le chiamate e gonfiava il ping. Adesso dice quello che i dati
dicono davvero: tre quarti delle puntate avversarie sono automatiche e scattano a
2 secondi, sotto quella soglia resta solo chi punta a mano, e il predefinito e'
500 ms perche' e' circa nove volte il ping misurato.

Righe della griglia da 28 a 22 px con carattere piccolo: su un cruscotto che
serve a tenere d'occhio molte aste insieme l'altezza e' la risorsa scarsa, e sei
pixel di aria per riga costano una decina di aste sullo schermo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 15:31:36 +02:00
Alby96andClaude Opus 5 9b94e2d405 Ritirati dai duelli con l'autopuntata, e punta a mezzo secondo
La strategia A, dai numeri dei dossier: 5410 aste, 2,08 milioni di puntate.

Bidoo dichiara per ogni puntata se e' automatica o manuale. Le automatiche sono
il 75,3% del totale ma vincono il 46,0% delle aste; le manuali sono il 24,7% e ne
vincono il 53,9%. Una puntata manuale vale 3,6 volte una automatica: 178 puntate
per vittoria contro 634.

Il meccanismo del duello e' stato ricostruito esattamente. L'intervallo mediano
fra due puntate consecutive e' 6,0 s su 2.799.681 intervalli: il timer si azzera
a ~8 s e l'autopuntata scatta a 2 s, quindi 8-2=6. Il ciclo da 13,0 s osservato
nelle prove reali era la somma di 7 s di attesa nostra e 6 s della sua risposta.
Puntare tardi non evita l'autopuntata avversaria: la sveglia, perche' si riarma
quando il suo padrone perde la testa della classifica, non a un orario fisso.

Contro una macchina cosi' non esiste attrito vincibile. Adesso il motore conta le
risposte con la firma dei 6+/-2,5 s e dopo cinque di fila smette di puntare su
quell'asta. Una risposta fuori firma azzera il conteggio invece di scalarlo:
l'errore costoso e' ritirarsi da un'asta che si poteva vincere, non spendere
cinque puntate di troppo.

Rigiocato sui dossier veri in cui il bot ha puntato: 366 puntate spese, 13 aste,
zero vittorie. Con la regola a soglia 3 sarebbero state 31 puntate, 335
risparmiate (91,5%), nessuna vittoria persa.

L'anticipo passa da 1000 a 500 ms. Le due ragioni per stare larghi sono cadute:
il p99,9 del ping a 444 ms era misurato col tetto di 40 richieste al secondo che
accodava le chiamate, e senza tetto il ping sta fra 47 e 78 ms; e la concorrenza
sotto i due secondi non e' fitta ma rada, visto che tre quarti degli avversari
sono autopuntate che hanno gia' sparato.

Il tipo di puntata non viene piu' inventato. In due punti il motore ricostruiva
una puntata da un cambio di prezzo fra due interrogazioni e la marcava "Auto"
comunque: adesso quelle portano un trattino, perche' il tipo non lo ha detto
nessuno. La colonna in cronologia mostra quindi solo tipi dichiarati dal server.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 15:20:34 +02:00
Alby96andClaude Opus 5 20434ca273 Riscatta aprendo il collegamento, e fai scorrere il conto alla rovescia
Due difetti che i registri di tre giorni mostravano chiaramente.

Il riscatto dei buoni giudicava la risposta del sito, e la giudicava male: ogni
singolo tentativo risultava "sessione scaduta: risposta la pagina di accesso" —
6 trovati, 0 riscossi, 6 non riusciti, giro dopo giro per tre giorni — mentre le
puntate arrivavano lo stesso. Il riconoscimento della pagina di accesso scattava
su pagine che di accesso non erano, e il servizio dichiarava di non funzionare
mentre funzionava.

Adesso aprire il collegamento e' il riscatto: si apre, si scrive che e' stato
aperto, e il numero vero lo da' la differenza di saldo, che e' l'unica misura che
non mente. Resta un solo fallimento possibile, l'errore di trasporto, dove il
collegamento non e' stato aperto affatto. Un premio si riscuote una volta sola:
fra i due errori, insistere su un premio gia' preso non costa niente, mentre
dichiarare falliti dei riscatti riusciti riempie il registro di allarmi inutili.
Il giudizio vecchio non e' stato cancellato, e' in GiudicaRisposta.

Il conto alla rovescia in griglia restava fermo fra un poll e l'altro. Il valore
era gia' giusto — si ricava dalla scadenza ancorata all'orologio del server, non
dall'ultimo poll — ma nessuno lo richiedeva finche' non arrivava una risposta, e
nelle prove reali fra un poll e l'altro passava anche un secondo e mezzo:
sembrava un'applicazione bloccata. Un battito da cinque volte al secondo notifica
ora le tre sole proprieta' che dipendono dal tempo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 21:29:05 +02:00
Alby96andClaude Opus 5 feb39ee2de Togli il tetto alle richieste e l'attesa fra i poll: era il collo di bottiglia
Misurato sui registri di tre giorni di prove reali (886 dossier, 4,86 milioni di
interrogazioni). Il tetto globale di 40 richieste al secondo veniva spartito fra
tutte le aste seguite, fino a 31 insieme: la distribuzione risultava tagliata
netta sul limite (p90 39, p99 41, saturo nell'8% dei secondi) e ogni singola asta
finiva interrogata ogni ~596 ms invece dei 220 configurati per la finestra
critica. Con il timer a un secondo restava un solo sguardo prima della scadenza,
e la puntata partiva su una stima della scadenza vecchia di mezzo secondo.

Le quattro soglie di cadenza (lontano/medio/vicino/critico) non descrivevano
quindi niente di reale: qualunque numero ci si scrivesse, il tetto decideva al
posto loro. Adesso non c'e' nessuna attesa fra un poll e il successivo: si
riparte appena la risposta e' arrivata, cioe' ~17 interrogazioni al secondo per
asta con il ping misurato di 57 ms. Resta la sola cadenza allargata per le aste
non ancora cominciate, che non e' una scelta di ritmo: fino all'apertura il
server non ha nulla di diverso da dire.

Senza tetto fisso serviva pero' qualcosa che reagisse, altrimenti l'unico segnale
di "troppe richieste" sarebbe stato un blocco dell'account. Il freno ora lo detta
il server: un 429 o un 503 mette in pausa il polling per un secondo, raddoppiando
fino a otto se insistono, e la pausa si azzera al primo giro riuscito. Le puntate
hanno priorita' Critical e non passano dal freno: se il momento giusto e' adesso,
una pausa preventiva costerebbe l'asta.

Il tetto resta configurabile per chi lo vuole, con 0 = nessun limite come nuovo
predefinito.

Scelta dell'utente, dopo avergli mostrato il rischio: niente tetto, il massimo
che regge la rete.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 18:28:02 +02:00
Alby96andClaude Opus 5 1d578debe2 Togli dal percorso caldo la rilettura dell'intero storico
Sei modifiche chieste in questa sessione, di cui una pesa piu' di tutte le altre.

CompletedAuctionsStore rileggeva e rideserializzava l'intero file a ogni chiamata:
misurati 72 ms su 5876 record, 3,9 MB. Append pagava quel prezzo piu' la
riserializzazione e la riscrittura completa per cambiare un record solo, 120 ms
in tutto, e la chiamata arriva da un gestore di eventi di MainWindow, quindi dal
thread dell'interfaccia, che restava fermo a ogni asta conclusa. Il recupero
delle puntate dei vincitori lo faceva in un ciclo: 4675 aste erano 7,2 minuti di
solo rimescolamento JSON e 17,8 GB scritti sul disco.

Adesso c'e' una copia in memoria invalidata sulla data di modifica del file, cosi'
resta giusta anche se il file viene sostituito da fuori (ripristino di un backup),
e un AppendMany che applica molti record con un salvataggio solo, con un indice
per id perche' il caso per cui esiste non torni quadratico. Il recupero salva a
blocchi di cinquanta e deposita comunque quello che ha in mano quando esce, anche
per annullamento: le richieste gia' fatte sono state pagate al server.

Misurato sullo stesso file: 95 volte piu' veloce, 7,2 minuti diventano 4,5
secondi, 17,8 GB diventano 0,36 GB.

Le altre cinque:

- Il setup si chiama AutoBidder_<versione>.exe.
- I pulsanti di stop sono rossi, sia quello globale sia quelli per asta nella
  griglia: erano gli unici tre senza colore mentre avvia e osserva ce l'avevano.
- La pulizia dello storico sa togliere le aste seguite solo in parte. Non serve
  nessun campo nuovo: il prezzo finale in centesimi e' il totale delle puntate
  dell'asta, quindi il rapporto con i reset osservati dice quanta asta si e'
  vista, e la regola vale anche sullo storico gia' raccolto. Verificata sui dati
  veri: su 5876 aste nessuna supera copertura 1,02, che e' quanto ci si aspetta
  se l'identita' e' giusta.
- La griglia ricicla i contenitori di riga invece di ricrearli.
- Il costo puntata gia' esisteva col predefinito giusto: al suggerimento e' stato
  aggiunto perche' 0,20 e' il numero da tenere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 14:48:54 +02:00
Alby96andClaude Opus 5 5adc4a6526 Passa le note di rilascio dall'ambiente: -p: le spezzava sulle virgole
MSBuild divide il valore di una proprieta' sulle virgole: -p:Note=uno, due
diventa la proprieta' Note=uno piu' un'opzione ` due`, e si finisce su MSB1006
`proprieta' non valida`. Una nota di rilascio in italiano contiene quasi sempre
una virgola, quindi l'attivita' di rilascio si sarebbe rotta al primo uso reale
del prompt delle note.

Le note arrivano ora in AUTOBIDDER_NOTE, che MSBuild legge come proprieta' senza
passare dal parsing della riga di comando. Provato con virgole, apostrofi, due
punti e virgolette: arrivano integri. -p:Note= resta accettato per chi lo passa
a mano senza virgole.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 23:02:18 +02:00
Alby96andClaude Opus 5 66e3af043c Riformatta tasks.json come lo scrive il formattatore di VS Code
Solo impaginazione: gli oggetti group e presentation passano da una riga a piu'
righe. Nessuna differenza di contenuto (git diff --ignore-all-space e' vuoto).
Committato per non lasciare l'albero sporco a ogni salvataggio del file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 23:00:08 +02:00
Alby96 9a632b8d62 Aggiungi un elenco di cose da fare nel file Modifiche.txt 2026-08-04 22:51:35 +02:00
Alby96andClaude Opus 5 cb0e838964 Rifiuta il rilascio se il tag non e' arrivato sul remoto
Il push dei tag non porta con se' i commit: a ramo indietro il tag punta a un
oggetto che il remoto non conosce e viene rifiutato. La creazione della release
pero' non falliva — Gitea, non trovando il tag, lo crea da se' sulla testa del
ramo predefinito. Sarebbe stata pubblicata una release che dichiara un commit e
allega il codice di un altro, senza che niente lo segnalasse.

Adesso il tag viene riletto dal remoto dopo il push e il rilascio si ferma se
non c'e', dicendo di spingere il ramo. I commit restano da spingere a mano:
quando farlo lo decide chi lavora, non la catena di rilascio.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 22:45:48 +02:00
Alby96andClaude Opus 5 8954f9aaba Correggi i commenti che indicavano ancora il .csproj come fonte della versione
AppInfo e il .csproj dicevano entrambi di alzare <Version> per pubblicare. Da
quando la versione viene dal tag non e' piu' vero, ed e' il tipo di commento che
fa perdere tempo fra sei mesi: adesso dicono a cosa serve davvero quel numero
(le compilazioni di sviluppo) e dove sta la fonte dei rilasci.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 22:41:55 +02:00
Alby96andClaude Opus 5 94843311b4 Il tag git diventa la fonte unica della versione
Il .csproj non viene piu' riscritto a ogni rilascio: i quattro numeri di
versione arrivano a dotnet publish come proprieta' da riga di comando, ricavati
dal tag su HEAD. Tag, eseguibile, installatore e release portano lo stesso
numero per costruzione invece che per disciplina, e un rilascio non lascia piu'
una modifica al progetto da committare dopo il fatto.

Se HEAD non ha un tag la catena lo crea da se' — numero chiesto o minor
successiva alla piu' alta esistente — ma lo fa in fondo, a installatore
prodotto: un giro fallito non lascia dietro il tag di una versione mai
costruita. Rifiuta di taggare un albero con modifiche non committate, perche'
sarebbe un tag che non indica niente di ricostruibile.

La configurazione di Gitea viene letta prima di compilare: senza token ci si
ferma in due secondi invece che dopo tre minuti di build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 22:38:45 +02:00
Alby96andClaude Opus 5 52e5f68da0 Completa .gitignore con le regole del vecchio repository
.idea/, Thumbs.db e Desktop.ini erano coperte nella cartella BidooBidder ma
non qui. Aggiunte prima di cancellarla, cosi' non si perde nulla.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 22:17:28 +02:00
Alby96andClaude Opus 5 99b3030180 Porta la catena di rilascio nel repository ufficiale
Il progetto viveva in una cartella senza alcun commit: tutto il lavoro stava
solo sul disco. Trasferito qui, dove esiste una storia e un remoto.

Mancava soltanto build/: i sorgenti erano gia' allineati e identici. Nessun
percorso e' stato toccato — Release.proj usa MSBuildThisFileDirectory e
tasks.json usa ${workspaceFolder}, quindi il trasloco non li riguarda.

- build/Release.proj: verifica, backtest, pacchetto e rilascio in un solo file
  MSBuild, senza script. Provato dalla nuova posizione: 287 test verdi,
  installatore prodotto, backtest eseguito.
- build/gitea.example.json: url, owner e repo veri; resta da mettere il token
  in build/gitea.json, che e' ignorato da git.
- build/sostituiti/: i quattro .ps1 rimpiazzati da Release.proj. Entrano qui
  solo per non perderli con la vecchia cartella: da ora sono recuperabili
  dalla storia e si possono cancellare.
- .gitignore: gitea.json (contiene una credenziale), i riepiloghi del backtest
  e i pacchetti in bin/installer.

Versione 4.12.0 -> 4.13.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 22:15:53 +02:00
Alby96 7ca504a70a Add utility classes for theme management, waiting, watched products, and notifications
- Implement ThemeManager for dynamic light/dark theme switching in the application.
- Create Wait class for cancellable delays without exceptions for smoother user experience.
- Introduce WatchedProductsStore to manage and persist watched products in JSON format.
- Add WindowsNotifier for system notifications to inform users of important events.
- Develop ProductViewModel to encapsulate product data and manage UI interactions effectively.
2026-08-04 21:49:53 +02:00
267 changed files with 123268 additions and 26888 deletions
+18
View File
@@ -414,3 +414,21 @@ FodyWeavers.xsd
# Built Visual Studio Code Extensions
*.vsix
# ---> AutoBidder / Mimante
# Configurazione di rilascio: contiene un token di Gitea
gitea.json
# Riepilogo della rigiocata sui dossier (target Backtest)
*backtest-report.txt
# Pacchetti prodotti da build/Release.proj
bin/installer/
# Rider / JetBrains
.idea/
# Windows
Thumbs.db
Desktop.ini
+23
View File
@@ -0,0 +1,23 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug AutoBidder (WPF)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/bin/Debug/net10.0-windows/AutoBidder.exe",
"args": [],
"cwd": "${workspaceFolder}",
"console": "internalConsole",
"stopAtEntry": false,
"enableStepFiltering": true
},
{
"name": "Attach to AutoBidder (processo già avviato)",
"type": "coreclr",
"request": "attach",
"processName": "AutoBidder.exe"
}
]
}
+180
View File
@@ -0,0 +1,180 @@
{
// Tutte le attività passano da build/Release.proj: la catena è un solo file
// MSBuild versionato col codice, e qui restano soltanto i nomi e le domande.
// MSBuild non può chiedere niente a nessuno — i prompt stanno in "inputs".
"version": "2.0.0",
"tasks": [
{
"label": "build",
"detail": "Compilazione di debug, per F5 e per il controllo rapido degli errori.",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/AutoBidder.csproj",
"-c",
"Debug",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": "$msCompile"
},
{
"label": "verifica",
"detail": "Compila e lancia i test. Da eseguire dopo ogni modifica.",
"type": "process",
"command": "dotnet",
"args": [
"msbuild",
"${workspaceFolder}/build/Release.proj",
"-t:Verifica",
"-nologo",
"-v:m"
],
"group": {
"kind": "test",
"isDefault": true
},
"problemMatcher": "$msCompile"
},
{
"label": "backtest",
"detail": "Rigioca i dossier delle aste concluse: quante puntate costerebbe ciascun anticipo, e quante ne fermano i controlli di budget.",
"type": "process",
"command": "dotnet",
"args": [
"msbuild",
"${workspaceFolder}/build/Release.proj",
"-t:Backtest",
"-nologo",
"-v:m"
],
"presentation": {
"reveal": "always",
"panel": "dedicated"
},
"problemMatcher": []
},
{
"label": "backtest (giro veloce, 400 aste)",
"detail": "Come sopra ma su un campione: utile mentre si tarano le strategie.",
"type": "process",
"command": "dotnet",
"args": [
"msbuild",
"${workspaceFolder}/build/Release.proj",
"-t:Backtest",
"-p:MaxDossier=400",
"-nologo",
"-v:m"
],
"presentation": {
"reveal": "always",
"panel": "dedicated"
},
"problemMatcher": []
},
{
"label": "valuta apprendimento",
"detail": "Addestra il modello sui dossier più vecchi e lo giudica sui più recenti: separazione, calibrazione, e cosa avrebbe fatto il cancello sulle tue puntate vere.",
"type": "process",
"command": "dotnet",
"args": [
"msbuild",
"${workspaceFolder}/build/Release.proj",
"-t:Apprendimento",
"-nologo",
"-v:m"
],
"presentation": {
"reveal": "always",
"panel": "dedicated"
},
"problemMatcher": []
},
{
"label": "crea installatore",
"detail": "Verifica, pubblica ed esegue Inno Setup: bin/installer/AutoBidder-<versione>-setup.exe. Crea il tag a pacchetto pronto. Non tocca Gitea.",
"type": "process",
"command": "dotnet",
"args": [
"msbuild",
"${workspaceFolder}/build/Release.proj",
"-t:Pacchetto",
"-p:Versione=${input:versione}",
"-nologo",
"-v:m"
],
"presentation": {
"reveal": "always",
"panel": "dedicated"
},
"problemMatcher": "$msCompile"
},
{
"label": "crea installatore (senza rieseguire i test)",
"detail": "Solo pubblicazione e Inno Setup. Da usare quando i test sono appena passati.",
"type": "process",
"command": "dotnet",
"args": [
"msbuild",
"${workspaceFolder}/build/Release.proj",
"-t:Pacchetto",
"-p:Versione=${input:versione}",
"-p:SaltaVerifica=true",
"-nologo",
"-v:m"
],
"presentation": {
"reveal": "always",
"panel": "dedicated"
},
"problemMatcher": "$msCompile"
},
{
"label": "rilascia su Gitea",
"detail": "Verifica, pubblica, installatore, tag e release su Gitea con i file allegati. La versione viene dal tag su HEAD. Richiede build/gitea.json.",
"type": "process",
"command": "dotnet",
"args": [
"msbuild",
"${workspaceFolder}/build/Release.proj",
"-t:Rilascia",
"-p:Versione=${input:versione}",
"-nologo",
"-v:m"
],
// Le note passano dall'ambiente, non da -p:. MSBuild spezza il valore di
// una proprietà sulle virgole e una nota in italiano ne ha quasi sempre
// una: si otterrebbe MSB1006 «proprietà non valida». Vedi Release.proj.
"options": {
"env": {
"AUTOBIDDER_NOTE": "${input:note}"
}
},
"presentation": {
"reveal": "always",
"panel": "dedicated"
},
"problemMatcher": "$msCompile"
}
],
"inputs": [
{
"id": "versione",
"type": "promptString",
"description": "Versione — lascia vuoto se hai già taggato (git tag v4.14.0), o per la minor successiva",
"default": ""
},
{
"id": "note",
"type": "promptString",
"description": "Note di rilascio (vuoto = solo il numero di versione)",
"default": ""
}
]
}
+8 -21
View File
@@ -1,29 +1,16 @@
<Application x:Class="Mimante.App"
<Application x:Class="AutoBidder.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Mimante"
xmlns:local="clr-namespace:AutoBidder"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<!-- Stile pulsanti globale -->
<Style x:Key="SmallButtonStyle" TargetType="Button">
<Setter Property="Foreground" Value="White" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="FontSize" Value="12" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="12"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<ResourceDictionary.MergedDictionaries>
<!-- [0] SLOT TEMA: sostituito a runtime da ThemeManager (Dark/Light) -->
<ResourceDictionary Source="Themes/Tokens.Dark.xaml"/>
<!-- [1] Stili controlli (usano DynamicResource sui token) -->
<ResourceDictionary Source="Themes/Controls.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
+93 -4
View File
@@ -1,14 +1,103 @@
using System.Configuration;
using System.Data;
using System;
using System.Windows;
using AutoBidder.Utilities;
namespace Mimante
namespace AutoBidder
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
}
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// Le descrizioni delle impostazioni vivono nei suggerimenti invece che stampate
// sotto ogni campo. Quello predefinito di Windows sparisce dopo cinque secondi:
// per un paragrafo di tre righe significa doverlo rincorrere.
System.Windows.Controls.ToolTipService.ShowDurationProperty.OverrideMetadata(
typeof(DependencyObject), new FrameworkPropertyMetadata(60000));
// I percorsi vanno decisi per primi: da qui in poi ogni archivio li usa.
// Le impostazioni stanno nella radice fissa, quindi si possono leggere anche
// prima di sapere dove finiranno gli altri dati.
var settings = SettingsManager.Load();
AppPaths.Configure(settings.DatabaseFolder);
AppPaths.EnsureFolders();
// I due database si aprono per primi: da qui in poi ogni archivio e ogni
// registro ci scrive. Quello d'esercizio per primo, perché il registro
// applicativo sta lì dentro.
Data.SqliteDatabase.OnLog += m => TextLogService.App("INFO", m);
try
{
var op = Data.OperationalDatabase.Instance;
TextLogService.SessionStarted(AppInfo.Version);
TextLogService.App("INFO", $"[DATABASE] Esercizio: {op.Path} ({op.SizeBytes / 1024:N0} KB)");
var db = Data.AuctionDatabase.Instance;
TextLogService.App("INFO", $"[DATABASE] Osservazioni: {db.Path} (SQLite {Data.SqliteDatabase.LibraryVersion}, {db.SizeBytes / 1024:N0} KB)");
db.Enqueue("INSERT INTO sessions(started_at, app_version) VALUES(?1, ?2)", Data.SqliteDatabase.Now(), AppInfo.Version);
}
catch (Exception ex)
{
TextLogService.App("ERROR", $"[DATABASE] Apertura non riuscita: {ex.Message}");
}
TextLogService.PurgeOldLogs();
// Le righe di registro delle aste confluiscono nel database.
Data.AuctionRecorder.CaptureAuctionLogs();
// Lo storico e' cresciuto per mesi con versioni diverse dell'applicazione:
// alcuni record hanno campi che allora non esistevano. Quelli deducibili si
// ricalcolano qui, una volta, prima che qualcuno li legga. Quelli NON
// deducibili non si toccano: se ne occupa la pulizia, che chiede all'utente.
try
{
var repair = CompletedAuctionsStore.RepairOnStartup();
if (repair.AnythingDone)
{
TextLogService.App("INFO", $"[STORICO] Riparati {repair.Total} campi mancanti " +
$"(chiavi prodotto {repair.ProductKeys}, esiti {repair.Outcomes}, " +
$"nomi {repair.Names})");
}
}
catch (Exception ex)
{
TextLogService.App("WARN", $"[STORICO] Riparazione non riuscita: {ex.Message}");
}
// L'apprendimento è sempre acceso: carica ciò che sa, ricostruisce il profilo
// dallo storico se manca, e in sottofondo legge i dossier non ancora appresi.
Ml.LearningService.OnLog += m => TextLogService.App("INFO", m);
Ml.LearningService.Start(settings);
// Applica il tema salvato (chiaro/scuro) prima di mostrare la finestra
ThemeManager.ApplyFromSettings();
}
protected override void OnExit(ExitEventArgs e)
{
// Quello che è ancora in coda va sul disco adesso: un registro che perde
// proprio le ultime righe è inutile esattamente quando serve di più.
TextLogService.SessionEnded();
try
{
var db = Data.AuctionDatabase.Instance;
db.Enqueue("UPDATE sessions SET ended_at = ?1 WHERE session_id = (SELECT MAX(session_id) FROM sessions)", Data.SqliteDatabase.Now());
Data.AuctionDatabase.CloseInstance();
Data.OperationalDatabase.CloseInstance();
}
catch { /* all'uscita non c'è più nessuno a cui dirlo */ }
// L'icona di notifica sopravvive al processo se non la si rimuove:
// resterebbe nell'area di notifica come icona fantasma.
WindowsNotifier.Dispose();
base.OnExit(e);
}
}
}
+32 -14
View File
@@ -1,37 +1,55 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<TargetFramework>net10.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<!-- Solo per NotifyIcon: le notifiche moderne richiederebbero un'applicazione
registrata, incompatibile con l'avvio da singolo eseguibile. -->
<UseWindowsForms>true</UseWindowsForms>
<AssemblyName>AutoBidder</AssemblyName>
<RootNamespace>AutoBidder</RootNamespace>
<ApplicationIcon>Icon\favicon.ico</ApplicationIcon>
<!-- App desktop leggera per Bidoo. Nessun software aggiuntivo:
WebView2 runtime è preinstallato su Windows 11. -->
<!-- Versione delle sole compilazioni di sviluppo. NON va alzata per rilasciare:
nei pacchetti pubblicati questi quattro numeri vengono dal tag git, passati
da build/Release.proj a dotnet publish. Vedi Utilities/AppInfo. -->
<Version>4.13.0</Version>
<AssemblyVersion>4.13.0.0</AssemblyVersion>
<FileVersion>4.13.0.0</FileVersion>
<InformationalVersion>4.13.0</InformationalVersion>
<!-- Runtime unico supportato per la pubblicazione self-contained -->
<RuntimeIdentifiers>win-x64</RuntimeIdentifiers>
<SatelliteResourceLanguages>it;en</SatelliteResourceLanguages>
</PropertyGroup>
<ItemGroup>
<Compile Remove=".github\**" />
<Compile Remove=".vscode\**" />
<EmbeddedResource Remove=".github\**" />
<EmbeddedResource Remove=".vscode\**" />
<None Remove=".github\**" />
<None Remove=".vscode\**" />
<Page Remove=".github\**" />
<Page Remove=".vscode\**" />
<!-- Il progetto di test vive in una sottocartella: senza questa esclusione i
progetti SDK-style, che raccolgono **/*.cs, lo compilerebbero qui dentro. -->
<Compile Remove="Tests\**" />
<None Remove="Tests\**" />
</ItemGroup>
<ItemGroup>
<None Remove="Icon\favicon.ico" />
<!-- UseWindowsForms aggiunge questi using in tutti i file, e ogni nome in comune
con WPF (UserControl, TextBox, Application, Color...) diventa ambiguo.
Serve solo a WindowsNotifier, che li importa per conto proprio. -->
<Using Remove="System.Windows.Forms" />
<Using Remove="System.Drawing" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.1343.22" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6584" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3351.48" />
</ItemGroup>
<ItemGroup>
<!-- Incorporata come Resource (non Content) così l'icona della finestra
funziona anche in modalità single-file self-contained. -->
<Resource Include="Icon\favicon.ico" />
</ItemGroup>
+21 -14
View File
@@ -1,48 +1,55 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.0.11217.181 d18.0
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoBidder", "AutoBidder.csproj", "{9BBAEF93-DF66-432C-9349-459E272D6538}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoBidder.Tests", "Tests\AutoBidder.Tests.csproj", "{CAC096C0-E399-4268-B2FF-E205947CB733}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|ARM = Debug|ARM
Debug|ARM64 = Debug|ARM64
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|ARM = Release|ARM
Release|ARM64 = Release|ARM64
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{9BBAEF93-DF66-432C-9349-459E272D6538}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9BBAEF93-DF66-432C-9349-459E272D6538}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9BBAEF93-DF66-432C-9349-459E272D6538}.Debug|ARM.ActiveCfg = Debug|Any CPU
{9BBAEF93-DF66-432C-9349-459E272D6538}.Debug|ARM.Build.0 = Debug|Any CPU
{9BBAEF93-DF66-432C-9349-459E272D6538}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{9BBAEF93-DF66-432C-9349-459E272D6538}.Debug|ARM64.Build.0 = Debug|Any CPU
{9BBAEF93-DF66-432C-9349-459E272D6538}.Debug|x64.ActiveCfg = Debug|Any CPU
{9BBAEF93-DF66-432C-9349-459E272D6538}.Debug|x64.Build.0 = Debug|Any CPU
{9BBAEF93-DF66-432C-9349-459E272D6538}.Debug|x86.ActiveCfg = Debug|Any CPU
{9BBAEF93-DF66-432C-9349-459E272D6538}.Debug|x86.Build.0 = Debug|Any CPU
{9BBAEF93-DF66-432C-9349-459E272D6538}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9BBAEF93-DF66-432C-9349-459E272D6538}.Release|Any CPU.Build.0 = Release|Any CPU
{9BBAEF93-DF66-432C-9349-459E272D6538}.Release|ARM.ActiveCfg = Release|Any CPU
{9BBAEF93-DF66-432C-9349-459E272D6538}.Release|ARM.Build.0 = Release|Any CPU
{9BBAEF93-DF66-432C-9349-459E272D6538}.Release|ARM64.ActiveCfg = Release|Any CPU
{9BBAEF93-DF66-432C-9349-459E272D6538}.Release|ARM64.Build.0 = Release|Any CPU
{9BBAEF93-DF66-432C-9349-459E272D6538}.Release|x64.ActiveCfg = Release|Any CPU
{9BBAEF93-DF66-432C-9349-459E272D6538}.Release|x64.Build.0 = Release|Any CPU
{9BBAEF93-DF66-432C-9349-459E272D6538}.Release|x86.ActiveCfg = Release|Any CPU
{9BBAEF93-DF66-432C-9349-459E272D6538}.Release|x86.Build.0 = Release|Any CPU
{CAC096C0-E399-4268-B2FF-E205947CB733}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CAC096C0-E399-4268-B2FF-E205947CB733}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CAC096C0-E399-4268-B2FF-E205947CB733}.Debug|x64.ActiveCfg = Debug|Any CPU
{CAC096C0-E399-4268-B2FF-E205947CB733}.Debug|x64.Build.0 = Debug|Any CPU
{CAC096C0-E399-4268-B2FF-E205947CB733}.Debug|x86.ActiveCfg = Debug|Any CPU
{CAC096C0-E399-4268-B2FF-E205947CB733}.Debug|x86.Build.0 = Debug|Any CPU
{CAC096C0-E399-4268-B2FF-E205947CB733}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CAC096C0-E399-4268-B2FF-E205947CB733}.Release|Any CPU.Build.0 = Release|Any CPU
{CAC096C0-E399-4268-B2FF-E205947CB733}.Release|x64.ActiveCfg = Release|Any CPU
{CAC096C0-E399-4268-B2FF-E205947CB733}.Release|x64.Build.0 = Release|Any CPU
{CAC096C0-E399-4268-B2FF-E205947CB733}.Release|x86.ActiveCfg = Release|Any CPU
{CAC096C0-E399-4268-B2FF-E205947CB733}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{CAC096C0-E399-4268-B2FF-E205947CB733} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {1C55CA56-D270-4D9A-91DA-410BF131E905}
EndGlobalSection
File diff suppressed because it is too large Load Diff
+181 -37
View File
@@ -1,4 +1,4 @@
using System.Windows;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
@@ -67,6 +67,20 @@ namespace AutoBidder.Controls
RaiseEvent(new RoutedEventArgs(StopClickedEvent, this));
}
private void LearningToggle_Changed(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(LearningToggledEvent, this));
}
public static readonly RoutedEvent LearningToggledEvent = EventManager.RegisterRoutedEvent(
"LearningToggled", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
public event RoutedEventHandler LearningToggled
{
add { AddHandler(LearningToggledEvent, value); }
remove { RemoveHandler(LearningToggledEvent, value); }
}
private void AddUrlButton_Click(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(AddUrlClickedEvent, this));
@@ -82,9 +96,137 @@ namespace AutoBidder.Controls
RaiseEvent(new RoutedEventArgs(RemoveAllClickedEvent, this));
}
private void ExportButton_Click(object sender, RoutedEventArgs e)
private void RemoveFinishedButton_Click(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(ExportClickedEvent, this));
RaiseEvent(new RoutedEventArgs(RemoveFinishedClickedEvent, this));
}
/// <summary>
/// Aggiorna i contatori della barra strumenti. Li calcola il chiamante, che ha
/// l'elenco completo: qui si presentano soltanto.
/// </summary>
public void UpdateCounters(int total, int active, int watch, int stopped, int won, int lost)
{
PillTotal.Text = total == 1 ? "1 asta" : $"{total} aste";
PillActive.Text = $"{active} attive";
PillWatch.Text = $"{watch} osserva";
PillStopped.Text = $"{stopped} ferme";
PillWon.Text = $"{won} vinte";
PillLost.Text = $"{lost} perse";
}
/// <summary>
/// Stato del motore: traffico prodotto, aggancio all'orologio del server e ritardo
/// medio di rete. Tutti e tre i valori arrivano già misurati dal chiamante.
/// </summary>
public void UpdateEngineStatus(long requestsSent, bool clockSynced, int clockSamples, double avgPingMs)
{
RequestsText.Text = requestsSent.ToString("N0");
// Senza campioni non c'è nulla da dire: meglio un trattino di un numero finto.
if (clockSamples == 0)
{
ClockText.Text = "—";
SetClockTone("Brush.TextFaint");
ClockPill.ToolTip =
"Nessuna misura: il motore non sta interrogando Bidoo.\n" +
"Il pallino diventa verde quando l'orologio è agganciato e la rete risponde in fretta.";
return;
}
ClockText.Text = avgPingMs > 0 ? $"{avgPingMs:F0} ms" : "—";
var tone = !clockSynced ? "Brush.Warning" : PingTone(avgPingMs);
SetClockTone(tone);
var sync = clockSynced
? $"Orologio agganciato al server ({clockSamples} campioni)."
: $"Orologio in aggancio: {clockSamples} campioni su 3.";
ClockPill.ToolTip =
$"{sync}\n" +
$"Ritardo medio di rete: {avgPingMs:F0} ms — {PingVerdict(avgPingMs)}.\n" +
"Tieni l'anticipo puntata sopra questo valore.";
}
/// <summary>
/// Soglie del ritardo di rete. Sono tarate su ciò che serve al motore: con un
/// anticipo tipico di qualche centinaio di millisecondi, oltre i 400 ms di
/// round-trip il margine per arrivare in tempo si assottiglia davvero.
/// </summary>
private static string PingTone(double ms) => ms switch
{
<= 0 => "Brush.TextFaint",
< 150 => "Brush.Success",
< 400 => "Brush.Warning",
_ => "Brush.Danger"
};
private static string PingVerdict(double ms) => ms switch
{
<= 0 => "non misurato",
< 150 => "rete pronta",
< 400 => "rete lenta, alza l'anticipo",
_ => "rete molto lenta, puntate a rischio"
};
private void SetClockTone(string brushKey)
{
ClockDot.SetResourceReference(ForegroundProperty, brushKey);
ClockText.SetResourceReference(ForegroundProperty, brushKey);
}
/// <summary>
/// Stato del conto: puntate, credito e vincite da confermare.
///
/// <para>I valori arrivano annullabili di proposito: finché non si è connessi, o
/// finché una risposta non è arrivata, la barra mostra un trattino invece di uno
/// zero. Uno zero inventato è peggio di un dato mancante — è esattamente l'errore
/// per cui le aste da confermare risultavano sempre nessuna.</para>
/// </summary>
public void UpdateAccountStatus(int? remainingBids, decimal? shopCredit, int? auctionsToConfirm)
{
RemainingBidsText.Text = remainingBids?.ToString("N0") ?? "—";
ShopCreditText.Text = shopCredit.HasValue ? $"{shopCredit.Value:N2} €" : "— €";
if (auctionsToConfirm is > 0)
{
BannerAsteDaRiscattare.Text = auctionsToConfirm.Value.ToString("N0");
ToConfirmPill.Visibility = Visibility.Visible;
ToConfirmPill.ToolTip = auctionsToConfirm.Value == 1
? "1 asta vinta in attesa di conferma su Bidoo"
: $"{auctionsToConfirm.Value} aste vinte in attesa di conferma su Bidoo";
}
else
{
ToConfirmPill.Visibility = Visibility.Collapsed;
}
}
/// <summary>Contatori del motore relativi alla sola asta selezionata.</summary>
public void UpdateSelectedStats(int resets, int myBids, double avgPingMs, long polls, long pollErrors)
{
StatResets.Text = resets.ToString();
StatMyBids.Text = myBids.ToString();
StatPing.Text = avgPingMs > 0 ? $"{avgPingMs:F0} ms" : "—";
StatPolls.Text = polls.ToString("N0");
StatPollErrors.Text = pollErrors.ToString("N0");
StatPollErrors.SetResourceReference(ForegroundProperty,
pollErrors > 0 ? "Brush.Warning" : "Brush.Text");
}
/// <summary>Verdetto di convenienza nella scheda Prodotto.</summary>
public void SetVerdict(string label, string value, string tone)
{
VerdictLabel.Text = label;
VerdictText.Text = value;
VerdictText.SetResourceReference(ForegroundProperty, tone switch
{
"ok" => "Brush.Success",
"danger" => "Brush.Danger",
_ => "Brush.Text"
});
}
private void MultiAuctionsGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
@@ -115,14 +257,14 @@ namespace AutoBidder.Controls
System.Diagnostics.Debug.WriteLine("[DELETE KEY] Tasto Canc premuto su asta selezionata");
// Lancia direttamente l'evento senza chiedere conferma
// La conferma verrà mostrata dal gestore RemoveUrlButton_Click
// La conferma verr mostrata dal gestore RemoveUrlButton_Click
System.Diagnostics.Debug.WriteLine("[DELETE KEY] Lancio evento RemoveUrlClicked");
RaiseEvent(new RoutedEventArgs(RemoveUrlClickedEvent, this));
// Previeni che l'evento venga gestito da altri controlli
e.Handled = true;
}
// NUOVO: Gestione esplicita frecce Su/Giù per navigazione
// NUOVO: Gestione esplicita frecce Su/Gi per navigazione
else if (e.Key == Key.Up && MultiAuctionsGrid.Items.Count > 0)
{
int currentIndex = MultiAuctionsGrid.SelectedIndex;
@@ -190,10 +332,6 @@ namespace AutoBidder.Controls
RaiseEvent(new RoutedEventArgs(OpenAuctionExternalClickedEvent, this));
}
private void ExportAuctionButton_Click(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(ExportAuctionClickedEvent, this));
}
private void RefreshProductInfoButton_Click(object sender, RoutedEventArgs e)
{
@@ -210,11 +348,6 @@ namespace AutoBidder.Controls
RaiseEvent(new RoutedEventArgs(BidBeforeDeadlineMsChangedEvent, this));
}
private void SelectedCheckAuctionOpen_Changed(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(CheckAuctionOpenChangedEvent, this));
}
private void SelectedMinPrice_TextChanged(object sender, TextChangedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(MinPriceChangedEvent, this));
@@ -230,7 +363,23 @@ namespace AutoBidder.Controls
RaiseEvent(new RoutedEventArgs(MaxClicksChangedEvent, this));
}
private void SelectedMaxSpend_TextChanged(object sender, TextChangedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(MaxSpendChangedEvent, this));
}
private void SelectedStopAtBreakEven_Changed(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(BreakEvenChangedEvent, this));
}
// Routed Events
public static readonly RoutedEvent MaxSpendChangedEvent = EventManager.RegisterRoutedEvent(
"MaxSpendChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
public static readonly RoutedEvent BreakEvenChangedEvent = EventManager.RegisterRoutedEvent(
"BreakEvenChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
public static readonly RoutedEvent StartClickedEvent = EventManager.RegisterRoutedEvent(
"StartClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
@@ -246,12 +395,12 @@ namespace AutoBidder.Controls
public static readonly RoutedEvent RemoveUrlClickedEvent = EventManager.RegisterRoutedEvent(
"RemoveUrlClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
public static readonly RoutedEvent RemoveFinishedClickedEvent = EventManager.RegisterRoutedEvent(
"RemoveFinishedClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
public static readonly RoutedEvent RemoveAllClickedEvent = EventManager.RegisterRoutedEvent(
"RemoveAllClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
public static readonly RoutedEvent ExportClickedEvent = EventManager.RegisterRoutedEvent(
"ExportClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
public static readonly RoutedEvent AuctionSelectionChangedEvent = EventManager.RegisterRoutedEvent(
"AuctionSelectionChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
@@ -273,9 +422,6 @@ namespace AutoBidder.Controls
public static readonly RoutedEvent BidBeforeDeadlineMsChangedEvent = EventManager.RegisterRoutedEvent(
"BidBeforeDeadlineMsChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
public static readonly RoutedEvent CheckAuctionOpenChangedEvent = EventManager.RegisterRoutedEvent(
"CheckAuctionOpenChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
public static readonly RoutedEvent MinPriceChangedEvent = EventManager.RegisterRoutedEvent(
"MinPriceChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
@@ -291,8 +437,6 @@ namespace AutoBidder.Controls
public static readonly RoutedEvent OpenAuctionExternalClickedEvent = EventManager.RegisterRoutedEvent(
"OpenAuctionExternalClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
public static readonly RoutedEvent ExportAuctionClickedEvent = EventManager.RegisterRoutedEvent(
"ExportAuctionClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
public static readonly RoutedEvent RefreshProductInfoClickedEvent = EventManager.RegisterRoutedEvent(
"RefreshProductInfoClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
@@ -343,10 +487,10 @@ namespace AutoBidder.Controls
remove { RemoveHandler(RemoveAllClickedEvent, value); }
}
public event RoutedEventHandler ExportClicked
public event RoutedEventHandler RemoveFinishedClicked
{
add { AddHandler(ExportClickedEvent, value); }
remove { RemoveHandler(ExportClickedEvent, value); }
add { AddHandler(RemoveFinishedClickedEvent, value); }
remove { RemoveHandler(RemoveFinishedClickedEvent, value); }
}
public event RoutedEventHandler AuctionSelectionChanged
@@ -391,12 +535,6 @@ namespace AutoBidder.Controls
remove { RemoveHandler(BidBeforeDeadlineMsChangedEvent, value); }
}
public event RoutedEventHandler CheckAuctionOpenChanged
{
add { AddHandler(CheckAuctionOpenChangedEvent, value); }
remove { RemoveHandler(CheckAuctionOpenChangedEvent, value); }
}
public event RoutedEventHandler MinPriceChanged
{
add { AddHandler(MinPriceChangedEvent, value); }
@@ -415,6 +553,18 @@ namespace AutoBidder.Controls
remove { RemoveHandler(MaxClicksChangedEvent, value); }
}
public event RoutedEventHandler MaxSpendChanged
{
add { AddHandler(MaxSpendChangedEvent, value); }
remove { RemoveHandler(MaxSpendChangedEvent, value); }
}
public event RoutedEventHandler BreakEvenChanged
{
add { AddHandler(BreakEvenChangedEvent, value); }
remove { RemoveHandler(BreakEvenChangedEvent, value); }
}
public event RoutedEventHandler OpenAuctionInternalClicked
{
add { AddHandler(OpenAuctionInternalClickedEvent, value); }
@@ -427,12 +577,6 @@ namespace AutoBidder.Controls
remove { RemoveHandler(OpenAuctionExternalClickedEvent, value); }
}
public event RoutedEventHandler ExportAuctionClicked
{
add { AddHandler(ExportAuctionClickedEvent, value); }
remove { RemoveHandler(ExportAuctionClickedEvent, value); }
}
public event RoutedEventHandler RefreshProductInfoClicked
{
add { AddHandler(RefreshProductInfoClickedEvent, value); }
+282 -92
View File
@@ -1,127 +1,317 @@
<UserControl x:Class="AutoBidder.Controls.BrowserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
mc:Ignorable="d"
mc:Ignorable="d"
d:DesignHeight="800" d:DesignWidth="1200"
Background="#1E1E1E">
Background="{DynamicResource Brush.Bg}">
<UserControl.Resources>
<Style x:Key="RoundedButton" TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="4"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Cursor" Value="Hand"/>
<Style x:Key="Glyph" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Segoe MDL2 Assets"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
</Style>
<!-- Nav Button Style (text only) -->
<Style x:Key="NavButton" TargetType="Button" BasedOn="{StaticResource RoundedButton}">
<Setter Property="MinWidth" Value="50"/>
<Setter Property="Height" Value="30"/>
<Setter Property="FontSize" Value="11"/>
<Setter Property="Padding" Value="8,0"/>
</Style>
<!-- Scheda di una singola asta del catalogo -->
<DataTemplate x:Key="AuctionCardTemplate">
<Border Style="{StaticResource AuctionCard}" Width="250">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Immagine prodotto: arriva dalla rete, quindi puo' mancare -->
<Border Grid.Row="0" Height="110" CornerRadius="8"
Background="{DynamicResource Brush.SurfaceAlt}" Margin="0,0,0,8">
<Image Source="{Binding ImageUrl}" Stretch="Uniform" Margin="6"
RenderOptions.BitmapScalingMode="HighQuality"/>
</Border>
<TextBlock Grid.Row="1" Text="{Binding Name}" ToolTip="{Binding Name}"
FontWeight="SemiBold" TextTrimming="CharacterEllipsis"
MaxHeight="34" TextWrapping="Wrap"
Foreground="{DynamicResource Brush.Text}" Margin="0,0,0,6"/>
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,0,0,6">
<TextBlock Text="{Binding PriceDisplay}" FontWeight="Bold"
Foreground="{DynamicResource Brush.Accent}" Margin="0,0,10,0"/>
<TextBlock Text="{Binding TimerDisplay}" Margin="0,0,10,0">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource Brush.TextMuted}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding TimerUrgency}" Value="warn">
<Setter Property="Foreground" Value="{DynamicResource Brush.Warning}"/>
</DataTrigger>
<DataTrigger Binding="{Binding TimerUrgency}" Value="danger">
<Setter Property="Foreground" Value="{DynamicResource Brush.Danger}"/>
<Setter Property="FontWeight" Value="Bold"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<TextBlock Text="{Binding BuyNowDisplay}"
FontSize="{StaticResource Font.Size.Sm}"
Foreground="{DynamicResource Brush.TextFaint}"
ToolTip="Valore del prodotto (Compra Subito)"/>
</StackPanel>
<Grid Grid.Row="3">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="{Binding LastBidderDisplay}"
FontSize="{StaticResource Font.Size.Sm}"
Foreground="{DynamicResource Brush.TextMuted}"
TextTrimming="CharacterEllipsis" MaxWidth="90"/>
<!-- Solo l'icona della mano: la parola "manuale" mangiava spazio
al nome dell'ultimo puntatore, che è il dato che si legge
davvero. Il significato resta nel suggerimento. -->
<Border Background="{DynamicResource Brush.WarningSubtle}" CornerRadius="5"
Padding="4,1" Margin="6,0,0,0"
Visibility="{Binding ManualBadgeVisibility}"
ToolTip="Asta solo manuale: non accetta puntate automatiche">
<TextBlock FontFamily="Segoe MDL2 Assets" Text="&#xE815;" FontSize="10"
Foreground="{DynamicResource Brush.Warning}"/>
</Border>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<!-- Segui il prodotto: le aste future dello stesso articolo
entreranno nel monitor da sole. -->
<Button Style="{StaticResource MiniIconButton}"
ToolTip="{Binding WatchTooltip}"
Command="{Binding DataContext.CatalogWatchCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}">
<TextBlock FontFamily="Segoe MDL2 Assets" Text="{Binding WatchGlyph}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource Brush.TextFaint}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsWatched}" Value="True">
<Setter Property="Foreground" Value="{DynamicResource Brush.Gold}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Button>
<!-- Mette il prodotto nella scheda Prodotti e ci porta sopra:
serve a dargli limiti propri anche senza seguirlo. -->
<Button Style="{StaticResource MiniIconButton}"
ToolTip="{Binding ConfigureTooltip}"
Command="{Binding DataContext.CatalogConfigureCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}">
<TextBlock FontFamily="Segoe MDL2 Assets" Text="&#xE713;">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource Brush.TextFaint}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsListed}" Value="True">
<Setter Property="Foreground" Value="{DynamicResource Brush.Accent}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Button>
<Button Style="{StaticResource MiniIconButton}"
ToolTip="Apri su Bidoo"
Command="{Binding DataContext.CatalogOpenCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}">
<TextBlock FontFamily="Segoe MDL2 Assets" Text="&#xE8A7;"/>
</Button>
<Button Style="{StaticResource MiniIconButton}"
Foreground="{DynamicResource Brush.Accent}"
ToolTip="Aggiungi al monitor"
Command="{Binding DataContext.CatalogAddCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}">
<TextBlock FontFamily="Segoe MDL2 Assets" Text="&#xE710;"/>
</Button>
</StackPanel>
</Grid>
</Grid>
</Border>
</DataTemplate>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Browser Toolbar -->
<Border Grid.Row="0" Background="#2D2D30" Padding="10" BorderBrush="#3E3E42" BorderThickness="0,0,0,1">
<!-- ═══ Barra strumenti ═══ -->
<Border Grid.Row="0" Style="{StaticResource TabToolbar}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Navigation Buttons (TEXT ONLY - NO SYMBOLS) -->
<StackPanel Grid.Column="0" Orientation="Horizontal">
<Button x:Name="BrowserBackButton"
Content="Indietro"
Margin="0,0,5,0"
Background="#3E3E42"
Style="{StaticResource NavButton}"
Click="BrowserBackButton_Click"
ToolTip="Torna alla pagina precedente"/>
<Button x:Name="BrowserForwardButton"
Content="Avanti"
Margin="0,0,5,0"
Background="#3E3E42"
Style="{StaticResource NavButton}"
Click="BrowserForwardButton_Click"
ToolTip="Vai alla pagina successiva"/>
<Button x:Name="BrowserRefreshButton"
Content="Ricarica"
Margin="0,0,5,0"
Background="#3E3E42"
Style="{StaticResource NavButton}"
Click="BrowserRefreshButton_Click"
ToolTip="Ricarica la pagina corrente"/>
<Button x:Name="BrowserHomeButton"
Content="Home"
Margin="0,0,10,0"
Background="#3E3E42"
Style="{StaticResource NavButton}"
Click="BrowserHomeButton_Click"
ToolTip="Vai alla homepage Bidoo"/>
<!-- Catalogo e Browser ora sono due schede separate nella barra laterale
("Cerca" e "Browser"): l'interruttore interno resta solo come stato
pilotato da fuori, non più visibile all'utente. -->
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center"
Visibility="Collapsed">
<RadioButton x:Name="ModeCatalogRadio" Style="{StaticResource SegmentedButton}"
GroupName="ExploreMode" IsChecked="True" Content="Catalogo"
Checked="ModeCatalogRadio_Checked"
ToolTip="Griglia delle aste: veloce, senza caricare pagine"/>
<RadioButton x:Name="ModeBrowserRadio" Style="{StaticResource SegmentedButton}"
GroupName="ExploreMode" Content="Browser"
Checked="ModeBrowserRadio_Checked"
ToolTip="Browser Bidoo integrato: serve per il login (il cookie viene importato da solo)"/>
<Border Width="1" Background="{DynamicResource Brush.Border}" Margin="8,4"/>
</StackPanel>
<!-- Address Bar -->
<Border Grid.Column="1"
Background="#1E1E1E"
BorderBrush="#3E3E42"
BorderThickness="1"
CornerRadius="4"
Margin="10,0">
<TextBox x:Name="BrowserAddress"
VerticalAlignment="Center"
BorderThickness="0"
Background="Transparent"
Foreground="#CCCCCC"
Padding="10,0"
FontSize="13"
IsReadOnly="True"
Cursor="Arrow"
ToolTip="Indirizzo della pagina corrente (non editabile)"/>
<StackPanel Grid.Column="1" x:Name="CatalogToolbarLeft" Orientation="Horizontal"
VerticalAlignment="Center">
<Button x:Name="CatalogRefreshButton" Style="{StaticResource IconButton}"
ToolTip="Ricarica categorie e prezzi" Click="CatalogRefreshButton_Click">
<TextBlock Style="{StaticResource Glyph}" Text="&#xE72C;"/>
</Button>
<CheckBox x:Name="CatalogAutoRefresh" Content="Prezzi in tempo reale"
Margin="10,0,0,0" VerticalAlignment="Center"
Checked="CatalogAutoRefresh_Changed" Unchecked="CatalogAutoRefresh_Changed"
ToolTip="Aggiorna prezzi e timer sul posto. Spento di default: acceso, i valori cambiano mentre stai leggendo."/>
<CheckBox x:Name="CatalogHideManual" Content="Nascondi aste manuali"
Margin="14,0,0,0" VerticalAlignment="Center"
Checked="CatalogFilterChanged" Unchecked="CatalogFilterChanged"
ToolTip="Le aste manuali non accettano puntate automatiche"/>
</StackPanel>
<!-- Barra indirizzi del browser: occupa lo stesso posto dei filtri -->
<Border Grid.Column="1" Grid.ColumnSpan="2" x:Name="BrowserToolbar"
Visibility="Collapsed" Margin="0,0,10,0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
<Button x:Name="BrowserBackButton" Style="{StaticResource IconButton}"
ToolTip="Indietro" Click="BrowserBackButton_Click">
<TextBlock Style="{StaticResource Glyph}" Text="&#xE72B;"/>
</Button>
<Button x:Name="BrowserForwardButton" Style="{StaticResource IconButton}"
ToolTip="Avanti" Click="BrowserForwardButton_Click">
<TextBlock Style="{StaticResource Glyph}" Text="&#xE72A;"/>
</Button>
<Button x:Name="BrowserRefreshButton" Style="{StaticResource IconButton}"
ToolTip="Ricarica" Click="BrowserRefreshButton_Click">
<TextBlock Style="{StaticResource Glyph}" Text="&#xE72C;"/>
</Button>
<Button x:Name="BrowserHomeButton" Style="{StaticResource IconButton}"
ToolTip="Homepage Bidoo" Click="BrowserHomeButton_Click">
<TextBlock Style="{StaticResource Glyph}" Text="&#xE80F;"/>
</Button>
</StackPanel>
<Border Grid.Column="1" Background="{DynamicResource Brush.Bg}"
BorderBrush="{DynamicResource Brush.Border}" BorderThickness="1"
CornerRadius="8" Margin="8,0,0,0">
<TextBox x:Name="BrowserAddress" VerticalAlignment="Center"
BorderThickness="0" Background="Transparent"
Foreground="{DynamicResource Brush.Text}" Padding="10,0"
IsReadOnly="True" Cursor="Arrow"
ToolTip="Indirizzo della pagina corrente"/>
</Border>
</Grid>
</Border>
<!-- Action Buttons -->
<StackPanel Grid.Column="2" Orientation="Horizontal" Margin="10,0,0,0">
<Button x:Name="BrowserAddAuctionButton"
Content="Aggiungi Asta"
Padding="20,7"
FontSize="13"
Background="#00D800"
Style="{StaticResource RoundedButton}"
Click="BrowserAddAuctionButton_Click"
ToolTip="Aggiungi l'asta corrente al monitoraggio"/>
<!-- Campo di ricerca con suggerimento: senza, un riquadro vuoto in barra
non si capisce a cosa serva. -->
<Grid Grid.Column="2" x:Name="CatalogSearchHost" Margin="16,0,10,0"
Width="240" HorizontalAlignment="Right" VerticalAlignment="Center">
<TextBox x:Name="CatalogSearchBox"
TextChanged="CatalogSearchBox_TextChanged"
ToolTip="Filtra per nome prodotto"/>
<TextBlock x:Name="CatalogSearchHint" Text="Filtra per nome…"
IsHitTestVisible="False" Margin="10,0,0,0"
VerticalAlignment="Center"
Foreground="{DynamicResource Brush.TextFaint}"/>
</Grid>
<StackPanel Grid.Column="3" Orientation="Horizontal" VerticalAlignment="Center">
<Border Style="{StaticResource Pill}">
<TextBlock x:Name="CatalogCountText" Text="0 aste"
FontSize="{StaticResource Font.Size.Sm}"
Foreground="{DynamicResource Brush.TextMuted}"/>
</Border>
<Button x:Name="BrowserAddAuctionButton" Style="{StaticResource IconButton}"
Foreground="{DynamicResource Brush.Accent}" Visibility="Collapsed"
ToolTip="Aggiungi al monitor l'asta aperta nel browser"
Click="BrowserAddAuctionButton_Click">
<TextBlock Style="{StaticResource Glyph}" Text="&#xE710;"/>
</Button>
</StackPanel>
</Grid>
</Border>
<!-- WebView2 -->
<Border Grid.Row="1" Background="#1E1E1E">
<wv2:WebView2 x:Name="EmbeddedWebView"
<!-- Riga vuota: tiene la struttura a tre righe leggibile -->
<Border Grid.Row="1" Height="0"/>
<!-- ═══ Catalogo ═══ -->
<Grid Grid.Row="2" x:Name="CatalogPanel">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Categorie -->
<Border Grid.Column="0" Width="190" Background="{DynamicResource Brush.Surface}"
BorderBrush="{DynamicResource Brush.Border}" BorderThickness="0,0,1,0">
<ScrollViewer VerticalScrollBarVisibility="Auto" Padding="6,8">
<ItemsControl x:Name="CatalogCategoryList">
<ItemsControl.ItemTemplate>
<DataTemplate>
<RadioButton Style="{StaticResource CategoryButton}"
GroupName="CatalogCategories"
Content="{Binding DisplayName}"
IsChecked="{Binding IsSelected, Mode=TwoWay}"
Checked="CategoryButton_Checked"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Border>
<!-- Schede -->
<Grid Grid.Column="1">
<ScrollViewer x:Name="CatalogScroller" VerticalScrollBarVisibility="Auto" Padding="12,12,2,12">
<ItemsControl x:Name="CatalogItems" ItemTemplate="{StaticResource AuctionCardTemplate}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</ScrollViewer>
<!-- Stato vuoto / caricamento -->
<StackPanel x:Name="CatalogEmptyState" VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Style="{StaticResource Glyph}" Text="&#xE721;" FontSize="34"
Foreground="{DynamicResource Brush.TextFaint}"/>
<TextBlock x:Name="CatalogEmptyText" Text="Caricamento categorie…"
Margin="0,10,0,0" HorizontalAlignment="Center"
Foreground="{DynamicResource Brush.TextMuted}"/>
</StackPanel>
</Grid>
</Grid>
<!-- ═══ Browser integrato ═══ -->
<Border Grid.Row="2" x:Name="BrowserPanel" Background="{DynamicResource Brush.Bg}" Visibility="Collapsed">
<wv2:WebView2 x:Name="EmbeddedWebView"
PreviewMouseRightButtonUp="EmbeddedWebView_PreviewMouseRightButtonUp"/>
</Border>
</Grid>
+167 -2
View File
@@ -1,4 +1,4 @@
using System.Windows;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using Microsoft.Web.WebView2.Core;
@@ -44,7 +44,7 @@ namespace AutoBidder.Controls
}
/// <summary>
/// ? NUOVO: Aggiorna address bar quando la navigazione è completata
/// ? NUOVO: Aggiorna address bar quando la navigazione completata
/// </summary>
private void WebView_NavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs e)
{
@@ -93,6 +93,133 @@ namespace AutoBidder.Controls
e.Handled = true;
}
// ===== MODALITA' ESPLORA =====
private void ModeCatalogRadio_Checked(object sender, RoutedEventArgs e) => ApplyMode(catalog: true);
private void ModeBrowserRadio_Checked(object sender, RoutedEventArgs e) => ApplyMode(catalog: false);
private void ApplyMode(bool catalog)
{
// Chiamato anche durante InitializeComponent, quando gli elementi dichiarati piu'
// in basso nel XAML non esistono ancora. Vanno verificati tutti: fidarsi
// dell'ordine di dichiarazione rende il controllo fragile a ogni riordino.
if (CatalogPanel == null || BrowserPanel == null || BrowserToolbar == null ||
CatalogToolbarLeft == null || CatalogSearchHost == null ||
CatalogCountText == null || BrowserAddAuctionButton == null)
{
return;
}
CatalogPanel.Visibility = catalog ? Visibility.Visible : Visibility.Collapsed;
BrowserPanel.Visibility = catalog ? Visibility.Collapsed : Visibility.Visible;
// I due gruppi di comandi occupano lo stesso spazio: ne vive uno solo per volta.
BrowserToolbar.Visibility = catalog ? Visibility.Collapsed : Visibility.Visible;
CatalogToolbarLeft.Visibility = catalog ? Visibility.Visible : Visibility.Collapsed;
CatalogSearchHost.Visibility = catalog ? Visibility.Visible : Visibility.Collapsed;
CatalogCountText.Visibility = catalog ? Visibility.Visible : Visibility.Collapsed;
BrowserAddAuctionButton.Visibility = catalog ? Visibility.Collapsed : Visibility.Visible;
}
/// <summary>Porta in primo piano il browser integrato (usato dal login e dal catalogo).</summary>
public void ShowBrowser() => ModeBrowserRadio.IsChecked = true;
/// <summary>Porta in primo piano il catalogo nativo (scheda "Cerca").</summary>
public void ShowCatalog() => ModeCatalogRadio.IsChecked = true;
/// <summary>True quando l'utente sta guardando il catalogo nativo.</summary>
public bool IsCatalogMode => ModeCatalogRadio.IsChecked == true;
// ===== CATALOGO =====
/// <summary>
/// Impedisce che il riempimento iniziale dell'elenco categorie faccia partire
/// un caricamento per ogni voce aggiunta.
/// </summary>
private bool _catalogReady;
private void CategoryButton_Checked(object sender, RoutedEventArgs e)
{
if (!_catalogReady) return;
RaiseEvent(new RoutedEventArgs(CatalogCategoryChangedEvent, this));
}
private void CatalogRefreshButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(CatalogRefreshClickedEvent, this));
private void CatalogSearchBox_TextChanged(object sender, TextChangedEventArgs e)
{
if (CatalogSearchHint != null)
{
CatalogSearchHint.Visibility = string.IsNullOrEmpty(CatalogSearchBox.Text)
? Visibility.Visible
: Visibility.Collapsed;
}
RaiseEvent(new RoutedEventArgs(CatalogSearchChangedEvent, this));
}
private void CatalogFilterChanged(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(CatalogSearchChangedEvent, this));
private void CatalogAutoRefresh_Changed(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(CatalogAutoRefreshChangedEvent, this));
/// <summary>Riempie l'elenco categorie senza scatenare un caricamento per voce.</summary>
public void SetCategories(System.Collections.IEnumerable categories)
{
_catalogReady = false;
try { CatalogCategoryList.ItemsSource = categories; }
finally { _catalogReady = true; }
}
/// <summary>Mostra le schede oppure, se non ce ne sono, un messaggio di stato.</summary>
public void SetCatalogItems(System.Collections.IList items, int totalCount)
{
CatalogItems.ItemsSource = items;
CatalogCountText.Text = items.Count == 1 ? "1 asta" : $"{items.Count} aste";
var empty = items.Count == 0;
CatalogEmptyState.Visibility = empty ? Visibility.Visible : Visibility.Collapsed;
CatalogScroller.Visibility = empty ? Visibility.Collapsed : Visibility.Visible;
if (empty)
{
CatalogEmptyText.Text = totalCount > 0
? "Nessuna asta corrisponde ai filtri."
: "Nessuna asta in questa categoria.";
}
}
/// <summary>Messaggio a tutto pannello (caricamento in corso, errore di rete...).</summary>
public void SetCatalogMessage(string message)
{
CatalogEmptyState.Visibility = Visibility.Visible;
CatalogScroller.Visibility = Visibility.Collapsed;
CatalogEmptyText.Text = message;
}
public bool AutoRefreshEnabled => CatalogAutoRefresh.IsChecked == true;
/// <summary>
/// Allinea l'interruttore all'impostazione salvata senza far partire un giro di
/// aggiornamento: chi chiama sta ancora costruendo la pagina.
/// </summary>
public void SetAutoRefresh(bool enabled)
{
CatalogAutoRefresh.Checked -= CatalogAutoRefresh_Changed;
CatalogAutoRefresh.Unchecked -= CatalogAutoRefresh_Changed;
try { CatalogAutoRefresh.IsChecked = enabled; }
finally
{
CatalogAutoRefresh.Checked += CatalogAutoRefresh_Changed;
CatalogAutoRefresh.Unchecked += CatalogAutoRefresh_Changed;
}
}
public bool HideManualAuctions => CatalogHideManual.IsChecked == true;
public string SearchText => CatalogSearchBox.Text?.Trim() ?? "";
// Routed Events
public static readonly RoutedEvent BrowserBackClickedEvent = EventManager.RegisterRoutedEvent(
"BrowserBackClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(BrowserControl));
@@ -144,6 +271,44 @@ namespace AutoBidder.Controls
add { AddHandler(BrowserAddAuctionClickedEvent, value); }
remove { RemoveHandler(BrowserAddAuctionClickedEvent, value); }
}
// ===== Eventi del catalogo =====
public static readonly RoutedEvent CatalogCategoryChangedEvent = EventManager.RegisterRoutedEvent(
"CatalogCategoryChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(BrowserControl));
public static readonly RoutedEvent CatalogRefreshClickedEvent = EventManager.RegisterRoutedEvent(
"CatalogRefreshClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(BrowserControl));
public static readonly RoutedEvent CatalogAutoRefreshChangedEvent = EventManager.RegisterRoutedEvent(
"CatalogAutoRefreshChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(BrowserControl));
public static readonly RoutedEvent CatalogSearchChangedEvent = EventManager.RegisterRoutedEvent(
"CatalogSearchChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(BrowserControl));
public event RoutedEventHandler CatalogCategoryChanged
{
add { AddHandler(CatalogCategoryChangedEvent, value); }
remove { RemoveHandler(CatalogCategoryChangedEvent, value); }
}
public event RoutedEventHandler CatalogRefreshClicked
{
add { AddHandler(CatalogRefreshClickedEvent, value); }
remove { RemoveHandler(CatalogRefreshClickedEvent, value); }
}
public event RoutedEventHandler CatalogAutoRefreshChanged
{
add { AddHandler(CatalogAutoRefreshChangedEvent, value); }
remove { RemoveHandler(CatalogAutoRefreshChangedEvent, value); }
}
public event RoutedEventHandler CatalogSearchChanged
{
add { AddHandler(CatalogSearchChangedEvent, value); }
remove { RemoveHandler(CatalogSearchChangedEvent, value); }
}
}
public class BrowserNavigationEventArgs : RoutedEventArgs
+364
View File
@@ -0,0 +1,364 @@
<UserControl x:Class="AutoBidder.Controls.FreeBidsControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ctl="clr-namespace:AutoBidder.Controls"
mc:Ignorable="d"
d:DesignHeight="800" d:DesignWidth="1200"
Background="{DynamicResource Brush.Bg}">
<UserControl.Resources>
<Style x:Key="Glyph" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Segoe MDL2 Assets"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
</Style>
<Style x:Key="CounterCaption" TargetType="TextBlock">
<Setter Property="FontSize" Value="{StaticResource Font.Size.Sm}"/>
<Setter Property="Foreground" Value="{DynamicResource Brush.TextFaint}"/>
<Setter Property="Margin" Value="0,0,0,3"/>
</Style>
<Style x:Key="CounterValue" TargetType="TextBlock">
<Setter Property="FontSize" Value="22"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Foreground" Value="{DynamicResource Brush.Text}"/>
</Style>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- ═══ Barra strumenti ═══ -->
<Border Grid.Row="0" Style="{StaticResource TabToolbar}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
<Button x:Name="RefreshButton" Style="{StaticResource IconButton}"
ToolTip="Aggiorna il saldo puntate" Click="RefreshButton_Click">
<TextBlock Style="{StaticResource Glyph}" Text="&#xE72C;"/>
</Button>
<TextBlock Text="Puntate" FontWeight="SemiBold" Margin="8,0,0,0"
FontSize="{StaticResource Font.Size.Lg}"
Foreground="{DynamicResource Brush.Text}" VerticalAlignment="Center"/>
<Border Width="1" Background="{DynamicResource Brush.Border}" Margin="10,4"/>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<Border Style="{StaticResource Pill}" ToolTip="Puntate attualmente sul conto">
<TextBlock x:Name="BalanceText" Text="— puntate"
FontSize="{StaticResource Font.Size.Sm}"
Foreground="{DynamicResource Brush.Text}"/>
</Border>
<Border Style="{StaticResource Pill}" ToolTip="Stato del riscatto automatico">
<TextBlock x:Name="StatusPillText" Text="riscatto spento"
FontSize="{StaticResource Font.Size.Sm}"
Foreground="{DynamicResource Brush.TextMuted}"/>
</Border>
</StackPanel>
<StackPanel Grid.Column="3" Orientation="Horizontal" VerticalAlignment="Center">
<Button x:Name="CheckNowButton" Style="{StaticResource SmallRoundedButton}"
Background="{DynamicResource Brush.Accent}"
Foreground="{DynamicResource Brush.TextOnAccent}"
Content="Controlla adesso" Margin="0,0,8,0"
ToolTip="Cerca subito ricompense da riscuotere, senza aspettare il prossimo giro"
Click="CheckNowButton_Click"/>
<Button x:Name="OpenExternalButton" Style="{StaticResource SmallRoundedButton}"
Background="{DynamicResource Brush.SurfaceAlt}"
Foreground="{DynamicResource Brush.Text}"
Margin="0,0,8,0"
ToolTip="Apre la pagina delle ricompense nel browser predefinito di Windows"
Click="OpenExternalButton_Click">
<StackPanel Orientation="Horizontal">
<TextBlock FontFamily="Segoe MDL2 Assets" Text="&#xE8A7;" FontSize="11"
VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock Text="Apri su Bidoo" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button x:Name="OpenInternalButton" Style="{StaticResource IconButton}"
ToolTip="Apri le ricompense nel browser integrato (è lì che arrivano le notifiche)"
Click="OpenInternalButton_Click">
<TextBlock Style="{StaticResource Glyph}" Text="&#xE774;"/>
</Button>
</StackPanel>
</Grid>
</Border>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<StackPanel Margin="20,16" MaxWidth="1000" HorizontalAlignment="Left">
<!-- ═══ Riscatto automatico ═══ -->
<Border Style="{StaticResource CardBlock}">
<StackPanel>
<ctl:HelpHeader Title="Riscatto automatico"
Margin="0,0,0,12"
Help="Bidoo lascia in attesa le ricompense dei giorni scorsi. Con questo acceso, l'applicazione controlla a intervalli regolari e riscuote quello che trova, mentre continua a seguire le aste."/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Riscuoti da solo le ricompense"
Foreground="{DynamicResource Brush.Text}" Margin="0,8"
VerticalAlignment="Center"/>
<CheckBox Grid.Row="0" Grid.Column="1" x:Name="AutoClaimCheckBox"
Margin="10,8" VerticalAlignment="Center"
Checked="AutoClaimCheckBox_Changed" Unchecked="AutoClaimCheckBox_Changed"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Controlla ogni (minuti)"
Foreground="{DynamicResource Brush.Text}" Margin="0,8"
VerticalAlignment="Center"
ToolTip="Minimo 5 minuti: le ricompense sono giornaliere"/>
<TextBox Grid.Row="1" Grid.Column="1" x:Name="CheckMinutesTextBox"
Text="30" Width="90" Margin="10,8" HorizontalAlignment="Left"
LostFocus="CheckMinutesTextBox_LostFocus"/>
</Grid>
<Border Background="{DynamicResource Brush.SurfaceAlt}" CornerRadius="8"
Padding="12,10" Margin="0,10,0,0">
<StackPanel Orientation="Horizontal">
<TextBlock Style="{StaticResource Glyph}" Text="&#xE72C;" FontSize="13"
Foreground="{DynamicResource Brush.Accent}" Margin="0,0,8,0"/>
<TextBlock x:Name="ScheduleText" VerticalAlignment="Center"
Text="Riscatto automatico spento."
FontSize="{StaticResource Font.Size.Sm}"
Foreground="{DynamicResource Brush.TextMuted}"/>
</StackPanel>
</Border>
</StackPanel>
</Border>
<!-- ═══ Contatori ═══ -->
<Border Style="{StaticResource CardBlock}">
<StackPanel>
<Grid Margin="0,0,0,12">
<TextBlock Text="Quanto ho riscattato" Style="{StaticResource BlockTitle}"
Margin="0"/>
<Button x:Name="ResetCountersButton" HorizontalAlignment="Right"
Style="{StaticResource SmallRoundedButton}"
Background="{DynamicResource Brush.SurfaceAlt}"
Foreground="{DynamicResource Brush.Danger}"
Content="Azzera contatori"
Click="ResetCountersButton_Click"/>
</Grid>
<UniformGrid Columns="4">
<StackPanel Margin="0,0,16,0">
<TextBlock Text="Puntate — questa sessione" Style="{StaticResource CounterCaption}"/>
<TextBlock x:Name="SessionBidsText" Text="0" Style="{StaticResource CounterValue}"
Foreground="{DynamicResource Brush.Accent}"/>
</StackPanel>
<StackPanel Margin="0,0,16,0">
<TextBlock Text="Puntate — oggi" Style="{StaticResource CounterCaption}"/>
<TextBlock x:Name="TodayBidsText" Text="0" Style="{StaticResource CounterValue}"/>
</StackPanel>
<StackPanel Margin="0,0,16,0">
<TextBlock Text="Puntate — da sempre" Style="{StaticResource CounterCaption}"/>
<TextBlock x:Name="TotalBidsText" Text="0" Style="{StaticResource CounterValue}"
Foreground="{DynamicResource Brush.Gold}"/>
</StackPanel>
<StackPanel>
<TextBlock Text="Ricompense riscosse" Style="{StaticResource CounterCaption}"/>
<TextBlock x:Name="TotalClaimedText" Text="0" Style="{StaticResource CounterValue}"/>
</StackPanel>
</UniformGrid>
<TextBlock x:Name="CountersDetailText" Margin="0,12,0,0"
FontSize="{StaticResource Font.Size.Sm}"
Foreground="{DynamicResource Brush.TextFaint}"
Text="Nessun controllo ancora eseguito."/>
</StackPanel>
</Border>
<!-- ═══ Registro ═══ -->
<Border Style="{StaticResource CardBlock}">
<StackPanel>
<Grid Margin="0,0,0,10">
<TextBlock Text="Registro dei riscatti" Style="{StaticResource BlockTitle}"
Margin="0"/>
<Button x:Name="ClearLogButton" HorizontalAlignment="Right"
Style="{StaticResource SmallRoundedButton}"
Background="{DynamicResource Brush.SurfaceAlt}"
Foreground="{DynamicResource Brush.Text}"
Content="Pulisci" Click="ClearLogButton_Click"/>
</Grid>
<Border Background="{DynamicResource Brush.Bg}" CornerRadius="8"
BorderBrush="{DynamicResource Brush.Border}" BorderThickness="1"
Height="190">
<Grid>
<ListBox x:Name="ActivityList" Background="Transparent"
BorderThickness="0" Margin="4"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="2,1">
<TextBlock Text="{Binding Time}" FontSize="11"
Foreground="{DynamicResource Brush.TextFaint}"
Margin="0,0,10,0" VerticalAlignment="Top"/>
<TextBlock Text="{Binding Message}" FontSize="12"
TextWrapping="Wrap" MaxWidth="760">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource Brush.Text}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsProblem}" Value="True">
<Setter Property="Foreground" Value="{DynamicResource Brush.Warning}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBlock x:Name="ActivityEmptyText"
Text="Nessun riscatto registrato in questa sessione."
HorizontalAlignment="Center" VerticalAlignment="Center"
FontSize="12"
Foreground="{DynamicResource Brush.TextMuted}"/>
</Grid>
</Border>
</StackPanel>
</Border>
<!-- ═══ Collegamenti promozionali pubblicati ═══ -->
<Border Style="{StaticResource CardBlock}">
<StackPanel>
<ctl:HelpHeader Title="Puntate gratis dai collegamenti pubblicati"
Margin="0,0,0,12"
Help="Bidoo regala puntate con collegamenti già firmati che i siti di raccolta ripubblicano. L'applicazione legge quella pagina, scarta i collegamenti già presi e apre i nuovi con la tua sessione, uno alla volta. I codici presi restano in memoria: senza, ogni giro riaprirebbe gli stessi."/>
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
<Button x:Name="HarvestButton" Style="{StaticResource SmallRoundedButton}"
Background="{DynamicResource Brush.Accent}"
Foreground="{DynamicResource Brush.TextOnAccent}"
Content="Cerca collegamenti adesso" Margin="0,0,8,0"
ToolTip="Legge subito la pagina dei collegamenti e riscuote quelli nuovi"
Click="HarvestButton_Click"/>
<Button x:Name="OpenSourceButton" Style="{StaticResource SmallRoundedButton}"
Background="{DynamicResource Brush.SurfaceAlt}"
Foreground="{DynamicResource Brush.Text}"
Content="Apri la pagina dei collegamenti" Margin="0,0,8,0"
ToolTip="Apre nel browser integrato la pagina da cui vengono presi i collegamenti"
Click="OpenSourceButton_Click"/>
<Button x:Name="ForgetPromosButton" Style="{StaticResource SmallRoundedButton}"
Background="{DynamicResource Brush.SurfaceAlt}"
Foreground="{DynamicResource Brush.Text}"
Content="Dimentica i codici presi"
ToolTip="Azzera la memoria dei collegamenti già aperti (serve dopo un cambio di account)"
Click="ForgetPromosButton_Click"/>
</StackPanel>
<Border Background="{DynamicResource Brush.SurfaceAlt}" CornerRadius="8"
Padding="12,10" Margin="0,0,0,4">
<StackPanel Orientation="Horizontal">
<TextBlock Style="{StaticResource Glyph}" Text="&#xE8A9;" FontSize="13"
Foreground="{DynamicResource Brush.Accent}" Margin="0,0,8,0"/>
<TextBlock x:Name="HarvestStatusText" VerticalAlignment="Center"
TextWrapping="Wrap"
Text="Nessuna raccolta ancora eseguita."
FontSize="{StaticResource Font.Size.Sm}"
Foreground="{DynamicResource Brush.TextMuted}"/>
</StackPanel>
</Border>
</StackPanel>
</Border>
<!-- ═══ Riscatto da codice o collegamento ═══ -->
<Border Style="{StaticResource CardBlock}">
<StackPanel>
<ctl:HelpHeader Title="Codice o collegamento singolo"
Margin="0,0,0,12"
Help="Per un collegamento ricevuto per email o per messaggio, che la pagina di raccolta non ha. Incollalo intero: viene aperto con la sessione attiva, come se ci avessi cliccato sopra. Quelli che non portano a Bidoo vengono rifiutati. Un codice da solo di norma non basta — questi indirizzi portano una firma (sign) che non si può ricostruire."/>
<StackPanel Orientation="Horizontal">
<TextBox x:Name="PromoCodeTextBox" Width="420" VerticalAlignment="Center"
KeyDown="PromoCodeTextBox_KeyDown"
ToolTip="Codice promozionale, oppure indirizzo completo del riscatto"/>
<Button x:Name="ClaimPromoButton" Style="{StaticResource SmallRoundedButton}"
Background="{DynamicResource Brush.Accent}"
Foreground="{DynamicResource Brush.TextOnAccent}"
Content="Riscatta" Margin="8,0,0,0"
Click="ClaimPromoButton_Click"/>
</StackPanel>
<Border Background="{DynamicResource Brush.SurfaceAlt}" CornerRadius="8"
Padding="12,10" Margin="0,12,0,0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" x:Name="ConfigPathText" TextWrapping="Wrap"
VerticalAlignment="Center"
FontSize="{StaticResource Font.Size.Sm}"
Foreground="{DynamicResource Brush.TextMuted}"
Text="Indirizzi, intestazioni e parametri del riscatto stanno in site-config.json."/>
<Button Grid.Column="1" x:Name="OpenConfigButton"
Style="{StaticResource SmallRoundedButton}"
Background="{DynamicResource Brush.Surface}"
Foreground="{DynamicResource Brush.Text}"
Content="Apri i riferimenti" Margin="10,0,0,0"
ToolTip="Apre il file con indirizzi, intestazioni e parametri del riscatto"
Click="OpenConfigButton_Click"/>
</Grid>
</Border>
</StackPanel>
</Border>
<!-- ═══ Cosa resta manuale ═══ -->
<Border Style="{StaticResource CardBlock}">
<StackPanel>
<TextBlock Text="Cosa resta da fare a mano" Style="{StaticResource BlockTitle}"/>
<Border Style="{StaticResource InfoBox}">
<TextBlock TextWrapping="Wrap" FontSize="12" LineHeight="18"
Foreground="{DynamicResource Brush.Text}">
<Run Text="Le puntate consegnate per notifica non passano da qui." FontWeight="Bold"/>
<Run Text=" Bidoo le manda tramite le notifiche push del browser (OneSignal) e per email, con collegamenti personali validi una volta sola: sono legate all'abbonamento del browser, non all'account, quindi non esiste una chiamata da fare con il cookie di sessione. Per quelle apri le ricompense nel browser integrato, concedi le notifiche e lascia la scheda aperta."/>
</TextBlock>
</Border>
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
<Button Content="Buoni e voucher" Style="{StaticResource SmallRoundedButton}"
Background="{DynamicResource Brush.SurfaceAlt}"
Foreground="{DynamicResource Brush.Text}"
Margin="0,0,8,0" Click="OpenVouchersButton_Click"/>
<Button Content="Acquista puntate" Style="{StaticResource SmallRoundedButton}"
Background="{DynamicResource Brush.SurfaceAlt}"
Foreground="{DynamicResource Brush.Text}"
Click="OpenBuyBidsButton_Click"/>
</StackPanel>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</Grid>
</UserControl>
+352
View File
@@ -0,0 +1,352 @@
using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using AutoBidder.Utilities;
namespace AutoBidder.Controls
{
/// <summary>
/// Scheda Puntate: stato del riscatto automatico, contatori e registro.
///
/// <para>Come gli altri pannelli non conosce né la rete né gli archivi: mostra ciò che
/// gli viene passato e segnala le richieste dell'utente con eventi.</para>
/// </summary>
public partial class FreeBidsControl : UserControl
{
/// <summary>Una riga del registro dei riscatti.</summary>
public sealed record ActivityEntry(string Time, string Message, bool IsProblem);
/// <summary>
/// Il registro è volutamente limitato e solo in memoria: serve a capire cosa è
/// successo mentre si guardava altrove, non a essere uno storico — quello sono i
/// contatori, che invece durano.
/// </summary>
private const int MaxActivityEntries = 200;
private readonly ObservableCollection<ActivityEntry> _activity = new();
/// <summary>Evita che riempire i campi dalle impostazioni scateni un salvataggio.</summary>
private bool _loading;
public FreeBidsControl()
{
InitializeComponent();
ActivityList.ItemsSource = _activity;
}
// ── Aggiornamento dello stato ────────────────────────────────────
public void SetBalance(int? bids) =>
BalanceText.Text = bids.HasValue
? $"{bids.Value} puntate sul conto"
: "saldo non disponibile";
/// <summary>Allinea i campi alle impostazioni salvate senza scatenare eventi.</summary>
public void SetOptions(bool autoClaimEnabled, int checkMinutes)
{
_loading = true;
try
{
AutoClaimCheckBox.IsChecked = autoClaimEnabled;
CheckMinutesTextBox.Text = checkMinutes.ToString();
}
finally { _loading = false; }
}
public bool AutoClaimEnabled => AutoClaimCheckBox.IsChecked == true;
/// <summary>Codice o collegamento digitato per il riscatto manuale.</summary>
public string PromoCode => PromoCodeTextBox.Text?.Trim() ?? "";
/// <summary>Svuota il campo dopo un riscatto andato a buon fine.</summary>
public void ClearPromoCode() => PromoCodeTextBox.Text = "";
/// <summary>
/// Riga di stato della raccolta: da dove legge, quanti codici sono già stati presi e
/// com'è andato l'ultimo giro.
/// </summary>
public void SetHarvestStatus(string sourceUrl, int claimedCodes, string? lastOutcome)
{
var known = claimedCodes == 1
? "1 codice già preso"
: $"{claimedCodes} codici già presi";
HarvestStatusText.Text = lastOutcome == null
? $"Sorgente: {sourceUrl} · {known}."
: $"Sorgente: {sourceUrl} · {known} · ultimo giro: {lastOutcome}";
}
/// <summary>
/// Mostra dove sta il file dei riferimenti, ed eventualmente perché non è stato
/// possibile usarlo: sono i valori che decidono ogni chiamata di riscatto, e sapere
/// che se ne stanno usando altri è la differenza fra correggere il file e cercare un
/// guasto che non c'è.
/// </summary>
public void SetConfigPath(string path, string? problem)
{
ConfigPathText.Text = problem == null
? $"Indirizzi, intestazioni e parametri del riscatto: {path}"
: $"{problem} — {path}";
ConfigPathText.SetResourceReference(TextBlock.ForegroundProperty,
problem == null ? "Brush.TextMuted" : "Brush.Warning");
}
/// <summary>Minuti richiesti, già riportati entro il minimo consentito.</summary>
public int CheckMinutes =>
int.TryParse(CheckMinutesTextBox.Text?.Trim(), out var minutes) ? Math.Max(5, minutes) : 30;
/// <summary>Riga di stato: acceso o spento, e quando tocca al prossimo giro.</summary>
public void SetSchedule(bool running, DateTime? lastCheck, DateTime? nextCheck)
{
StatusPillText.Text = running ? "riscatto attivo" : "riscatto spento";
StatusPillText.SetResourceReference(TextBlock.ForegroundProperty,
running ? "Brush.Success" : "Brush.TextMuted");
if (!running)
{
ScheduleText.Text = "Riscatto automatico spento: puoi comunque usare «Controlla adesso».";
return;
}
var last = lastCheck.HasValue
? $"ultimo controllo alle {lastCheck.Value:HH:mm}"
: "nessun controllo ancora eseguito";
var next = nextCheck.HasValue
? $", il prossimo alle {nextCheck.Value:HH:mm}"
: "";
ScheduleText.Text = char.ToUpper(last[0]) + last[1..] + next + ".";
}
/// <summary>Ridisegna i contatori.</summary>
public void SetCounters(FreeBidsStats.Snapshot s)
{
SessionBidsText.Text = s.SessionBids.ToString();
TodayBidsText.Text = s.BidsToday.ToString();
TotalBidsText.Text = s.TotalBids.ToString();
TotalClaimedText.Text = s.TotalClaimed.ToString();
var lastClaim = s.LastClaimAt.HasValue
? $"ultimo riscatto il {s.LastClaimAt.Value:dd/MM/yyyy} alle {s.LastClaimAt.Value:HH:mm}"
: "nessun riscatto ancora andato a buon fine";
CountersDetailText.Text =
$"{s.SessionChecks} controlli in questa sessione · {s.TotalChecks} da sempre · " +
$"{s.SessionClaimed} ricompense prese in sessione · {lastClaim}.";
}
/// <summary>
/// Aggiunge una riga al registro, a video e su file.
///
/// <para>Il file serve perché questo pannello è l'unico posto in cui si vede cosa ha
/// fatto il riscatto automatico, e il riscatto lavora quando nessuno guarda: senza
/// registro su disco, tutto ciò che è successo mentre l'applicazione era chiusa —
/// o nelle duecento righe precedenti — sarebbe perso.</para>
/// </summary>
public void AppendActivity(string message, bool isProblem = false)
{
TextLogService.FreeBids(message, isProblem);
_activity.Insert(0, new ActivityEntry(DateTime.Now.ToString("HH:mm:ss"), message, isProblem));
while (_activity.Count > MaxActivityEntries)
_activity.RemoveAt(_activity.Count - 1);
ActivityEmptyText.Visibility = Visibility.Collapsed;
}
// ── Eventi dell'interfaccia ──────────────────────────────────────
private void RefreshButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(RefreshBalanceClickedEvent, this));
private void CheckNowButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(CheckNowClickedEvent, this));
private void OpenExternalButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(OpenExternalClickedEvent, this));
private void OpenInternalButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(OpenInternalClickedEvent, this));
private void OpenVouchersButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(OpenVouchersClickedEvent, this));
private void OpenBuyBidsButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(OpenBuyBidsClickedEvent, this));
private void ResetCountersButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(ResetCountersClickedEvent, this));
private void AutoClaimCheckBox_Changed(object sender, RoutedEventArgs e)
{
if (_loading) return;
RaiseEvent(new RoutedEventArgs(OptionsChangedEvent, this));
}
private void CheckMinutesTextBox_LostFocus(object sender, RoutedEventArgs e)
{
if (_loading) return;
// Si riscrive il valore corretto: chi ha digitato "2" deve vedere che il
// minimo applicato è 5, invece di credere di aver impostato due minuti.
var minutes = CheckMinutes;
if (CheckMinutesTextBox.Text != minutes.ToString())
CheckMinutesTextBox.Text = minutes.ToString();
RaiseEvent(new RoutedEventArgs(OptionsChangedEvent, this));
}
private void HarvestButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(HarvestClickedEvent, this));
private void OpenSourceButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(OpenPromoSourceClickedEvent, this));
private void ForgetPromosButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(ForgetPromosClickedEvent, this));
private void ClaimPromoButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(ClaimPromoClickedEvent, this));
/// <summary>Invio nel campo vale come clic: si incolla e si preme invio.</summary>
private void PromoCodeTextBox_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
if (e.Key != System.Windows.Input.Key.Enter) return;
e.Handled = true;
RaiseEvent(new RoutedEventArgs(ClaimPromoClickedEvent, this));
}
private void OpenConfigButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(OpenConfigClickedEvent, this));
private void ClearLogButton_Click(object sender, RoutedEventArgs e)
{
_activity.Clear();
ActivityEmptyText.Visibility = Visibility.Visible;
}
// ── Routed events ────────────────────────────────────────────────
public static readonly RoutedEvent RefreshBalanceClickedEvent = EventManager.RegisterRoutedEvent(
"RefreshBalanceClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(FreeBidsControl));
public static readonly RoutedEvent CheckNowClickedEvent = EventManager.RegisterRoutedEvent(
"CheckNowClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(FreeBidsControl));
public static readonly RoutedEvent OpenExternalClickedEvent = EventManager.RegisterRoutedEvent(
"OpenExternalClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(FreeBidsControl));
public static readonly RoutedEvent OpenInternalClickedEvent = EventManager.RegisterRoutedEvent(
"OpenInternalClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(FreeBidsControl));
public static readonly RoutedEvent OpenVouchersClickedEvent = EventManager.RegisterRoutedEvent(
"OpenVouchersClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(FreeBidsControl));
public static readonly RoutedEvent OpenBuyBidsClickedEvent = EventManager.RegisterRoutedEvent(
"OpenBuyBidsClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(FreeBidsControl));
public static readonly RoutedEvent ResetCountersClickedEvent = EventManager.RegisterRoutedEvent(
"ResetCountersClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(FreeBidsControl));
public static readonly RoutedEvent OptionsChangedEvent = EventManager.RegisterRoutedEvent(
"OptionsChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(FreeBidsControl));
public static readonly RoutedEvent ClaimPromoClickedEvent = EventManager.RegisterRoutedEvent(
"ClaimPromoClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(FreeBidsControl));
public static readonly RoutedEvent HarvestClickedEvent = EventManager.RegisterRoutedEvent(
"HarvestClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(FreeBidsControl));
public static readonly RoutedEvent OpenPromoSourceClickedEvent = EventManager.RegisterRoutedEvent(
"OpenPromoSourceClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(FreeBidsControl));
public static readonly RoutedEvent ForgetPromosClickedEvent = EventManager.RegisterRoutedEvent(
"ForgetPromosClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(FreeBidsControl));
public static readonly RoutedEvent OpenConfigClickedEvent = EventManager.RegisterRoutedEvent(
"OpenConfigClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(FreeBidsControl));
public event RoutedEventHandler RefreshBalanceClicked
{
add { AddHandler(RefreshBalanceClickedEvent, value); }
remove { RemoveHandler(RefreshBalanceClickedEvent, value); }
}
public event RoutedEventHandler CheckNowClicked
{
add { AddHandler(CheckNowClickedEvent, value); }
remove { RemoveHandler(CheckNowClickedEvent, value); }
}
public event RoutedEventHandler OpenExternalClicked
{
add { AddHandler(OpenExternalClickedEvent, value); }
remove { RemoveHandler(OpenExternalClickedEvent, value); }
}
public event RoutedEventHandler OpenInternalClicked
{
add { AddHandler(OpenInternalClickedEvent, value); }
remove { RemoveHandler(OpenInternalClickedEvent, value); }
}
public event RoutedEventHandler OpenVouchersClicked
{
add { AddHandler(OpenVouchersClickedEvent, value); }
remove { RemoveHandler(OpenVouchersClickedEvent, value); }
}
public event RoutedEventHandler OpenBuyBidsClicked
{
add { AddHandler(OpenBuyBidsClickedEvent, value); }
remove { RemoveHandler(OpenBuyBidsClickedEvent, value); }
}
public event RoutedEventHandler ResetCountersClicked
{
add { AddHandler(ResetCountersClickedEvent, value); }
remove { RemoveHandler(ResetCountersClickedEvent, value); }
}
public event RoutedEventHandler OptionsChanged
{
add { AddHandler(OptionsChangedEvent, value); }
remove { RemoveHandler(OptionsChangedEvent, value); }
}
public event RoutedEventHandler ClaimPromoClicked
{
add { AddHandler(ClaimPromoClickedEvent, value); }
remove { RemoveHandler(ClaimPromoClickedEvent, value); }
}
public event RoutedEventHandler HarvestClicked
{
add { AddHandler(HarvestClickedEvent, value); }
remove { RemoveHandler(HarvestClickedEvent, value); }
}
public event RoutedEventHandler OpenPromoSourceClicked
{
add { AddHandler(OpenPromoSourceClickedEvent, value); }
remove { RemoveHandler(OpenPromoSourceClickedEvent, value); }
}
public event RoutedEventHandler ForgetPromosClicked
{
add { AddHandler(ForgetPromosClickedEvent, value); }
remove { RemoveHandler(ForgetPromosClickedEvent, value); }
}
public event RoutedEventHandler OpenConfigClicked
{
add { AddHandler(OpenConfigClickedEvent, value); }
remove { RemoveHandler(OpenConfigClickedEvent, value); }
}
}
}
+42
View File
@@ -0,0 +1,42 @@
<UserControl x:Class="AutoBidder.Controls.HelpHeader"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
x:Name="Root"
HorizontalAlignment="Left">
<!-- Titolo di sezione con la spiegazione a portata di puntatore.
Le descrizioni stavano stampate sotto ogni titolo: si leggono una volta e poi
restano lì a occupare metà pagina per sempre. Qui il testo c'è ancora — non è
stato buttato — ma si mostra passandoci sopra, come per tutti gli altri comandi
dell'applicazione. Il pallino accanto al titolo serve a far sapere che c'è. -->
<StackPanel Orientation="Horizontal"
Background="Transparent"
Margin="{Binding Margin, ElementName=Root}"
ToolTipService.InitialShowDelay="250"
ToolTipService.ShowDuration="60000">
<StackPanel.ToolTip>
<ToolTip Visibility="{Binding HelpVisibility, ElementName=Root}">
<TextBlock Text="{Binding Help, ElementName=Root}"
TextWrapping="Wrap"
MaxWidth="420"/>
</ToolTip>
</StackPanel.ToolTip>
<TextBlock Text="{Binding Title, ElementName=Root}"
Style="{StaticResource BlockTitle}"
VerticalAlignment="Center"/>
<TextBlock Text="&#xE946;"
FontFamily="Segoe MDL2 Assets"
FontSize="12"
Margin="6,2,0,0"
VerticalAlignment="Center"
Foreground="{DynamicResource Brush.TextFaint}"
Visibility="{Binding HelpVisibility, ElementName=Root}"/>
</StackPanel>
</UserControl>
+58
View File
@@ -0,0 +1,58 @@
using System.Windows;
using System.Windows.Controls;
namespace AutoBidder.Controls
{
/// <summary>
/// Titolo di sezione con la spiegazione nel suggerimento invece che stampata sotto.
///
/// <para>Serve a togliere dalla pagina i paragrafi descrittivi senza perderne il
/// contenuto: si leggono una volta e poi restano a occupare spazio a ogni apertura
/// delle Impostazioni. Passando il puntatore il testo torna, esattamente come per i
/// pulsanti del resto dell'applicazione.</para>
///
/// <para>Uso: <c>&lt;ctl:HelpHeader Title="Motore di Precisione" Help="…"/&gt;</c>.
/// Senza <see cref="Help"/> si comporta come un titolo semplice, senza pallino.</para>
/// </summary>
public partial class HelpHeader : UserControl
{
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register(nameof(Title), typeof(string), typeof(HelpHeader),
new PropertyMetadata(""));
public static readonly DependencyProperty HelpProperty =
DependencyProperty.Register(nameof(Help), typeof(string), typeof(HelpHeader),
new PropertyMetadata("", OnHelpChanged));
private static readonly DependencyPropertyKey HelpVisibilityKey =
DependencyProperty.RegisterReadOnly(nameof(HelpVisibility), typeof(Visibility), typeof(HelpHeader),
new PropertyMetadata(Visibility.Collapsed));
public static readonly DependencyProperty HelpVisibilityProperty = HelpVisibilityKey.DependencyProperty;
public HelpHeader() => InitializeComponent();
/// <summary>Il titolo della sezione.</summary>
public string Title
{
get => (string)GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
/// <summary>La spiegazione, mostrata al passaggio del puntatore.</summary>
public string Help
{
get => (string)GetValue(HelpProperty);
set => SetValue(HelpProperty, value);
}
/// <summary>Il pallino compare solo quando c'è davvero qualcosa da leggere.</summary>
public Visibility HelpVisibility => (Visibility)GetValue(HelpVisibilityProperty);
private static void OnHelpChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var visible = !string.IsNullOrWhiteSpace(e.NewValue as string);
d.SetValue(HelpVisibilityKey, visible ? Visibility.Visible : Visibility.Collapsed);
}
}
}
+377
View File
@@ -0,0 +1,377 @@
<UserControl x:Class="AutoBidder.Controls.LearningControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ctl="clr-namespace:AutoBidder.Controls"
mc:Ignorable="d"
d:DesignHeight="800" d:DesignWidth="1200"
Background="{DynamicResource Brush.Bg}">
<UserControl.Resources>
<Style x:Key="Glyph" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Segoe MDL2 Assets"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
</Style>
<Style x:Key="Stat" TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource Brush.Text}"/>
<Setter Property="FontSize" Value="{StaticResource Font.Size.Lg}"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
<Style x:Key="StatLabel" TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource Brush.TextMuted}"/>
<Setter Property="FontSize" Value="{StaticResource Font.Size.Sm}"/>
<Setter Property="Margin" Value="0,2,0,0"/>
</Style>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- ═══ Barra strumenti ═══ -->
<Border Grid.Row="0" Style="{StaticResource TabToolbar}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
<Button x:Name="RefreshButton" Style="{StaticResource IconButton}"
ToolTip="Aggiorna i numeri" Click="RefreshButton_Click">
<TextBlock Style="{StaticResource Glyph}" Text="&#xE72C;"/>
</Button>
<TextBlock Text="Apprendimento" FontWeight="SemiBold" Margin="8,0,0,0"
FontSize="{StaticResource Font.Size.Lg}"
Foreground="{DynamicResource Brush.Text}" VerticalAlignment="Center"/>
<Border Width="1" Background="{DynamicResource Brush.Border}" Margin="10,4"/>
<Border Style="{StaticResource Pill}" ToolTip="Aste da cui il modello ha imparato">
<TextBlock x:Name="PillAuctions" Text="—" FontSize="{StaticResource Font.Size.Sm}"
Foreground="{DynamicResource Brush.Text}"/>
</Border>
<Border Style="{StaticResource Pill}" ToolTip="Esempi (puntate) che hanno aggiornato i pesi">
<TextBlock x:Name="PillUpdates" Text="—" FontSize="{StaticResource Font.Size.Sm}"
Foreground="{DynamicResource Brush.TextMuted}"/>
</Border>
<Border Style="{StaticResource Pill}" ToolTip="Fattore di calibrazione: osservato/previsto sugli ultimi esempi. 1 = scala giusta">
<TextBlock x:Name="PillCalibration" Text="—" FontSize="{StaticResource Font.Size.Sm}"
Foreground="{DynamicResource Brush.TextMuted}"/>
</Border>
<Border x:Name="PillReadyBorder" Style="{StaticResource Pill}"
ToolTip="Pronto = ha appreso abbastanza aste per fermare una puntata. Prima parla soltanto.">
<TextBlock x:Name="PillReady" Text="—" FontSize="{StaticResource Font.Size.Sm}"
Foreground="{DynamicResource Brush.Warning}"/>
</Border>
</StackPanel>
<!-- Solo icone, come nel monitor: il tooltip dice esattamente cosa succede. -->
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
<Button x:Name="EvaluateButton" Style="{StaticResource IconButton}"
Foreground="{DynamicResource Brush.Accent}"
ToolTip="Valuta ora.&#10;Addestra sulle aste più vecchie del database e giudica sulle più recenti, mai viste. Poi la prova prequenziale: ogni asta prima prevista e poi appresa, come fa il motore. Qualche minuto in sottofondo; il rapporto compare nella scheda «Valutazione»."
Click="EvaluateButton_Click">
<TextBlock x:Name="EvaluateGlyph" Style="{StaticResource Glyph}" Text="&#xE9F5;"/>
</Button>
<Button x:Name="ExportDecisionsButton" Style="{StaticResource IconButton}"
ToolTip="Esporta le decisioni in CSV.&#10;Tutte le decisioni del motore, dal vivo e in shadow, con esito e motivazione per esteso (reason_detail). Punto e virgola, pronto per un foglio di calcolo. Chiede dove salvare il file."
Click="ExportDecisionsButton_Click">
<TextBlock Style="{StaticResource Glyph}" Text="&#xE9F9;"/>
</Button>
<Border Width="1" Background="{DynamicResource Brush.Border}" Margin="6,4"/>
<ToggleButton x:Name="OptionsButton" Style="{StaticResource IconToggle}"
ToolTip="Altre operazioni sull'apprendimento.&#10;Studiare le aste non ancora apprese, ricostruire il profilo, rifare il bandit, azzerare lo sfidante o la calibrazione. Ognuna mostra l'avanzamento e si può annullare.">
<TextBlock Style="{StaticResource Glyph}" Text="&#xE9E9;"/>
</ToggleButton>
<Popup x:Name="OptionsPopup" PlacementTarget="{Binding ElementName=OptionsButton}" Placement="Bottom"
StaysOpen="False" AllowsTransparency="True" HorizontalOffset="-300" VerticalOffset="4"
IsOpen="{Binding IsChecked, ElementName=OptionsButton, Mode=TwoWay}">
<Border Background="{DynamicResource Brush.Surface}" BorderBrush="{DynamicResource Brush.Border}"
BorderThickness="1" CornerRadius="10" Padding="6" Width="340">
<StackPanel>
<TextBlock Style="{StaticResource Hint}" Margin="10,4,10,6" Text="Operazioni sull'apprendimento"/>
<Button Style="{StaticResource MenuEntry}" Content="Studia le aste chiuse non ancora apprese"
ToolTip="Lo studio automatico all'avvio si ferma dopo qualche minuto: questo va in fondo a tutte le aste chiuse del database che il modello non ha ancora visto. Non tocca ciò che è già appreso."
Click="LearnPending_Click"/>
<Button Style="{StaticResource MenuEntry}" Content="Ricostruisci il profilo per prodotto e fascia oraria"
ToolTip="Rifà da tutto lo storico le puntate tipiche del vincitore e il rapporto di chiusura per prodotto, fascia oraria e giorno. Modello e bandit non si toccano."
Click="RebuildProfile_Click"/>
<Button Style="{StaticResource MenuEntry}" Content="Rifai il bandit dalle decisioni registrate"
ToolTip="Butta il bandit Thompson e lo rifà da tutte le decisioni con esito registrate nel database: le stesse osservazioni che avrebbe raccolto asta per asta, senza doppioni."
Click="RebuildBandit_Click"/>
<Button Style="{StaticResource MenuEntry}" Content="Azzera lo sfidante (rete neurale)"
ToolTip="La rete riparte da zero e il confronto Brier ricomincia. Il campione logistico resta com'è: è lui che risponde al motore finché lo sfidante non lo batte."
Click="ResetChallenger_Click"/>
<Button Style="{StaticResource MenuEntry}" Content="Azzera la calibrazione del modello"
ToolTip="Il fattore osservato/previsto torna a 1 e si rimisura sugli esempi nuovi. Utile se il mercato è cambiato di colpo e la scala delle probabilità è rimasta indietro."
Click="ResetCalibration_Click"/>
<Border Height="1" Background="{DynamicResource Brush.Border}" Margin="6,6"/>
<Button Style="{StaticResource MenuEntry}" Content="Ricomincia da capo: azzera tutto e ristudia lo storico"
Foreground="{DynamicResource Brush.Danger}"
ToolTip="Butta modello, profilo, sfidante, bandit e contatore delle aste studiate, e ristudia tutte le aste chiuse del database. Non tocca lo storico né i prodotti. Chiede conferma."
Click="RetrainButton_Click"/>
</StackPanel>
</Border>
</Popup>
<Button x:Name="RetrainButton" Style="{StaticResource IconButton}"
Foreground="{DynamicResource Brush.Danger}"
ToolTip="Ricomincia da capo.&#10;Butta modello, profilo, sfidante, bandit e latenza appresa, e ristudia tutte le aste chiuse del database. Non tocca lo storico né i prodotti. Chiede conferma."
Click="RetrainButton_Click">
<TextBlock Style="{StaticResource Glyph}" Text="&#xE7A6;"/>
</Button>
</StackPanel>
</Grid>
</Border>
<!-- ═══ Corpo ═══ -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<Grid Margin="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="12"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Stato -->
<Border Grid.Row="0" Grid.Column="0" Style="{StaticResource CardBlock}">
<StackPanel>
<ctl:HelpHeader Title="Stato" Margin="0,0,0,10"
Help="Il modello impara da ogni asta che chiude: per ogni puntata, è rimasta senza risposta? È l'unica etichetta abbondante — milioni di esempi — mentre le vittorie proprie sono poche decine. Il comportamento avversario non dipende da chi ha puntato, e questo rende utilizzabili le puntate altrui.&#10;&#10;Decide col valore atteso: probabilità appresa × margine residuo costo della puntata. Sotto zero non punta, e lo scrive nel registro con i numeri. Decide solo dopo la soglia di aste apprese: prima parla soltanto."/>
<UniformGrid Columns="3" Rows="2">
<StackPanel Margin="0,0,8,10">
<TextBlock x:Name="StatAuctions" Style="{StaticResource Stat}" Text="—"/>
<TextBlock Style="{StaticResource StatLabel}" Text="aste apprese"/>
</StackPanel>
<StackPanel Margin="0,0,8,10">
<TextBlock x:Name="StatUpdates" Style="{StaticResource Stat}" Text="—"/>
<TextBlock Style="{StaticResource StatLabel}" Text="esempi visti"/>
</StackPanel>
<StackPanel Margin="0,0,8,10">
<TextBlock x:Name="StatProfile" Style="{StaticResource Stat}" Text="—"/>
<TextBlock Style="{StaticResource StatLabel}" Text="aste nel profilo"/>
</StackPanel>
<StackPanel Margin="0,0,8,0">
<TextBlock x:Name="StatCalibration" Style="{StaticResource Stat}" Text="—"/>
<TextBlock Style="{StaticResource StatLabel}" Text="calibrazione (1 = giusta)"/>
</StackPanel>
<StackPanel Margin="0,0,8,0">
<TextBlock x:Name="StatThreshold" Style="{StaticResource Stat}" Text="—"/>
<TextBlock Style="{StaticResource StatLabel}" Text="soglia per decidere"/>
</StackPanel>
<StackPanel Margin="0,0,8,0">
<TextBlock x:Name="StatBootstrap" Style="{StaticResource Stat}" Text="—"/>
<TextBlock Style="{StaticResource StatLabel}" Text="studio delle aste"/>
</StackPanel>
</UniformGrid>
<TextBlock x:Name="StatDecisions" Style="{StaticResource Hint}" Margin="0,10,0,0" Text=""/>
</StackPanel>
</Border>
<!-- Cosa ha imparato -->
<Border Grid.Row="0" Grid.Column="2" Style="{StaticResource CardBlock}">
<StackPanel>
<ctl:HelpHeader Title="Cosa ha imparato" Margin="0,0,0,10"
Help="I pesi del modello, dal più forte. Positivo = quella condizione rende più probabile che una puntata resti senza risposta; negativo = meno probabile. Le variabili sono a fasce e il modello è lineare proprio per poterli leggere così.&#10;&#10;Il prodotto è ridotto a 32 cassetti con una funzione di hash: due prodotti possono condividere un cassetto, ed è accettato."/>
<DataGrid x:Name="WeightsGrid" MaxHeight="260" IsReadOnly="True" HeadersVisibility="Column"
AutoGenerateColumns="False" CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Condizione" Binding="{Binding Nome}" Width="*"/>
<DataGridTextColumn Header="Peso" Binding="{Binding PesoDisplay}" Width="Auto" MinWidth="70"/>
<DataGridTextColumn Header="Effetto" Binding="{Binding Effetto}" Width="Auto" MinWidth="110"/>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</Border>
<!-- Ultime decisioni -->
<Border Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Style="{StaticResource CardBlock}">
<StackPanel>
<ctl:HelpHeader Title="Ultime decisioni" Margin="0,0,0,10"
Help="Ogni volta che il motore era pronto a puntare ha chiesto al modello. P è la probabilità che la puntata resti senza risposta; il valore atteso è P × margine residuo costo. «Fermata» = il cancello ha detto no. «Parere» = il modello non aveva ancora appreso abbastanza per decidere, e si è limitato a dirlo."/>
<DataGrid x:Name="DecisionsGrid" MaxHeight="280" IsReadOnly="True" HeadersVisibility="Column"
AutoGenerateColumns="False" CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Ora" Binding="{Binding Ora}" Width="Auto" MinWidth="80"/>
<DataGridTextColumn Header="Asta" Binding="{Binding Asta}" Width="*"/>
<DataGridTextColumn Header="Prezzo" Binding="{Binding Prezzo}" Width="Auto" MinWidth="70"/>
<DataGridTextColumn Header="P senza risposta" Binding="{Binding Prob}" Width="Auto" MinWidth="110"/>
<DataGridTextColumn Header="Valore atteso" Binding="{Binding Ev}" Width="Auto" MinWidth="100"/>
<DataGridTextColumn Header="Esito" Binding="{Binding Esito}" Width="Auto" MinWidth="90"/>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</Border>
<!-- Profilo per prodotto -->
<Border Grid.Row="2" Grid.Column="0" Style="{StaticResource CardBlock}">
<StackPanel>
<ctl:HelpHeader Title="Profilo per prodotto e fascia oraria" Margin="0,0,0,10"
Help="Cosa aspettarsi prima che un'asta cominci: puntate del vincitore e prezzo di chiusura in percentuale del valore, per prodotto, fascia oraria e tipo di giorno. Solo le combinazioni con aste osservate; quando i dati sono pochi il motore restringe verso il prodotto e poi verso la media generale.&#10;&#10;Fasce: 0-8 notte e ore sospese · 9 · 10-12 · 13-17 · 18-20 · 21-23."/>
<DataGrid x:Name="ProfileGrid" MaxHeight="320" IsReadOnly="True" HeadersVisibility="Column"
AutoGenerateColumns="False" CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Prodotto" Binding="{Binding Prodotto}" Width="*"/>
<DataGridTextColumn Header="Fascia" Binding="{Binding Fascia}" Width="Auto" MinWidth="60"/>
<DataGridTextColumn Header="Giorno" Binding="{Binding Giorno}" Width="Auto" MinWidth="60"/>
<DataGridTextColumn Header="Aste" Binding="{Binding Aste}" Width="Auto" MinWidth="50"/>
<DataGridTextColumn Header="Punt. vincitore" Binding="{Binding Puntate}" Width="Auto" MinWidth="100"/>
<DataGridTextColumn Header="Chiusura" Binding="{Binding Chiusura}" Width="Auto" MinWidth="80"/>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</Border>
<!-- Valutazione -->
<Border Grid.Row="2" Grid.Column="2" Style="{StaticResource CardBlock}">
<StackPanel>
<ctl:HelpHeader Title="Ultima valutazione" Margin="0,0,0,10"
Help="Il rapporto dell'ultima valutazione sulle aste registrate in questo database. Le misure che contano: il sollevamento (quanto più spesso le puntate a probabilità alta erano davvero finali, rispetto al caso), la calibrazione per fasce (la probabilità prevista deve somigliare a quella osservata), e cosa il cancello avrebbe fatto sulle tue puntate vere."/>
<TextBox x:Name="EvaluationBox" IsReadOnly="True" TextWrapping="NoWrap"
MinHeight="200" MaxHeight="320"
FontFamily="Consolas" FontSize="11.5"
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
Background="{DynamicResource Brush.SurfaceAlt}"
Foreground="{DynamicResource Brush.Text}"
BorderBrush="{DynamicResource Brush.Border}"
Text="Nessuna valutazione ancora. Premi «Valuta ora»."/>
</StackPanel>
</Border>
<!-- Autonomia sul momento -->
<Border Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3" Style="{StaticResource CardBlock}">
<StackPanel>
<ctl:HelpHeader Title="Autonomia sul momento" Margin="0,0,0,10"
Help="Due cose che il sistema decide da solo sulla sessione in corso, dentro i paletti delle impostazioni.&#10;&#10;ANTICIPO: margine di sicurezza più la coda alta (p99) della latenza misurata adesso. Una puntata arrivata tardi alza il margine di 150 ms subito; venti puntate in tempo lo abbassano di 25. Il risultato resta fra il minimo e il massimo impostati; un anticipo scritto a mano su una singola asta vince sempre.&#10;&#10;REGIME: per ogni asta seguita. Calmo = punta se il valore atteso è positivo. Sfogo = gli altri si stanno battendo, si aspetta che si sfoghino: si rientra solo dopo «pazienza» cicli buoni di fila. Sondaggio = una puntata di prova; se viene coperta entro 9 s si torna a Sfogo e la pazienza raddoppia (fino a 24)."/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="12"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="Anticipo adattivo" FontWeight="SemiBold" Margin="0,0,0,8"
Foreground="{DynamicResource Brush.Text}"/>
<UniformGrid Columns="3" Rows="2">
<StackPanel Margin="0,0,8,10">
<TextBlock x:Name="StatLeadNow" Style="{StaticResource Stat}" Text="—"/>
<TextBlock Style="{StaticResource StatLabel}" Text="anticipo adesso"/>
</StackPanel>
<StackPanel Margin="0,0,8,10">
<TextBlock x:Name="StatLatP99" Style="{StaticResource Stat}" Text="—"/>
<TextBlock Style="{StaticResource StatLabel}" Text="coda del ping (p99)"/>
</StackPanel>
<StackPanel Margin="0,0,8,10">
<TextBlock x:Name="StatLatMargin" Style="{StaticResource Stat}" Text="—"/>
<TextBlock Style="{StaticResource StatLabel}" Text="margine"/>
</StackPanel>
<StackPanel Margin="0,0,8,0">
<TextBlock x:Name="StatLatP50" Style="{StaticResource Stat}" Text="—"/>
<TextBlock Style="{StaticResource StatLabel}" Text="ping mediano"/>
</StackPanel>
<StackPanel Margin="0,0,8,0">
<TextBlock x:Name="StatLatSamples" Style="{StaticResource Stat}" Text="—"/>
<TextBlock Style="{StaticResource StatLabel}" Text="campioni"/>
</StackPanel>
<StackPanel Margin="0,0,8,0">
<TextBlock x:Name="StatLatLate" Style="{StaticResource Stat}" Text="—"/>
<TextBlock Style="{StaticResource StatLabel}" Text="puntate tardive / totali"/>
</StackPanel>
</UniformGrid>
<TextBlock x:Name="StatLeadHint" Style="{StaticResource Hint}" Margin="0,8,0,0" Text=""/>
</StackPanel>
<StackPanel Grid.Column="2">
<TextBlock Text="Regime per asta" FontWeight="SemiBold" Margin="0,0,0,8"
Foreground="{DynamicResource Brush.Text}"/>
<DataGrid x:Name="RegimeGrid" MaxHeight="240" IsReadOnly="True" HeadersVisibility="Column"
AutoGenerateColumns="False" CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Asta" Binding="{Binding Asta}" Width="*"/>
<DataGridTextColumn Header="Stato" Binding="{Binding StatoAsta}" Width="Auto" MinWidth="60"/>
<DataGridTextColumn Header="Regime" Binding="{Binding Regime}" Width="Auto" MinWidth="80"/>
<DataGridTextColumn Header="Pazienza" Binding="{Binding Pazienza}" Width="Auto" MinWidth="70"/>
<DataGridTextColumn Header="Sondaggi coperti" Binding="{Binding Sondaggi}" Width="Auto" MinWidth="110"/>
<DataGridTextColumn Header="P senza risposta" Binding="{Binding Prob}" Width="Auto" MinWidth="110"/>
<DataGridTextColumn Header="Valore atteso" Binding="{Binding Ev}" Width="Auto" MinWidth="100"/>
<DataGridTextColumn Header="Duello" Binding="{Binding Duello}" Width="Auto" MinWidth="60"/>
</DataGrid.Columns>
</DataGrid>
<TextBlock x:Name="RegimeHint" Style="{StaticResource Hint}" Margin="0,8,0,0" Text=""/>
</StackPanel>
</Grid>
</StackPanel>
</Border>
<!-- Shadow: cosa avrebbe reso -->
<Border Grid.Row="4" Grid.Column="0" Style="{StaticResource CardBlock}">
<StackPanel>
<ctl:HelpHeader Title="Shadow: cosa avrebbero reso le decisioni" Margin="0,0,0,10"
Help="Ogni decisione registrata (in Osserva come in Attiva) riceve un esito alla chiusura dell'asta: se il prezzo non è più salito dopo quell'istante, una puntata lì sarebbe stata l'ultima. Con questo si giudicano sugli stessi istanti la policy in uso (modello + regime), il bandit Thompson che gira in shadow, «punta sempre» e «non punta mai».&#10;&#10;ROI per puntata = netto / puntate. L'intervallo di confidenza dice se il null-model (ROI = 0) è battuto davvero o per caso. Chi risponde al motore fra modello logistico e rete neurale lo decide il punteggio di Brier prequenziale qui sotto."/>
<UniformGrid Columns="3" Rows="1" Margin="0,0,0,8">
<StackPanel Margin="0,0,8,0">
<TextBlock x:Name="StatChampion" Style="{StaticResource Stat}" Text="—"/>
<TextBlock Style="{StaticResource StatLabel}" Text="chi risponde al motore"/>
</StackPanel>
<StackPanel Margin="0,0,8,0">
<TextBlock x:Name="StatBrier" Style="{StaticResource Stat}" Text="—"/>
<TextBlock Style="{StaticResource StatLabel}" Text="Brier logistico / rete"/>
</StackPanel>
<StackPanel Margin="0,0,8,0">
<TextBlock x:Name="StatBandit" Style="{StaticResource Stat}" Text="—"/>
<TextBlock Style="{StaticResource StatLabel}" Text="bandit: contesti / osservazioni"/>
</StackPanel>
</UniformGrid>
<TextBox x:Name="ShadowBox" IsReadOnly="True" TextWrapping="NoWrap"
MinHeight="180" MaxHeight="300"
FontFamily="Consolas" FontSize="11.5"
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
Background="{DynamicResource Brush.SurfaceAlt}"
Foreground="{DynamicResource Brush.Text}"
BorderBrush="{DynamicResource Brush.Border}"
Text="Nessuna decisione con esito ancora."/>
</StackPanel>
</Border>
<!-- Simulatore -->
<Border Grid.Row="4" Grid.Column="2" Style="{StaticResource CardBlock}">
<StackPanel>
<ctl:HelpHeader Title="Simulatore: le policy a confronto" Margin="0,0,0,10"
Help="Aste sintetiche con avversari tarati sul database (puntatori per asta, profondità dei cicli, quota di autopuntate, chiusura mediana): aggressivi, cecchini, autopuntate a 2 s, principianti, ognuno con il costo affondato degli umani. Le policy giocano le stesse aste (stessi semi): non puntare mai, puntare sempre, la regola del valore atteso, il bandit che impara giocando, e l'agente Q addestrato qui dentro con penalità conservativa.&#10;&#10;Il simulatore non è Bidoo: i numeri assoluti non contano, il confronto sì. Ed è l'unico posto dove si vede come reagirebbero gli altri a una nostra puntata."/>
<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
<Button x:Name="SimulateButton" Style="{StaticResource SmallRoundedButton}"
Background="{DynamicResource Brush.Accent}"
Foreground="{DynamicResource Brush.TextOnAccent}"
Content="Simula 500 aste per policy" Margin="0,0,8,0"
Click="SimulateButton_Click"/>
<TextBlock x:Name="SimulateHint" Style="{StaticResource Hint}" VerticalAlignment="Center" Text=""/>
</StackPanel>
<TextBox x:Name="SimulationBox" IsReadOnly="True" TextWrapping="NoWrap"
MinHeight="180" MaxHeight="300"
FontFamily="Consolas" FontSize="11.5"
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
Background="{DynamicResource Brush.SurfaceAlt}"
Foreground="{DynamicResource Brush.Text}"
BorderBrush="{DynamicResource Brush.Border}"
Text="Premi «Simula» per mettere le policy a confronto."/>
</StackPanel>
</Border>
</Grid>
</ScrollViewer>
</Grid>
</UserControl>
+445
View File
@@ -0,0 +1,445 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
using AutoBidder.Ml;
using AutoBidder.Utilities;
namespace AutoBidder.Controls
{
/// <summary>
/// La scheda dell'apprendimento: cosa sa il modello, cosa ha deciso di recente, cosa
/// aspettarsi per prodotto e ora, e l'ultima valutazione.
///
/// <para>Legge direttamente da <see cref="LearningService"/>: non ha bisogno della
/// finestra principale per niente, e si aggiorna da sola ogni pochi secondi mentre è
/// visibile. Tutto il resto dell'applicazione non sa che esiste.</para>
/// </summary>
public partial class LearningControl : UserControl
{
private readonly DispatcherTimer _refresh;
private CancellationTokenSource? _evaluation;
private sealed class PesoRow
{
public string Nome { get; init; } = "";
public double Peso { get; init; }
public string PesoDisplay => Peso.ToString("+0.000;-0.000");
public string Effetto => Math.Abs(Peso) < 0.05 ? "quasi nullo"
: Peso > 0 ? "più senza risposta" : "più risposte";
}
private sealed class ProfiloRow
{
public string Prodotto { get; init; } = "";
public string Fascia { get; init; } = "";
public string Giorno { get; init; } = "";
public long Aste { get; init; }
public string Puntate { get; init; } = "";
public string Chiusura { get; init; } = "";
}
private sealed class RegimeRow
{
public string Asta { get; init; } = "";
public string StatoAsta { get; init; } = "";
public string Regime { get; init; } = "";
public string Pazienza { get; init; } = "";
public string Sondaggi { get; init; } = "";
public string Prob { get; init; } = "";
public string Ev { get; init; } = "";
public string Duello { get; init; } = "";
}
/// <summary>
/// Da dove prendere le aste seguite adesso, per la tabella dei regimi. Lo imposta
/// la finestra principale: la scheda non conosce il monitor, e non deve.
/// </summary>
public Func<IReadOnlyList<Models.AuctionInfo>>? AuctionsProvider { get; set; }
private sealed class DecisioneRow
{
public string Ora { get; init; } = "";
public string Asta { get; init; } = "";
public string Prezzo { get; init; } = "";
public string Prob { get; init; } = "";
public string Ev { get; init; } = "";
public string Esito { get; init; } = "";
}
public LearningControl()
{
InitializeComponent();
_refresh = new DispatcherTimer { Interval = TimeSpan.FromSeconds(5) };
_refresh.Tick += (_, _) => { if (IsVisible) Refresh(); };
IsVisibleChanged += (_, e) =>
{
if ((bool)e.NewValue) { Refresh(); _refresh.Start(); }
else _refresh.Stop();
};
}
/// <summary>Rilegge tutto dal servizio. Costa poco: sono numeri già in memoria.</summary>
public void Refresh()
{
try
{
var settings = SettingsManager.Load();
var apprese = LearningService.AuctionsLearned;
var soglia = Math.Max(1, settings.LearningMinAuctions);
var pronto = LearningService.IsReady(settings);
var calib = LearningService.CalibrationFactor;
PillAuctions.Text = $"{apprese:N0} aste";
PillUpdates.Text = $"{LearningService.ModelUpdates:N0} esempi";
PillCalibration.Text = $"calibrazione {calib:N2}";
// L'interruttore in barra: spento, il modello studia ma non decide.
PillReady.Text = !settings.LearningEnabled ? "SPENTO (interruttore in barra)"
: pronto ? "pronto" : $"in ascolto ({apprese}/{soglia})";
PillReady.Foreground = (System.Windows.Media.Brush)FindResource(
!settings.LearningEnabled ? "Brush.Danger" : pronto ? "Brush.Success" : "Brush.Warning");
StatAuctions.Text = apprese.ToString("N0");
StatUpdates.Text = LearningService.ModelUpdates.ToString("N0");
StatProfile.Text = LearningService.ProfileAuctions.ToString("N0");
StatCalibration.Text = calib.ToString("N2");
StatThreshold.Text = soglia.ToString("N0");
StatBootstrap.Text = LearningService.BootstrapRunning ? "in corso" : "fermo";
RefreshAutonomy(settings);
RefreshShadow(settings);
var decisioni = LearningService.RecentDecisions();
var fermate = decisioni.Count(d => d.Blocked);
StatDecisions.Text = decisioni.Count == 0
? "Nessuna decisione ancora: il modello risponde quando il motore è pronto a puntare."
: $"Ultime {decisioni.Count} decisioni: {fermate} fermate, {decisioni.Count - fermate} lasciate passare" +
(settings.LearningGateEnabled ? "." : ". Il cancello è spento nelle impostazioni: il modello parla ma non decide.");
WeightsGrid.ItemsSource = LearningService.Pesi()
.Take(30)
.Select(p => new PesoRow { Nome = p.Nome, Peso = p.Peso })
.ToList();
DecisionsGrid.ItemsSource = decisioni
.Select(d => new DecisioneRow
{
Ora = d.At.ToString("HH:mm:ss"),
Asta = d.Auction,
Prezzo = d.Price.ToString("F2") + " €",
Prob = d.Probability.ToString("P2"),
Ev = d.ExpectedValue.ToString("+0.000;-0.000") + " €",
Esito = !d.Ready ? "parere" : d.Blocked ? "fermata" : "passa"
})
.ToList();
ProfileGrid.ItemsSource = LearningService.ProfileRows(150)
.Select(r => new ProfiloRow
{
Prodotto = r.Product,
Fascia = FasciaLabel(r.HourBand),
Giorno = r.Weekend ? "festivo" : "feriale",
Aste = r.Auctions,
Puntate = double.IsNaN(r.WinnerBids) ? "—" : r.WinnerBids.ToString("N0"),
Chiusura = double.IsNaN(r.CloseRatio) ? "—" : r.CloseRatio.ToString("P1")
})
.ToList();
if (_evaluation == null)
{
var ultima = LearningService.LastEvaluation();
if (!string.IsNullOrWhiteSpace(ultima)) EvaluationBox.Text = ultima;
}
}
catch (Exception ex)
{
StatDecisions.Text = $"Aggiornamento non riuscito: {ex.Message}";
}
}
private static string FasciaLabel(int band) => band switch
{
0 => "0-8", 1 => "9", 2 => "10-12", 3 => "13-17", 4 => "18-20", _ => "21-23"
};
/// <summary>La sezione «Autonomia sul momento»: modello di latenza e regime per asta.</summary>
private void RefreshAutonomy(AppSettings settings)
{
var lat = LatencyModel.Snapshot();
var lead = LatencyModel.RecommendedLeadMs(settings);
StatLeadNow.Text = $"{lead} ms";
StatLatP99.Text = lat.Samples > 0 ? $"{lat.P99} ms" : "—";
StatLatP50.Text = lat.Samples > 0 ? $"{lat.P50} ms" : "—";
StatLatMargin.Text = $"{lat.MarginMs} ms";
StatLatSamples.Text = lat.Samples.ToString("N0");
StatLatLate.Text = $"{lat.LateBids} / {lat.Bids}";
StatLeadHint.Text = !settings.AdaptiveLeadEnabled
? $"Anticipo adattivo spento nelle impostazioni: vale l'anticipo fisso di {settings.DefaultBidBeforeDeadlineMs} ms."
: lat.Samples < 10
? $"Ancora pochi campioni: finché non ce ne sono dieci vale l'anticipo fisso di {settings.DefaultBidBeforeDeadlineMs} ms."
: $"Margine {lat.MarginMs} + coda {lat.P99} = {lat.MarginMs + lat.P99} ms, tenuto fra {settings.LeadMinMs} e {settings.LeadMaxMs}.";
var aste = AuctionsProvider?.Invoke() ?? Array.Empty<Models.AuctionInfo>();
var righe = aste
.Where(a => a.State != Models.RunState.Stopped)
.Select(a => new RegimeRow
{
Asta = a.Name,
StatoAsta = a.State == Models.RunState.Active ? "Attiva" : "Osserva",
Regime = a.Regime.StatoAttuale switch
{
CompetitionRegime.Stato.Sfogo => $"Sfogo ({a.Regime.CicliBuoni}/{a.Regime.Pazienza})",
CompetitionRegime.Stato.Sondaggio => "Sondaggio",
_ => "Calmo"
},
Pazienza = a.Regime.Pazienza.ToString(),
Sondaggi = a.Regime.SondaggiFalliti.ToString(),
Prob = a.LearnedUnansweredProbability is { } p ? p.ToString("P1") : "—",
Ev = a.LearnedExpectedValue is { } ev ? ev.ToString("+0.000;-0.000") + " €" : "—",
Duello = a.AutoBidDuelDetected ? "sì" : a.AutoResponsesInARow > 0 ? $"{a.AutoResponsesInARow}/5" : ""
})
.ToList();
RegimeGrid.ItemsSource = righe;
var sfogo = righe.Count(r => r.Regime.StartsWith("Sfogo"));
RegimeHint.Text = righe.Count == 0
? "Nessuna asta seguita in questo momento."
: $"{righe.Count} aste seguite: {sfogo} in Sfogo, {righe.Count(r => r.Regime == "Sondaggio")} in Sondaggio, {righe.Count - sfogo - righe.Count(r => r.Regime == "Sondaggio")} Calme.";
}
private DateTime _shadowRefreshedAt = DateTime.MinValue;
/// <summary>Campione, Brier, bandit e il rapporto shadow (ogni mezzo minuto: interroga il database).</summary>
private void RefreshShadow(AppSettings settings)
{
var (bl, bn, samples) = LearningService.BrierScores;
StatChampion.Text = LearningService.ChallengerLeads ? "rete neurale" : "logistico";
StatBrier.Text = samples == 0 ? "—" : $"{bl:F5} / {bn:F5}";
StatBandit.Text = $"{LearningService.Bandit.Arms:N0} / {LearningService.Bandit.Observations:N0}";
if ((DateTime.Now - _shadowRefreshedAt).TotalSeconds < 30) return;
_shadowRefreshedAt = DateTime.Now;
try
{
var report = ShadowReport.Compute(Data.AuctionDatabase.Instance, settings, 30);
ShadowBox.Text = report.WithOutcome == 0
? $"Decisioni registrate negli ultimi 30 giorni: {report.TotalDecisions:N0}, nessuna con esito ancora (arriva alla chiusura dell'asta)."
: report.Text;
}
catch (Exception ex)
{
ShadowBox.Text = $"Rapporto non disponibile: {ex.Message}";
}
}
private void ExportDecisionsButton_Click(object sender, RoutedEventArgs e)
{
string? path;
try
{
var dialog = new Microsoft.Win32.SaveFileDialog
{
Title = "Esporta le decisioni in CSV",
FileName = $"decisioni-{DateTime.Now:yyyyMMdd-HHmm}.csv",
Filter = "CSV (foglio di calcolo)|*.csv",
OverwritePrompt = true,
AddExtension = true
};
path = dialog.ShowDialog() == true ? dialog.FileName : null;
}
catch { path = null; }
if (path == null) return;
var result = DecisionExporter.ExportCsv(path);
if (result.Success)
{
try { Process.Start(new ProcessStartInfo { FileName = Path.GetDirectoryName(result.Path)!, UseShellExecute = true }); } catch { }
MessageBox.Show($"{result.Message}\n{result.Path}", "Decisioni", MessageBoxButton.OK, MessageBoxImage.Information);
}
else
{
MessageBox.Show(result.Message, "Decisioni", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
private CancellationTokenSource? _simulation;
private async void SimulateButton_Click(object sender, RoutedEventArgs e)
{
if (_simulation != null) { _simulation.Cancel(); return; }
_simulation = new CancellationTokenSource();
var ct = _simulation.Token;
SimulateButton.Content = "Annulla";
SimulationBox.Text = "Simulazione in corso…";
var settings = SettingsManager.Load();
try
{
var text = await Task.Run(() => SimulationLab.Run(Data.AuctionDatabase.Instance, settings, 500,
msg => Dispatcher.BeginInvoke(() => SimulateHint.Text = msg), ct), ct);
SimulationBox.Text = text;
SimulateHint.Text = $"fatto alle {DateTime.Now:HH:mm}";
}
catch (OperationCanceledException)
{
SimulationBox.Text = "Simulazione annullata.";
SimulateHint.Text = "";
}
catch (Exception ex)
{
SimulationBox.Text = $"Simulazione non riuscita: {ex.Message}";
SimulateHint.Text = "";
}
finally
{
_simulation = null;
SimulateButton.Content = "Simula 500 aste per policy";
}
}
private void RefreshButton_Click(object sender, RoutedEventArgs e) => Refresh();
private async void EvaluateButton_Click(object sender, RoutedEventArgs e)
{
if (_evaluation != null)
{
_evaluation.Cancel();
return;
}
_evaluation = new CancellationTokenSource();
// Il pulsante è solo icona: mentre gira diventa una X, e il tooltip lo dice.
EvaluateGlyph.Text = "\uE711";
EvaluateButton.ToolTip = "Annulla la valutazione in corso.";
EvaluationBox.Text = "Valutazione in corso: lettura delle aste…";
try
{
// Al massimo tremila aste, le più recenti: bastano a giudicare, e la
// lettura di tutto l'archivio in sottofondo mentre le aste corrono non serve.
var report = await LearningService.EvaluateAsync(3000,
msg => Dispatcher.BeginInvoke(() => EvaluationBox.Text = "Valutazione in corso: " + msg),
_evaluation.Token);
EvaluationBox.Text = report.Text;
}
catch (OperationCanceledException)
{
EvaluationBox.Text = "Valutazione annullata.";
}
catch (Exception ex)
{
EvaluationBox.Text = $"Valutazione non riuscita: {ex.Message}";
}
finally
{
_evaluation = null;
EvaluateGlyph.Text = "\uE9F5";
EvaluateButton.ToolTip = "Valuta ora.\nAddestra sulle aste più vecchie del database e giudica sulle più recenti, mai viste. Poi la prova prequenziale: ogni asta prima prevista e poi appresa, come fa il motore. Qualche minuto in sottofondo; il rapporto compare nella scheda «Valutazione».";
}
}
// ── Operazioni sull'apprendimento: tutte con la finestra di avanzamento ──
private Window? Host => Window.GetWindow(this);
private void CloseOptions() => OptionsButton.IsChecked = false;
/// <summary>Dice com'è finita un'operazione: errore, annullata, o il testo di riepilogo.</summary>
private void Conclude<T>(ProgressOutcome<T> esito, string titolo, Func<T, string> riepilogo)
{
Refresh();
if (esito.Error != null)
MessageBox.Show(Host!, esito.Error.Message, titolo, MessageBoxButton.OK, MessageBoxImage.Warning);
else if (esito.Cancelled)
MessageBox.Show(Host!, "Operazione annullata. Quello che era già fatto resta fatto.", titolo, MessageBoxButton.OK, MessageBoxImage.Information);
else
MessageBox.Show(Host!, riepilogo(esito.Result!), titolo, MessageBoxButton.OK, MessageBoxImage.Information);
}
private async void LearnPending_Click(object sender, RoutedEventArgs e)
{
CloseOptions();
var esito = await Dialogs.ProgressDialog.RunAsync(Host, "Studio delle aste non ancora apprese",
"Cerco le aste chiuse che il modello non ha ancora visto…",
(p, ct) => Task.Run(() => LearningService.LearnPendingAsync(p, ct), ct), modal: false);
Conclude(esito, "Apprendimento", r => $"Studiate {r.Done:N0} aste, {r.Usable:N0} utilizzabili per il modello.");
}
private async void RebuildProfile_Click(object sender, RoutedEventArgs e)
{
CloseOptions();
var esito = await Dialogs.ProgressDialog.RunAsync(Host, "Profilo per prodotto e fascia oraria",
"Rileggo lo storico…",
(p, ct) => Task.Run(() => LearningService.RebuildProfileFromHistory(p, ct), ct));
Conclude(esito, "Profilo", n => $"Profilo ricostruito da {n:N0} aste dello storico.");
}
private async void RebuildBandit_Click(object sender, RoutedEventArgs e)
{
CloseOptions();
var esito = await Dialogs.ProgressDialog.RunAsync(Host, "Bandit dalle decisioni registrate",
"Leggo le decisioni con esito…",
(p, ct) => LearningService.RebuildBanditFromDecisionsAsync(p, ct));
Conclude(esito, "Bandit", r => $"Bandit rifatto da {r.Auctions:N0} aste: {r.Observations:N0} osservazioni.");
}
private void ResetChallenger_Click(object sender, RoutedEventArgs e)
{
CloseOptions();
var answer = MessageBox.Show(Host!, "Azzero lo sfidante (la rete) e il confronto Brier? Il campione logistico resta.",
"Sfidante", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (answer != MessageBoxResult.Yes) return;
LearningService.ResetChallenger();
Refresh();
}
private void ResetCalibration_Click(object sender, RoutedEventArgs e)
{
CloseOptions();
var answer = MessageBox.Show(Host!, "Azzero la calibrazione? Il fattore torna a 1 e si rimisura sugli esempi nuovi.",
"Calibrazione", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (answer != MessageBoxResult.Yes) return;
LearningService.ResetCalibration();
Refresh();
}
private async void RetrainButton_Click(object sender, RoutedEventArgs e)
{
CloseOptions();
var answer = MessageBox.Show(Host!,
"Butto modello, profilo, sfidante, bandit e contatore delle aste studiate, e ricomincio a studiare tutte le aste chiuse del database.\n\n" +
"Lo storico e i prodotti non vengono toccati. Puoi continuare a usare l'applicazione; la finestra mostra a che punto è.\n\nProcedo?",
"Ricomincia da capo", MessageBoxButton.YesNo, MessageBoxImage.Warning);
if (answer != MessageBoxResult.Yes) return;
RetrainButton.IsEnabled = false;
try
{
var esito = await Dialogs.ProgressDialog.RunAsync(Host, "Ricomincio da capo",
"Azzero modello, profilo, sfidante e bandit…",
(p, ct) => Task.Run(() => LearningService.RetrainFromScratchAsync(p, ct), ct), modal: false);
Conclude(esito, "Ricomincia da capo", r => $"Ristudiate {r.Done:N0} aste, {r.Usable:N0} utilizzabili per il modello.");
}
finally
{
RetrainButton.IsEnabled = true;
}
}
}
}
+468
View File
@@ -0,0 +1,468 @@
<UserControl x:Class="AutoBidder.Controls.ProductsControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:vm="clr-namespace:AutoBidder.ViewModels"
mc:Ignorable="d"
d:DesignHeight="800" d:DesignWidth="1200"
Background="{DynamicResource Brush.Bg}">
<UserControl.Resources>
<Style x:Key="Glyph" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Segoe MDL2 Assets"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
</Style>
<!-- ═══ Modificabile o no: deve vedersi a colpo d'occhio ═══
Le celle che si possono scrivere hanno il riquadro di una casella di testo;
le statistiche restano testo semplice e smorzato. Senza questa differenza la
griglia sembra tutta uguale e non si capisce dove si può intervenire. -->
<Style x:Key="EditableCell" TargetType="DataGridCell">
<Setter Property="Foreground" Value="{DynamicResource Brush.Text}"/>
<Setter Property="VerticalAlignment" Value="Stretch"/>
<Setter Property="Cursor" Value="IBeam"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="DataGridCell">
<Border x:Name="box"
Background="{DynamicResource Brush.Bg}"
BorderBrush="{DynamicResource Brush.Border}"
BorderThickness="1"
CornerRadius="5"
Margin="3,3"
Padding="6,0">
<ContentPresenter VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="box" Property="BorderBrush"
Value="{DynamicResource Brush.BorderStrong}"/>
</Trigger>
<Trigger Property="IsEditing" Value="True">
<Setter TargetName="box" Property="BorderBrush"
Value="{DynamicResource Brush.Accent}"/>
<Setter TargetName="box" Property="Background"
Value="{DynamicResource Brush.Surface}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Il pennino segnala nell'intestazione le colonne che accettano scrittura -->
<DataTemplate x:Key="EditableHeader">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding}" VerticalAlignment="Center"/>
<TextBlock FontFamily="Segoe MDL2 Assets" Text="&#xE70F;" FontSize="9"
Margin="5,0,0,0" VerticalAlignment="Center"
Foreground="{DynamicResource Brush.Accent}"/>
</StackPanel>
</DataTemplate>
<!-- Numeri modificabili: allineati a destra, colore pieno -->
<Style x:Key="EditableText" TargetType="TextBlock">
<Setter Property="HorizontalAlignment" Value="Right"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Foreground" Value="{DynamicResource Brush.Text}"/>
</Style>
<!-- Statistiche: sola lettura, quindi smorzate -->
<Style x:Key="StatText" TargetType="TextBlock">
<Setter Property="HorizontalAlignment" Value="Right"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Foreground" Value="{DynamicResource Brush.TextMuted}"/>
</Style>
<!-- Statistiche estese: il numero da solo non basta, il dettaglio (quartili,
campione verificato, frequenza) sta nel suggerimento. -->
<Style x:Key="StatsText" TargetType="TextBlock" BasedOn="{StaticResource StatText}">
<Setter Property="ToolTip" Value="{Binding StatsExplanation}"/>
</Style>
<!-- Valore, costo reale e risparmio: il ragionamento sta nel suggerimento.
In rosso quando vincere costa piu' che comprare: e' il caso che va visto
subito, non nascosto in mezzo agli altri numeri. -->
<Style x:Key="ValueText" TargetType="TextBlock" BasedOn="{StaticResource StatText}">
<Setter Property="ToolTip" Value="{Binding ValueExplanation}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding HasSavings}" Value="True">
<Setter Property="Foreground" Value="{DynamicResource Brush.Success}"/>
</DataTrigger>
<DataTrigger Binding="{Binding SavingsIsNegative}" Value="True">
<Setter Property="Foreground" Value="{DynamicResource Brush.Danger}"/>
</DataTrigger>
</Style.Triggers>
</Style>
<!-- Limiti consigliati: il numero da solo non basta, il ragionamento sta nel
suggerimento. In arancione quando il tetto non copre il prezzo tipico:
è il caso in cui l'articolo, con quelle puntate, non conviene. -->
<Style x:Key="AdviceText" TargetType="TextBlock" BasedOn="{StaticResource StatText}">
<Setter Property="ToolTip" Value="{Binding AdviceExplanation}"/>
<Setter Property="Foreground" Value="{DynamicResource Brush.Info}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding HasAdvice}" Value="False">
<Setter Property="Foreground" Value="{DynamicResource Brush.TextFaint}"/>
</DataTrigger>
<DataTrigger Binding="{Binding AdviceWarns}" Value="True">
<Setter Property="Foreground" Value="{DynamicResource Brush.Warning}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- ═══ Barra strumenti ═══ -->
<Border Grid.Row="0" Style="{StaticResource TabToolbar}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
<Button x:Name="RefreshButton" Style="{StaticResource IconButton}"
ToolTip="Ricarica elenco e statistiche" Click="RefreshButton_Click">
<TextBlock Style="{StaticResource Glyph}" Text="&#xE72C;"/>
</Button>
<TextBlock Text="Prodotti" FontWeight="SemiBold" Margin="8,0,0,0"
FontSize="{StaticResource Font.Size.Lg}"
Foreground="{DynamicResource Brush.Text}" VerticalAlignment="Center"/>
<Border Width="1" Background="{DynamicResource Brush.Border}" Margin="10,4"/>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<Border Style="{StaticResource Pill}" ToolTip="Prodotti in elenco">
<TextBlock x:Name="ProductsCountText" Text="0 prodotti"
FontSize="{StaticResource Font.Size.Sm}"
Foreground="{DynamicResource Brush.TextMuted}"/>
</Border>
<Border Style="{StaticResource Pill}" ToolTip="Prodotti con la stellina accesa">
<TextBlock x:Name="ProductsWatchedText" Text="0 seguiti"
FontSize="{StaticResource Font.Size.Sm}"
Foreground="{DynamicResource Brush.Gold}"/>
</Border>
</StackPanel>
<!-- Ricerca -->
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center"
Margin="14,0,10,0">
<Border Background="{DynamicResource Brush.Bg}"
BorderBrush="{DynamicResource Brush.Border}" BorderThickness="1"
CornerRadius="6" Padding="8,0" Margin="0,0,12,0"
VerticalAlignment="Center">
<Grid Width="230">
<StackPanel Orientation="Horizontal">
<TextBlock Style="{StaticResource Glyph}" Text="&#xE721;" FontSize="12"
Foreground="{DynamicResource Brush.TextFaint}" Margin="0,0,6,0"/>
<TextBox x:Name="SearchBox" Width="196" BorderThickness="0"
Background="Transparent" VerticalAlignment="Center"
Foreground="{DynamicResource Brush.Text}"
TextChanged="SearchBox_TextChanged"
ToolTip="Filtra i prodotti per nome mentre scrivi"/>
</StackPanel>
<!-- Testo guida: sparisce appena si scrive qualcosa -->
<TextBlock x:Name="SearchPlaceholder" Text="Cerca un prodotto…"
IsHitTestVisible="False" Margin="20,0,0,0"
VerticalAlignment="Center"
FontSize="{StaticResource Font.Size.Sm}"
Foreground="{DynamicResource Brush.TextFaint}"/>
</Grid>
</Border>
</StackPanel>
<!-- Solo icone, come nel monitor: il tooltip dice esattamente cosa succede. -->
<StackPanel Grid.Column="3" Orientation="Horizontal" VerticalAlignment="Center">
<Button x:Name="ScanNowButton" Style="{StaticResource IconButton}"
Foreground="{DynamicResource Brush.Accent}"
ToolTip="Cerca aste ora.&#10;Interroga subito il catalogo di Bidoo per i prodotti con la stellina accesa e mette nel monitor le aste nuove che trova, con i limiti del prodotto. Senza aspettare il prossimo giro automatico."
Click="ScanNowButton_Click">
<TextBlock Style="{StaticResource Glyph}" Text="&#xE721;"/>
</Button>
<Button x:Name="ReapplyButton" Style="{StaticResource IconButton}"
Foreground="{DynamicResource Brush.Accent}"
ToolTip="Riapplica i limiti.&#10;Riscrive prezzo minimo e massimo, puntate massime, avvio e valore reale del prodotto selezionato sulle aste di quel prodotto già presenti nel monitor. Serve dopo aver cambiato i limiti in questa scheda."
Click="ReapplyButton_Click">
<TextBlock Style="{StaticResource Glyph}" Text="&#xE895;"/>
</Button>
<Border Width="1" Background="{DynamicResource Brush.Border}" Margin="6,4"/>
<Button x:Name="RecalculateButton" Style="{StaticResource IconButton}"
ToolTip="Ricalcola dallo storico.&#10;Rilegge tutte le aste concluse dello storico e rifà da capo i numeri di ogni prodotto in elenco: aste viste, prezzo minimo, massimo e medio, puntate del vincitore, vittorie, osservazioni. I limiti scritti a mano e la stellina non cambiano; cambiano i numeri, e quindi i limiti consigliati. Mostra l'avanzamento e si può annullare."
Click="RecalculateButton_Click">
<TextBlock Style="{StaticResource Glyph}" Text="&#xE81C;"/>
</Button>
<Button x:Name="ApplySuggestedButton" Style="{StaticResource IconButton}"
ToolTip="Usa i limiti consigliati sul prodotto selezionato.&#10;Scrive prezzo minimo e massimo consigliati dallo storico: il massimo copre l'85% delle chiusure passate (regolabile in Impostazioni), abbassato se il budget non lo regge — valore del prodotto meno il costo delle puntate massime. Il minimo è il decimo percentile: sotto quel prezzo l'asta non si chiude quasi mai."
Click="ApplySuggestedButton_Click">
<TextBlock Style="{StaticResource Glyph}" Text="&#xE8EF;"/>
</Button>
<Button x:Name="ApplyAllSuggestedButton" Style="{StaticResource IconButton}"
ToolTip="Usa i limiti consigliati su tutti i prodotti.&#10;Scrive i limiti consigliati su ogni prodotto che ha abbastanza aste concluse per ricavarli. Chiede conferma e dice prima quante righe cambierebbe."
Click="ApplyAllSuggestedButton_Click">
<TextBlock Style="{StaticResource Glyph}" Text="&#xE762;"/>
</Button>
<Border Width="1" Background="{DynamicResource Brush.Border}" Margin="6,4"/>
<Button x:Name="RemoveButton" Style="{StaticResource IconButton}"
Foreground="{DynamicResource Brush.Danger}"
ToolTip="Togli il prodotto selezionato.&#10;Lo elimina dall'elenco con i suoi limiti. Le aste già nel monitor restano dove sono; le statistiche del prodotto restano."
Click="RemoveButton_Click">
<TextBlock Style="{StaticResource Glyph}" Text="&#xE738;"/>
</Button>
<Button x:Name="UnwatchAllButton" Style="{StaticResource IconButton}"
ToolTip="Spegni tutte le stelline.&#10;Nessun prodotto viene più cercato in automatico. I prodotti restano in elenco con i loro limiti: si riaccendono una a una."
Click="UnwatchAllButton_Click">
<TextBlock Style="{StaticResource Glyph}" Text="&#xE734;"/>
</Button>
<Button x:Name="RemoveUnwatchedButton" Style="{StaticResource IconButton}"
Foreground="{DynamicResource Brush.Danger}"
ToolTip="Togli i prodotti senza stellina.&#10;Elimina dall'elenco tutti i prodotti non seguiti, con i loro limiti. Restano solo quelli con la stellina accesa. Le statistiche per prodotto restano."
Click="RemoveUnwatchedButton_Click">
<TextBlock Style="{StaticResource Glyph}" Text="&#xE8D9;"/>
</Button>
<Button x:Name="ClearButton" Style="{StaticResource IconButton}"
Foreground="{DynamicResource Brush.Danger}"
ToolTip="Svuota l'elenco.&#10;Toglie tutti i prodotti e i loro limiti. Le statistiche per prodotto (aste concluse, prezzi tipici, consigli) restano: se il prodotto ricompare, i consigli ci sono ancora."
Click="ClearButton_Click">
<TextBlock Style="{StaticResource Glyph}" Text="&#xE74D;"/>
</Button>
<Button x:Name="ClearAllButton" Style="{StaticResource IconButton}"
Foreground="{DynamicResource Brush.Danger}"
ToolTip="Pulizia completa.&#10;Toglie tutti i prodotti, i loro limiti E le statistiche per prodotto. Lo storico delle aste non viene toccato: le statistiche si possono ricostruire da lì con «Pulisci dati incompleti» nello Storico."
Click="ClearAllButton_Click">
<TextBlock Style="{StaticResource Glyph}" Text="&#xE894;"/>
</Button>
</StackPanel>
</Grid>
</Border>
<!-- ═══ Elenco prodotti: impostazioni e statistiche sulla stessa riga ═══ -->
<Grid Grid.Row="1" Margin="12">
<Border Background="{DynamicResource Brush.Surface}"
BorderBrush="{DynamicResource Brush.Border}" BorderThickness="1" CornerRadius="12">
<DataGrid x:Name="ProductsGrid" AutoGenerateColumns="False"
Background="Transparent" Margin="4"
CanUserAddRows="False" CanUserDeleteRows="False"
SelectionMode="Single" HeadersVisibility="Column"
RowHeight="32"
HorizontalScrollBarVisibility="Auto"
CellEditEnding="ProductsGrid_CellEditEnding"
SelectionChanged="ProductsGrid_SelectionChanged">
<DataGrid.Columns>
<!-- Stellina: acceso = le aste nuove entrano da sole -->
<DataGridTemplateColumn Header="" Width="Auto" MinWidth="38" IsReadOnly="True"
CanUserSort="False">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Style="{StaticResource MiniIconButton}"
ToolTip="{Binding WatchTooltip}"
Command="{Binding DataContext.ProductWatchCommand,
RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}">
<TextBlock FontFamily="Segoe MDL2 Assets" Text="{Binding WatchGlyph}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource Brush.TextFaint}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsWatched}" Value="True">
<Setter Property="Foreground" Value="{DynamicResource Brush.Gold}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Button>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<!-- Auto, non *: con venti colonne accanto lo spazio residuo non basta mai
e i nomi lunghi restano tagliati. Cosi' la colonna prende la
larghezza del nome piu' lungo e la griglia scorre in orizzontale. -->
<DataGridTextColumn Header="Prodotto" Binding="{Binding DisplayName}"
Width="Auto" MinWidth="220" IsReadOnly="True"/>
<!-- ── Limiti propri del prodotto: modificabili ── -->
<DataGridTextColumn Header="Min €" Binding="{Binding MinPriceText}"
Width="Auto" MinWidth="74"
CellStyle="{StaticResource EditableCell}"
HeaderTemplate="{StaticResource EditableHeader}"
ElementStyle="{StaticResource EditableText}"/>
<DataGridTextColumn Header="Max €" Binding="{Binding MaxPriceText}"
Width="Auto" MinWidth="74"
CellStyle="{StaticResource EditableCell}"
HeaderTemplate="{StaticResource EditableHeader}"
ElementStyle="{StaticResource EditableText}"/>
<DataGridTextColumn Header="Puntate" Binding="{Binding MaxClicksText}"
Width="Auto" MinWidth="78"
CellStyle="{StaticResource EditableCell}"
HeaderTemplate="{StaticResource EditableHeader}"
ElementStyle="{StaticResource EditableText}"/>
<DataGridTextColumn Header="Valore reale €" Binding="{Binding MarketValueText}"
Width="Auto" MinWidth="96"
CellStyle="{StaticResource EditableCell}"
HeaderTemplate="{StaticResource EditableHeader}"
ElementStyle="{StaticResource EditableText}"/>
<DataGridTextColumn Header="Entro min" Binding="{Binding MaxStartMinutesText}"
Width="Auto" MinWidth="86"
CellStyle="{StaticResource EditableCell}"
HeaderTemplate="{StaticResource EditableHeader}"
ElementStyle="{StaticResource EditableText}"/>
<!-- Le colonne non stanno nell'albero visuale: un Binding qui non
erediterebbe alcun DataContext, quindi il valore è statico. -->
<DataGridComboBoxColumn Header="Stato" Width="Auto" MinWidth="112"
CellStyle="{StaticResource EditableCell}"
HeaderTemplate="{StaticResource EditableHeader}"
SelectedItemBinding="{Binding StateChoice}"
ItemsSource="{x:Static vm:ProductViewModel.StateChoices}"/>
<!-- ── Statistiche dello storico: sola lettura ── -->
<DataGridTextColumn Header="Aste" Binding="{Binding StatCountDisplay}"
Width="Auto" MinWidth="52" IsReadOnly="True"
ElementStyle="{StaticResource StatText}"/>
<DataGridTextColumn Header="Vinte" Binding="{Binding StatWonCount}"
Width="Auto" MinWidth="56" IsReadOnly="True"
ElementStyle="{StaticResource StatText}"/>
<DataGridTextColumn Header="%" Binding="{Binding StatWinRateDisplay}"
Width="Auto" MinWidth="52" IsReadOnly="True"
ElementStyle="{StaticResource StatText}"/>
<DataGridTextColumn Header="Media" Binding="{Binding StatAverageDisplay}"
Width="Auto" MinWidth="78" IsReadOnly="True"
ElementStyle="{StaticResource StatText}"/>
<DataGridTextColumn Header="Min" Binding="{Binding StatMinDisplay}"
Width="Auto" MinWidth="72" IsReadOnly="True"
ElementStyle="{StaticResource StatText}"/>
<DataGridTextColumn Header="Max" Binding="{Binding StatMaxDisplay}"
Width="Auto" MinWidth="72" IsReadOnly="True"
ElementStyle="{StaticResource StatText}"/>
<!-- ── Quanto vale, quanto costa vincerlo davvero ── -->
<DataGridTextColumn Header="Valore" Binding="{Binding StatValueDisplay}"
Width="Auto" MinWidth="80" IsReadOnly="True"
ElementStyle="{StaticResource ValueText}"/>
<DataGridTextColumn Header="Punt. vinc." Binding="{Binding StatWinnerBidsDisplay}"
Width="Auto" MinWidth="88" IsReadOnly="True"
ElementStyle="{StaticResource ValueText}"/>
<DataGridTextColumn Header="Costo reale" Binding="{Binding StatWinCostDisplay}"
Width="Auto" MinWidth="90" IsReadOnly="True"
ElementStyle="{StaticResource ValueText}"/>
<DataGridTextColumn Header="Risparmio" Binding="{Binding StatSavingsDisplay}"
Width="Auto" MinWidth="84" IsReadOnly="True"
ElementStyle="{StaticResource ValueText}"/>
<!-- ── La forbice, non solo il valore tipico ──
Su un articolo dove si vince con 12 puntate o con 900, la sola
mediana porta a impostare limiti che non reggono. -->
<DataGridTextColumn Header="Punt. min" Binding="{Binding StatMinWinnerBidsDisplay}"
Width="Auto" MinWidth="78" IsReadOnly="True"
ElementStyle="{StaticResource StatsText}"/>
<DataGridTextColumn Header="Punt. 75°" Binding="{Binding StatP75WinnerBidsDisplay}"
Width="Auto" MinWidth="78" IsReadOnly="True"
ElementStyle="{StaticResource StatsText}"/>
<DataGridTextColumn Header="Punt. max" Binding="{Binding StatMaxWinnerBidsDisplay}"
Width="Auto" MinWidth="80" IsReadOnly="True"
ElementStyle="{StaticResource StatsText}"/>
<DataGridTextColumn Header="Risp. min" Binding="{Binding StatMinSavingsDisplay}"
Width="Auto" MinWidth="80" IsReadOnly="True"
ElementStyle="{StaticResource StatsText}"/>
<DataGridTextColumn Header="Risp. max" Binding="{Binding StatMaxSavingsDisplay}"
Width="Auto" MinWidth="80" IsReadOnly="True"
ElementStyle="{StaticResource StatsText}"/>
<DataGridTextColumn Header="In utile" Binding="{Binding StatProfitableShareDisplay}"
Width="Auto" MinWidth="72" IsReadOnly="True"
ElementStyle="{StaticResource StatsText}"/>
<DataGridTextColumn Header="Ogni" Binding="{Binding StatFrequencyDisplay}"
Width="Auto" MinWidth="70" IsReadOnly="True"
ElementStyle="{StaticResource StatsText}"/>
<DataGridTextColumn Header="Verificate" Binding="{Binding StatWinnerBidsSampleDisplay}"
Width="Auto" MinWidth="82" IsReadOnly="True"
ElementStyle="{StaticResource StatsText}"/>
<!-- ── Limiti consigliati: sola lettura, più il bottone che li applica ── -->
<DataGridTextColumn Header="Min cons." Binding="{Binding SuggestedMinDisplay}"
Width="Auto" MinWidth="84" IsReadOnly="True"
ElementStyle="{StaticResource AdviceText}"/>
<DataGridTextColumn Header="Max cons." Binding="{Binding SuggestedMaxDisplay}"
Width="Auto" MinWidth="84" IsReadOnly="True"
ElementStyle="{StaticResource AdviceText}"/>
<DataGridTextColumn Header="Punt. cons." Binding="{Binding SuggestedMaxBidsDisplay}"
Width="Auto" MinWidth="88" IsReadOnly="True"
ElementStyle="{StaticResource AdviceText}"/>
<DataGridTemplateColumn Header="" Width="Auto" MinWidth="40" IsReadOnly="True" CanUserSort="False">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<!-- Applica i consigliati a questa riga soltanto. Il segno di
spunta si accende quando i limiti scritti sono già quelli
consigliati: serve a vedere a colpo d'occhio quali righe
resta da sistemare. -->
<Button Style="{StaticResource MiniIconButton}"
ToolTip="{Binding AdviceExplanation}"
Command="{Binding DataContext.ProductApplyAdviceCommand,
RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}">
<TextBlock FontFamily="Segoe MDL2 Assets" Text="&#xE73E;">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource Brush.TextFaint}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding HasAdvice}" Value="False">
<Setter Property="Opacity" Value="0.25"/>
</DataTrigger>
<DataTrigger Binding="{Binding MatchesAdvice}" Value="True">
<Setter Property="Foreground" Value="{DynamicResource Brush.Success}"/>
</DataTrigger>
<DataTrigger Binding="{Binding AdviceWarns}" Value="True">
<Setter Property="Foreground" Value="{DynamicResource Brush.Warning}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Button>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Punt./asta" Binding="{Binding StatAverageBidsDisplay}"
Width="Auto" MinWidth="82" IsReadOnly="True"
ElementStyle="{StaticResource StatText}"/>
<DataGridTextColumn Header="Aggiunte" Binding="{Binding AutoAddedCountDisplay}"
Width="Auto" MinWidth="74" IsReadOnly="True"
ElementStyle="{StaticResource StatText}"/>
</DataGrid.Columns>
</DataGrid>
</Border>
<!-- Stato vuoto -->
<StackPanel x:Name="ProductsEmptyState" VerticalAlignment="Center" HorizontalAlignment="Center"
Visibility="Collapsed">
<TextBlock Style="{StaticResource Glyph}" Text="&#xE734;" FontSize="34"
Foreground="{DynamicResource Brush.TextFaint}"/>
<TextBlock Text="Nessun prodotto in elenco."
Margin="0,10,0,0" HorizontalAlignment="Center"
Foreground="{DynamicResource Brush.TextMuted}"/>
<TextBlock Text="Aggiungili dalla scheda Cerca: la stellina lo segue, l'ingranaggio lo mette qui per dargli dei limiti."
Margin="0,4,0,0" HorizontalAlignment="Center" TextWrapping="Wrap" MaxWidth="420"
TextAlignment="Center"
FontSize="{StaticResource Font.Size.Sm}"
Foreground="{DynamicResource Brush.TextFaint}"/>
</StackPanel>
</Grid>
</Grid>
</UserControl>
+275
View File
@@ -0,0 +1,275 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using AutoBidder.ViewModels;
namespace AutoBidder.Controls
{
/// <summary>
/// Scheda Prodotti: una riga per articolo, con la stellina, i limiti su misura e le
/// statistiche dello storico tutte sulla stessa riga.
///
/// <para>Il controllo non conosce né gli archivi né il monitor: espone eventi e lascia
/// decidere a MainWindow, com'è per gli altri pannelli.</para>
/// </summary>
public partial class ProductsControl : UserControl
{
/// <summary>
/// Elenco completo, indipendente da quello mostrato: la ricerca filtra la vista, non
/// i dati. Senza, scrivere nella casella cancellerebbe le righe invece di nasconderle.
/// </summary>
private IList? _allProducts;
public ProductsControl()
{
InitializeComponent();
}
/// <summary>Prodotto selezionato, o <c>null</c> se la griglia è vuota o senza selezione.</summary>
public ProductViewModel? SelectedProduct => ProductsGrid.SelectedItem as ProductViewModel;
/// <summary>
/// Riempie l'elenco. Le righe sono già ViewModel: la griglia scrive direttamente
/// nelle schede prodotto, e la persistenza avviene alla conferma della cella.
/// </summary>
public void SetProducts(IList products)
{
_allProducts = products;
ApplySearch();
}
/// <summary>
/// Mostra solo i prodotti il cui nome contiene il testo cercato.
///
/// <para>Il filtro si applica a una copia: la griglia è modificabile, e gli oggetti
/// mostrati sono gli stessi dell'elenco completo — quindi i limiti scritti mentre
/// un filtro è attivo restano quando il filtro sparisce.</para>
/// </summary>
private void ApplySearch()
{
if (_allProducts == null) return;
var query = SearchBox.Text?.Trim() ?? "";
SearchPlaceholder.Visibility = query.Length == 0 ? Visibility.Visible : Visibility.Collapsed;
var all = _allProducts.OfType<ProductViewModel>().ToList();
var shown = query.Length == 0
? all
: all.Where(p => (p.DisplayName ?? "")
.Contains(query, System.StringComparison.OrdinalIgnoreCase))
.ToList();
ProductsGrid.ItemsSource = shown;
ProductsCountText.Text = query.Length == 0
? (all.Count == 1 ? "1 prodotto" : $"{all.Count} prodotti")
: $"{shown.Count} di {all.Count}";
ProductsWatchedText.Text = $"{all.Count(p => p.IsWatched)} seguiti";
// Con la ricerca attiva e nessun risultato non si mostra il messaggio di elenco
// vuoto: direbbe "non segui ancora nessun prodotto", che sarebbe falso.
ProductsEmptyState.Visibility = all.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
}
private void SearchBox_TextChanged(object sender, TextChangedEventArgs e) => ApplySearch();
/// <summary>Riallinea i contatori dopo che una stellina è cambiata.</summary>
public void RefreshCounters()
{
// Si conta sull'elenco completo, non su quello mostrato: con una ricerca attiva
// il numero dei seguiti non deve cambiare solo perché sono nascosti.
var source = _allProducts ?? ProductsGrid.ItemsSource as IList;
if (source == null) return;
ProductsWatchedText.Text = $"{source.OfType<ProductViewModel>().Count(p => p.IsWatched)} seguiti";
}
/// <summary>Porta la selezione sul prodotto indicato (usato arrivando dalla scheda Cerca).</summary>
public void SelectProduct(string identity)
{
if (ProductsGrid.ItemsSource is not IEnumerable items) return;
var match = items.OfType<ProductViewModel>()
.FirstOrDefault(p => p.Identity == identity);
if (match == null) return;
ProductsGrid.SelectedItem = match;
ProductsGrid.ScrollIntoView(match);
}
// ── Eventi dell'interfaccia ──────────────────────────────────────
private void ProductsGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(ProductSelectionChangedEvent, this));
/// <summary>
/// La cella è stata confermata: il valore è già nel ViewModel, resta da renderlo
/// durevole. Si rimanda al termine dell'operazione perché durante l'evento la
/// modifica non è ancora stata trasferita al modello.
/// </summary>
private void ProductsGrid_CellEditEnding(object? sender, DataGridCellEditEndingEventArgs e)
{
if (e.EditAction != DataGridEditAction.Commit) return;
Dispatcher.BeginInvoke(new System.Action(() =>
RaiseEvent(new RoutedEventArgs(ProductLimitsEditedEvent, this))),
System.Windows.Threading.DispatcherPriority.Background);
}
private void RefreshButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(RefreshClickedEvent, this));
private void ScanNowButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(ScanNowClickedEvent, this));
private void ReapplyButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(ReapplyClickedEvent, this));
private void RemoveButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(RemoveClickedEvent, this));
private void ClearButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(ClearClickedEvent, this));
private void ClearAllButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(ClearAllClickedEvent, this));
private void RecalculateButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(RecalculateClickedEvent, this));
private void UnwatchAllButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(UnwatchAllClickedEvent, this));
private void RemoveUnwatchedButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(RemoveUnwatchedClickedEvent, this));
private void ApplySuggestedButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(ApplySuggestedClickedEvent, this));
private void ApplyAllSuggestedButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(ApplyAllSuggestedClickedEvent, this));
// ── Routed events ────────────────────────────────────────────────
public static readonly RoutedEvent RefreshClickedEvent = EventManager.RegisterRoutedEvent(
"RefreshClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
public static readonly RoutedEvent ScanNowClickedEvent = EventManager.RegisterRoutedEvent(
"ScanNowClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
public static readonly RoutedEvent ReapplyClickedEvent = EventManager.RegisterRoutedEvent(
"ReapplyClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
public static readonly RoutedEvent RemoveClickedEvent = EventManager.RegisterRoutedEvent(
"RemoveClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
public static readonly RoutedEvent ClearClickedEvent = EventManager.RegisterRoutedEvent(
"ClearClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
public static readonly RoutedEvent ClearAllClickedEvent = EventManager.RegisterRoutedEvent(
"ClearAllClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
public static readonly RoutedEvent RecalculateClickedEvent = EventManager.RegisterRoutedEvent(
"RecalculateClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
public static readonly RoutedEvent UnwatchAllClickedEvent = EventManager.RegisterRoutedEvent(
"UnwatchAllClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
public static readonly RoutedEvent RemoveUnwatchedClickedEvent = EventManager.RegisterRoutedEvent(
"RemoveUnwatchedClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
public static readonly RoutedEvent ApplySuggestedClickedEvent = EventManager.RegisterRoutedEvent(
"ApplySuggestedClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
public static readonly RoutedEvent ApplyAllSuggestedClickedEvent = EventManager.RegisterRoutedEvent(
"ApplyAllSuggestedClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
public static readonly RoutedEvent ProductLimitsEditedEvent = EventManager.RegisterRoutedEvent(
"ProductLimitsEdited", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
public static readonly RoutedEvent ProductSelectionChangedEvent = EventManager.RegisterRoutedEvent(
"ProductSelectionChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
public event RoutedEventHandler RefreshClicked
{
add { AddHandler(RefreshClickedEvent, value); }
remove { RemoveHandler(RefreshClickedEvent, value); }
}
public event RoutedEventHandler ScanNowClicked
{
add { AddHandler(ScanNowClickedEvent, value); }
remove { RemoveHandler(ScanNowClickedEvent, value); }
}
public event RoutedEventHandler ReapplyClicked
{
add { AddHandler(ReapplyClickedEvent, value); }
remove { RemoveHandler(ReapplyClickedEvent, value); }
}
public event RoutedEventHandler RemoveClicked
{
add { AddHandler(RemoveClickedEvent, value); }
remove { RemoveHandler(RemoveClickedEvent, value); }
}
public event RoutedEventHandler ClearClicked
{
add { AddHandler(ClearClickedEvent, value); }
remove { RemoveHandler(ClearClickedEvent, value); }
}
public event RoutedEventHandler ClearAllClicked
{
add { AddHandler(ClearAllClickedEvent, value); }
remove { RemoveHandler(ClearAllClickedEvent, value); }
}
public event RoutedEventHandler RecalculateClicked
{
add { AddHandler(RecalculateClickedEvent, value); }
remove { RemoveHandler(RecalculateClickedEvent, value); }
}
public event RoutedEventHandler UnwatchAllClicked
{
add { AddHandler(UnwatchAllClickedEvent, value); }
remove { RemoveHandler(UnwatchAllClickedEvent, value); }
}
public event RoutedEventHandler RemoveUnwatchedClicked
{
add { AddHandler(RemoveUnwatchedClickedEvent, value); }
remove { RemoveHandler(RemoveUnwatchedClickedEvent, value); }
}
public event RoutedEventHandler ApplySuggestedClicked
{
add { AddHandler(ApplySuggestedClickedEvent, value); }
remove { RemoveHandler(ApplySuggestedClickedEvent, value); }
}
public event RoutedEventHandler ApplyAllSuggestedClicked
{
add { AddHandler(ApplyAllSuggestedClickedEvent, value); }
remove { RemoveHandler(ApplyAllSuggestedClickedEvent, value); }
}
public event RoutedEventHandler ProductLimitsEdited
{
add { AddHandler(ProductLimitsEditedEvent, value); }
remove { RemoveHandler(ProductLimitsEditedEvent, value); }
}
public event RoutedEventHandler ProductSelectionChanged
{
add { AddHandler(ProductSelectionChangedEvent, value); }
remove { RemoveHandler(ProductSelectionChangedEvent, value); }
}
}
}
File diff suppressed because it is too large Load Diff
+215 -46
View File
@@ -1,3 +1,4 @@
using System.Linq;
using System.Windows;
using System.Windows.Controls;
@@ -8,41 +9,240 @@ namespace AutoBidder.Controls
/// </summary>
public partial class SettingsControl : UserControl
{
/// <summary>Evita che i Checked scattino mentre si sincronizza la UI.</summary>
private bool _suppressThemeEvents;
public SettingsControl()
{
InitializeComponent();
Loaded += (_, _) => SyncThemeSelection();
}
// Non servono proprietà wrapper - MainWindow.xaml.cs accede direttamente ai controlli tramite:
/// <summary>
/// Allinea i radio button al tema attualmente applicato.
/// </summary>
public void SyncThemeSelection()
{
_suppressThemeEvents = true;
try
{
bool dark = Utilities.ThemeManager.IsDark;
ThemeDarkRadio.IsChecked = dark;
ThemeLightRadio.IsChecked = !dark;
}
finally
{
_suppressThemeEvents = false;
}
}
private void ThemeDarkRadio_Checked(object sender, RoutedEventArgs e)
{
if (_suppressThemeEvents) return;
Utilities.ThemeManager.SetAndSave(true);
}
private void ThemeLightRadio_Checked(object sender, RoutedEventArgs e)
{
if (_suppressThemeEvents) return;
Utilities.ThemeManager.SetAndSave(false);
}
// Non servono propriet wrapper - MainWindow.xaml.cs accede direttamente ai controlli tramite:
// Settings.DefaultBidBeforeDeadlineMsTextBox (definito nel XAML con x:Name)
// Settings.MaxLogLinesPerAuctionTextBox (definito nel XAML con x:Name)
// etc.
// Proprietà per limiti log
// Propriet per limiti log
public TextBox MaxLogLinesPerAuction => MaxLogLinesPerAuctionTextBox;
public TextBox MaxGlobalLogLines => MaxGlobalLogLinesTextBox;
// ?? NUOVO: Proprietà per limite storia puntate
// ?? NUOVO: Propriet per limite storia puntate
public TextBox MaxBidHistoryEntries => MaxBidHistoryEntriesTextBox;
// ========================================
// NOTA: Eventi cookie RIMOSSI
// Gestione automatica tramite browser
// ========================================
// ===== ANTICIPO, CARTELLE, ESPORTAZIONE =====
private void ExportBrowseButton_Click(object sender, RoutedEventArgs e)
private void BrowseDatabaseFolderButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(BrowseDatabaseFolderClickedEvent, this));
private void OpenDatabaseFolderButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(OpenDatabaseFolderClickedEvent, this));
public static readonly RoutedEvent BrowseDatabaseFolderClickedEvent = EventManager.RegisterRoutedEvent(
"BrowseDatabaseFolderClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
public static readonly RoutedEvent OpenDatabaseFolderClickedEvent = EventManager.RegisterRoutedEvent(
"OpenDatabaseFolderClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
public event RoutedEventHandler BrowseDatabaseFolderClicked
{
RaiseEvent(new RoutedEventArgs(ExportBrowseClickedEvent, this));
add { AddHandler(BrowseDatabaseFolderClickedEvent, value); }
remove { RemoveHandler(BrowseDatabaseFolderClickedEvent, value); }
}
private void SaveSettingsButton_Click(object sender, RoutedEventArgs e)
public event RoutedEventHandler OpenDatabaseFolderClicked
{
RaiseEvent(new RoutedEventArgs(SaveSettingsClickedEvent, this));
add { AddHandler(OpenDatabaseFolderClickedEvent, value); }
remove { RemoveHandler(OpenDatabaseFolderClickedEvent, value); }
}
private void CancelSettingsButton_Click(object sender, RoutedEventArgs e)
// ── Gestione del rischio: kill-switch e HALT si applicano subito ──
private void WipeAllDataButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(WipeAllDataClickedEvent, this));
private void ResetSettingsButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(ResetSettingsClickedEvent, this));
private void ExportAppLogButton_Click(object sender, RoutedEventArgs e)
=> RaiseEvent(new RoutedEventArgs(ExportAppLogClickedEvent, this));
public static readonly RoutedEvent WipeAllDataClickedEvent = EventManager.RegisterRoutedEvent(
"WipeAllDataClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
public static readonly RoutedEvent ResetSettingsClickedEvent = EventManager.RegisterRoutedEvent(
"ResetSettingsClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
public static readonly RoutedEvent ExportAppLogClickedEvent = EventManager.RegisterRoutedEvent(
"ExportAppLogClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
public event RoutedEventHandler WipeAllDataClicked
{
RaiseEvent(new RoutedEventArgs(CancelSettingsClickedEvent, this));
add { AddHandler(WipeAllDataClickedEvent, value); }
remove { RemoveHandler(WipeAllDataClickedEvent, value); }
}
public event RoutedEventHandler ResetSettingsClicked
{
add { AddHandler(ResetSettingsClickedEvent, value); }
remove { RemoveHandler(ResetSettingsClickedEvent, value); }
}
public event RoutedEventHandler ExportAppLogClicked
{
add { AddHandler(ExportAppLogClickedEvent, value); }
remove { RemoveHandler(ExportAppLogClickedEvent, value); }
}
private void ResumeHaltButton_Click(object sender, RoutedEventArgs e)
{
Services.RiskManager.Resume();
RefreshRiskStatus();
}
/// <summary>Riallinea lo stato dell'HALT e la contabilità a quello che dice il database.</summary>
public void RefreshRiskStatus()
{
try
{
var halt = Services.RiskManager.HaltReason;
var s = Services.RiskManager.Snapshot();
var conto = $"Oggi: spesi {s.TodaySpent:F2} €, incassati {s.TodayGained:F2} € (netto {s.TodayNet:+0.00;-0.00} €). " +
$"Saldo cumulato {s.Cumulative:+0.00;-0.00} €, picco {s.Peak:F2} €, drawdown {s.Drawdown:F2} €.";
RiskStatusText.Text = string.IsNullOrEmpty(halt)
? "Nessun HALT in corso.\n" + conto
: $"HALT in corso: {halt}\n{conto}";
ResumeHaltButton.Visibility = string.IsNullOrEmpty(halt) ? Visibility.Collapsed : Visibility.Visible;
}
catch { }
finally { }
}
// ===== AGGIUNTA AUTOMATICA =====
//
// L'elenco dei prodotti (stellina e limiti su misura) sta nella scheda Prodotti:
// qui restano solo gli interruttori che valgono per tutti.
/// <summary>Stato con cui entrano le aste aggiunte automaticamente ("Watch"/"Stopped"/"Active").</summary>
public string AutoAddState
{
get => (AutoAddStateCombo.SelectedItem as ComboBoxItem)?.Tag as string ?? "Watch";
set
{
foreach (var item in AutoAddStateCombo.Items.OfType<ComboBoxItem>())
{
if ((item.Tag as string) == value)
{
AutoAddStateCombo.SelectedItem = item;
return;
}
}
AutoAddStateCombo.SelectedIndex = 0;
}
}
// ===== SESSIONE / AUTENTICAZIONE =====
//
// Non c'è più un campo per incollare il cookie a mano: l'accesso si fa dal browser
// integrato e vale per tutta l'applicazione. Un secondo modo significava due
// sessioni possibili e diverse — quella digitata e quella del browser — con la
// domanda "quale sta usando adesso?" senza una risposta visibile.
/// <summary>
/// Aggiorna il riquadro di stato della sessione.
/// </summary>
public void SetSessionStatus(bool connected, string? username = null,
int remainingBids = 0, double shopCredit = 0)
{
if (connected && !string.IsNullOrEmpty(username))
{
SessionStatusText.Text = $"Connesso come {username}";
SessionStatusText.SetResourceReference(ForegroundProperty, "Brush.Success");
SessionStatusDetail.Text =
$"Puntate residue: {remainingBids} • Credito Shop: EUR {shopCredit:F2}";
}
else
{
SessionStatusText.Text = "Non connesso";
SessionStatusText.SetResourceReference(ForegroundProperty, "Brush.Danger");
SessionStatusDetail.Text = "Nessuna sessione attiva.";
}
}
private void ConnectSessionButton_Click(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(ConnectSessionClickedEvent, this));
}
private void DisconnectSessionButton_Click(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(DisconnectSessionClickedEvent, this));
}
private void OpenBrowserLoginButton_Click(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(OpenBrowserLoginClickedEvent, this));
}
public static readonly RoutedEvent ConnectSessionClickedEvent = EventManager.RegisterRoutedEvent(
"ConnectSessionClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
public static readonly RoutedEvent DisconnectSessionClickedEvent = EventManager.RegisterRoutedEvent(
"DisconnectSessionClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
public static readonly RoutedEvent OpenBrowserLoginClickedEvent = EventManager.RegisterRoutedEvent(
"OpenBrowserLoginClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
public event RoutedEventHandler ConnectSessionClicked
{
add { AddHandler(ConnectSessionClickedEvent, value); }
remove { RemoveHandler(ConnectSessionClickedEvent, value); }
}
public event RoutedEventHandler DisconnectSessionClicked
{
add { AddHandler(DisconnectSessionClickedEvent, value); }
remove { RemoveHandler(DisconnectSessionClickedEvent, value); }
}
public event RoutedEventHandler OpenBrowserLoginClicked
{
add { AddHandler(OpenBrowserLoginClickedEvent, value); }
remove { RemoveHandler(OpenBrowserLoginClickedEvent, value); }
}
private void SaveDefaultsButton_Click(object sender, RoutedEventArgs e)
@@ -60,10 +260,7 @@ namespace AutoBidder.Controls
{
try
{
// 1. Salva impostazioni export
RaiseEvent(new RoutedEventArgs(SaveSettingsClickedEvent, this));
// 2. Salva impostazioni predefinite aste
// Salva impostazioni predefinite aste (export rimosso)
RaiseEvent(new RoutedEventArgs(SaveDefaultsClickedEvent, this));
// UNICO MessageBox di conferma
@@ -88,44 +285,16 @@ namespace AutoBidder.Controls
private void CancelAllSettings_Click(object sender, RoutedEventArgs e)
{
// Annulla tutte le modifiche
RaiseEvent(new RoutedEventArgs(CancelSettingsClickedEvent, this));
RaiseEvent(new RoutedEventArgs(CancelDefaultsClickedEvent, this));
}
// Routed Events (cookie events RIMOSSI)
public static readonly RoutedEvent ExportBrowseClickedEvent = EventManager.RegisterRoutedEvent(
"ExportBrowseClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
public static readonly RoutedEvent SaveSettingsClickedEvent = EventManager.RegisterRoutedEvent(
"SaveSettingsClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
public static readonly RoutedEvent CancelSettingsClickedEvent = EventManager.RegisterRoutedEvent(
"CancelSettingsClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
// Routed Events
public static readonly RoutedEvent SaveDefaultsClickedEvent = EventManager.RegisterRoutedEvent(
"SaveDefaultsClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
public static readonly RoutedEvent CancelDefaultsClickedEvent = EventManager.RegisterRoutedEvent(
"CancelDefaultsClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
public event RoutedEventHandler ExportBrowseClicked
{
add { AddHandler(ExportBrowseClickedEvent, value); }
remove { RemoveHandler(ExportBrowseClickedEvent, value); }
}
public event RoutedEventHandler SaveSettingsClicked
{
add { AddHandler(SaveSettingsClickedEvent, value); }
remove { RemoveHandler(SaveSettingsClickedEvent, value); }
}
public event RoutedEventHandler CancelSettingsClicked
{
add { AddHandler(CancelSettingsClickedEvent, value); }
remove { RemoveHandler(CancelSettingsClickedEvent, value); }
}
public event RoutedEventHandler SaveDefaultsClicked
{
add { AddHandler(SaveDefaultsClickedEvent, value); }
+1 -1
View File
@@ -25,4 +25,4 @@ namespace AutoBidder.Controls
InitializeComponent();
}
}
}
}
-135
View File
@@ -1,135 +0,0 @@
<UserControl x:Class="AutoBidder.Controls.StatisticsControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="800" d:DesignWidth="1200"
Background="#1E1E1E">
<UserControl.Resources>
<Style x:Key="RoundedButton" TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="8"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Padding" Value="15,10"/>
</Style>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Header -->
<Border Grid.Row="0" Background="#2D2D30" Padding="15" BorderBrush="#3E3E42" BorderThickness="0,0,0,1">
<Grid>
<TextBlock Text="📊 Dati Statistici - Analisi Aste Chiuse"
Foreground="#00D800"
FontSize="16"
FontWeight="Bold"
VerticalAlignment="Center"/>
<Button x:Name="LoadClosedAuctionsButton"
Content="🔄 Carica Statistiche"
HorizontalAlignment="Right"
Background="#007ACC"
Style="{StaticResource RoundedButton}"
Click="LoadClosedAuctionsButton_Click"/>
</Grid>
</Border>
<!-- DataGrid Statistiche -->
<DataGrid Grid.Row="1"
x:Name="StatsDataGrid"
AutoGenerateColumns="False"
IsReadOnly="True"
Background="#1E1E1E"
Foreground="#CCCCCC"
RowBackground="#1E1E1E"
AlternatingRowBackground="#252526"
GridLinesVisibility="Horizontal"
HeadersVisibility="Column"
BorderThickness="0"
Margin="15">
<DataGrid.ColumnHeaderStyle>
<Style TargetType="DataGridColumnHeader">
<Setter Property="Background" Value="#2D2D30"/>
<Setter Property="Foreground" Value="#CCCCCC"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Padding" Value="10,8"/>
<Setter Property="BorderThickness" Value="0,0,1,1"/>
<Setter Property="BorderBrush" Value="#3E3E42"/>
</Style>
</DataGrid.ColumnHeaderStyle>
<DataGrid.CellStyle>
<Style TargetType="DataGridCell">
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="10,5"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="#CCCCCC"/>
</Style>
</DataGrid.CellStyle>
<DataGrid.Columns>
<DataGridTextColumn Header="Prodotto" Binding="{Binding ProductName}" Width="3*"/>
<DataGridTextColumn Header="Prezzo Medio" Binding="{Binding AverageFinalPrice, StringFormat=€{0:F2}}" Width="120"/>
<DataGridTextColumn Header="Click Medi" Binding="{Binding AverageBidsUsed, StringFormat={}{0:F0}}" Width="100"/>
<DataGridTextColumn Header="Vincitore Frequente" Binding="{Binding Winner}" Width="150"/>
<DataGridTextColumn Header="# Aste" Binding="{Binding Count}" Width="80"/>
</DataGrid.Columns>
</DataGrid>
<!-- Footer: Status -->
<Border Grid.Row="2"
Background="#252526"
Padding="15"
BorderBrush="#3E3E42"
BorderThickness="0,1,0,0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock x:Name="StatsStatusText"
Text="Pronto per caricare statistiche"
FontSize="13"
Foreground="#CCCCCC"
VerticalAlignment="Center"/>
<TextBlock x:Name="ExportProgressText"
Text=""
FontSize="11"
Foreground="#999999"
Margin="0,5,0,0"
Visibility="Collapsed"/>
</StackPanel>
<ProgressBar Grid.Column="1"
x:Name="ExportProgressBar"
Width="200"
Height="20"
IsIndeterminate="True"
Foreground="#007ACC"
Background="#1E1E1E"
BorderBrush="#3E3E42"
Visibility="Collapsed"/>
</Grid>
</Border>
</Grid>
</UserControl>
@@ -1,31 +0,0 @@
using System.Windows;
using System.Windows.Controls;
namespace AutoBidder.Controls
{
/// <summary>
/// Interaction logic for StatisticsControl.xaml
/// </summary>
public partial class StatisticsControl : UserControl
{
public StatisticsControl()
{
InitializeComponent();
}
private void LoadClosedAuctionsButton_Click(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(LoadClosedAuctionsClickedEvent, this));
}
// Routed Events
public static readonly RoutedEvent LoadClosedAuctionsClickedEvent = EventManager.RegisterRoutedEvent(
"LoadClosedAuctionsClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(StatisticsControl));
public event RoutedEventHandler LoadClosedAuctionsClicked
{
add { AddHandler(LoadClosedAuctionsClickedEvent, value); }
remove { RemoveHandler(LoadClosedAuctionsClickedEvent, value); }
}
}
}
@@ -1,351 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Xml.Linq;
using AutoBidder.Utilities;
namespace AutoBidder
{
/// <summary>
/// Export functionality event handlers
/// </summary>
public partial class MainWindow
{
private CancellationTokenSource? _exportCts;
private void LoadExportSettings()
{
try
{
var s = SettingsManager.Load();
if (s != null)
{
ExportPathTextBox.Text = s.ExportPath ?? string.Empty;
if (!string.IsNullOrEmpty(s.LastExportExt))
{
var ext = s.LastExportExt.ToLowerInvariant();
if (ext == ".json") ExtJson.IsChecked = true;
else if (ext == ".xml") ExtXml.IsChecked = true;
else ExtCsv.IsChecked = true;
}
else
{
ExtCsv.IsChecked = true;
}
try { var cbOpen = this.FindName("ExportOpenToolbar") as System.Windows.Controls.CheckBox; if (cbOpen != null) cbOpen.IsChecked = s.ExportOpen; } catch { }
try { var cbClosed = this.FindName("ExportClosedToolbar") as System.Windows.Controls.CheckBox; if (cbClosed != null) cbClosed.IsChecked = s.ExportClosed; } catch { }
try { var cbUnknown = this.FindName("ExportUnknownToolbar") as System.Windows.Controls.CheckBox; if (cbUnknown != null) cbUnknown.IsChecked = s.ExportUnknown; } catch { }
try { IncludeUsedBids.IsChecked = s.IncludeOnlyUsedBids; } catch { }
try { IncludeLogs.IsChecked = s.IncludeLogs; } catch { }
try { IncludeUserBids.IsChecked = s.IncludeUserBids; } catch { }
try { IncludeMetadata.IsChecked = s.IncludeMetadata; } catch { }
try { RemoveAfterExport.IsChecked = s.RemoveAfterExport; } catch { }
try { OverwriteExisting.IsChecked = s.OverwriteExisting; } catch { }
}
}
catch { }
}
private async void ExportAllButton_Click(object sender, RoutedEventArgs e)
{
try
{
var settings = SettingsManager.Load();
string ext = ExtJson.IsChecked == true ? ".json" : ExtXml.IsChecked == true ? ".xml" : ".csv";
var dlg = new Microsoft.Win32.SaveFileDialog() { FileName = "auctions_export" + ext, Filter = "CSV files|*.csv|JSON files|*.json|XML files|*.xml|All files|*.*" };
if (dlg.ShowDialog(this) != true) return;
var path = dlg.FileName;
var all = _auctionMonitor.GetAuctions();
var includeOpen = (this.FindName("ExportOpenToolbar") as System.Windows.Controls.CheckBox)?.IsChecked == true;
var includeClosed = (this.FindName("ExportClosedToolbar") as System.Windows.Controls.CheckBox)?.IsChecked == true;
var includeUnknown = (this.FindName("ExportUnknownToolbar") as System.Windows.Controls.CheckBox)?.IsChecked == true;
var selection = all.Where(a =>
(includeOpen && a.IsActive) ||
(includeClosed && !a.IsActive) ||
(includeUnknown && ((a.BidHistory == null || a.BidHistory.Count == 0) && (a.BidderStats == null || a.BidderStats.Count == 0)))
).ToList();
if (selection.Count == 0)
{
MessageBox.Show(this, "Nessuna asta da esportare.", "Esporta Aste", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
Log("[INFO] Esportazione in corso...", LogLevel.Info);
await Task.Run(() =>
{
if (path.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
{
var json = System.Text.Json.JsonSerializer.Serialize(selection, new System.Text.Json.JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(path, json, Encoding.UTF8);
}
else if (path.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
{
var doc = new XDocument(new XElement("Auctions",
from a in selection
select new XElement("Auction",
new XElement("AuctionId", a.AuctionId),
new XElement("Name", a.Name),
new XElement("OriginalUrl", a.OriginalUrl ?? string.Empty)
)
));
doc.Save(path);
}
else
{
CsvExporter.ExportAllAuctions(selection, path);
}
});
try { ExportPreferences.SaveLastExportExtension(Path.GetExtension(path)); } catch { }
MessageBox.Show(this, "Esportazione completata.", "Esporta Aste", MessageBoxButton.OK, MessageBoxImage.Information);
Log($"[EXPORT] Aste esportate -> {path}", LogLevel.Success);
}
catch (Exception ex)
{
Log($"[ERRORE] Esportazione massiva: {ex.Message}", LogLevel.Error);
MessageBox.Show(this, "Errore durante esportazione: " + ex.Message, "Esporta Aste", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private async void ExportToolbarButton_Click(object sender, RoutedEventArgs e)
{
try
{
var settings = SettingsManager.Load();
var chosenExt = ExtJson.IsChecked == true ? ".json" : ExtXml.IsChecked == true ? ".xml" : ".csv";
var includeOpen = (this.FindName("ExportOpenToolbar") as System.Windows.Controls.CheckBox)?.IsChecked == true;
var includeClosed = (this.FindName("ExportClosedToolbar") as System.Windows.Controls.CheckBox)?.IsChecked == true;
var includeUnknown = (this.FindName("ExportUnknownToolbar") as System.Windows.Controls.CheckBox)?.IsChecked == true;
var all = _auctionMonitor.GetAuctions();
var selection = all.Where(a =>
(includeOpen && a.IsActive) ||
(includeClosed && !a.IsActive) ||
(includeUnknown && ((a.BidHistory == null || a.BidHistory.Count == 0) && (a.BidderStats == null || a.BidderStats.Count == 0)))
).ToList();
if (selection.Count == 0)
{
MessageBox.Show(this, "Nessuna asta da esportare.", "Esporta Aste", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
string folder;
if (!string.IsNullOrWhiteSpace(settings?.ExportPath) && Directory.Exists(settings.ExportPath))
{
folder = settings.ExportPath!;
}
else
{
MessageBox.Show(this, "Percorso export non configurato o non valido.\nConfigura il percorso nelle Impostazioni.", "Percorso Export", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var confirm = MessageBox.Show(this, $"Esportare {selection.Count} asta/e in:\n{folder}\n\nFormato: {chosenExt.ToUpperInvariant()}\n(Un file separato per ogni asta)", "Conferma Esportazione", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (confirm != MessageBoxResult.Yes) return;
Log("[INFO] Esportazione in corso...", LogLevel.Info);
int exported = 0;
int skipped = 0;
await Task.Run(() =>
{
foreach (var a in selection)
{
try
{
var filename = $"auction_{a.AuctionId}{chosenExt}";
var path = Path.Combine(folder, filename);
if (File.Exists(path) && settings != null && settings.OverwriteExisting != true)
{
skipped++;
Log($"[SKIP] File già esistente: {filename}", LogLevel.Warn);
continue;
}
if (chosenExt.Equals(".json", StringComparison.OrdinalIgnoreCase))
{
// JSON EXPORT - AGGIORNATO
var obj = new
{
AuctionId = a.AuctionId,
Name = a.Name,
OriginalUrl = a.OriginalUrl,
MinPrice = a.MinPrice,
MaxPrice = a.MaxPrice,
BidBeforeDeadlineMs = a.BidBeforeDeadlineMs,
CheckAuctionOpenBeforeBid = a.CheckAuctionOpenBeforeBid,
IsActive = a.IsActive,
IsPaused = a.IsPaused,
BidHistory = a.BidHistory,
Bidders = a.BidderStats.Values.ToList(),
AuctionLog = a.AuctionLog.ToList()
};
var json = System.Text.Json.JsonSerializer.Serialize(obj, new System.Text.Json.JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(path, json, Encoding.UTF8);
}
else if (chosenExt.Equals(".xml", StringComparison.OrdinalIgnoreCase))
{
// XML EXPORT - AGGIORNATO
var doc = new XDocument(
new XElement("AuctionExport",
new XElement("Metadata",
new XElement("AuctionId", a.AuctionId),
new XElement("Name", a.Name ?? string.Empty),
new XElement("OriginalUrl", a.OriginalUrl ?? string.Empty),
new XElement("MinPrice", a.MinPrice),
new XElement("MaxPrice", a.MaxPrice),
new XElement("BidBeforeDeadlineMs", a.BidBeforeDeadlineMs),
new XElement("CheckAuctionOpenBeforeBid", a.CheckAuctionOpenBeforeBid),
new XElement("IsActive", a.IsActive),
new XElement("IsPaused", a.IsPaused)
),
new XElement("FinalPrice", a.BidHistory?.LastOrDefault()?.Price.ToString("F2", CultureInfo.InvariantCulture) ?? string.Empty),
new XElement("TotalBids", a.BidHistory?.Count ?? 0),
new XElement("Bidders",
from b in a.BidderStats.Values.Where(x => x.BidCount > 0)
select new XElement("Bidder",
new XAttribute("Username", b.Username ?? string.Empty),
new XAttribute("BidCount", b.BidCount),
new XElement("LastBidTime", b.LastBidTimeDisplay ?? string.Empty)
)
),
new XElement("AuctionLog",
from l in a.AuctionLog
select new XElement("Entry", l)
),
new XElement("BidHistory",
from bh in a.BidHistory
select new XElement("Entry",
new XElement("Timestamp", bh.Timestamp.ToString("o")),
new XElement("EventType", bh.EventType),
new XElement("Bidder", bh.Bidder),
new XElement("Price", bh.Price.ToString("F2", CultureInfo.InvariantCulture)),
new XElement("Timer", bh.Timer.ToString("F2", CultureInfo.InvariantCulture)),
new XElement("LatencyMs", bh.LatencyMs),
new XElement("Success", bh.Success),
new XElement("Notes", bh.Notes)
)
)
)
);
doc.Save(path);
}
else
{
// CSV EXPORT - AGGIORNATO
using var sw = new StreamWriter(path, false, Encoding.UTF8);
sw.WriteLine("Field,Value");
sw.WriteLine($"AuctionId,{a.AuctionId}");
sw.WriteLine($"Name,\"{EscapeCsv(a.Name)}\"");
sw.WriteLine($"OriginalUrl,\"{EscapeCsv(a.OriginalUrl)}\"");
sw.WriteLine($"MinPrice,{a.MinPrice}");
sw.WriteLine($"MaxPrice,{a.MaxPrice}");
sw.WriteLine($"BidBeforeDeadlineMs,{a.BidBeforeDeadlineMs}");
sw.WriteLine($"CheckAuctionOpenBeforeBid,{a.CheckAuctionOpenBeforeBid}");
sw.WriteLine($"IsActive,{a.IsActive}");
sw.WriteLine($"IsPaused,{a.IsPaused}");
sw.WriteLine();
sw.WriteLine("--Auction Log--");
sw.WriteLine("Message");
foreach (var l in a.AuctionLog)
{
sw.WriteLine($"\"{EscapeCsv(l)}\"");
}
sw.WriteLine();
sw.WriteLine("--Bidders--");
sw.WriteLine("Username,BidCount,LastBidTime");
foreach (var b in a.BidderStats.Values)
{
sw.WriteLine($"\"{EscapeCsv(b.Username)}\",{b.BidCount},\"{EscapeCsv(b.LastBidTimeDisplay)}\"");
}
sw.WriteLine();
sw.WriteLine("--BidHistory--");
sw.WriteLine("Timestamp,EventType,Bidder,Price,Timer,LatencyMs,Success,Notes");
foreach (var bh in a.BidHistory)
{
sw.WriteLine($"\"{EscapeCsv(bh.Timestamp.ToString("o"))}\",{bh.EventType},\"{EscapeCsv(bh.Bidder)}\",{bh.Price:F2},{bh.Timer:F2},{bh.LatencyMs},{bh.Success},\"{EscapeCsv(bh.Notes)}\"");
}
}
exported++;
Log($"[EXPORT] Asta esportata -> {path}", LogLevel.Success);
}
catch (Exception ex)
{
Log($"[ERRORE] Export asta {a.AuctionId}: {ex.Message}", LogLevel.Error);
skipped++;
}
}
});
try { ExportPreferences.SaveLastExportExtension(chosenExt); } catch { }
MessageBox.Show(this, $"Esportazione completata.\n\nEsportate: {exported}\nIgnorate: {skipped}\nPercorso: {folder}", "Esporta Aste", MessageBoxButton.OK, MessageBoxImage.Information);
Log($"[EXPORT] Completato: {exported} esportate, {skipped} ignorate -> {folder}", LogLevel.Success);
if ((this.FindName("RemoveAfterExport") as System.Windows.Controls.CheckBox)?.IsChecked == true && selection.Count > 0)
{
Dispatcher.Invoke(() =>
{
foreach (var a in selection)
{
try
{
_auctionMonitor.RemoveAuction(a.AuctionId);
var vm = _auctionViewModels.FirstOrDefault(x => x.AuctionId == a.AuctionId);
if (vm != null)
{
_auctionViewModels.Remove(vm);
}
}
catch (Exception ex)
{
Log($"[WARN] Errore rimozione asta {a.AuctionId}: {ex.Message}", LogLevel.Warn);
}
}
SaveAuctions();
UpdateTotalCount();
});
}
}
catch (Exception ex)
{
Log($"[ERRORE] Esportazione toolbar: {ex.Message}", LogLevel.Error);
MessageBox.Show(this, "Errore durante esportazione: " + ex.Message, "Esporta Aste", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void ExportBrowseButton_Click(object sender, RoutedEventArgs e)
{
var dlg = new Microsoft.Win32.SaveFileDialog() { FileName = "export.csv", Filter = "CSV files|*.csv|All files|*.*" };
if (dlg.ShowDialog(this) == true)
{
ExportPathTextBox.Text = Path.GetDirectoryName(dlg.FileName) ?? string.Empty;
}
}
private string EscapeCsv(string? value)
{
if (string.IsNullOrEmpty(value)) return string.Empty;
return value.Replace("\"", "\"\"");
}
}
}
@@ -1,4 +1,4 @@
using System;
using System;
using System.Linq;
using System.Windows;
using AutoBidder.Utilities;
@@ -21,7 +21,9 @@ namespace AutoBidder
// Carica impostazioni predefinite aste
DefaultBidBeforeDeadlineMs.Text = settings.DefaultBidBeforeDeadlineMs.ToString();
DefaultCheckAuctionOpen.IsChecked = settings.DefaultCheckAuctionOpenBeforeBid;
Settings.AdaptiveLeadCheckBox.IsChecked = settings.AdaptiveLeadEnabled;
Settings.LeadMinMsTextBox.Text = settings.LeadMinMs.ToString();
Settings.LeadMaxMsTextBox.Text = settings.LeadMaxMs.ToString();
DefaultMinPrice.Text = settings.DefaultMinPrice.ToString("F2", System.Globalization.CultureInfo.InvariantCulture);
DefaultMaxPrice.Text = settings.DefaultMaxPrice.ToString("F2", System.Globalization.CultureInfo.InvariantCulture);
DefaultMaxClicks.Text = settings.DefaultMaxClicks.ToString();
@@ -36,11 +38,100 @@ namespace AutoBidder
// ?? NUOVO: Carica limite minimo puntate
MinimumRemainingBidsTextBox.Text = settings.MinimumRemainingBids.ToString();
// ?? NUOVO: Carica livello log
var logLevelErrorOnly = Settings.FindName("LogLevelErrorOnly") as System.Windows.Controls.RadioButton;
var logLevelNormal = Settings.FindName("LogLevelNormal") as System.Windows.Controls.RadioButton;
var logLevelInformational = Settings.FindName("LogLevelInformational") as System.Windows.Controls.RadioButton;
var logLevelDebug = Settings.FindName("LogLevelDebug") as System.Windows.Controls.RadioButton;
var logLevelTrace = Settings.FindName("LogLevelTrace") as System.Windows.Controls.RadioButton;
switch (settings.MinLogLevel)
{
case "ErrorOnly":
if (logLevelErrorOnly != null) logLevelErrorOnly.IsChecked = true;
break;
case "Informational":
if (logLevelInformational != null) logLevelInformational.IsChecked = true;
break;
case "Debug":
if (logLevelDebug != null) logLevelDebug.IsChecked = true;
break;
case "Trace":
if (logLevelTrace != null) logLevelTrace.IsChecked = true;
break;
case "Normal":
default:
if (logLevelNormal != null) logLevelNormal.IsChecked = true;
break;
}
// Motore di precisione
Settings.MaxRequestsPerSecondTextBox.Text = settings.MaxRequestsPerSecond.ToString("F0", System.Globalization.CultureInfo.InvariantCulture);
Settings.PrecisionTimerCheckBox.IsChecked = settings.PrecisionTimerEnabled;
RefreshEngineDiagnostics();
// Prodotti seguiti e catalogo
Settings.AutoAddEnabledCheckBox.IsChecked = settings.AutoAddProductsEnabled;
Settings.AutoAddState = settings.AutoAddNewAuctionState;
Settings.AutoAddScanSecondsTextBox.Text = settings.AutoAddScanSeconds.ToString();
Settings.AutoAddMaxAuctionsTextBox.Text = settings.AutoAddMaxAuctions.ToString();
Settings.AutoAddMaxStartMinutesTextBox.Text = settings.AutoAddMaxStartMinutes.ToString();
Settings.AutoAddOnlyNotStartedCheckBox.IsChecked = settings.AutoAddOnlyNotStarted;
Settings.SuggestedCoverageTextBox.Text = settings.SuggestedPriceCoveragePercent.ToString("0", System.Globalization.CultureInfo.CurrentCulture);
Settings.AverageBidCostTextBox.Text = settings.AverageBidCostEuro.ToString("0.00", System.Globalization.CultureInfo.CurrentCulture);
Settings.AutoAddScanDepthTextBox.Text = settings.AutoAddScanMaxAuctions.ToString();
Settings.CatalogMaxAuctionsTextBox.Text = settings.CatalogMaxAuctions.ToString();
Settings.CatalogAutoRefreshCheckBox.IsChecked = settings.CatalogAutoRefresh;
// Aste programmate, notifiche, cartelle
Settings.ScheduledBackoffCheckBox.IsChecked = settings.ScheduledAuctionBackoffEnabled;
Settings.ScheduledPollFarTextBox.Text = settings.ScheduledPollFarSeconds.ToString();
Settings.ScheduledPollMidTextBox.Text = settings.ScheduledPollMidSeconds.ToString();
Settings.ScheduledPollWakeTextBox.Text = settings.ScheduledPollWakeSeconds.ToString();
Settings.NotifyOnWinCheckBox.IsChecked = settings.NotifyOnWin;
Settings.NotifyOnLossCheckBox.IsChecked = settings.NotifyOnLoss;
// I percorsi si mostrano <b>risolti</b>, non come sono salvati: vuoto nel
// file significa "predefinito", ma a video il predefinito ha un nome preciso
// ed è quello che serve sapere.
RefreshDataFolderFields();
Settings.CatalogCacheSecondsTextBox.Text = settings.CatalogCacheSeconds.ToString();
Settings.QuietHoursCheckBox.IsChecked = settings.QuietHoursEnabled;
Settings.QuietHoursStartTextBox.Text = settings.QuietHoursStart.ToString();
Settings.QuietHoursEndTextBox.Text = settings.QuietHoursEnd.ToString();
Settings.AutoRemoveFinishedCheckBox.IsChecked = settings.AutoRemoveFinished;
Settings.AutoRemoveKeepMyBidsCheckBox.IsChecked = settings.AutoRemoveKeepWithMyBids;
Settings.AutoRemoveKeepWonCheckBox.IsChecked = settings.AutoRemoveKeepWon;
Settings.AutoRemoveKeepUnclearCheckBox.IsChecked = settings.AutoRemoveKeepUnclear;
Settings.WriteAppLogCheckBox.IsChecked = settings.WriteAppLog;
Settings.WriteFreeBidsLogCheckBox.IsChecked = settings.WriteFreeBidsLog;
Settings.WriteDossiersCheckBox.IsChecked = settings.RecordAuctions;
Settings.RawPollsCheckBox.IsChecked = settings.RecordPolls;
Settings.LogRetentionTextBox.Text = settings.LogRetentionDays.ToString();
Settings.DatabaseFolderTextBox.Text = AppPaths.DatabaseFolder;
Settings.BankrollEnabledCheckBox.IsChecked = settings.BankrollManagerEnabled;
Settings.MaxBidsPerAuctionTextBox.Text = settings.MaxBidsPerAuction.ToString();
Settings.MaxBidsPerSessionTextBox.Text = settings.MaxBidsPerSession.ToString();
Settings.DailyBudgetTextBox.Text = settings.DailyBudgetEuro.ToString("F2", System.Globalization.CultureInfo.CurrentCulture);
Settings.DailyStopLossTextBox.Text = settings.DailyStopLossEuro.ToString("F2", System.Globalization.CultureInfo.CurrentCulture);
Settings.MaxDrawdownTextBox.Text = settings.MaxDrawdownEuro.ToString("F2", System.Globalization.CultureInfo.CurrentCulture);
Settings.MaxConcurrentTextBox.Text = settings.MaxConcurrentActiveAuctions.ToString();
Settings.TransactionFeeTextBox.Text = settings.TransactionFeeEuro.ToString("F2", System.Globalization.CultureInfo.CurrentCulture);
Settings.HedgeCheckBox.IsChecked = settings.BuyNowHedgeEnabled;
Settings.RefreshRiskStatus();
// Aggiorna indicatore visivo
UpdateMinBidsIndicator(settings.MinimumRemainingBids);
// Carica stato iniziale aste
// ? NUOVO: Se RememberAuctionStates è attivo, seleziona "Ricorda Stato"
// ? NUOVO: Se RememberAuctionStates attivo, seleziona "Ricorda Stato"
if (settings.RememberAuctionStates)
{
Settings.LoadAuctionsRemember.IsChecked = true;
@@ -85,67 +176,6 @@ namespace AutoBidder
}
}
private void SaveSettingsButton_Click(object sender, RoutedEventArgs e)
{
try
{
// ? Carica le impostazioni esistenti per non perdere gli altri valori
var settings = Utilities.SettingsManager.Load() ?? new Utilities.AppSettings();
// === SEZIONE EXPORT: Percorso e Formato ===
settings.ExportPath = ExportPathTextBox.Text;
settings.LastExportExt = ExtJson.IsChecked == true ? ".json" : ExtXml.IsChecked == true ? ".xml" : ".csv";
// === SEZIONE EXPORT: Scope (Aste da esportare) ===
var cbClosed = this.FindName("ExportClosedToolbar") as System.Windows.Controls.CheckBox;
var cbUnknown = this.FindName("ExportUnknownToolbar") as System.Windows.Controls.CheckBox;
var cbOpen = this.FindName("ExportOpenToolbar") as System.Windows.Controls.CheckBox;
var scope = "All";
if (cbClosed != null && cbClosed.IsChecked == true) scope = "Closed";
else if (cbUnknown != null && cbUnknown.IsChecked == true) scope = "Unknown";
else if (cbOpen != null && cbOpen.IsChecked == true) scope = "Open";
settings.ExportScope = scope;
settings.ExportOpen = cbOpen?.IsChecked ?? true;
settings.ExportClosed = cbClosed?.IsChecked ?? true;
settings.ExportUnknown = cbUnknown?.IsChecked ?? true;
// === SEZIONE EXPORT: Opzioni ? FIX: Aggiunte le 3 checkbox mancanti ===
settings.IncludeOnlyUsedBids = IncludeUsedBids.IsChecked == true;
settings.IncludeLogs = IncludeLogs.IsChecked == true;
settings.IncludeUserBids = IncludeUserBids.IsChecked == true;
settings.IncludeMetadata = IncludeMetadata.IsChecked == true; // ? AGGIUNTO
settings.RemoveAfterExport = RemoveAfterExport.IsChecked == true; // ? AGGIUNTO
settings.OverwriteExisting = OverwriteExisting.IsChecked == true; // ? AGGIUNTO
SettingsManager.Save(settings);
ExportPreferences.SaveLastExportExtension(settings.LastExportExt);
}
catch (Exception ex)
{
Log($"[ERRORE] Salvataggio impostazioni export: {ex.Message}", LogLevel.Error);
}
}
private void CancelSettingsButton_Click(object sender, RoutedEventArgs e)
{
try
{
// Ricarica impostazioni export
LoadExportSettings();
// NOTA: Reload cookie RIMOSSO - ora automatico tramite browser
MessageBox.Show(this, "Impostazioni ripristinate alle ultime salvate.", "Annulla", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
Log($"[ERRORE] Ripristino impostazioni: {ex.Message}", LogLevel.Error);
MessageBox.Show(this, "Errore durante ripristino: " + ex.Message, "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void SaveDefaultsButton_Click(object sender, RoutedEventArgs e)
{
try
@@ -163,8 +193,17 @@ namespace AutoBidder
Log("[ERRORE] Valore anticipo puntata non valido (deve essere 0-5000ms)", LogLevel.Error);
return;
}
// Paletti dell'anticipo adattivo: minimo sotto il massimo, entrambi sensati.
settings.AdaptiveLeadEnabled = Settings.AdaptiveLeadCheckBox.IsChecked == true;
settings.LeadMinMs = ReadBounded(Settings.LeadMinMsTextBox.Text, settings.LeadMinMs, 100, 5000, "anticipo minimo", " ms");
settings.LeadMaxMs = ReadBounded(Settings.LeadMaxMsTextBox.Text, settings.LeadMaxMs, 100, 5000, "anticipo massimo", " ms");
if (settings.LeadMaxMs < settings.LeadMinMs)
{
Log($"[ERRORE] Anticipo massimo ({settings.LeadMaxMs} ms) sotto il minimo ({settings.LeadMinMs} ms): riportato al minimo", LogLevel.Error);
settings.LeadMaxMs = settings.LeadMinMs;
}
settings.DefaultCheckAuctionOpenBeforeBid = DefaultCheckAuctionOpen.IsChecked ?? false;
if (double.TryParse(DefaultMinPrice.Text.Replace(',', '.'), System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out var minPrice))
@@ -239,6 +278,29 @@ namespace AutoBidder
Log("[ERRORE] Valore limite minimo puntate non valido (deve essere >= 0)", LogLevel.Error);
}
// ?? NUOVO: Salva livello log
var logLevelErrorOnly = Settings.FindName("LogLevelErrorOnly") as System.Windows.Controls.RadioButton;
var logLevelNormal = Settings.FindName("LogLevelNormal") as System.Windows.Controls.RadioButton;
var logLevelInformational = Settings.FindName("LogLevelInformational") as System.Windows.Controls.RadioButton;
var logLevelDebug = Settings.FindName("LogLevelDebug") as System.Windows.Controls.RadioButton;
var logLevelTrace = Settings.FindName("LogLevelTrace") as System.Windows.Controls.RadioButton;
string selectedLogLevel = "Normal"; // Default
if (logLevelErrorOnly?.IsChecked == true)
selectedLogLevel = "ErrorOnly";
else if (logLevelInformational?.IsChecked == true)
selectedLogLevel = "Informational";
else if (logLevelDebug?.IsChecked == true)
selectedLogLevel = "Debug";
else if (logLevelTrace?.IsChecked == true)
selectedLogLevel = "Trace";
else if (logLevelNormal?.IsChecked == true)
selectedLogLevel = "Normal";
settings.MinLogLevel = selectedLogLevel;
Log($"[LOG] Livello log impostato: {selectedLogLevel}", LogLevel.Info);
// === SEZIONE DEFAULTS: Stati Iniziali Aste ===
var loadAuctionsRemember = Settings.FindName("LoadAuctionsRemember") as System.Windows.Controls.RadioButton;
var loadAuctionsActive = Settings.FindName("LoadAuctionsActive") as System.Windows.Controls.RadioButton;
@@ -249,7 +311,7 @@ namespace AutoBidder
{
// Attiva RememberAuctionStates
settings.RememberAuctionStates = true;
// DefaultStartAuctionsOnLoad diventa irrilevante, ma lo lasciamo a "Stopped" per compatibilità
// DefaultStartAuctionsOnLoad diventa irrilevante, ma lo lasciamo a "Stopped" per compatibilit
settings.DefaultStartAuctionsOnLoad = "Stopped";
}
else
@@ -265,10 +327,139 @@ namespace AutoBidder
var newAuctionPaused = Settings.FindName("NewAuctionPaused") as System.Windows.Controls.RadioButton;
settings.DefaultNewAuctionState = newAuctionActive?.IsChecked == true ? "Active" :
newAuctionPaused?.IsChecked == true ? "Paused" :
newAuctionPaused?.IsChecked == true ? "Paused" :
"Stopped";
// === SEZIONE: Motore di precisione ===
// Ogni cadenza ha un minimo sensato: sotto i 100 ms si spreca banda senza
// guadagnare precisione, perché la puntata la decide il cecchino, non il poll.
if (double.TryParse(Settings.MaxRequestsPerSecondTextBox.Text.Replace(',', '.'),
System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out var rps) && rps >= 1 && rps <= 200)
{
settings.MaxRequestsPerSecond = rps;
}
else
{
Log("[ERRORE] Limite richieste al secondo non valido (1-200): valore precedente mantenuto", LogLevel.Error);
}
settings.PrecisionTimerEnabled = Settings.PrecisionTimerCheckBox.IsChecked ?? true;
// === SEZIONE: Prodotti seguiti ===
settings.AutoAddProductsEnabled = Settings.AutoAddEnabledCheckBox.IsChecked ?? true;
settings.AutoAddNewAuctionState = Settings.AutoAddState;
settings.AutoAddScanSeconds = ReadBounded(Settings.AutoAddScanSecondsTextBox.Text,
settings.AutoAddScanSeconds, 30, 3600, "intervallo scansione prodotti seguiti", " s");
if (int.TryParse(Settings.AutoAddMaxAuctionsTextBox.Text, out var autoMax) && autoMax >= 0)
settings.AutoAddMaxAuctions = autoMax;
else
Log("[ERRORE] Massimo aste aggiunte automaticamente non valido (>= 0)", LogLevel.Error);
settings.AutoAddScanMaxAuctions = ReadBounded(Settings.AutoAddScanDepthTextBox.Text,
settings.AutoAddScanMaxAuctions, 50, 6000, "profondita ricerca prodotti seguiti", " aste");
// 0 = nessun orizzonte, ed e' una scelta vera: per questo il minimo del
// controllo di validita' e' zero e non uno.
settings.AutoAddOnlyNotStarted = Settings.AutoAddOnlyNotStartedCheckBox.IsChecked ?? false;
// Limiti consigliati per prodotto: due valori che entrano nel calcolo del
// tetto di prezzo. Si accetta la virgola oltre al punto, come nel resto
// dell'applicazione: chi scrive "0,20" non deve vedersi rifiutare il valore.
if (TryReadDouble(Settings.SuggestedCoverageTextBox.Text, out var copertura) &&
copertura is >= 10 and <= 99)
settings.SuggestedPriceCoveragePercent = copertura;
else
Log("[ERRORE] Copertura delle chiusure non valida (10-99%)", LogLevel.Error);
if (TryReadDouble(Settings.AverageBidCostTextBox.Text, out var costo) &&
costo is > 0 and <= 5)
settings.AverageBidCostEuro = costo;
else
Log("[ERRORE] Costo medio di una puntata non valido (0-5 EUR)", LogLevel.Error);
if (int.TryParse(Settings.AutoAddMaxStartMinutesTextBox.Text, out var orizzonte) && orizzonte >= 0)
settings.AutoAddMaxStartMinutes = orizzonte;
else
Log("[ERRORE] Orizzonte di aggiunta automatica non valido (>= 0 minuti)", LogLevel.Error);
if (settings.AutoAddProductsEnabled && settings.AutoAddNewAuctionState == "Active")
{
Log("[ATTENZIONE] Le aste seguite entreranno in stato Attiva: punteranno da sole, spendendo puntate reali.",
LogLevel.Warning);
}
// === SEZIONE: Catalogo ===
settings.CatalogMaxAuctions = ReadBounded(Settings.CatalogMaxAuctionsTextBox.Text,
settings.CatalogMaxAuctions, 20, 5000, "numero massimo aste catalogo", " aste");
settings.CatalogAutoRefresh = Settings.CatalogAutoRefreshCheckBox.IsChecked ?? false;
settings.CatalogCacheSeconds = ReadBounded(Settings.CatalogCacheSecondsTextBox.Text,
settings.CatalogCacheSeconds, 0, 3600, "cache catalogo", " s");
// === SEZIONE: Aste programmate ===
settings.ScheduledAuctionBackoffEnabled = Settings.ScheduledBackoffCheckBox.IsChecked ?? true;
settings.ScheduledPollFarSeconds = ReadBounded(Settings.ScheduledPollFarTextBox.Text,
settings.ScheduledPollFarSeconds, 60, 3600, "polling aste lontane", " s");
settings.ScheduledPollMidSeconds = ReadBounded(Settings.ScheduledPollMidTextBox.Text,
settings.ScheduledPollMidSeconds, 30, 1800, "polling aste vicine all'apertura", " s");
settings.ScheduledPollWakeSeconds = ReadBounded(Settings.ScheduledPollWakeTextBox.Text,
settings.ScheduledPollWakeSeconds, 30, 600, "margine di risveglio", " s");
// === SEZIONE: Notifiche ===
settings.NotifyOnWin = Settings.NotifyOnWinCheckBox.IsChecked ?? true;
settings.NotifyOnLoss = Settings.NotifyOnLossCheckBox.IsChecked ?? false;
// === SEZIONE: Cartelle dei dati ===
var previousDatabaseFolder = AppPaths.DatabaseFolder;
settings.QuietHoursEnabled = Settings.QuietHoursCheckBox.IsChecked ?? true;
if (int.TryParse(Settings.QuietHoursStartTextBox.Text?.Trim(), out var qStart) && qStart is >= 0 and <= 23)
settings.QuietHoursStart = qStart;
if (int.TryParse(Settings.QuietHoursEndTextBox.Text?.Trim(), out var qEnd) && qEnd is >= 0 and <= 24)
settings.QuietHoursEnd = qEnd;
settings.AutoRemoveFinished = Settings.AutoRemoveFinishedCheckBox.IsChecked ?? true;
settings.AutoRemoveKeepWithMyBids = Settings.AutoRemoveKeepMyBidsCheckBox.IsChecked ?? true;
settings.AutoRemoveKeepWon = Settings.AutoRemoveKeepWonCheckBox.IsChecked ?? true;
settings.AutoRemoveKeepUnclear = Settings.AutoRemoveKeepUnclearCheckBox.IsChecked ?? true;
settings.WriteAppLog = Settings.WriteAppLogCheckBox.IsChecked ?? true;
settings.WriteFreeBidsLog = Settings.WriteFreeBidsLogCheckBox.IsChecked ?? true;
settings.RecordAuctions = Settings.WriteDossiersCheckBox.IsChecked ?? true;
settings.RecordPolls = Settings.RawPollsCheckBox.IsChecked ?? true;
if (int.TryParse(Settings.LogRetentionTextBox.Text?.Trim(), out var retention) && retention >= 0)
settings.LogRetentionDays = retention;
settings.DatabaseFolder = NormalizeFolderChoice(Settings.DatabaseFolderTextBox.Text, AppPaths.DatabaseFolder, settings.DatabaseFolder);
settings.BankrollManagerEnabled = Settings.BankrollEnabledCheckBox.IsChecked ?? true;
settings.MaxBidsPerAuction = ReadBounded(Settings.MaxBidsPerAuctionTextBox.Text, settings.MaxBidsPerAuction, 0, 100000, "puntate massime per asta", "");
settings.MaxBidsPerSession = ReadBounded(Settings.MaxBidsPerSessionTextBox.Text, settings.MaxBidsPerSession, 0, 1000000, "puntate massime per sessione", "");
settings.DailyBudgetEuro = ReadEuro(Settings.DailyBudgetTextBox.Text, settings.DailyBudgetEuro, "tetto di spesa del giorno");
settings.DailyStopLossEuro = ReadEuro(Settings.DailyStopLossTextBox.Text, settings.DailyStopLossEuro, "stop-loss del giorno");
settings.MaxDrawdownEuro = ReadEuro(Settings.MaxDrawdownTextBox.Text, settings.MaxDrawdownEuro, "drawdown massimo");
settings.MaxConcurrentActiveAuctions = ReadBounded(Settings.MaxConcurrentTextBox.Text, settings.MaxConcurrentActiveAuctions, 0, 1000, "aste in gioco insieme", "");
settings.TransactionFeeEuro = ReadEuro(Settings.TransactionFeeTextBox.Text, settings.TransactionFeeEuro, "fee di transazione");
settings.BuyNowHedgeEnabled = Settings.HedgeCheckBox.IsChecked ?? true;
Utilities.SettingsManager.Save(settings);
ApplyDatabaseFolderSetting(settings, previousDatabaseFolder);
// Il limite di ritmo si applica a caldo, senza ricreare il trasporto.
_auctionMonitor.ApplyTransportSettings(settings);
ApplyProductWatchSettings(settings);
// L'interruttore in Esplora non deve dissentire dalle impostazioni, e il
// ciclo va fermato o riavviato di conseguenza.
Browser.SetAutoRefresh(settings.CatalogAutoRefresh);
if (settings.CatalogAutoRefresh) StartCatalogAutoRefresh();
else StopCatalogAutoRefresh();
RefreshEngineDiagnostics();
}
catch (Exception ex)
{
@@ -276,6 +467,75 @@ namespace AutoBidder
}
}
/// <summary>
/// Legge un intero entro limiti, mantenendo il valore precedente se il testo non è
/// utilizzabile: un campo sbagliato non deve far ripartire il motore a caso.
/// </summary>
/// <summary>
/// Legge un numero con la virgola o col punto. Le impostazioni si scrivono a mano
/// in un'applicazione italiana: rifiutare "0,20" sarebbe un dispetto.
/// </summary>
private static bool TryReadDouble(string? text, out double value)
{
value = 0;
if (string.IsNullOrWhiteSpace(text)) return false;
return double.TryParse(text.Trim().Replace(',', '.'),
System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out value);
}
private int ReadBounded(string text, int current, int min, int max, string label, string unit = "ms")
{
if (int.TryParse(text, out var value) && value >= min && value <= max)
return value;
Log($"[ERRORE] Valore {label} non valido ({min}-{max}{unit}): mantenuto {current}{unit}", LogLevel.Error);
return current;
}
/// <summary>Un importo in euro, con virgola o punto; se non valido si tiene il valore attuale.</summary>
private double ReadEuro(string? text, double current, string label)
{
var t = (text ?? "").Trim().Replace(',', '.');
if (double.TryParse(t, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var v) && v >= 0 && v < 1_000_000)
return Math.Round(v, 2);
Log($"[ERRORE] Valore {label} non valido: mantenuto {current:F2} €", LogLevel.Error);
return current;
}
/// <summary>
/// Mostra lo stato del motore: aggancio all'orologio del server e traffico prodotto.
/// </summary>
private void RefreshEngineDiagnostics()
{
try
{
var box = Settings.EngineDiagnosticsText;
if (box == null) return;
var clock = _auctionMonitor.Clock;
var sent = _auctionMonitor.RequestsSent;
var failed = _auctionMonitor.RequestsFailed;
// Attenzione a come si presenta lo scarto: Bidoo dichiara i secondi interi,
// quindi i campioni si distribuiscono per forza su una finestra di circa
// 1000 ms. Non è l'errore della stima — la stima usa il minimo, che converge
// al confine reale del secondo. Un valore molto oltre i 1000 ms segnala
// invece una rete instabile.
var clockLine = clock.IsSynced
? $"Orologio server agganciato su {clock.SampleCount} campioni: le scadenze seguono il server, non l'orologio locale.\n" +
$"Finestra campioni {clock.SpreadMs:F0} ms (intorno a 1000 ms è normale: Bidoo dichiara i secondi interi)."
: $"Orologio server in sincronizzazione ({clock.SampleCount}/3 campioni): finché non è agganciato la scadenza è stimata localmente.";
box.Text = $"{clockLine}\n" +
$"Richieste inviate: {sent:N0} — fallite: {failed:N0}.\n" +
$"Motori attivi: {_auctionMonitor.ActiveRunners}.";
}
catch { /* la diagnostica non deve mai far fallire il salvataggio */ }
}
private void CancelDefaultsButton_Click(object sender, RoutedEventArgs e)
{
try
@@ -290,5 +550,43 @@ namespace AutoBidder
MessageBox.Show(this, "Errore durante ripristino: " + ex.Message, "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
// === HANDLER PER PULSANTI UNIFICATI ===
private void SaveAllSettings_Click(object sender, RoutedEventArgs e)
{
try
{
// Salva tutte le impostazioni (ora solo defaults, export rimosso)
SaveDefaultsButton_Click(sender, e);
MessageBox.Show(
"Tutte le impostazioni sono state salvate con successo.\n\nLe nuove impostazioni verranno applicate alle aste future.",
"Impostazioni Salvate",
MessageBoxButton.OK,
MessageBoxImage.Information
);
}
catch (Exception ex)
{
Log($"[ERRORE] Salvataggio impostazioni: {ex.Message}", LogLevel.Error);
MessageBox.Show(this, "Errore durante salvataggio: " + ex.Message, "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void CancelAllSettings_Click(object sender, RoutedEventArgs e)
{
try
{
// Annulla tutte le modifiche
LoadDefaultSettings();
MessageBox.Show(this, "Impostazioni ripristinate alle ultime salvate.", "Annulla", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
Log($"[ERRORE] Ripristino impostazioni: {ex.Message}", LogLevel.Error);
MessageBox.Show(this, "Errore durante ripristino: " + ex.Message, "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
}
@@ -1,271 +1,271 @@
using System;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using System.Xml.Linq;
using AutoBidder.Models;
using AutoBidder.Utilities;
using Microsoft.EntityFrameworkCore;
namespace AutoBidder
{
/// <summary>
/// Statistics and closed auctions event handlers
/// NOTA: Funzionalità statistiche temporaneamente disabilitate - in sviluppo
/// Registrazione delle aste concluse.
///
/// Ne escono due archivi con scopi diversi: un riepilogo compatto che alimenta la
/// pagina Statistiche, e una scheda dettagliata per ogni asta destinata alle analisi
/// successive. Il secondo esiste perché quei dati, se non raccolti mentre l'asta è in
/// corso, non sono più recuperabili: Bidoo non li espone a posteriori.
/// </summary>
public partial class MainWindow
{
private void ExportStatsButton_Click(object sender, RoutedEventArgs e)
private void AuctionMonitor_OnAuctionCompleted(AuctionInfo auction, AuctionState state, bool won)
{
MessageBox.Show(this, "Funzionalità statistiche in sviluppo", "Info", MessageBoxButton.OK, MessageBoxImage.Information);
}
private async void LoadClosedAuctionsButton_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show(this, "Funzionalità statistiche in sviluppo", "Info", MessageBoxButton.OK, MessageBoxImage.Information);
/* CODICE TEMPORANEAMENTE DISABILITATO - Statistiche in sviluppo
try
{
StatsStatusText.Text = "Avvio caricamento statistiche...";
var settings = Utilities.SettingsManager.Load();
if (settings == null || string.IsNullOrWhiteSpace(settings.ExportPath) || !Directory.Exists(settings.ExportPath))
var settings = SettingsManager.Load();
var record = new CompletedAuctionRecord
{
MessageBox.Show(this, "Percorso export non configurato o non valido. Configuralo nelle impostazioni.", "Carica Statistiche", MessageBoxButton.OK, MessageBoxImage.Warning);
StatsStatusText.Text = "Percorso export non valido";
return;
AuctionId = auction.AuctionId,
Name = string.IsNullOrWhiteSpace(auction.Name) ? auction.AuctionId : auction.Name,
ProductKey = ProductKeyHelper.GenerateProductKey(auction.Name),
OriginalUrl = auction.OriginalUrl,
FinalPrice = state?.Price ?? 0,
Winner = state?.LastBidder ?? "",
WonByMe = won,
Outcome = won ? "Vinta"
: (state?.Status == AuctionStatus.EndedLost ? "Persa" : "Chiusa"),
MyBidsUsed = auction.BidsUsedOnThisAuction ?? 0,
TotalResets = auction.ResetCount,
BuyNowPrice = auction.BuyNowPrice,
AddedAt = auction.AddedAt,
// Le puntate spese dal vincitore arrivano dal server insieme allo
// stato finale: e' il numero che il sito mostra come "Puntate
// utilizzate", e l'unico affidabile (lo storico si ferma a cinquanta).
WinnerBidsPaid = state?.WinnerBidsPaid,
WinnerBidsFree = state?.WinnerBidsFree,
// L'ora della fine vera, non quella della registrazione. Un'asta
// terminata resta nel monitor finché non la si toglie: prendere
// DateTime.Now qui significherebbe datare la chiusura al momento in cui
// si è fatto pulizia, magari il giorno dopo.
EndedAt = ResolveEndedAt(auction, state)
};
CompletedAuctionsStore.Append(record);
// Un dato incoerente non va nelle medie: si dice e si scarta, invece di
// spostare in silenzio i limiti di prezzo del prodotto.
var integrity = AuctionIntegrity.CheckWinnerBids(record);
if (record.WinnerBidsUsed.HasValue && !integrity.IsTrustworthy)
{
Log($"[STATISTICHE] {record.Name}: puntate del vincitore scartate ({integrity.Reason})",
LogLevel.Warning);
}
else if (integrity.IsTrustworthy)
{
Log($"[STATISTICHE] {record.Name}: vinta con {record.WinnerBidsUsed} puntate " +
$"(prezzo {record.FinalPrice:F2} EUR)", LogLevel.Info);
}
ExportProgressBar.Visibility = Visibility.Visible;
ExportProgressText.Visibility = Visibility.Visible;
ExportProgressText.Text = "Caricamento statistiche...";
// La scheda dettagliata vive solo nel riepilogo del dossier: l'archivio
// mensile che la duplicava non c'è più (vedi AuctionDetailStore).
var detail = BuildDetail(auction, state, record);
var folder = settings.ExportPath!;
var files = Directory.GetFiles(folder, "auction_*.*");
if (files.Length == 0)
// La registrazione si chiude con il riepilogo: da lì in poi la riga è una
// storia completa, ed è così che l'analisi sa di poterla usare.
Data.AuctionRecorder.Close(auction, detail, state);
ProductStatsStore.RecordCompleted(record, detail);
// L'apprendimento impara subito da questa asta: il dossier è già su disco
// (Close lo svuota) e lo storico è già scritto. Fuori dal percorso di
// chiusura, che deve restare corto.
_ = System.Threading.Tasks.Task.Run(() => Ml.LearningService.OnAuctionClosed(record));
NotifyOutcome(record, settings);
Dispatcher.BeginInvoke(() =>
{
MessageBox.Show(this, "Nessun file di aste trovato nella cartella di export.", "Carica Statistiche", MessageBoxButton.OK, MessageBoxImage.Information);
StatsStatusText.Text = "Nessun file trovato";
ExportProgressBar.Visibility = Visibility.Collapsed;
ExportProgressText.Visibility = Visibility.Collapsed;
return;
}
Log($"[STORICO] Asta '{record.Name}' salvata ({record.Outcome}, €{record.FinalPrice:F2})",
won ? LogLevel.Success : LogLevel.Info);
var aggregated = new Dictionary<string, List<ClosedAuctionRecord>>(StringComparer.OrdinalIgnoreCase);
// Una vincita è il solo momento in cui il conto delle aste da confermare
// cambia: chiederlo subito evita che la barra resti indietro fino al
// giro successivo del timer.
if (won) _ = RefreshAuctionsToConfirmAsync();
await Task.Run(() =>
{
foreach (var f in files)
if (StatisticsPanel != null && StatisticsPanel.Visibility == Visibility.Visible)
{
try
{
var ext = Path.GetExtension(f).ToLowerInvariant();
if (ext == ".json")
{
var txt = File.ReadAllText(f, Encoding.UTF8);
try
{
var rec = System.Text.Json.JsonSerializer.Deserialize<ClosedAuctionRecord>(txt);
if (rec != null)
{
var key = (rec.ProductName ?? ExtractProductFromFilename(f) ?? "<unknown>").Trim();
if (!aggregated.ContainsKey(key)) aggregated[key] = new List<ClosedAuctionRecord>();
aggregated[key].Add(rec);
continue;
}
}
catch { }
try
{
var arr = System.Text.Json.JsonSerializer.Deserialize<List<ClosedAuctionRecord>>(txt);
if (arr != null)
{
foreach (var r in arr)
{
var key = (r.ProductName ?? ExtractProductFromFilename(f) ?? "<unknown>").Trim();
if (!aggregated.ContainsKey(key)) aggregated[key] = new List<ClosedAuctionRecord>();
aggregated[key].Add(r);
}
}
}
catch { }
}
else if (ext == ".xml")
{
try
{
var doc = XDocument.Load(f);
var auctionElems = doc.Descendants("Auction");
if (!auctionElems.Any()) auctionElems = doc.Descendants("AuctionExport");
foreach (var n in auctionElems)
{
var name = n.Descendants("Name").FirstOrDefault()?.Value ?? n.Descendants("ProductName").FirstOrDefault()?.Value;
double d = 0; double.TryParse(n.Descendants("FinalPrice").FirstOrDefault()?.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out d);
int bids = 0; int.TryParse(n.Descendants("TotalBids").FirstOrDefault()?.Value, out bids);
var winner = n.Descendants("Winner").FirstOrDefault()?.Value ?? string.Empty;
var url = n.Descendants("OriginalUrl").FirstOrDefault()?.Value ?? string.Empty;
var rec = new ClosedAuctionRecord { ProductName = name, FinalPrice = d == 0 ? null : (double?)d, Winner = winner, BidsUsed = bids, AuctionUrl = url };
var key = (rec.ProductName ?? ExtractProductFromFilename(f) ?? "<unknown>").Trim();
if (!aggregated.ContainsKey(key)) aggregated[key] = new List<ClosedAuctionRecord>();
aggregated[key].Add(rec);
}
}
catch { }
}
else // CSV or text
{
try
{
var lines = File.ReadAllLines(f, Encoding.UTF8);
string product = ExtractProductFromFilename(f) ?? "<unknown>";
double? price = null; int? bids = null; string winner = string.Empty; string url = string.Empty;
foreach (var l in lines)
{
var line = l.Trim();
if (line.StartsWith("Name,") || line.StartsWith("ProductName,"))
{
var parts = line.Split(',', 2);
if (parts.Length == 2) product = parts[1].Trim('"');
}
else if (line.StartsWith("FinalPrice", StringComparison.OrdinalIgnoreCase) || line.StartsWith("Price,"))
{
var parts = line.Split(',', 2);
if (parts.Length == 2 && double.TryParse(parts[1].Trim('"').Replace('€', ' ').Trim(), NumberStyles.Any, CultureInfo.InvariantCulture, out var p)) price = p;
}
else if (line.StartsWith("TotalBids", StringComparison.OrdinalIgnoreCase) || line.StartsWith("BidsUsed", StringComparison.OrdinalIgnoreCase))
{
var parts = line.Split(',', 2);
if (parts.Length == 2 && int.TryParse(parts[1].Trim('"'), out var b)) bids = b;
}
else if (line.StartsWith("Winner", StringComparison.OrdinalIgnoreCase))
{
var parts = line.Split(',', 2);
if (parts.Length == 2) winner = parts[1].Trim('"');
}
else if (line.StartsWith("OriginalUrl", StringComparison.OrdinalIgnoreCase) || line.StartsWith("AuctionUrl", StringComparison.OrdinalIgnoreCase))
{
var parts = line.Split(',', 2);
if (parts.Length == 2) url = parts[1].Trim('"');
}
}
var rec = new ClosedAuctionRecord { ProductName = product, FinalPrice = price, BidsUsed = bids, Winner = winner, AuctionUrl = url };
var key = (rec.ProductName ?? ExtractProductFromFilename(f) ?? "<unknown>").Trim();
if (!aggregated.ContainsKey(key)) aggregated[key] = new List<ClosedAuctionRecord>();
aggregated[key].Add(rec);
}
catch { }
}
}
catch { }
LoadStatistics();
}
});
var stats = new List<object>();
foreach (var kv in aggregated)
{
var list = kv.Value.Where(x => x.FinalPrice.HasValue || x.BidsUsed.HasValue).ToList();
if (list.Count == 0) continue;
var avgPrice = list.Where(x => x.FinalPrice.HasValue).Select(x => x.FinalPrice!.Value).DefaultIfEmpty(0).Average();
var avgBids = list.Where(x => x.BidsUsed.HasValue).Select(x => x.BidsUsed!.Value).DefaultIfEmpty(0).Average();
var winner = list.Where(x => !string.IsNullOrEmpty(x.Winner)).GroupBy(x => x.Winner).OrderByDescending(g => g.Count()).Select(g => g.Key).FirstOrDefault() ?? string.Empty;
var example = list.FirstOrDefault(x => !string.IsNullOrEmpty(x.AuctionUrl))?.AuctionUrl ?? string.Empty;
stats.Add(new
{
ProductName = kv.Key,
FinalPrice = avgPrice,
Winner = winner,
BidsUsed = (int)Math.Round(avgBids),
AuctionUrl = example,
Count = list.Count,
AverageFinalPrice = avgPrice,
AverageBidsUsed = avgBids
});
}
Dispatcher.Invoke(() =>
{
StatsDataGrid.ItemsSource = stats.OrderByDescending(s => (int)s.GetType().GetProperty("Count")!.GetValue(s)).ToList();
StatsStatusText.Text = $"Caricati {stats.Count} prodotti ({files.Length} file analizzati)";
ExportProgressBar.Visibility = Visibility.Collapsed;
ExportProgressText.Visibility = Visibility.Collapsed;
// Ultimo passo, e non prima: lo storico è già scritto, il dossier
// chiuso, le statistiche del prodotto aggiornate. Togliere l'asta
// dall'elenco a questo punto non perde niente — tutto ciò che aveva
// da dire è già altrove.
RimuoviSeConclusaEDaTogliere(auction, won, settings);
});
}
catch (Exception ex)
{
StatsStatusText.Text = "Errore caricamento statistiche";
Log($"[ERRORE] Carica statistiche: {ex.Message}", LogLevel.Error);
MessageBox.Show(this, "Errore durante caricamento statistiche: " + ex.Message, "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
ExportProgressBar.Visibility = Visibility.Collapsed;
ExportProgressText.Visibility = Visibility.Collapsed;
Console.WriteLine($"[STORICO ERROR] {ex.Message}");
}
*/
}
private static string? ExtractProductFromFilename(string path)
/// <summary>
/// Toglie dall'elenco l'asta appena conclusa, se la regola lo consente.
///
/// <para>Con l'aggiunta automatica accesa le aste concluse si accumulano a centinaia
/// e seppelliscono quelle vive, che è il contrario di quello che serve guardando un
/// cruscotto. Restano però quelle su cui si è speso: sono le uniche che vale la pena
/// riguardare, e i soldi erano veri. Vedi <see cref="FinishedAuctionCleanup"/>.</para>
///
/// <para>Il motivo finisce sempre nel registro, anche quando l'asta resta: senza,
/// un'asta che sparisce o che non sparisce sarebbero entrambe inspiegabili.</para>
/// </summary>
private void RimuoviSeConclusaEDaTogliere(AuctionInfo auction, bool won, AppSettings settings)
{
try
{
var name = Path.GetFileNameWithoutExtension(path);
var m = Regex.Match(name, @"auction_(.+)");
if (m.Success)
var verdict = FinishedAuctionCleanup.Decide(auction, won, new FinishedAuctionCleanup.Options
{
var v = m.Groups[1].Value;
if (Regex.IsMatch(v, "^\\d+$")) return null;
return v.Replace('_', ' ');
}
return null;
}
catch { return null; }
}
Enabled = settings.AutoRemoveFinished,
KeepWithMyBids = settings.AutoRemoveKeepWithMyBids,
KeepWon = settings.AutoRemoveKeepWon,
KeepUnclear = settings.AutoRemoveKeepUnclear
});
private async void ApplyInsightsButton_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show(this, "Funzionalità statistiche in sviluppo", "Info", MessageBoxButton.OK, MessageBoxImage.Information);
/* CODICE TEMPORANEAMENTE DISABILITATO
try
{
if (_selectedAuction == null)
if (!verdict.Remove)
{
MessageBox.Show(this, "Seleziona un'asta prima di applicare le raccomandazioni.", "Applica Raccomandazioni", MessageBoxButton.OK, MessageBoxImage.Information);
if (settings.AutoRemoveFinished)
Log($"[ELENCO] '{auction.Name}' resta in elenco: {verdict.Reason}", LogLevel.Info);
return;
}
var optionsBuilder = new Microsoft.EntityFrameworkCore.DbContextOptionsBuilder<Data.StatisticsContext>();
var dbPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "stats.db");
optionsBuilder.UseSqlite($"Data Source={dbPath}");
using var ctx = new Data.StatisticsContext(optionsBuilder.Options);
var svc = new Services.StatsService(ctx);
// Stesso giro della rimozione a mano: prima il motore, poi la griglia,
// poi il salvataggio. Toglierla solo dalla griglia lascerebbe il runner
// a interrogare un'asta che non esiste piu' per chi guarda.
_auctionMonitor.RemoveAuction(auction.AuctionId);
var (recBids, recPrice) = await svc.GetRecommendationAsync(_selectedAuction.AuctionInfo.Name, _selectedAuction.AuctionInfo.OriginalUrl);
_selectedAuction.MaxClicks = Math.Max(_selectedAuction.MaxClicks, recBids);
_selectedAuction.MaxPrice = Math.Max(_selectedAuction.MaxPrice, recPrice);
Log($"[OK] Raccomandazioni: MaxClicks={recBids}, MaxPrice={recPrice:F2} applicate a {_selectedAuction.Name}", LogLevel.Success);
UpdateSelectedAuctionDetails(_selectedAuction);
var vm = _auctionViewModels.FirstOrDefault(x => x.AuctionId == auction.AuctionId);
if (vm != null) _auctionViewModels.Remove(vm);
SaveAuctions();
UpdateTotalCount();
UpdateGlobalControlButtons();
Log($"[ELENCO] '{auction.Name}' tolta dall'elenco: {verdict.Reason}", LogLevel.Info);
}
catch (Exception ex)
{
Log($"[ERRORE] ApplyInsights: {ex.Message}", LogLevel.Error);
MessageBox.Show(this, "Errore applicazione raccomandazioni: " + ex.Message, "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
Console.WriteLine($"[ELENCO ERROR] {ex.Message}");
}
*/
}
private void FreeBidsStart_Click(object sender, RoutedEventArgs e)
/// <summary>
/// Quando l'asta è finita davvero, in ordine di attendibilità: l'istante fissato dal
/// monitor quando ha visto la conclusione dal vivo; la scadenza dichiarata dal
/// server nell'ultima risposta; l'istante di quella risposta. Solo se non c'è
/// nessuna delle tre si ripiega sull'ora corrente.
///
/// <para>La differenza non è cosmetica: lo storico si legge per capire a che ora del
/// giorno chiudono le aste che conviene seguire, e una data presa al momento della
/// rimozione rende quel numero privo di significato.</para>
/// </summary>
private static DateTime ResolveEndedAt(AuctionInfo auction, AuctionState? state)
{
MessageBox.Show(this, "Funzionalità non ancora implementata", "Info", MessageBoxButton.OK, MessageBoxImage.Information);
if (auction.ConcludedAt.HasValue) return auction.ConcludedAt.Value;
if (state != null && state.ExpiryUnixSeconds > 0)
return DateTimeOffset.FromUnixTimeSeconds(state.ExpiryUnixSeconds).ToLocalTime().DateTime;
if (state != null && state.SnapshotTime != default)
return state.SnapshotTime.ToLocalTime();
return DateTime.Now;
}
private void FreeBidsStop_Click(object sender, RoutedEventArgs e)
/// <summary>
/// Costruisce la scheda dettagliata a partire da ciò che il monitor ha osservato.
/// </summary>
private AuctionDetailRecord BuildDetail(AuctionInfo auction, AuctionState? state, CompletedAuctionRecord summary)
{
MessageBox.Show(this, "Funzionalità non ancora implementata", "Info", MessageBoxButton.OK, MessageBoxImage.Information);
var bidders = auction.SnapshotBidderStats();
var bidsByUser = bidders.ToDictionary(b => b.Username, b => b.BidCount, StringComparer.OrdinalIgnoreCase);
var series = auction.SnapshotPriceSeries();
var firstSeen = auction.FirstSeenAt ?? auction.AddedAt;
var observedMinutes = Math.Max(0, (summary.EndedAt - firstSeen).TotalMinutes);
return new AuctionDetailRecord
{
AuctionId = summary.AuctionId,
Name = summary.Name,
ProductKey = summary.ProductKey,
Url = summary.OriginalUrl,
Outcome = summary.Outcome,
WonByMe = summary.WonByMe,
Winner = summary.Winner,
FinalPrice = summary.FinalPrice,
BuyNowPrice = auction.BuyNowPrice,
ShippingCost = auction.ShippingCost,
FirstSeenAt = firstSeen,
EndedAt = summary.EndedAt,
ObservedMinutes = observedMinutes,
MyBids = summary.MyBidsUsed,
TotalObservedBids = bidsByUser.Values.Sum(),
DistinctBidders = bidsByUser.Count,
Resets = auction.ResetCount,
BidsByUser = bidsByUser,
TopBidderShare = AuctionDetailRecord.ComputeTopShare(bidsByUser),
PriceSeries = series,
PriceVelocityPerMinute = observedMinutes > 0.01 ? summary.FinalPrice / observedMinutes : 0,
AveragePingMs = auction.AverageLatencyMs,
PollCount = auction.PollCount,
PollErrors = auction.PollErrors,
ConfiguredLeadMs = auction.BidBeforeDeadlineMs,
TimerExpiredCount = auction.TimerExpiredCount,
SuccessfulBids = auction.SuccessfulBidCount,
FailedBids = auction.FailedBidCount,
BidCostEuro = auction.BidCost,
ObservedFromStart = auction.ObservedFromStart,
ObservedToEnd = auction.ObservedToEnd
};
}
/// <summary>
/// Avvisa con una notifica di sistema. Una vittoria va confermata su Bidoo, e
/// accorgersene solo riaprendo la finestra è facile da dimenticare.
/// </summary>
private static void NotifyOutcome(CompletedAuctionRecord record, AppSettings settings)
{
try
{
if (record.WonByMe && settings.NotifyOnWin)
{
WindowsNotifier.Show(
"Asta vinta!",
$"{record.Name}\nAggiudicata a € {record.FinalPrice:F2} con {record.MyBidsUsed} puntate.\n" +
"Ricordati di completare l'acquisto su Bidoo.");
}
else if (!record.WonByMe && settings.NotifyOnLoss && record.MyBidsUsed > 0)
{
// Solo se ci avevamo davvero puntato: le aste solo osservate non
// meritano un avviso.
WindowsNotifier.Show(
"Asta persa",
$"{record.Name}\nChiusa a € {record.FinalPrice:F2} da {record.Winner}.",
warning: true);
}
}
catch { }
}
}
}
+76 -64
View File
@@ -1,11 +1,12 @@
using System;
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using AutoBidder.Models;
using AutoBidder.ViewModels;
using AutoBidder.Utilities;
using AutoBidder.Services; // ✅ AGGIUNTO per RequestPriority e HtmlResponse
using AutoBidder.Services; // HtmlCacheService, HtmlResponse
using AutoBidder.Net; // RequestPriority
namespace AutoBidder
{
@@ -24,14 +25,14 @@ namespace AutoBidder
return;
}
string auctionId;
string? auctionId;
string? productName = null;
string originalUrl;
// Verifica se è un URL o solo un ID
// Verifica se un URL o solo un ID
if (input.Contains("bidoo.com") || input.Contains("http"))
{
// È un URL - estrai ID e nome prodotto dall'URL stesso
// un URL - estrai ID e nome prodotto dall'URL stesso
originalUrl = input.Trim();
auctionId = ExtractAuctionId(originalUrl);
if (string.IsNullOrEmpty(auctionId))
@@ -44,7 +45,7 @@ namespace AutoBidder
}
else
{
// È solo un ID numerico - costruisci URL generico
// solo un ID numerico - costruisci URL generico
auctionId = input.Trim();
originalUrl = $"https://it.bidoo.com/auction.php?a=asta_{auctionId}";
}
@@ -52,11 +53,11 @@ namespace AutoBidder
// Verifica duplicati
if (_auctionViewModels.Any(a => a.AuctionId == auctionId))
{
MessageBox.Show("Asta già monitorata!", "Duplicato", MessageBoxButton.OK, MessageBoxImage.Information);
MessageBox.Show("Asta gi monitorata!", "Duplicato", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
// MODIFICATO: Nome senza ID (già nella colonna separata)
// ? MODIFICATO: Nome senza ID (gi nella colonna separata)
var displayName = string.IsNullOrEmpty(productName)
? $"Asta {auctionId}"
: DecodeAllHtmlEntities(productName);
@@ -64,7 +65,7 @@ namespace AutoBidder
// CARICA IMPOSTAZIONI PREDEFINITE SALVATE
var settings = Utilities.SettingsManager.Load();
// Determina stato iniziale dalla configurazione
// ? Determina stato iniziale dalla configurazione
bool isActive = false;
bool isPaused = false;
@@ -92,7 +93,6 @@ namespace AutoBidder
Name = DecodeAllHtmlEntities(displayName),
OriginalUrl = originalUrl,
BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs,
CheckAuctionOpenBeforeBid = settings.DefaultCheckAuctionOpenBeforeBid,
IsActive = isActive,
IsPaused = isPaused
};
@@ -100,16 +100,20 @@ namespace AutoBidder
// Aggiungi al monitor
_auctionMonitor.AddAuction(auction);
// Crea ViewModel con valori dalle impostazioni
// Limiti del prodotto se ne ha di suoi, altrimenti i predefiniti. Lo stato
// non si tocca: qui l'ha scelto l'utente nella finestra di aggiunta, mentre
// lo stato del prodotto vale per le aste che entrano da sole.
var limits = ProductRuleResolver.ResolveByName(auction.Name, settings);
var vm = new AuctionViewModel(auction)
{
MinPrice = settings.DefaultMinPrice,
MaxPrice = settings.DefaultMaxPrice,
MaxClicks = settings.DefaultMaxClicks
MinPrice = limits.MinPrice,
MaxPrice = limits.MaxPrice,
MaxClicks = limits.MaxClicks
};
_auctionViewModels.Add(vm);
// Auto-start del monitoraggio se l'asta è attiva e il monitoraggio è fermo
// ? Auto-start del monitoraggio se l'asta attiva e il monitoraggio fermo
if (isActive && !_isAutomationActive)
{
_auctionMonitor.Start();
@@ -124,7 +128,7 @@ namespace AutoBidder
var stateText = isActive ? (isPaused ? "Paused" : "Active") : "Stopped";
Log($"[ADD] Asta aggiunta con stato={stateText}, Anticipo={settings.DefaultBidBeforeDeadlineMs}ms", Utilities.LogLevel.Info);
// NUOVO: Se il nome non è stato estratto, recuperalo in background DOPO l'aggiunta
// ? NUOVO: Se il nome non stato estratto, recuperalo in background DOPO l'aggiunta
if (string.IsNullOrEmpty(productName))
{
_ = FetchAuctionNameInBackgroundAsync(auction, vm);
@@ -144,7 +148,7 @@ namespace AutoBidder
{
try
{
// USA IL SERVIZIO CENTRALIZZATO invece di HttpClient diretto
// ? USA IL SERVIZIO CENTRALIZZATO invece di HttpClient diretto
var response = await _htmlCacheService.GetHtmlAsync(
auction.OriginalUrl,
RequestPriority.Normal,
@@ -153,7 +157,7 @@ namespace AutoBidder
if (!response.Success)
{
Log($"[WARN] Impossibile recuperare nome per asta {auction.AuctionId}: {response.Error}", LogLevel.Warn);
Log($"[WARN] Impossibile recuperare nome per asta {auction.AuctionId}: {response.Error}", LogLevel.Warning);
return;
}
@@ -163,9 +167,9 @@ namespace AutoBidder
if (match.Success)
{
var productName = match.Groups[1].Value.Trim().Replace(" - Bidoo", "");
// Decodifica entity HTML (incluse quelle non standard)
// ? Decodifica entity HTML (incluse quelle non standard)
productName = DecodeAllHtmlEntities(productName);
// MODIFICATO: Nome senza ID
// ? MODIFICATO: Nome senza ID
var newName = productName;
// Aggiorna il nome su thread UI
@@ -182,12 +186,12 @@ namespace AutoBidder
}
else
{
Log($"[WARN] Nome non trovato nell'HTML per asta {auction.AuctionId}", LogLevel.Warn);
Log($"[WARN] Nome non trovato nell'HTML per asta {auction.AuctionId}", LogLevel.Warning);
}
}
catch (Exception ex)
{
Log($"[WARN] Errore recupero nome per asta {auction.AuctionId}: {ex.Message}", LogLevel.Warn);
Log($"[WARN] Errore recupero nome per asta {auction.AuctionId}: {ex.Message}", LogLevel.Warning);
}
}
@@ -202,16 +206,16 @@ namespace AutoBidder
// Prima decodifica entity standard
var decoded = System.Net.WebUtility.HtmlDecode(text);
// Poi sostituisci entity non standard che WebUtility.HtmlDecode non gestisce
// ? Poi sostituisci entity non standard che WebUtility.HtmlDecode non gestisce
decoded = decoded.Replace("&plus;", "+");
decoded = decoded.Replace("&equals;", "=");
decoded = decoded.Replace("&minus;", "-");
decoded = decoded.Replace("&times;", "×");
decoded = decoded.Replace("&divide;", "÷");
decoded = decoded.Replace("&times;", "");
decoded = decoded.Replace("&divide;", "");
decoded = decoded.Replace("&percnt;", "%");
decoded = decoded.Replace("&dollar;", "$");
decoded = decoded.Replace("&euro;", "");
decoded = decoded.Replace("&pound;", "£");
decoded = decoded.Replace("&euro;", "");
decoded = decoded.Replace("&pound;", "");
return decoded;
}
@@ -236,7 +240,7 @@ namespace AutoBidder
// Verifica duplicati
if (_auctionViewModels.Any(a => a.AuctionId == auctionId))
{
MessageBox.Show("Asta già monitorata!", "Duplicato", MessageBoxButton.OK, MessageBoxImage.Information);
MessageBox.Show("Asta gi monitorata!", "Duplicato", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
@@ -244,7 +248,7 @@ namespace AutoBidder
var name = $"Asta {auctionId}";
try
{
// USA IL SERVIZIO CENTRALIZZATO
// ? USA IL SERVIZIO CENTRALIZZATO
var response = await _htmlCacheService.GetHtmlAsync(url, RequestPriority.Normal);
if (response.Success)
@@ -261,7 +265,7 @@ namespace AutoBidder
// CARICA IMPOSTAZIONI PREDEFINITE SALVATE
var settings = Utilities.SettingsManager.Load();
// Determina stato iniziale dalla configurazione
// ? Determina stato iniziale dalla configurazione
bool isActive = false;
bool isPaused = false;
@@ -289,7 +293,6 @@ namespace AutoBidder
Name = DecodeAllHtmlEntities(name),
OriginalUrl = url,
BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs,
CheckAuctionOpenBeforeBid = settings.DefaultCheckAuctionOpenBeforeBid,
IsActive = isActive,
IsPaused = isPaused
};
@@ -297,16 +300,20 @@ namespace AutoBidder
// Aggiungi al monitor
_auctionMonitor.AddAuction(auction);
// Crea ViewModel con valori dalle impostazioni
// Limiti del prodotto se ne ha di suoi, altrimenti i predefiniti. Lo stato
// non si tocca: qui l'ha scelto l'utente nella finestra di aggiunta, mentre
// lo stato del prodotto vale per le aste che entrano da sole.
var limits = ProductRuleResolver.ResolveByName(auction.Name, settings);
var vm = new AuctionViewModel(auction)
{
MinPrice = settings.DefaultMinPrice,
MaxPrice = settings.DefaultMaxPrice,
MaxClicks = settings.DefaultMaxClicks
MinPrice = limits.MinPrice,
MaxPrice = limits.MaxPrice,
MaxClicks = limits.MaxClicks
};
_auctionViewModels.Add(vm);
// Auto-start del monitoraggio se l'asta è attiva e il monitoraggio è fermo
// ? Auto-start del monitoraggio se l'asta attiva e il monitoraggio fermo
if (isActive && !_isAutomationActive)
{
_auctionMonitor.Start();
@@ -353,12 +360,12 @@ namespace AutoBidder
{
try
{
// Aspetta 30 secondi prima di ritentare (dà tempo alle altre richieste di completare)
// Aspetta 30 secondi prima di ritentare (d tempo alle altre richieste di completare)
await System.Threading.Tasks.Task.Delay(TimeSpan.FromSeconds(30));
// Trova aste con nomi generici "Asta XXXX"
var auctionsWithGenericNames = _auctionViewModels
.Where(vm => vm.Name.StartsWith("Asta ") && !vm.Name.Contains("Shop") && !vm.Name.Contains(""))
.Where(vm => vm.Name.StartsWith("Asta ") && !vm.Name.Contains("Shop") && !vm.Name.Contains(""))
.ToList();
if (auctionsWithGenericNames.Count > 0)
@@ -375,7 +382,7 @@ namespace AutoBidder
}
catch (Exception ex)
{
Log($"[WARN] Errore retry nomi aste: {ex.Message}", LogLevel.Warn);
Log($"[WARN] Errore retry nomi aste: {ex.Message}", LogLevel.Warning);
}
}
@@ -396,7 +403,7 @@ namespace AutoBidder
{
try
{
// Carica impostazioni
// ? Carica impostazioni
var settings = Utilities.SettingsManager.Load();
// Ottieni username corrente dalla sessione per ripristinare IsMyBid
@@ -409,10 +416,10 @@ namespace AutoBidder
// Protezione: rimuovi eventuali BidHistory null
auction.BidHistory = auction.BidHistory?.Where(b => b != null).ToList() ?? new System.Collections.Generic.List<BidHistory>();
// Decode HTML entities (incluse quelle non standard)
// ? Decode HTML entities (incluse quelle non standard)
try { auction.Name = DecodeAllHtmlEntities(auction.Name ?? string.Empty); } catch { }
// Ripristina IsMyBid per tutte le puntate in RecentBids
// ? Ripristina IsMyBid per tutte le puntate in RecentBids
if (auction.RecentBids != null && auction.RecentBids.Count > 0 && !string.IsNullOrEmpty(currentUsername))
{
foreach (var bid in auction.RecentBids)
@@ -422,11 +429,11 @@ namespace AutoBidder
}
// NUOVO: Gestione stato in base a RememberAuctionStates
// ? NUOVO: Gestione stato in base a RememberAuctionStates
if (settings.RememberAuctionStates)
{
// MODO 1: Ripristina lo stato salvato di ogni asta (IsActive e IsPaused vengono dal file salvato)
// Non serve fare nulla, lo stato è già quello salvato nel file
// Non serve fare nulla, lo stato gi quello salvato nel file
}
else
{
@@ -455,7 +462,7 @@ namespace AutoBidder
_auctionViewModels.Add(vm);
}
// Avvia monitoraggio se ci sono aste in stato Active O Paused
// ? Avvia monitoraggio se ci sono aste in stato Active O Paused
bool hasActiveOrPausedAuctions = auctions.Any(a => a.IsActive);
if (hasActiveOrPausedAuctions && auctions.Count > 0)
@@ -517,9 +524,10 @@ namespace AutoBidder
if (vm == null || vm.AuctionInfo == null)
{
// Resetta campi se nessuna asta selezionata
AuctionMonitor.ProductBuyNowPriceText.Text = "-";
AuctionMonitor.ProductShippingCostText.Text = "-";
AuctionMonitor.ProductWinLimitText.Text = "-";
AuctionMonitor.ProductBuyNowPriceText.Text = "";
AuctionMonitor.ProductShippingCostText.Text = "";
AuctionMonitor.ProductWinLimitText.Text = "";
RefreshProductVerdict(null);
return;
}
@@ -538,21 +546,21 @@ namespace AutoBidder
// Aggiorna Valore (Compra Subito)
if (auction.BuyNowPrice.HasValue)
{
AuctionMonitor.ProductBuyNowPriceText.Text = $"{auction.BuyNowPrice.Value:F2}";
AuctionMonitor.ProductBuyNowPriceText.Text = $"{auction.BuyNowPrice.Value:F2}";
}
else
{
AuctionMonitor.ProductBuyNowPriceText.Text = "-";
AuctionMonitor.ProductBuyNowPriceText.Text = "";
}
// Aggiorna Spese di Spedizione
if (auction.ShippingCost.HasValue)
{
AuctionMonitor.ProductShippingCostText.Text = $"{auction.ShippingCost.Value:F2}";
AuctionMonitor.ProductShippingCostText.Text = $"{auction.ShippingCost.Value:F2}";
}
else
{
AuctionMonitor.ProductShippingCostText.Text = "-";
AuctionMonitor.ProductShippingCostText.Text = "";
}
// Aggiorna Limiti di Vincita
@@ -562,12 +570,16 @@ namespace AutoBidder
}
else if (!auction.HasWinLimit)
{
AuctionMonitor.ProductWinLimitText.Text = "Nessun limite";
AuctionMonitor.ProductWinLimitText.Text = "";
}
else
{
AuctionMonitor.ProductWinLimitText.Text = "-";
AuctionMonitor.ProductWinLimitText.Text = "";
}
// Verdetto di convenienza: costo totale se vinci contro valore del prodotto.
RefreshProductVerdict(vm);
RefreshSelectedStats(vm);
}
/// <summary>
@@ -579,28 +591,28 @@ namespace AutoBidder
{
bool hasGenericName = auction.Name.StartsWith("Asta ") &&
!auction.Name.Contains("Shop") &&
!auction.Name.Contains("") &&
!auction.Name.Contains("") &&
!auction.Name.Contains("Buono") &&
!auction.Name.Contains("Carburante");
Log($"[PRODUCT INFO] Caricamento automatico per: {auction.Name}{(hasGenericName ? " (+ nome generico)" : "")}", Utilities.LogLevel.Info);
// USA IL SERVIZIO CENTRALIZZATO
// ? USA IL SERVIZIO CENTRALIZZATO
var response = await _htmlCacheService.GetHtmlAsync(
auction.OriginalUrl,
RequestPriority.High, // Priorità alta per info prodotto
RequestPriority.Normal, // Priorit alta per info prodotto
bypassCache: false
);
if (!response.Success)
{
Log($"[PRODUCT INFO] Errore caricamento: {response.Error}", Utilities.LogLevel.Warn);
Log($"[PRODUCT INFO] Errore caricamento: {response.Error}", Utilities.LogLevel.Warning);
return;
}
bool updated = false;
// 1. Se nome generico, estrai nome reale dal <title>
// 1. ? Se nome generico, estrai nome reale dal <title>
if (hasGenericName)
{
var matchTitle = System.Text.RegularExpressions.Regex.Match(response.Html, @"<title>([^<]+)</title>");
@@ -608,7 +620,7 @@ namespace AutoBidder
{
var productName = matchTitle.Groups[1].Value.Trim().Replace(" - Bidoo", "");
productName = DecodeAllHtmlEntities(productName);
// MODIFICATO: Nome senza ID
// ? MODIFICATO: Nome senza ID
var newName = productName;
auction.Name = newName;
@@ -617,15 +629,15 @@ namespace AutoBidder
}
}
// 2. Estrai informazioni prodotto (prezzo, spedizione, limiti)
// 2. ? Estrai informazioni prodotto (prezzo, spedizione, limiti)
var extracted = Utilities.ProductValueCalculator.ExtractProductInfo(response.Html, auction);
if (extracted)
{
updated = true;
Log($"[PRODUCT INFO] Valore={auction.BuyNowPrice:F2}, Spedizione={auction.ShippingCost:F2}{(response.FromCache ? " (cached)" : "")}", Utilities.LogLevel.Success);
Log($"[PRODUCT INFO] Valore={auction.BuyNowPrice:F2}, Spedizione={auction.ShippingCost:F2}{(response.FromCache ? " (cached)" : "")}", Utilities.LogLevel.Success);
}
// 3. Salva e aggiorna UI solo se qualcosa è cambiato
// 3. ? Salva e aggiorna UI solo se qualcosa cambiato
if (updated)
{
SaveAuctions();
@@ -650,7 +662,7 @@ namespace AutoBidder
}
catch (Exception ex)
{
Log($"[PRODUCT INFO] Errore caricamento: {ex.Message}", Utilities.LogLevel.Warn);
Log($"[PRODUCT INFO] Errore caricamento: {ex.Message}", Utilities.LogLevel.Warning);
}
}
}
+44 -58
View File
@@ -1,4 +1,4 @@
using System;
using System;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows;
@@ -14,7 +14,8 @@ namespace AutoBidder
/// </summary>
public partial class MainWindow
{
private void StartButton_Click(object sender, RoutedEventArgs e)
// sender null = azione interna (non richiesta dall'utente): si evita di loggarla.
private void StartButton_Click(object? sender, RoutedEventArgs e)
{
try
{
@@ -41,7 +42,7 @@ namespace AutoBidder
Log("[START ALL] Tutte le aste avviate/riprese", LogLevel.Info);
}
// Salva gli stati aggiornati su disco
// ? Salva gli stati aggiornati su disco
SaveAuctions();
UpdateGlobalControlButtons();
}
@@ -52,7 +53,7 @@ namespace AutoBidder
}
}
private void StopButton_Click(object sender, RoutedEventArgs e)
private void StopButton_Click(object? sender, RoutedEventArgs e)
{
try
{
@@ -69,13 +70,13 @@ namespace AutoBidder
_isAutomationActive = false;
}
// Salva gli stati aggiornati su disco
// ? Salva gli stati aggiornati su disco
SaveAuctions();
UpdateGlobalControlButtons();
if (sender != null) // Solo se chiamato dall'utente
{
Log("[STOP ALL] Monitoraggio fermato e tutte le aste arrestate", LogLevel.Warn);
Log("[STOP ALL] Monitoraggio fermato e tutte le aste arrestate", LogLevel.Warning);
}
}
catch (Exception ex)
@@ -84,22 +85,31 @@ namespace AutoBidder
}
}
private async void PauseAllButton_Click(object sender, RoutedEventArgs e)
private async void PauseAllButton_Click(object? sender, RoutedEventArgs e)
{
try
{
foreach (var vm in _auctionViewModels.Where(a => a.IsActive))
// Osserva vale anche per le aste ferme: servono comunque i motori accesi
// per seguirle, semplicemente non punteranno.
foreach (var vm in _auctionViewModels)
{
vm.IsActive = true;
vm.IsPaused = true;
}
// ✅ Salva gli stati aggiornati su disco
if (_auctionViewModels.Count > 0 && !_isAutomationActive)
{
_auctionMonitor.Start();
_isAutomationActive = true;
}
// ? Salva gli stati aggiornati su disco
SaveAuctions();
UpdateGlobalControlButtons();
if (sender != null) // Solo se chiamato dall'utente
{
Log("[PAUSE ALL] Tutte le aste in pausa", LogLevel.Warn);
Log("[OSSERVA] Tutte le aste seguite senza puntare", LogLevel.Warning);
}
}
catch (Exception ex)
@@ -166,7 +176,7 @@ namespace AutoBidder
MessageBox.Show(summary, "Aggiunta aste", MessageBoxButton.OK, MessageBoxImage.Information);
// RIMOSSO: Retry automatico ora avviene alla selezione on-demand
// ? RIMOSSO: Retry automatico ora avviene alla selezione on-demand
// Le aste con nome generico vengono aggiornate automaticamente quando l'utente le seleziona
}
}
@@ -187,7 +197,7 @@ namespace AutoBidder
// Conferma rimozione
var result = MessageBox.Show(
$"Rimuovere l'asta dal monitoraggio?\n\n{auctionName}\n(ID: {auctionId})\n\nL'asta verrà eliminata dalla lista e non sarà più monitorata.",
$"Rimuovere l'asta dal monitoraggio?\n\n{auctionName}\n(ID: {auctionId})\n\nL'asta verr eliminata dalla lista e non sar pi monitorata.",
"Conferma Rimozione",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
@@ -213,10 +223,10 @@ namespace AutoBidder
Log($"[REMOVE] Asta rimossa: {auctionName} (ID: {auctionId})", LogLevel.Success);
// NUOVO: Sposta il focus sulla riga successiva
// ? NUOVO: Sposta il focus sulla riga successiva
if (_auctionViewModels.Count > 0)
{
// Se c'è ancora almeno un'asta nella lista
// Se c' ancora almeno un'asta nella lista
int newIndex;
if (currentIndex >= _auctionViewModels.Count)
@@ -234,7 +244,7 @@ namespace AutoBidder
MultiAuctionsGrid.SelectedIndex = newIndex;
_selectedAuction = _auctionViewModels[newIndex];
// FIX: Salva il nome della NUOVA asta selezionata per il log
// ? FIX: Salva il nome della NUOVA asta selezionata per il log
var newAuctionName = _selectedAuction?.Name ?? "Sconosciuta";
// Forza il focus sulla griglia dopo un breve delay per permettere alla UI di aggiornarsi
@@ -248,7 +258,7 @@ namespace AutoBidder
MultiAuctionsGrid.ScrollIntoView(MultiAuctionsGrid.SelectedItem);
}
// FIX: Usa la variabile locale invece di _selectedAuction.Name
// ? FIX: Usa la variabile locale invece di _selectedAuction.Name
Log($"[FOCUS] Focus spostato su: {newAuctionName}", LogLevel.Info);
}), System.Windows.Threading.DispatcherPriority.Background);
}
@@ -278,7 +288,7 @@ namespace AutoBidder
// Conferma rimozione
var result = MessageBox.Show(
$"Rimuovere TUTTE le aste dal monitoraggio?\n\nSono presenti {count} aste monitorate.\n\nTutte le aste verranno eliminate dalla lista e non saranno più monitorate.",
$"Rimuovere TUTTE le aste dal monitoraggio?\n\nSono presenti {count} aste monitorate.\n\nTutte le aste verranno eliminate dalla lista e non saranno pi monitorate.",
"Conferma Rimozione Totale",
MessageBoxButton.YesNo,
MessageBoxImage.Warning);
@@ -368,7 +378,7 @@ namespace AutoBidder
}
// Ultimo tentativo fallito
Log($"[WARN] Clipboard temporaneamente occupato. Il testo potrebbe essere stato copiato.", LogLevel.Warn);
Log($"[WARN] Clipboard temporaneamente occupato. Il testo potrebbe essere stato copiato.", LogLevel.Warning);
return;
}
catch (Exception ex)
@@ -394,9 +404,10 @@ namespace AutoBidder
if (string.IsNullOrEmpty(url))
url = $"https://it.bidoo.com/auction.php?a=asta_{_selectedAuction.AuctionId}";
// Naviga alla scheda Browser
// Naviga alla scheda Browser, in modalita' browser (non catalogo)
TabBrowser.IsChecked = true;
Browser.ShowBrowser();
// Naviga all'URL
if (EmbeddedWebView?.CoreWebView2 != null)
{
@@ -405,8 +416,8 @@ namespace AutoBidder
}
else
{
Log($"[WARN] Browser interno non ancora inizializzato", LogLevel.Warn);
MessageBox.Show("Il browser interno non è ancora pronto.\nRiprova tra qualche secondo.", "Browser", MessageBoxButton.OK, MessageBoxImage.Warning);
Log($"[WARN] Browser interno non ancora inizializzato", LogLevel.Warning);
MessageBox.Show("Il browser interno non ancora pronto.\nRiprova tra qualche secondo.", "Browser", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
catch (Exception ex)
@@ -445,31 +456,6 @@ namespace AutoBidder
}
}
private void ExportAuctionButton_Click(object sender, RoutedEventArgs e)
{
if (_selectedAuction == null)
{
MessageBox.Show("Seleziona un'asta dalla griglia", "Nessuna Selezione", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
try
{
MessageBox.Show(
$"Esportazione singola asta:\n\n{_selectedAuction.Name}\n(ID: {_selectedAuction.AuctionId})\n\nFunzionalità in sviluppo.\nUsa 'Esporta' dalla toolbar per esportare tutte le aste.",
"Export Asta",
MessageBoxButton.OK,
MessageBoxImage.Information);
Log($"[INFO] Richiesto export singolo per asta: {_selectedAuction.Name} (funzionalità in sviluppo)", LogLevel.Info);
}
catch (Exception ex)
{
Log($"[ERRORE] Export asta: {ex.Message}", LogLevel.Error);
MessageBox.Show($"Errore: {ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private async void RefreshProductInfoButton_Click(object sender, RoutedEventArgs e)
{
try
@@ -503,12 +489,12 @@ namespace AutoBidder
double shippingCost = auction.ShippingCost ?? 0;
double totalValue = buyNowPrice + shippingCost;
// Max EUR = 40% del valore TOTALE (più conservativo del 50%)
// Max EUR = 40% del valore TOTALE (pi conservativo del 50%)
double suggestedMaxPrice = totalValue * 0.40;
suggestedMaxPrice = Math.Round(suggestedMaxPrice, 2);
// CALCOLA MAX CLICKS (numero massimo puntate conservativo)
// Formula: (Valore Totale - Max EUR) / 0.20 per puntata
// Formula: (Valore Totale - Max EUR) / 0.20 per puntata
// Poi riduciamo del 20% per maggiore margine di sicurezza
int maxClicksTheoretical = (int)Math.Floor((totalValue - suggestedMaxPrice) / 0.20);
int suggestedMaxClicks = (int)Math.Floor(maxClicksTheoretical * 0.80); // 80% del teorico
@@ -516,12 +502,12 @@ namespace AutoBidder
// Minimo 10 puntate per dare comunque una chance
if (suggestedMaxClicks < 10) suggestedMaxClicks = 10;
Log($"[LIMITI] Valore={buyNowPrice:F2} + Extra={shippingCost:F2} = Tot={totalValue:F2}€ → MaxEUR={suggestedMaxPrice:F2} (40%), MaxClicks={suggestedMaxClicks}", LogLevel.Info);
Log($"[LIMITI] Valore={buyNowPrice:F2} + Extra={shippingCost:F2} = Tot={totalValue:F2} ? MaxEUR={suggestedMaxPrice:F2} (40%), MaxClicks={suggestedMaxClicks}", LogLevel.Info);
// CHIEDI CONFERMA
var result = MessageBox.Show(
$"Limiti suggeriti (conservativi):\n\n" +
$"Max EUR: {suggestedMaxPrice:F2}\n" +
$"Max EUR: {suggestedMaxPrice:F2}\n" +
$"Max Clicks: {suggestedMaxClicks}\n\n" +
$"Applicare questi valori?",
"Conferma Limiti",
@@ -544,7 +530,7 @@ namespace AutoBidder
// SALVA
SaveAuctions();
Log($"[LIMITI] Applicati: MaxEUR={suggestedMaxPrice:F2}, MaxClicks={suggestedMaxClicks}", LogLevel.Success);
Log($"[LIMITI] Applicati: MaxEUR={suggestedMaxPrice:F2}, MaxClicks={suggestedMaxClicks}", LogLevel.Success);
}
catch (Exception ex)
{
@@ -574,8 +560,8 @@ namespace AutoBidder
if (currentIndex <= 0)
{
// Già in cima o non trovata
Log($"[MOVE] L'asta è già in cima alla lista", LogLevel.Info);
// Gi in cima o non trovata
Log($"[MOVE] L'asta gi in cima alla lista", LogLevel.Info);
return;
}
@@ -615,8 +601,8 @@ namespace AutoBidder
if (currentIndex < 0 || currentIndex >= _auctionViewModels.Count - 1)
{
// Già in fondo o non trovata
Log($"[MOVE] L'asta è già in fondo alla lista", LogLevel.Info);
// Gi in fondo o non trovata
Log($"[MOVE] L'asta gi in fondo alla lista", LogLevel.Info);
return;
}
+405
View File
@@ -0,0 +1,405 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using AutoBidder.Models;
using AutoBidder.Services;
using AutoBidder.Utilities;
namespace AutoBidder
{
/// <summary>
/// Catalogo nativo della scheda Esplora.
///
/// Affianca il browser integrato senza sostituirlo: il browser resta l'unico modo per
/// fare il login (da cui il cookie viene importato da solo), questa e' la via veloce
/// per confrontare molte aste senza caricare pagine.
/// </summary>
public partial class MainWindow
{
private BidooCatalogClient? _catalogClient;
private readonly List<CatalogCategory> _catalogCategories = new();
/// <summary>Aste della categoria corrente, prima dei filtri.</summary>
private List<CatalogAuction> _catalogAuctions = new();
/// <summary>
/// Le aste effettivamente mostrate, nell'ordine in cui compaiono. È su queste che
/// lavora l'aggiornamento prezzi: aggiornare anche quelle nascoste dai filtri
/// costerebbe richieste per numeri che nessuno sta guardando.
/// </summary>
private List<CatalogAuction> _catalogVisible = new();
private CatalogCategory? _catalogCategory;
private CancellationTokenSource? _catalogCts;
private CancellationTokenSource? _catalogRefreshCts;
/// <summary>Comandi usati dai pulsanti sulle singole schede prodotto.</summary>
public RelayCommand? CatalogAddCommand { get; private set; }
public RelayCommand? CatalogOpenCommand { get; private set; }
public RelayCommand? CatalogWatchCommand { get; private set; }
public RelayCommand? CatalogConfigureCommand { get; private set; }
private BidooCatalogClient CatalogClient
{
get
{
if (_catalogClient == null)
{
_catalogClient = new BidooCatalogClient(_auctionMonitor.GetApiClient().Transport)
{
Diagnostic = msg => Dispatcher.Invoke(() => Log(msg, LogLevel.Info))
};
}
return _catalogClient;
}
}
private void InitializeCatalogCommands()
{
CatalogAddCommand = new RelayCommand(async p => await AddFromCatalogAsync(p as CatalogAuction));
CatalogOpenCommand = new RelayCommand(p => OpenCatalogAuction(p as CatalogAuction));
CatalogWatchCommand = new RelayCommand(p => ToggleWatchedProduct(p as CatalogAuction));
CatalogConfigureCommand = new RelayCommand(p => ConfigureCatalogProduct(p as CatalogAuction));
}
/// <summary>Carica l'elenco categorie la prima volta che si apre la scheda Esplora.</summary>
private async Task EnsureCatalogLoadedAsync()
{
if (_catalogCategories.Count > 0) return;
// L'interruttore riflette l'impostazione salvata.
Browser.SetAutoRefresh(SettingsManager.Load().CatalogAutoRefresh);
try
{
Browser.SetCatalogMessage("Caricamento categorie…");
var categories = await CatalogClient.GetCategoriesAsync(false, CancellationToken.None);
_catalogCategories.Clear();
_catalogCategories.AddRange(categories);
Browser.SetCategories(_catalogCategories);
var first = _catalogCategories.FirstOrDefault();
if (first != null)
{
// Prima si registra la categoria corrente, poi si spunta il pulsante:
// spuntarlo scatena CategoryChanged, e senza questo ordine partirebbero
// due caricamenti concorrenti che si annullano a vicenda.
_catalogCategory = first;
first.IsSelected = true;
await LoadCategoryAsync(first);
}
else
{
Browser.SetCatalogMessage("Nessuna categoria disponibile.");
}
}
catch (Exception ex)
{
Browser.SetCatalogMessage($"Impossibile caricare le categorie: {ex.Message}");
Log($"[CATALOGO] Errore categorie: {ex.Message}", LogLevel.Error);
}
}
private async Task LoadCategoryAsync(CatalogCategory category)
{
// Una richiesta per volta: cambiare categoria durante un caricamento deve
// annullare il precedente, non accodarne un altro.
var previous = _catalogCts;
var cts = new CancellationTokenSource();
_catalogCts = cts;
previous?.Cancel();
previous?.Dispose();
_catalogCategory = category;
StopCatalogAutoRefresh();
try
{
Browser.SetCatalogMessage($"Carico \"{category.DisplayName}\"…");
var settings = SettingsManager.Load();
var max = Math.Max(20, settings.CatalogMaxAuctions);
var auctions = await CatalogClient.GetAllAuctionsAsync(
category, max, cts.Token, settings.CatalogCacheSeconds);
if (cts.Token.IsCancellationRequested) return;
// La pagina HTML da sola darebbe sempre 0,01 € e il timer di partenza:
// un secondo giro su data.php porta prezzi e scadenze reali.
if (auctions.Count > 0)
await CatalogClient.UpdateStatesAsync(auctions, cts.Token);
if (cts.Token.IsCancellationRequested) return;
MarkWatchedProducts(auctions);
_catalogAuctions = auctions;
RebuildCatalogView();
StartCatalogAutoRefresh();
}
catch (OperationCanceledException)
{
// Sostituita da una richiesta piu' recente: nessun messaggio, e' normale.
}
catch (Exception ex)
{
Browser.SetCatalogMessage($"Errore: {ex.Message}");
Log($"[CATALOGO] {ex.Message}", LogLevel.Error);
}
finally
{
if (ReferenceEquals(_catalogCts, cts))
{
_catalogCts = null;
cts.Dispose();
}
}
}
/// <summary>
/// Segna quali schede appartengono a un prodotto gia' in elenco, distinguendo
/// "seguito" (stellina) da "solo configurato" (ingranaggio acceso).
/// </summary>
private static void MarkWatchedProducts(IEnumerable<CatalogAuction> auctions)
{
var products = WatchedProductsStore.GetAll();
foreach (var auction in auctions)
{
var rule = products.FirstOrDefault(p => p.Matches(auction));
auction.IsWatched = rule?.IsWatched == true;
auction.IsListed = rule != null;
}
}
/// <summary>
/// Ricostruisce (e riordina) l'elenco mostrato. Si chiama SOLO al caricamento o
/// quando cambiano i filtri: riordinare mentre si legge farebbe saltare le schede
/// da una posizione all'altra sotto il cursore.
/// </summary>
private void RebuildCatalogView()
{
var filter = Browser.SearchText;
var hideManual = Browser.HideManualAuctions;
var view = _catalogAuctions
.Where(a => !hideManual || !a.IsManualOnly)
.Where(a => filter.Length == 0 || a.Name.Contains(filter, StringComparison.OrdinalIgnoreCase))
// Le aste che stanno per chiudere sono quelle su cui si decide: prima.
.OrderBy(a => a.RemainingSeconds <= 0 ? int.MaxValue : a.RemainingSeconds)
.ToList();
_catalogVisible = view;
Browser.SetCatalogItems(view, _catalogAuctions.Count);
}
// ── Aggiornamento prezzi sul posto ───────────────────────────────
/// <summary>
/// Quante aste tenere aggiornate mentre si guarda la griglia. Il listato si sfoglia
/// ormai a migliaia, e <c>data.php</c> ne accetta una sessantina per chiamata: senza
/// un tetto, un catalogo grande significherebbe decine di richieste ogni due secondi
/// per aggiornare aste che chiudono fra ore.
/// </summary>
private const int CatalogRefreshBudget = 300;
private void StartCatalogAutoRefresh()
{
StopCatalogAutoRefresh();
if (!Browser.AutoRefreshEnabled || _catalogAuctions.Count == 0) return;
var cts = new CancellationTokenSource();
_catalogRefreshCts = cts;
_ = CatalogRefreshLoopAsync(cts.Token);
}
private void StopCatalogAutoRefresh()
{
var cts = _catalogRefreshCts;
_catalogRefreshCts = null;
cts?.Cancel();
cts?.Dispose();
}
/// <summary>
/// Aggiorna prezzi e timer delle aste in vista. I valori cambiano <i>sul posto</i>:
/// nessun riordino e nessuna ricostruzione della lista, altrimenti le schede
/// ballerebbero mentre le si guarda.
///
/// <para>Si aggiornano le prime <see cref="CatalogRefreshBudget"/> della griglia,
/// che essendo ordinata per scadenza sono quelle che stanno per chiudere: sono le
/// uniche i cui numeri cambiano da un momento all'altro.</para>
/// </summary>
private async Task CatalogRefreshLoopAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
if (!await Wait.DelayAsync(2000, ct).ConfigureAwait(false)) return;
var visible = _catalogVisible;
if (visible.Count == 0) continue;
var snapshot = visible.Count > CatalogRefreshBudget
? visible.GetRange(0, CatalogRefreshBudget)
: visible;
try
{
await CatalogClient.UpdateStatesAsync(snapshot, ct).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
return;
}
catch
{
// Un aggiornamento saltato non compromette la pagina.
continue;
}
if (ct.IsCancellationRequested) return;
await Dispatcher.InvokeAsync(() =>
{
foreach (var auction in snapshot) auction.NotifyStateChanged();
});
}
}
// ── Azioni sulle schede ──────────────────────────────────────────
private async Task AddFromCatalogAsync(CatalogAuction? auction)
{
if (auction == null) return;
if (_auctionViewModels.Any(a => a.AuctionId == auction.AuctionId))
{
Log($"[CATALOGO] {auction.Name} è già nel monitor", LogLevel.Info);
return;
}
await AddAuctionById(auction.Url);
WatchedProductsStore.MarkHandled(auction.AuctionId);
Log($"[CATALOGO] Aggiunta al monitor: {auction.Name}", LogLevel.Info);
}
private void OpenCatalogAuction(CatalogAuction? auction)
{
if (auction == null) return;
try
{
// Passa alla scheda Browser: TabBrowser_Checked mostra il pannello e il
// browser integrato, poi si naviga all'asta.
TabBrowser.IsChecked = true;
Browser.ShowBrowser();
Browser.EmbeddedWebView?.CoreWebView2?.Navigate(auction.Url);
Browser.BrowserAddress.Text = auction.Url;
}
catch (Exception ex)
{
Log($"[CATALOGO] Apertura nel browser fallita: {ex.Message}", LogLevel.Error);
}
}
/// <summary>
/// Attiva o disattiva la sorveglianza del prodotto (stellina). Da qui in poi le aste
/// nuove dello stesso articolo entrano nel monitor da sole.
/// </summary>
private void ToggleWatchedProduct(CatalogAuction? auction)
{
if (auction == null) return;
try
{
if (auction.IsWatched)
{
// Spegnere la stellina non butta via i limiti scritti a mano: la scheda
// resta fra i Prodotti, solo senza aggiunta automatica.
WatchedProductsStore.SetWatched(auction, false);
Log($"[SEGUITI] Non seguo più: {auction.Name}", LogLevel.Info);
}
else
{
WatchedProductsStore.SetWatched(auction, true);
var settings = SettingsManager.Load();
if (settings.AutoAddProductsEnabled)
{
Log($"[SEGUITI] Ora seguo: {auction.Name} — le aste nuove entreranno in stato {StateLabel(settings.AutoAddNewAuctionState)}",
LogLevel.Success);
_ = _productWatcher?.ScanNowAsync();
}
else
{
Log($"[SEGUITI] Ora seguo: {auction.Name} — l'aggiunta automatica è però disattivata in Impostazioni",
LogLevel.Warning);
}
}
// Tutte le schede dello stesso prodotto cambiano stella insieme.
MarkWatchedProducts(_catalogAuctions);
}
catch (Exception ex)
{
Log($"[SEGUITI] Errore: {ex.Message}", LogLevel.Error);
}
}
private static string StateLabel(string state) => state switch
{
"Active" => "Attiva",
"Stopped" => "Ferma",
_ => "Osserva"
};
// ── Handler degli eventi del controllo ───────────────────────────
private async void Browser_CatalogCategoryChanged(object sender, RoutedEventArgs e)
{
var selected = _catalogCategories.FirstOrDefault(c => c.IsSelected);
if (selected == null || ReferenceEquals(selected, _catalogCategory)) return;
await LoadCategoryAsync(selected);
}
private async void Browser_CatalogRefreshClicked(object sender, RoutedEventArgs e)
{
// Chi preme "Aggiorna" vuole i dati dal server, non quelli in cache.
CatalogClient.InvalidateCache();
if (_catalogCategory == null)
{
_catalogCategories.Clear();
await EnsureCatalogLoadedAsync();
return;
}
await LoadCategoryAsync(_catalogCategory);
}
private void Browser_CatalogSearchChanged(object sender, RoutedEventArgs e)
{
// Filtri cambiati: qui il riordino ci sta, è l'utente ad averlo chiesto.
RebuildCatalogView();
}
private void Browser_CatalogAutoRefreshChanged(object sender, RoutedEventArgs e)
{
var settings = SettingsManager.Load();
settings.CatalogAutoRefresh = Browser.AutoRefreshEnabled;
SettingsManager.Save(settings);
if (Browser.AutoRefreshEnabled) StartCatalogAutoRefresh();
else StopCatalogAutoRefresh();
}
}
}
+21 -7
View File
@@ -1,4 +1,4 @@
using System.Linq;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using AutoBidder.Utilities;
@@ -21,21 +21,24 @@ namespace AutoBidder
GridPauseCommand = new RelayCommand(param => ExecuteGridPause(param as AuctionViewModel));
GridStopCommand = new RelayCommand(param => ExecuteGridStop(param as AuctionViewModel));
GridBidCommand = new RelayCommand(async param => await ExecuteGridBidAsync(param as AuctionViewModel));
InitializeCatalogCommands();
InitializeProductCommands();
}
private void ExecuteStartAll()
{
StartButton_Click(null, null);
StartButton_Click(null, new RoutedEventArgs());
}
private void ExecuteStopAll()
{
StopButton_Click(null, null);
StopButton_Click(null, new RoutedEventArgs());
}
private void ExecutePauseAll()
{
PauseAllButton_Click(null, null);
PauseAllButton_Click(null, new RoutedEventArgs());
}
private void ExecuteGridStart(AuctionViewModel? vm)
@@ -46,7 +49,7 @@ namespace AutoBidder
vm.IsActive = true;
vm.IsPaused = false;
// Se il monitoraggio globale non è attivo, avvialo automaticamente
// Se il monitoraggio globale non attivo, avvialo automaticamente
if (!_isAutomationActive)
{
_auctionMonitor.Start();
@@ -66,9 +69,20 @@ namespace AutoBidder
private void ExecuteGridPause(AuctionViewModel? vm)
{
if (vm == null) return;
// Osserva significa "segui ma non puntare": serve comunque un motore acceso,
// quindi l'asta va resa attiva anche se partiva da ferma.
vm.IsActive = true;
vm.IsPaused = true;
Log($"[PAUSA] Asta in pausa: {vm.Name}", LogLevel.Info);
if (!_isAutomationActive)
{
_auctionMonitor.Start();
_isAutomationActive = true;
}
Log($"[OSSERVA] Asta seguita senza puntare: {vm.Name}", LogLevel.Info);
// ? Salva gli stati aggiornati su disco
SaveAuctions();
UpdateGlobalControlButtons();
+101 -3
View File
@@ -30,7 +30,7 @@ namespace AutoBidder
if (session != null && !string.IsNullOrEmpty(session.Username))
{
// Già connesso - Mostra opzioni
// Gi connesso - Mostra opzioni
var result = MessageBox.Show(
this,
$"Connesso come: {session.Username}\n" +
@@ -54,14 +54,15 @@ namespace AutoBidder
this,
"Per accedere:\n\n" +
"1. Fai login su Bidoo nella scheda Browser\n" +
"2. La connessione sarà automatica\n\n" +
"2. La connessione sar automatica\n\n" +
"Apertura scheda Browser...",
"Accedi a Bidoo",
MessageBoxButton.OK,
MessageBoxImage.Information);
// Apri tab Browser
// Apri tab Browser, in modalita' browser: il login passa da li'
TabBrowser.IsChecked = true;
Browser.ShowBrowser();
}
}
catch (Exception ex)
@@ -70,6 +71,103 @@ namespace AutoBidder
}
}
// ===== SEZIONE SESSIONE NELLE IMPOSTAZIONI =====
/// <summary>
/// Aggiorna il riquadro di stato sessione nelle Impostazioni.
/// </summary>
private void RefreshSettingsSessionStatus()
{
try
{
var session = _sessionService?.GetCurrentSession();
bool connected = session != null && !string.IsNullOrEmpty(session.Username);
Settings.SetSessionStatus(
connected,
session?.Username,
session?.RemainingBids ?? 0,
session?.ShopCredit ?? 0);
}
catch { }
}
/// <summary>
/// "Verifica connessione": ricontrolla contro il sito la sessione già in uso.
///
/// <para>Non chiede più un cookie da incollare: l'accesso si fa una volta dal
/// browser integrato e vale per tutta l'applicazione. Questo pulsante serve a
/// rispondere alla domanda che ci si fa davvero — "sono ancora connesso?" — che
/// prima si poteva solo dedurre da un'asta che smetteva di funzionare.</para>
/// </summary>
private async void Settings_ConnectSessionClicked(object sender, RoutedEventArgs e)
{
var cookie = _auctionMonitor.GetApiClient().Transport.Cookie;
if (string.IsNullOrWhiteSpace(cookie))
{
MessageBox.Show(this,
"Nessuna sessione da verificare.\n\n" +
"Accedi a Bidoo dalla scheda Browser: il cookie viene rilevato e importato da solo.",
"Non connesso", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
try
{
Log("[SESSION] Verifica della sessione in corso...", Utilities.LogLevel.Info);
var result = await _sessionService.ValidateAndActivateSessionAsync(cookie);
if (result.Success && result.Session != null)
{
_sessionService.SaveSession(result.Session);
SetUserBanner(result.Session.Username, result.Session.RemainingBids);
RefreshSettingsSessionStatus();
Log($"[SESSION] Connesso come {result.Session.Username} " +
$"({result.Session.RemainingBids} puntate)", Utilities.LogLevel.Success);
MessageBox.Show(this,
$"Connesso come: {result.Session.Username}\n" +
$"Puntate residue: {result.Session.RemainingBids}\n" +
$"Credito Shop: EUR {result.Session.ShopCredit:F2}",
"Sessione attiva", MessageBoxButton.OK, MessageBoxImage.Information);
}
else
{
Log($"[SESSION] Sessione non più valida: {result.ErrorMessage}", Utilities.LogLevel.Error);
RefreshSettingsSessionStatus();
MessageBox.Show(this,
"La sessione non è più valida.\n\n" +
(result.ErrorMessage ?? "Cookie scaduto.") +
"\n\nRiaccedi a Bidoo dalla scheda Browser.",
"Sessione scaduta", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
catch (Exception ex)
{
Log($"[ERRORE] Verifica sessione: {ex.Message}", Utilities.LogLevel.Error);
MessageBox.Show(this, "Errore durante la verifica: " + ex.Message,
"Errore", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>"Disconnetti" dalla sezione Impostazioni.</summary>
private void Settings_DisconnectSessionClicked(object sender, RoutedEventArgs e)
{
DisconnectSession();
RefreshSettingsSessionStatus();
}
/// <summary>"Apri Browser per il login": passa alla scheda Browser.</summary>
private void Settings_OpenBrowserLoginClicked(object sender, RoutedEventArgs e)
{
TabBrowser.IsChecked = true;
Browser.ShowBrowser();
}
/// <summary>
/// Disconnette la sessione corrente
/// </summary>
+84 -66
View File
@@ -1,4 +1,4 @@
using System.Windows;
using System.Windows;
namespace AutoBidder
{
@@ -17,16 +17,38 @@ namespace AutoBidder
private void TabBrowser_Checked(object sender, RoutedEventArgs e)
{
ShowPanel(Browser);
Browser.ShowBrowser();
}
private async void TabCerca_Checked(object sender, RoutedEventArgs e)
{
ShowPanel(Browser);
Browser.ShowCatalog();
await EnsureCatalogLoadedAsync();
}
private void TabProdotti_Checked(object sender, RoutedEventArgs e)
{
ShowPanel(Products);
LoadProducts();
}
private void TabPuntateGratis_Checked(object sender, RoutedEventArgs e)
{
ShowPanel(PuntateGratisPanel);
ShowPanel(FreeBids);
RefreshFreeBidsPanel();
}
private void TabDatiStatistici_Checked(object sender, RoutedEventArgs e)
{
ShowPanel(StatisticsPanel);
LoadStatistics();
}
private void TabApprendimento_Checked(object sender, RoutedEventArgs e)
{
ShowPanel(Learning);
Learning.Refresh();
}
private void TabImpostazioni_Checked(object sender, RoutedEventArgs e)
@@ -38,8 +60,9 @@ namespace AutoBidder
// Carica impostazioni quando si apre la tab
LoadDefaultSettings();
// NOTA: Caricamento cookie RIMOSSO - ora automatico tramite browser
// Aggiorna il riquadro di stato della sezione Sessione
RefreshSettingsSessionStatus();
}
catch { }
}
@@ -47,15 +70,19 @@ namespace AutoBidder
private void ShowPanel(System.Windows.UIElement? panelToShow)
{
// Prevent NullReferenceException during initialization
if (AuctionMonitor == null || Browser == null || StatisticsPanel == null || Settings == null || PuntateGratisPanel == null)
if (AuctionMonitor == null || Browser == null || StatisticsPanel == null ||
Settings == null || FreeBids == null || Products == null ||
Learning == null)
return;
// Hide all panels
AuctionMonitor.Visibility = Visibility.Collapsed;
Browser.Visibility = Visibility.Collapsed;
PuntateGratisPanel.Visibility = Visibility.Collapsed;
Products.Visibility = Visibility.Collapsed;
FreeBids.Visibility = Visibility.Collapsed;
StatisticsPanel.Visibility = Visibility.Collapsed;
Settings.Visibility = Visibility.Collapsed;
Learning.Visibility = Visibility.Collapsed;
// Show selected panel
if (panelToShow != null)
@@ -79,29 +106,6 @@ namespace AutoBidder
StopButton_Click(sender, e);
}
private void AuctionMonitor_ExportClicked(object sender, RoutedEventArgs e)
{
// Chiama il metodo di export esistente
try
{
// Esporta tutte le aste monitorate
var auctions = _auctionMonitor.GetAuctions();
if (auctions.Count == 0)
{
System.Windows.MessageBox.Show("Nessuna asta da esportare", "Export", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Information);
return;
}
// TODO: Implementare dialog export con scelta formato
System.Windows.MessageBox.Show($"Export di {auctions.Count} aste.\n\nFunzionalità in sviluppo.\nUsa le impostazioni nella scheda Impostazioni per configurare l'export.", "Export Aste", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Information);
Log($"[INFO] Richiesto export di {auctions.Count} aste (funzionalità in sviluppo)", Utilities.LogLevel.Info);
}
catch (System.Exception ex)
{
Log($"[ERRORE] Export: {ex.Message}", Utilities.LogLevel.Error);
}
}
private void AuctionMonitor_AddUrlClicked(object sender, RoutedEventArgs e)
{
AddUrlButton_Click(sender, e);
@@ -138,7 +142,7 @@ namespace AutoBidder
var auction = selected.AuctionInfo;
bool hasGenericName = auction.Name.StartsWith("Asta ") &&
!auction.Name.Contains("Shop") &&
!auction.Name.Contains("") &&
!auction.Name.Contains("") &&
!auction.Name.Contains("Buono") &&
!auction.Name.Contains("Carburante");
@@ -170,11 +174,6 @@ namespace AutoBidder
OpenAuctionExternalButton_Click(sender, e);
}
private void AuctionMonitor_ExportAuctionClicked(object sender, RoutedEventArgs e)
{
ExportAuctionButton_Click(sender, e);
}
private void AuctionMonitor_ResetSettingsClicked(object sender, RoutedEventArgs e)
{
ResetSettingsButton_Click(sender, e);
@@ -209,27 +208,26 @@ namespace AutoBidder
private void AuctionMonitor_BidBeforeDeadlineMsChanged(object sender, RoutedEventArgs e)
{
// Gestito internamente dal binding WPF
// Riempire i campi alla selezione di un'asta scatena i TextChanged:
// senza questa guardia ogni selezione riscriveva auctions.json quattro volte
// con gli stessi valori appena letti.
if (_isUpdatingSelection) return;
if (_selectedAuction != null && int.TryParse(AuctionMonitor.SelectedBidBeforeDeadlineMs.Text, out int ms))
{
_selectedAuction.AuctionInfo.BidBeforeDeadlineMs = ms;
SaveAuctions();
}
}
private void AuctionMonitor_CheckAuctionOpenChanged(object sender, RoutedEventArgs e)
{
// Gestito internamente dal binding WPF
if (_selectedAuction != null)
{
_selectedAuction.AuctionInfo.CheckAuctionOpenBeforeBid = AuctionMonitor.SelectedCheckAuctionOpen.IsChecked ?? false;
_selectedAuction.AuctionInfo.BidLeadIsManual = true;
SaveAuctions();
}
}
private void AuctionMonitor_MinPriceChanged(object sender, RoutedEventArgs e)
{
// Gestito internamente dal binding WPF
// Riempire i campi alla selezione di un'asta scatena i TextChanged:
// senza questa guardia ogni selezione riscriveva auctions.json quattro volte
// con gli stessi valori appena letti.
if (_isUpdatingSelection) return;
if (_selectedAuction != null && double.TryParse(AuctionMonitor.SelectedMinPrice.Text, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double price))
{
_selectedAuction.MinPrice = price;
@@ -239,7 +237,11 @@ namespace AutoBidder
private void AuctionMonitor_MaxPriceChanged(object sender, RoutedEventArgs e)
{
// Gestito internamente dal binding WPF
// Riempire i campi alla selezione di un'asta scatena i TextChanged:
// senza questa guardia ogni selezione riscriveva auctions.json quattro volte
// con gli stessi valori appena letti.
if (_isUpdatingSelection) return;
if (_selectedAuction != null && double.TryParse(AuctionMonitor.SelectedMaxPrice.Text, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double price))
{
_selectedAuction.MaxPrice = price;
@@ -249,7 +251,11 @@ namespace AutoBidder
private void AuctionMonitor_MaxClicksChanged(object sender, RoutedEventArgs e)
{
// Gestito internamente dal binding WPF
// Riempire i campi alla selezione di un'asta scatena i TextChanged:
// senza questa guardia ogni selezione riscriveva auctions.json quattro volte
// con gli stessi valori appena letti.
if (_isUpdatingSelection) return;
if (_selectedAuction != null && int.TryParse(AuctionMonitor.SelectedMaxClicks.Text, out int clicks))
{
_selectedAuction.MaxClicks = clicks;
@@ -257,6 +263,35 @@ namespace AutoBidder
}
}
/// <summary>
/// Tetto di spesa in euro per l'asta selezionata. E' il fratello di MaxClicks detto
/// in denaro: chi ragiona in euro non deve fare la divisione a mente.
/// </summary>
private void AuctionMonitor_MaxSpendChanged(object sender, RoutedEventArgs e)
{
if (_isUpdatingSelection) return;
if (_selectedAuction == null) return;
var testo = AuctionMonitor.SelectedMaxSpend.Text.Trim().Replace(',', '.');
if (double.TryParse(testo, System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out var spesa) && spesa >= 0)
{
_selectedAuction.AuctionInfo.MaxTotalSpendEuro = spesa;
SaveAuctions();
}
}
/// <summary>Interruttore del controllo di pareggio sull'asta selezionata.</summary>
private void AuctionMonitor_BreakEvenChanged(object sender, RoutedEventArgs e)
{
if (_isUpdatingSelection) return;
if (_selectedAuction == null) return;
_selectedAuction.AuctionInfo.StopAtBreakEven = AuctionMonitor.SelectedStopAtBreakEven.IsChecked == true;
SaveAuctions();
}
// ===== BROWSER CONTROL EVENTS =====
private void Browser_BrowserBackClicked(object sender, RoutedEventArgs e)
@@ -324,23 +359,6 @@ namespace AutoBidder
// ===== SETTINGS CONTROL EVENTS =====
// NOTA: Handler cookie RIMOSSI - gestione automatica tramite browser
private void Settings_ExportBrowseClicked(object sender, RoutedEventArgs e)
{
ExportBrowseButton_Click(sender, e);
}
private void Settings_SaveSettingsClicked(object sender, RoutedEventArgs e)
{
SaveSettingsButton_Click(sender, e);
}
private void Settings_CancelSettingsClicked(object sender, RoutedEventArgs e)
{
CancelSettingsButton_Click(sender, e);
}
private void Settings_SaveDefaultsClicked(object sender, RoutedEventArgs e)
{
SaveDefaultsButton_Click(sender, e);
+241
View File
@@ -0,0 +1,241 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Windows;
using AutoBidder.Data;
using AutoBidder.Utilities;
namespace AutoBidder
{
/// <summary>
/// La cartella dei database (con la migrazione quando cambia) e le esportazioni dello
/// storico, che chiedono sempre dove salvare.
/// </summary>
public partial class MainWindow
{
// ── Cartella dei database ────────────────────────────────────────
/// <summary>Il campo mostra sempre il percorso in uso: selezionabile, copiabile.</summary>
private void RefreshDataFolderFields()
{
try { Settings.DatabaseFolderTextBox.Text = AppPaths.DatabaseFolder; }
catch { }
}
/// <summary>
/// Traduce quello che c'è nel campo in ciò che va salvato: vuoto = predefinito,
/// uguale al predefinito = vuoto, un percorso davvero diverso = quel percorso.
/// </summary>
private static string NormalizeFolderChoice(string? typed, string resolvedDefault, string previouslySaved)
{
var text = typed?.Trim() ?? "";
if (text.Length == 0) return "";
if (string.Equals(text, resolvedDefault, StringComparison.OrdinalIgnoreCase) &&
string.IsNullOrWhiteSpace(previouslySaved))
return "";
return text;
}
private void Settings_BrowseDatabaseFolderClicked(object sender, RoutedEventArgs e)
{
var chosen = PickFolder("Scegli la cartella dei database (non una cartella sincronizzata)", AppPaths.DatabaseFolder);
if (chosen != null) Settings.DatabaseFolderTextBox.Text = chosen;
}
private void Settings_OpenDatabaseFolderClicked(object sender, RoutedEventArgs e)
=> OpenFolder(AppPaths.DatabaseFolder);
/// <summary>
/// Selettore di cartella. Usa quello di WinForms perché WPF non ne offre uno.
/// </summary>
private static string? PickFolder(string description, string startFrom)
{
try
{
using var dialog = new System.Windows.Forms.FolderBrowserDialog
{
Description = description,
UseDescriptionForTitle = true,
SelectedPath = Directory.Exists(startFrom) ? startFrom : "",
ShowNewFolderButton = true
};
return dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK
? dialog.SelectedPath
: null;
}
catch
{
return null;
}
}
private void OpenFolder(string path)
{
try
{
Directory.CreateDirectory(path);
Process.Start(new ProcessStartInfo { FileName = path, UseShellExecute = true });
}
catch (Exception ex)
{
Log($"[DATI] Impossibile aprire {path}: {ex.Message}", LogLevel.Error);
}
}
/// <summary>
/// Dopo un salvataggio con la cartella dei database cambiata: chiede se spostare i
/// file nella nuova posizione, li sposta a database chiusi, e riavvia. Senza
/// riavvio gli archivi terrebbero in memoria i dati letti dalla posizione vecchia.
/// </summary>
private void ApplyDatabaseFolderSetting(AppSettings settings, string previousFolder)
{
var newFolder = settings.DatabaseFolder.Length > 0 ? settings.DatabaseFolder : AppPaths.DefaultDatabaseFolder;
try { newFolder = Path.GetFullPath(newFolder); } catch { }
if (string.Equals(previousFolder.TrimEnd('\\'), newFolder.TrimEnd('\\'), StringComparison.OrdinalIgnoreCase))
{
RefreshDataFolderFields();
return;
}
var answer = MessageBox.Show(this,
$"La cartella dei database cambia da\n{previousFolder}\na\n{newFolder}\n\n" +
"Vuoi spostare i database esistenti nella nuova cartella?\n\n" +
"Sì: sposto i file e riavvio l'applicazione.\n" +
"No: la nuova cartella parte vuota (i vecchi file restano dove sono) e riavvio.\n" +
"Annulla: torno alla cartella di prima.",
"Cartella dei database", MessageBoxButton.YesNoCancel, MessageBoxImage.Question);
if (answer == MessageBoxResult.Cancel)
{
settings.DatabaseFolder = string.Equals(previousFolder, AppPaths.DefaultDatabaseFolder, StringComparison.OrdinalIgnoreCase) ? "" : previousFolder;
SettingsManager.Save(settings);
RefreshDataFolderFields();
return;
}
try
{
if (answer == MessageBoxResult.Yes)
{
// Prima si chiudono: un file SQLite aperto non si sposta, e il giornale
// WAL va con lui.
AuctionDatabase.CloseInstance();
OperationalDatabase.CloseInstance();
Directory.CreateDirectory(newFolder);
var moved = 0;
foreach (var db in new[] { AppPaths.DatabaseFile, AppPaths.OperationalDatabaseFile })
{
foreach (var f in AppPaths.DatabaseFiles(db))
{
var target = Path.Combine(newFolder, Path.GetFileName(f));
if (File.Exists(target)) File.Delete(target);
File.Move(f, target);
moved++;
}
}
Log($"[DATI] Database spostati in {newFolder} ({moved} file). Riavvio.", LogLevel.Warning);
}
else
{
Log($"[DATI] Nuova cartella dei database: {newFolder} (vuota). Riavvio.", LogLevel.Warning);
}
}
catch (Exception ex)
{
MessageBox.Show(this, $"Spostamento non riuscito: {ex.Message}\n\nLa cartella resta quella di prima.",
"Cartella dei database", MessageBoxButton.OK, MessageBoxImage.Error);
settings.DatabaseFolder = string.Equals(previousFolder, AppPaths.DefaultDatabaseFolder, StringComparison.OrdinalIgnoreCase) ? "" : previousFolder;
SettingsManager.Save(settings);
RefreshDataFolderFields();
return;
}
RestartApplication();
}
/// <summary>Riavvia l'applicazione: lo stesso eseguibile, e questa istanza si chiude.</summary>
private void RestartApplication()
{
try
{
var exe = Environment.ProcessPath;
if (!string.IsNullOrEmpty(exe))
Process.Start(new ProcessStartInfo { FileName = exe, UseShellExecute = true });
}
catch (Exception ex)
{
Log($"[APP] Riavvio non riuscito: {ex.Message}. Riapri l'applicazione a mano.", LogLevel.Error);
}
Application.Current.Shutdown();
}
// ── Esportazione dello storico: chiede sempre dove ───────────────
private void ExportStatsCsvButton_Click(object sender, RoutedEventArgs e)
{
var path = AskWhereToSave("Esporta lo storico in CSV", $"storico-aste-{DateTime.Now:yyyyMMdd-HHmm}.csv", "CSV (foglio di calcolo)|*.csv");
if (path == null) return;
RunExport(StatsExporter.ExportCsv(path), "CSV");
}
private void ExportStatsJsonButton_Click(object sender, RoutedEventArgs e)
{
var path = AskWhereToSave("Esporta lo storico in JSON", $"storico-aste-{DateTime.Now:yyyyMMdd-HHmm}.json", "JSON|*.json");
if (path == null) return;
RunExport(StatsExporter.ExportJson(path), "JSON");
}
/// <summary>La finestra «Salva con nome»: nessun percorso predefinito, decide l'utente.</summary>
private string? AskWhereToSave(string title, string fileName, string filter)
{
try
{
var dialog = new Microsoft.Win32.SaveFileDialog
{
Title = title,
FileName = fileName,
Filter = filter,
OverwritePrompt = true,
AddExtension = true
};
return dialog.ShowDialog(this) == true ? dialog.FileName : null;
}
catch (Exception ex)
{
Log($"[ESPORTA] Finestra di salvataggio non disponibile: {ex.Message}", LogLevel.Error);
return null;
}
}
private void RunExport(StatsExporter.Result result, string format)
{
if (!result.Success)
{
MessageBox.Show(this, result.Error ?? "Esportazione non riuscita.",
$"Esporta {format}", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
Log($"[EXPORT] {result.Records} aste esportate in {result.Path}", LogLevel.Success);
var answer = MessageBox.Show(this,
$"Esportate {result.Records} aste in formato {format}.\n\n{result.Path}\n\n" +
"Vuoi aprire la cartella?",
$"Esporta {format}", MessageBoxButton.YesNo, MessageBoxImage.Information);
if (answer == MessageBoxResult.Yes)
{
OpenFolder(Path.GetDirectoryName(result.Path) ?? AppPaths.ConfigRoot);
}
}
}
}
+420
View File
@@ -0,0 +1,420 @@
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using AutoBidder.Models;
using AutoBidder.Services;
using AutoBidder.Utilities;
namespace AutoBidder
{
/// <summary>
/// Scheda "Puntate": riscatto automatico delle ricompense, contatori e registro.
///
/// <para>Il lavoro vero sta in <see cref="FreeBidsAutoClaimService"/> e
/// <see cref="BidooFreeBidsClaimer"/>: qui c'è solo il collegamento fra quei servizi e
/// l'interfaccia, compreso il passaggio sul thread dell'interfaccia — il ciclo gira su
/// un thread di lavoro e non deve toccare i controlli.</para>
///
/// <para>Resta invece manuale la parte consegnata per notifica push (OneSignal) e per
/// email: quei premi sono agganciati all'abbonamento del browser, non all'account, e
/// non esiste una chiamata da fare con il cookie di sessione.</para>
/// </summary>
public partial class MainWindow
{
private FreeBidsAutoClaimService? _freeBidsService;
private BidooFreeBidsClaimer? _freeBidsClaimer;
private BidooPromoRedeemer? _promoRedeemer;
/// <summary>Esito dell'ultima raccolta, da mostrare nella riga di stato.</summary>
private string? _lastHarvestOutcome;
/// <summary>
/// Indirizzi presi dai riferimenti configurati, non scritti qui: sono gli stessi che
/// usa il riscatto automatico, e devono restare gli stessi anche quando cambiano —
/// altrimenti «Apri su Bidoo» porterebbe a una pagina diversa da quella che
/// l'applicazione sta davvero interrogando.
/// </summary>
private static FreeBidsSiteConfig FreeBidsConfig => FreeBidsConfigStore.Current;
private static string ChestsUrl => FreeBidsConfig.Url(FreeBidsConfig.Endpoints.Rewards);
private void StartFreeBidsService()
{
try
{
// Alla prima esecuzione il file dei riferimenti non c'è: crearlo subito è il
// modo per farne sapere l'esistenza a chi dovrà correggerlo.
if (FreeBidsConfigStore.EnsureFileExists())
Log($"[PUNTATE] Riferimenti del riscatto creati in {FreeBidsConfigStore.FilePath}", LogLevel.Info);
// Il file va letto davvero prima di poter dire se ha qualcosa che non va:
// un problema non ancora incontrato non è un problema che si può segnalare.
FreeBidsConfigStore.Reload();
if (FreeBidsConfigStore.LastProblem is { } problem)
Log($"[PUNTATE] {problem}", LogLevel.Warning);
var transport = _auctionMonitor.GetApiClient().Transport;
_freeBidsClaimer = new BidooFreeBidsClaimer(transport)
{
Diagnostic = msg => Dispatcher.BeginInvoke(() => Log($"[PUNTATE] {msg}", LogLevel.Info))
};
_promoRedeemer = new BidooPromoRedeemer(transport)
{
Diagnostic = msg => Dispatcher.BeginInvoke(() =>
{
Log($"[PUNTATE] {msg}", LogLevel.Info);
FreeBids.AppendActivity(msg);
})
};
_freeBidsService = new FreeBidsAutoClaimService(_freeBidsClaimer, ReadBalanceAsync, _promoRedeemer);
_freeBidsService.OnLog += (message, isProblem) => Dispatcher.BeginInvoke(() =>
{
Log($"[PUNTATE] {message}", isProblem ? LogLevel.Warning : LogLevel.Success);
FreeBids.AppendActivity(message, isProblem);
});
_freeBidsService.OnCycleCompleted += _ => Dispatcher.BeginInvoke(() =>
{
if (_freeBidsService?.LastHarvest is { } harvest) _lastHarvestOutcome = harvest.Message;
RefreshFreeBidsPanel();
});
var settings = SettingsManager.Load();
FreeBids.SetOptions(settings.FreeBidsAutoClaimEnabled, settings.FreeBidsCheckMinutes);
if (settings.FreeBidsAutoClaimEnabled) _freeBidsService.Start();
RefreshFreeBidsPanel();
}
catch (Exception ex)
{
// Il riscatto è un accessorio: se non parte, il resto dell'applicazione
// deve continuare a funzionare senza accorgersene.
Log($"[PUNTATE] Riscatto automatico non avviato: {ex.Message}", LogLevel.Error);
}
}
/// <summary>
/// Rilegge il saldo dal sito. Serve al servizio per misurare quante puntate ha
/// portato davvero un riscatto, invece di fidarsi del messaggio di risposta.
/// </summary>
private async Task<int?> ReadBalanceAsync(CancellationToken ct)
{
await _auctionMonitor.UpdateUserInfoAsync().ConfigureAwait(false);
return _auctionMonitor.GetSession()?.RemainingBids;
}
private void RefreshFreeBidsPanel()
{
try
{
var session = _auctionMonitor.GetSession();
FreeBids.SetBalance(session?.RemainingBids);
FreeBids.SetSchedule(
running: _freeBidsService?.IsRunning == true,
lastCheck: _freeBidsService?.LastCheckAt,
nextCheck: _freeBidsService?.NextCheckAt);
FreeBids.SetCounters(FreeBidsStats.Read());
FreeBids.SetConfigPath(FreeBidsConfigStore.FilePath, FreeBidsConfigStore.LastProblem);
FreeBids.SetHarvestStatus(
FreeBidsConfig.PromoHarvest.SourceUrl,
ClaimedPromoStore.ClaimedCount,
_lastHarvestOutcome);
}
catch { }
}
// ── Eventi della scheda ──────────────────────────────────────────
private async void FreeBids_RefreshBalanceClicked(object sender, RoutedEventArgs e)
{
try
{
await _auctionMonitor.UpdateUserInfoAsync();
RefreshFreeBidsPanel();
UpdateRemainingBidsDisplay();
Log("[PUNTATE] Saldo aggiornato", LogLevel.Info);
}
catch (Exception ex)
{
Log($"[PUNTATE] Aggiornamento saldo non riuscito: {ex.Message}", LogLevel.Error);
}
}
private async void FreeBids_CheckNowClicked(object sender, RoutedEventArgs e)
{
if (_freeBidsService == null) return;
try
{
FreeBids.AppendActivity("Controllo richiesto a mano…");
var result = await _freeBidsService.CheckNowAsync();
if (result.IsSuccess && result.ClaimedCount == 0)
FreeBids.AppendActivity(result.Message);
RefreshFreeBidsPanel();
UpdateRemainingBidsDisplay();
}
catch (Exception ex)
{
Log($"[PUNTATE] Controllo non riuscito: {ex.Message}", LogLevel.Error);
FreeBids.AppendActivity($"controllo non riuscito: {ex.Message}", isProblem: true);
}
}
/// <summary>Apre la pagina nel browser predefinito di Windows, fuori dall'applicazione.</summary>
private void FreeBids_OpenExternalClicked(object sender, RoutedEventArgs e)
{
try
{
Process.Start(new ProcessStartInfo(ChestsUrl) { UseShellExecute = true });
}
catch (Exception ex)
{
Log($"[PUNTATE] Apertura nel browser esterno non riuscita: {ex.Message}", LogLevel.Error);
}
}
private void FreeBids_OpenInternalClicked(object sender, RoutedEventArgs e)
=> OpenInEmbeddedBrowser(ChestsUrl);
private void FreeBids_OpenVouchersClicked(object sender, RoutedEventArgs e)
=> OpenInEmbeddedBrowser(FreeBidsConfig.Url(FreeBidsConfig.Endpoints.Vouchers));
private void FreeBids_OpenBuyBidsClicked(object sender, RoutedEventArgs e)
=> OpenInEmbeddedBrowser(FreeBidsConfig.Url(FreeBidsConfig.Endpoints.BuyBids));
/// <summary>
/// Riscatta il codice o il collegamento incollato dall'utente.
///
/// <para>Il saldo viene riletto in ogni caso: il messaggio del sito dice quello che
/// vuole, la differenza di puntate sul conto no.</para>
/// </summary>
private async void FreeBids_ClaimPromoClicked(object sender, RoutedEventArgs e)
{
if (_freeBidsClaimer == null) return;
var code = FreeBids.PromoCode;
if (code.Length == 0)
{
FreeBids.AppendActivity("manca il codice o il collegamento da riscattare", isProblem: true);
return;
}
try
{
FreeBids.AppendActivity("Riscatto del codice richiesto a mano…");
var before = _auctionMonitor.GetSession()?.RemainingBids;
var result = await _freeBidsClaimer.ClaimPromoAsync(code);
if (!result.IsSuccess)
{
FreeBids.AppendActivity(result.Message, isProblem: true);
Log($"[PUNTATE] Riscatto codice non riuscito: {result.Message}", LogLevel.Warning);
return;
}
await _auctionMonitor.UpdateUserInfoAsync();
var after = _auctionMonitor.GetSession()?.RemainingBids;
var gained = before.HasValue && after.HasValue ? Math.Max(0, after.Value - before.Value) : 0;
if (gained > 0) FreeBidsStats.RecordClaim(claimedCount: 1, bidsGained: gained);
FreeBids.AppendActivity(gained > 0
? $"codice riscattato: +{gained} puntate"
: $"codice accettato ({result.Message}), saldo invariato");
FreeBids.ClearPromoCode();
RefreshFreeBidsPanel();
UpdateRemainingBidsDisplay();
}
catch (Exception ex)
{
Log($"[PUNTATE] Riscatto codice non riuscito: {ex.Message}", LogLevel.Error);
FreeBids.AppendActivity($"riscatto non riuscito: {ex.Message}", isProblem: true);
}
}
/// <summary>
/// Esegue subito una raccolta dei collegamenti pubblicati, senza aspettare il giro.
///
/// <para>Le puntate ottenute si misurano sul saldo prima e dopo: la pagina di Bidoo
/// risponde allo stesso modo per un codice appena riscosso e per uno già usato.</para>
/// </summary>
private async void FreeBids_HarvestClicked(object sender, RoutedEventArgs e)
{
if (_promoRedeemer == null) return;
try
{
FreeBids.AppendActivity("Raccolta dei collegamenti richiesta a mano…");
var before = _auctionMonitor.GetSession()?.RemainingBids;
var report = await _promoRedeemer.HarvestAndRedeemAsync();
_lastHarvestOutcome = report.Message;
if (!report.SourceReadable)
{
FreeBids.AppendActivity(report.Message, isProblem: true);
Log($"[PUNTATE] Raccolta non riuscita: {report.Message}", LogLevel.Warning);
RefreshFreeBidsPanel();
return;
}
if (report.Claimed > 0)
{
await _auctionMonitor.UpdateUserInfoAsync();
var after = _auctionMonitor.GetSession()?.RemainingBids;
var gained = before.HasValue && after.HasValue ? Math.Max(0, after.Value - before.Value) : 0;
FreeBidsStats.RecordClaim(report.Claimed, gained);
FreeBids.AppendActivity(gained > 0
? $"{report.Message}: +{gained} puntate"
: $"{report.Message} (saldo invariato)");
UpdateRemainingBidsDisplay();
}
else
{
FreeBids.AppendActivity(report.Message);
}
RefreshFreeBidsPanel();
}
catch (Exception ex)
{
Log($"[PUNTATE] Raccolta non riuscita: {ex.Message}", LogLevel.Error);
FreeBids.AppendActivity($"raccolta non riuscita: {ex.Message}", isProblem: true);
}
}
private void FreeBids_OpenPromoSourceClicked(object sender, RoutedEventArgs e)
=> OpenInEmbeddedBrowser(FreeBidsConfig.PromoHarvest.SourceUrl);
/// <summary>
/// Azzera la memoria dei collegamenti già aperti. Serve dopo un cambio di account:
/// i codici presi da un utente non dicono nulla su quelli disponibili per un altro.
/// </summary>
private void FreeBids_ForgetPromosClicked(object sender, RoutedEventArgs e)
{
var answer = MessageBox.Show(this,
$"Vuoi dimenticare i {ClaimedPromoStore.ClaimedCount} codici già presi?\n\n" +
"Alla raccolta successiva verranno riaperti tutti i collegamenti pubblicati, " +
"compresi quelli già usati: serve dopo un cambio di account, non nell'uso normale.",
"Puntate", MessageBoxButton.YesNo, MessageBoxImage.Warning);
if (answer != MessageBoxResult.Yes) return;
ClaimedPromoStore.Clear();
_lastHarvestOutcome = null;
RefreshFreeBidsPanel();
Log("[PUNTATE] Memoria dei collegamenti azzerata", LogLevel.Info);
}
/// <summary>
/// Apre <c>site-config.json</c> con l'editor predefinito. Il file viene creato se
/// manca: aprire il vuoto non direbbe a nessuno quali valori si possono cambiare.
/// </summary>
private void FreeBids_OpenConfigClicked(object sender, RoutedEventArgs e)
{
try
{
FreeBidsConfigStore.EnsureFileExists();
Process.Start(new ProcessStartInfo(FreeBidsConfigStore.FilePath) { UseShellExecute = true });
FreeBids.AppendActivity(
"Riferimenti aperti: salva il file e premi «Controlla adesso», " +
"le modifiche valgono subito.");
}
catch (Exception ex)
{
Log($"[PUNTATE] Apertura dei riferimenti non riuscita: {ex.Message}", LogLevel.Error);
}
}
private void FreeBids_ResetCountersClicked(object sender, RoutedEventArgs e)
{
var answer = MessageBox.Show(this,
"Vuoi azzerare i contatori dei riscatti?\n\n" +
"Si perde il totale storico delle puntate raccolte. Le puntate sul conto non vengono toccate.",
"Puntate", MessageBoxButton.YesNo, MessageBoxImage.Warning);
if (answer != MessageBoxResult.Yes) return;
FreeBidsStats.Clear();
RefreshFreeBidsPanel();
Log("[PUNTATE] Contatori azzerati", LogLevel.Info);
}
private void FreeBids_OptionsChanged(object sender, RoutedEventArgs e)
{
try
{
var settings = SettingsManager.Load();
settings.FreeBidsAutoClaimEnabled = FreeBids.AutoClaimEnabled;
settings.FreeBidsCheckMinutes = FreeBids.CheckMinutes;
SettingsManager.Save(settings);
if (_freeBidsService != null)
{
if (settings.FreeBidsAutoClaimEnabled && !_freeBidsService.IsRunning)
{
_freeBidsService.Start();
Log($"[PUNTATE] Riscatto automatico attivo, ogni {settings.FreeBidsCheckMinutes} minuti", LogLevel.Success);
}
else if (!settings.FreeBidsAutoClaimEnabled && _freeBidsService.IsRunning)
{
_freeBidsService.Stop();
Log("[PUNTATE] Riscatto automatico spento", LogLevel.Info);
}
}
RefreshFreeBidsPanel();
}
catch (Exception ex)
{
Log($"[PUNTATE] Impostazioni non salvate: {ex.Message}", LogLevel.Error);
}
}
/// <summary>
/// Apre una pagina nel browser integrato. Deve essere quello, non il browser di
/// sistema: è lì che la sessione è attiva ed è lì che vanno concesse le notifiche.
/// </summary>
private void OpenInEmbeddedBrowser(string url)
{
try
{
TabBrowser.IsChecked = true;
Browser.ShowBrowser();
Browser.EmbeddedWebView?.CoreWebView2?.Navigate(url);
Browser.BrowserAddress.Text = url;
}
catch (Exception ex)
{
Log($"[PUNTATE] Apertura pagina non riuscita: {ex.Message}", LogLevel.Error);
}
}
}
}
+54 -10
View File
@@ -6,27 +6,57 @@ using AutoBidder.Utilities;
namespace AutoBidder
{
/// <summary>
/// Logging functionality with color-coded severity levels
/// Logging functionality with color-coded severity levels and configurable minimum level filtering
/// </summary>
public partial class MainWindow
{
/// <summary>
/// Scrive un messaggio nel log globale con filtraggio basato sul livello minimo configurato
/// </summary>
/// <param name="message">Messaggio da loggare</param>
/// <param name="level">Livello di severit del messaggio</param>
private void Log(string message, LogLevel level = LogLevel.Info)
{
// Il file si scrive subito, fuori dal Dispatcher e senza filtro di livello:
// il registro serve proprio quando qualcosa è andato storto, e in quel momento
// il messaggio utile è spesso uno di quelli che a video sono nascosti. La
// scrittura è accodata, quindi non costa nulla al chiamante.
TextLogService.App(LevelTag(level), message);
Dispatcher.BeginInvoke(() =>
{
try
{
// Carica impostazioni per ottenere livello minimo e limite righe
var settings = SettingsManager.Load();
// Filtra messaggi in base al livello minimo configurato
MinimumLogLevel minLevel = MinimumLogLevel.Normal; // Default
if (Enum.TryParse<MinimumLogLevel>(settings.MinLogLevel, out var parsedLevel))
{
minLevel = parsedLevel;
}
// Se il livello del messaggio maggiore del minimo configurato, ignora
if ((int)level > (int)minLevel)
{
return;
}
var timestamp = DateTime.Now.ToString("HH:mm:ss");
var logEntry = $"[{timestamp}] {message}";
var logEntry = $"[{timestamp}] [{LevelTag(level)}] {message}";
// Color coding based on severity for dark theme
var color = level switch
{
LogLevel.Error => new SolidColorBrush(Color.FromRgb(232, 17, 35)), // #E81123 (Red)
LogLevel.Warn => new SolidColorBrush(Color.FromRgb(255, 183, 0)), // #FFB700 (Yellow/Orange)
LogLevel.Success => new SolidColorBrush(Color.FromRgb(0, 216, 0)), // #00D800 (Green)
LogLevel.Info => new SolidColorBrush(Color.FromRgb(100, 180, 255)), // #64B4FF (Light Blue - più chiaro e leggibile)
_ => new SolidColorBrush(Color.FromRgb(204, 204, 204)) // #CCCCCC (Light Gray)
LogLevel.Error => new SolidColorBrush(Color.FromRgb(232, 17, 35)), // #E81123 (Red)
LogLevel.Warning => new SolidColorBrush(Color.FromRgb(255, 191, 0)), // #FFBF00 (Yellow)
LogLevel.Success => new SolidColorBrush(Color.FromRgb(0, 216, 0)), // #00D800 (Green)
LogLevel.Info => new SolidColorBrush(Color.FromRgb(100, 180, 255)), // #64B4FF (Light Blue)
LogLevel.Debug => new SolidColorBrush(Color.FromRgb(255, 140, 255)), // #FF8CFF (Magenta)
LogLevel.Trace => new SolidColorBrush(Color.FromRgb(160, 160, 160)), // #A0A0A0 (Gray)
_ => new SolidColorBrush(Color.FromRgb(204, 204, 204)) // #CCCCCC (Light Gray)
};
var p = new System.Windows.Documents.Paragraph { Margin = new Thickness(0, 2, 0, 2) };
@@ -34,13 +64,12 @@ namespace AutoBidder
p.Inlines.Add(r);
LogBox.Document.Blocks.Add(p);
// ? Mantieni solo gli ultimi N paragrafi (configurabile dalle impostazioni)
var settings = SettingsManager.Load();
// Mantieni solo gli ultimi N paragrafi (configurabile dalle impostazioni)
int maxLogLines = settings.MaxGlobalLogLines;
if (LogBox.Document.Blocks.Count > maxLogLines)
{
// Rimuovi i paragrafi più vecchi (primi inseriti)
// Rimuovi i paragrafi pi vecchi (primi inseriti)
int excessCount = LogBox.Document.Blocks.Count - maxLogLines;
for (int i = 0; i < excessCount; i++)
{
@@ -60,5 +89,20 @@ namespace AutoBidder
catch { }
});
}
/// <summary>
/// Sigla del livello, uguale a video e su file: due formati diversi renderebbero
/// impossibile ritrovare nel file la riga che si è vista nella finestra.
/// </summary>
private static string LevelTag(LogLevel level) => level switch
{
LogLevel.Error => "ERROR",
LogLevel.Warning => "WARN",
LogLevel.Info => "INFO",
LogLevel.Success => "OK",
LogLevel.Debug => "DEBUG",
LogLevel.Trace => "TRACE",
_ => "LOG"
};
}
}
+216
View File
@@ -0,0 +1,216 @@
using System;
using System.Linq;
using System.Windows;
using System.Windows.Threading;
using AutoBidder.Models;
using AutoBidder.Utilities;
using AutoBidder.ViewModels;
namespace AutoBidder
{
/// <summary>
/// Barra strumenti del Monitor: contatori delle aste e stato del motore.
///
/// Sono informazioni che cambiano di continuo ma non a ogni singolo poll: un
/// battito fisso di un secondo le tiene aggiornate senza far ridisegnare la
/// barra decine di volte al secondo.
/// </summary>
public partial class MainWindow
{
private DispatcherTimer? _headerTimer;
private DispatcherTimer? _timerTicker;
private void StartMonitorHeaderTimer()
{
_headerTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
_headerTimer.Tick += (_, _) => RefreshMonitorHeader();
_headerTimer.Start();
// ── Il conto alla rovescia scorre da solo ────────────────────────
//
// Il tempo mancante non ha bisogno di una risposta dal server per essere
// ricalcolato: la scadenza è ancorata all'orologio di Bidoo e il resto è
// sottrazione. Prima però veniva notificato solo all'arrivo di un poll, e
// nelle prove reali fra un poll e l'altro passava anche un secondo e mezzo:
// il numero restava fermo e sembrava tutto bloccato.
//
// Cinque battiti al secondo: la colonna mostra i decimi sotto il minuto,
// quindi si vede scorrere davvero, e restano un ventesimo delle notifiche
// che servirebbero per seguire il decimo esatto.
_timerTicker = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(200) };
_timerTicker.Tick += (_, _) => TickTimers();
_timerTicker.Start();
RefreshMonitorHeader();
}
private void TickTimers()
{
var vms = _auctionViewModels;
for (var i = 0; i < vms.Count; i++)
{
try { vms[i].RefreshTimeDisplay(); }
catch { /* una riga che non si aggiorna non deve fermare le altre */ }
}
}
private void RefreshMonitorHeader()
{
try
{
var vms = _auctionViewModels;
var active = 0;
var watch = 0;
var stopped = 0;
var won = 0;
var lost = 0;
foreach (var vm in vms)
{
switch (vm.StatusKind)
{
case "won": won++; break;
case "lost": lost++; break;
case "leading":
case "active": active++; break;
case "watch": watch++; break;
default: stopped++; break;
}
}
AuctionMonitor.UpdateCounters(vms.Count, active, watch, stopped, won, lost);
var clock = _auctionMonitor.Clock;
AuctionMonitor.UpdateEngineStatus(
_auctionMonitor.RequestsSent, clock.IsSynced, clock.SampleCount, clock.AverageLatencyMs);
RefreshAccountPills();
// Lo stato dei pulsanti globali dipende da tutte le aste: ricalcolarlo a
// ogni risposta di ogni asta era uno scorrimento completo dell'elenco
// decine di volte al secondo. Una volta al secondo è più che sufficiente.
UpdateGlobalControlButtons();
if (_selectedAuction != null) RefreshSelectedStats(_selectedAuction);
}
catch { /* la barra informativa non deve mai disturbare il resto */ }
}
/// <summary>
/// Puntate, credito e vincite da confermare, riletti dalla sessione viva a ogni
/// battito.
///
/// <para>Prima ciascuno di questi numeri veniva scritto da chi lo aggiornava —
/// il ripristino della sessione, il timer di rinfresco, il codice dopo la puntata —
/// e bastava che uno dei tre non passasse perché la barra restasse su un valore
/// vecchio. Rileggerli qui costa una lettura di campo al secondo e toglie di mezzo
/// tutta quella categoria di errori.</para>
/// </summary>
private void RefreshAccountPills()
{
var session = _auctionMonitor.GetSession();
var connected = session != null && !string.IsNullOrEmpty(session.Username);
AuctionMonitor.UpdateAccountStatus(
connected ? session!.RemainingBids : null,
connected ? (decimal)session!.ShopCredit : null,
AuctionsToConfirm);
UpdateMinBidsIndicator(SettingsManager.Load().MinimumRemainingBids);
}
/// <summary>Contatori del motore relativi alla sola asta selezionata.</summary>
private void RefreshSelectedStats(AuctionViewModel vm)
{
try
{
var info = vm.AuctionInfo;
AuctionMonitor.UpdateSelectedStats(
info.ResetCount,
info.BidsUsedOnThisAuction ?? vm.MyClicks,
info.AverageLatencyMs,
info.PollCount,
info.PollErrors);
}
catch { }
}
/// <summary>
/// Scheda Prodotto: costo totale se si vince e verdetto di convenienza.
/// </summary>
private void RefreshProductVerdict(AuctionViewModel? vm)
{
try
{
var totalBox = AuctionMonitor.FindName("ProductTotalCostText") as System.Windows.Controls.TextBlock;
var value = vm?.AuctionInfo.CalculatedValue;
if (value == null)
{
if (totalBox != null) totalBox.Text = "—";
AuctionMonitor.SetVerdict("Convenienza", "non calcolabile", "neutral");
return;
}
if (totalBox != null) totalBox.Text = $"{value.TotalCostIfWin:F2} €";
if (value.Savings is not { } savings)
{
AuctionMonitor.SetVerdict("Convenienza", "valore prodotto ignoto", "neutral");
return;
}
var pct = Math.Abs(value.SavingsPercentage ?? 0);
AuctionMonitor.SetVerdict(
savings > 0 ? "Risparmio" : "Sovrapprezzo",
$"{Math.Abs(savings):F2} € ({pct:F1}%)",
savings > 0 ? "ok" : "danger");
}
catch { }
}
/// <summary>
/// "Rimuovi le aste concluse": ripulisce l'elenco da ciò che non serve più
/// senza toccare quelle ancora in corso.
/// </summary>
private void AuctionMonitor_RemoveFinishedClicked(object sender, RoutedEventArgs e)
{
try
{
var finished = _auctionViewModels
.Where(a => a.StatusKind is "won" or "lost")
.ToList();
if (finished.Count == 0)
{
MessageBox.Show(this, "Nessuna asta conclusa da rimuovere.", "Aste concluse",
MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
foreach (var vm in finished)
{
_auctionMonitor.RemoveAuction(vm.AuctionId);
_auctionViewModels.Remove(vm);
}
if (_selectedAuction != null && !_auctionViewModels.Contains(_selectedAuction))
_selectedAuction = null;
SaveAuctions();
UpdateTotalCount();
UpdateGlobalControlButtons();
RefreshMonitorHeader();
Log($"[REMOVE] Rimosse {finished.Count} aste concluse", LogLevel.Info);
}
catch (Exception ex)
{
Log($"[ERRORE] Rimozione aste concluse: {ex.Message}", LogLevel.Error);
}
}
}
}
+124
View File
@@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using AutoBidder.Models;
using AutoBidder.Services;
using AutoBidder.Utilities;
using AutoBidder.ViewModels;
namespace AutoBidder
{
/// <summary>
/// Prodotti seguiti: le aste nuove dello stesso articolo entrano nel monitor da sole.
///
/// L'aggiunta qui deve essere silenziosa — succede mentre l'utente sta facendo altro,
/// quindi niente finestre di dialogo: solo righe di log.
/// </summary>
public partial class MainWindow
{
private ProductWatchService? _productWatcher;
/// <summary>Aste presenti nel monitor perché aggiunte automaticamente.</summary>
private readonly HashSet<string> _autoAddedAuctionIds = new(StringComparer.Ordinal);
private void StartProductWatcher()
{
try
{
_productWatcher = new ProductWatchService(CatalogClient)
{
CountAutoAdded = () => _autoAddedAuctionIds.Count(id =>
_auctionViewModels.Any(a => a.AuctionId == id))
};
_productWatcher.OnLog += msg => Dispatcher.BeginInvoke(() => Log(msg, LogLevel.Info));
_productWatcher.OnAuctionFound += (auction, product) =>
Dispatcher.BeginInvoke(() => AutoAddAuction(auction, product));
_productWatcher.Start();
}
catch (Exception ex)
{
Log($"[SEGUITI] Impossibile avviare la sorveglianza: {ex.Message}", LogLevel.Error);
}
}
/// <summary>
/// Aggiunge al monitor un'asta trovata dalla sorveglianza. Volutamente separata da
/// AddAuctionById: quella mostra finestre di dialogo, qui inaccettabili.
/// </summary>
private void AutoAddAuction(CatalogAuction auction, WatchedProduct product)
{
try
{
// Marcala comunque: anche se la scartiamo, non va riproposta a ogni giro.
WatchedProductsStore.MarkHandled(auction.AuctionId);
if (_auctionViewModels.Any(a => a.AuctionId == auction.AuctionId)) return;
var settings = SettingsManager.Load();
// I limiti del prodotto, dove ci sono, hanno la precedenza sui predefiniti:
// è il senso della scheda Prodotti.
var limits = ProductRuleResolver.Resolve(product, settings);
var info = new AuctionInfo
{
AuctionId = auction.AuctionId,
Name = DecodeAllHtmlEntities(auction.Name),
OriginalUrl = auction.Url,
BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs,
BuyNowPrice = auction.BuyNowPrice.HasValue ? (double)auction.BuyNowPrice.Value : null,
State = limits.State
};
_auctionMonitor.AddAuction(info);
var vm = new AuctionViewModel(info)
{
MinPrice = limits.MinPrice,
MaxPrice = limits.MaxPrice,
MaxClicks = limits.MaxClicks
};
_auctionViewModels.Add(vm);
_autoAddedAuctionIds.Add(auction.AuctionId);
// Un'asta che entra in Osserva o Attiva ha bisogno del motore acceso.
if (info.State != RunState.Stopped && !_isAutomationActive)
{
_auctionMonitor.Start();
_isAutomationActive = true;
}
WatchedProductsStore.CountAutoAdd(product);
SaveAuctions();
UpdateTotalCount();
UpdateGlobalControlButtons();
RefreshMonitorHeader();
var origine = limits.FromProduct ? " — limiti del prodotto" : "";
Log($"[SEGUITI] Aggiunta automaticamente: {info.Name} ({StateLabel(product.AutoAddState ?? settings.AutoAddNewAuctionState)}){origine}",
LogLevel.Success);
}
catch (Exception ex)
{
Log($"[SEGUITI] Aggiunta automatica non riuscita: {ex.Message}", LogLevel.Error);
}
}
/// <summary>Applica a caldo le impostazioni della sorveglianza dopo un salvataggio.</summary>
private void ApplyProductWatchSettings(AppSettings settings)
{
if (_productWatcher == null) return;
if (settings.AutoAddProductsEnabled && !_productWatcher.IsRunning) _productWatcher.Start();
else if (!settings.AutoAddProductsEnabled && _productWatcher.IsRunning) _productWatcher.Stop();
}
// L'elenco dei prodotti, con stellina e limiti, vive ora nella scheda Prodotti:
// vedi MainWindow.Products.cs. Qui restano solo l'avvio della sorveglianza e
// l'aggiunta automatica delle aste che trova.
}
}
+496
View File
@@ -0,0 +1,496 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using AutoBidder.Models;
using AutoBidder.Utilities;
using AutoBidder.ViewModels;
namespace AutoBidder
{
/// <summary>
/// Scheda Prodotti: l'elenco degli articoli con la stellina e i limiti su misura.
///
/// <para>Raccoglie in un posto solo ciò che prima stava diviso fra le Impostazioni
/// (l'elenco dei prodotti seguiti) e i valori predefiniti delle aste. Ragionare per
/// prodotto è il modo naturale di usare Bidoo, che rimette all'asta lo stesso articolo
/// di continuo: i limiti giusti per un buono da 50 € non sono quelli di un telefono.</para>
/// </summary>
public partial class MainWindow
{
private readonly ObservableCollection<ProductViewModel> _productViewModels = new();
/// <summary>Stellina di una riga della scheda Prodotti.</summary>
public RelayCommand? ProductWatchCommand { get; private set; }
/// <summary>Applica i limiti consigliati alla singola riga.</summary>
public RelayCommand? ProductApplyAdviceCommand { get; private set; }
private void InitializeProductCommands()
{
ProductWatchCommand = new RelayCommand(p => ToggleProductWatch(p as ProductViewModel));
ProductApplyAdviceCommand = new RelayCommand(p => ApplyAdviceToProduct(p as ProductViewModel));
}
/// <summary>
/// Ricostruisce l'elenco unendo le schede prodotto e lo storico. Le statistiche
/// arrivano dallo stesso aggregato che alimenta la scheda Statistiche: un solo modo
/// di calcolarle, così i due pannelli non possono raccontare numeri diversi.
/// </summary>
private void LoadProducts()
{
try
{
var selectedIdentity = Products.SelectedProduct?.Identity;
var stats = CompletedAuctionsStore.AggregateByProduct()
.GroupBy(s => s.ProductKey, StringComparer.Ordinal)
.ToDictionary(g => g.Key, g => g.First(), StringComparer.Ordinal);
_productViewModels.Clear();
foreach (var rule in WatchedProductsStore.GetAll()
.OrderByDescending(r => r.IsWatched)
.ThenBy(r => r.DisplayName, StringComparer.CurrentCultureIgnoreCase))
{
stats.TryGetValue(StatsKeyFor(rule), out var summary);
_productViewModels.Add(new ProductViewModel(rule, summary));
}
Products.SetProducts(_productViewModels);
// Ricaricare non deve far perdere il posto in cui si stava lavorando.
if (selectedIdentity != null) Products.SelectProduct(selectedIdentity);
}
catch (Exception ex)
{
Log($"[PRODOTTI] Caricamento non riuscito: {ex.Message}", LogLevel.Error);
}
}
/// <summary>
/// Chiave con cui il prodotto compare nello storico. Le schede salvate senza chiave
/// (le più vecchie) la ricavano dal nome, com'è per le aste concluse.
/// </summary>
private static string StatsKeyFor(WatchedProduct rule) =>
string.IsNullOrEmpty(rule.ProductKey)
? ProductKeyHelper.GenerateProductKey(rule.DisplayName)
: rule.ProductKey;
private void ToggleProductWatch(ProductViewModel? product)
{
if (product == null) return;
try
{
product.IsWatched = !product.IsWatched;
WatchedProductsStore.Persist();
Products.RefreshCounters();
MarkWatchedProducts(_catalogAuctions);
Log(product.IsWatched
? $"[PRODOTTI] Ora seguo: {product.DisplayName}"
: $"[PRODOTTI] Non seguo più: {product.DisplayName} (i limiti restano)",
LogLevel.Info);
}
catch (Exception ex)
{
Log($"[PRODOTTI] {ex.Message}", LogLevel.Error);
}
}
/// <summary>
/// Mette il prodotto in elenco e apre la scheda Prodotti su quella riga. È la strada
/// dell'ingranaggio sulle schede del catalogo: serve a dare limiti a un articolo
/// anche senza volerlo seguire.
/// </summary>
private void ConfigureCatalogProduct(CatalogAuction? auction)
{
if (auction == null) return;
try
{
var existing = WatchedProductsStore.FindFor(auction);
if (existing == null)
{
// Senza stellina: chi arriva da qui vuole i limiti, non l'aggiunta automatica.
WatchedProductsStore.Add(auction, watched: false);
Log($"[PRODOTTI] Aggiunto all'elenco: {auction.Name}", LogLevel.Success);
}
else
{
Log($"[PRODOTTI] {auction.Name} era già in elenco: apro la sua riga", LogLevel.Info);
}
MarkWatchedProducts(_catalogAuctions);
// Il cambio scheda ricarica già l'elenco, ma solo se la scheda cambia
// davvero: ricaricare qui rende la riga nuova visibile in ogni caso.
TabProdotti.IsChecked = true;
LoadProducts();
var identity = WatchedProductsStore.FindFor(auction)?.Identity;
if (identity != null) Products.SelectProduct(identity);
}
catch (Exception ex)
{
Log($"[PRODOTTI] Aggiunta non riuscita: {ex.Message}", LogLevel.Error);
}
}
// ── Eventi della scheda ──────────────────────────────────────────
private void Products_RefreshClicked(object sender, RoutedEventArgs e) => LoadProducts();
private void Products_ProductLimitsEdited(object sender, RoutedEventArgs e)
{
try
{
PersistProductLimits();
// Il tetto consigliato dipende dalle puntate massime: se è quella che è
// appena cambiata, la colonna accanto deve rifarsi il conto.
Products.SelectedProduct?.RefreshAdvice();
}
catch (Exception ex)
{
Log($"[PRODOTTI] Salvataggio non riuscito: {ex.Message}", LogLevel.Error);
}
}
private void Products_ProductSelectionChanged(object sender, RoutedEventArgs e)
{
// Il riquadro statistiche si aggiorna da solo: è legato alla riga selezionata.
}
private async void Products_ScanNowClicked(object sender, RoutedEventArgs e)
{
if (_productWatcher == null) return;
Log("[PRODOTTI] Cerco aste dei prodotti seguiti…", LogLevel.Info);
await _productWatcher.ScanNowAsync();
LoadProducts();
}
private void Products_RemoveClicked(object sender, RoutedEventArgs e)
{
var product = Products.SelectedProduct;
if (product == null)
{
MessageBox.Show(this, "Seleziona prima un prodotto dall'elenco.",
"Prodotti", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
var answer = MessageBox.Show(this,
$"Vuoi togliere \"{product.DisplayName}\" dall'elenco?\n\n" +
"Si perdono i limiti impostati per questo prodotto. Le aste già nel monitor restano dove sono.",
"Prodotti", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (answer != MessageBoxResult.Yes) return;
WatchedProductsStore.RemoveByIdentity(product.Identity);
LoadProducts();
MarkWatchedProducts(_catalogAuctions);
Log($"[PRODOTTI] Tolto dall'elenco: {product.DisplayName}", LogLevel.Info);
}
private void Products_ClearClicked(object sender, RoutedEventArgs e)
{
if (_productViewModels.Count == 0) return;
var answer = MessageBox.Show(this,
$"Vuoi svuotare l'elenco dei {_productViewModels.Count} prodotti?\n\n" +
"Si perdono tutti i limiti impostati. Le aste già nel monitor restano dove sono.",
"Prodotti", MessageBoxButton.YesNo, MessageBoxImage.Warning);
if (answer != MessageBoxResult.Yes) return;
WatchedProductsStore.Clear();
LoadProducts();
MarkWatchedProducts(_catalogAuctions);
Log("[PRODOTTI] Elenco svuotato", LogLevel.Info);
}
/// <summary>
/// Rifà i numeri di ogni scheda prodotto dallo storico, con la finestra di
/// avanzamento. Vedi <see cref="ProductStatsStore.RebuildFromHistory"/>.
/// </summary>
private async void Products_RecalculateClicked(object sender, RoutedEventArgs e)
{
var aste = CompletedAuctionsStore.LoadAll().Count;
if (aste == 0)
{
MessageBox.Show(this, "Lo storico è vuoto: non c'è niente da cui ricalcolare.",
"Ricalcola dallo storico", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
var answer = MessageBox.Show(this,
$"Rileggo tutto lo storico ({aste:N0} aste concluse) e ricalcolo per ogni prodotto: aste viste, " +
"prezzo minimo, massimo e medio, puntate del vincitore, vittorie e osservazioni.\n\n" +
"I limiti scritti a mano e la stellina non cambiano; cambiano i numeri, e quindi i limiti consigliati.\n\nProcedo?",
"Ricalcola dallo storico", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (answer != MessageBoxResult.Yes) return;
var esito = await Dialogs.ProgressDialog.RunAsync(this, "Ricalcolo dallo storico", "Leggo lo storico…",
(progress, ct) => System.Threading.Tasks.Task.Run(() => ProductStatsStore.RebuildFromHistory(progress, ct), ct));
if (esito.Error != null)
{
Log($"[PRODOTTI] Ricalcolo non riuscito: {esito.Error.Message}", LogLevel.Error);
return;
}
if (esito.Cancelled)
{
Log("[PRODOTTI] Ricalcolo annullato: le schede restano come prima", LogLevel.Warning);
return;
}
LoadProducts();
Log($"[PRODOTTI] Statistiche per prodotto ricalcolate dallo storico ({esito.Result:N0} aste)", LogLevel.Success);
}
private void Products_ClearAllClicked(object sender, RoutedEventArgs e)
{
var answer = MessageBox.Show(this,
"Pulizia completa dei prodotti:\n\n" +
"• tolgo tutti i prodotti dall'elenco, con i loro limiti\n" +
"• azzero le statistiche per prodotto (aste concluse, prezzi tipici, consigli)\n\n" +
"Lo storico delle aste non viene toccato. Le aste già nel monitor restano dove sono.\n\nProcedo?",
"Prodotti", MessageBoxButton.YesNo, MessageBoxImage.Warning);
if (answer != MessageBoxResult.Yes) return;
WatchedProductsStore.Clear();
ProductStatsStore.ClearStatistics();
LoadProducts();
MarkWatchedProducts(_catalogAuctions);
Log("[PRODOTTI] Pulizia completa: elenco e statistiche per prodotto azzerati", LogLevel.Warning);
}
private void Products_UnwatchAllClicked(object sender, RoutedEventArgs e)
{
var accese = _productViewModels.Count(p => p.IsWatched);
if (accese == 0)
{
MessageBox.Show(this, "Nessun prodotto ha la stellina accesa.",
"Prodotti", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
var answer = MessageBox.Show(this,
$"Spengo la stellina di {accese} prodotti?\n\n" +
"Nessun prodotto verrà più cercato in automatico. Restano tutti in elenco con i loro limiti.",
"Prodotti", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (answer != MessageBoxResult.Yes) return;
var n = WatchedProductsStore.UnwatchAll();
LoadProducts();
MarkWatchedProducts(_catalogAuctions);
Log($"[PRODOTTI] Stelline spente su {n} prodotti", LogLevel.Info);
}
private void Products_RemoveUnwatchedClicked(object sender, RoutedEventArgs e)
{
var spente = _productViewModels.Count(p => !p.IsWatched);
if (spente == 0)
{
MessageBox.Show(this, "Tutti i prodotti in elenco hanno la stellina accesa: non c'è nulla da togliere.",
"Prodotti", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
var answer = MessageBox.Show(this,
$"Tolgo dall'elenco i {spente} prodotti senza stellina?\n\n" +
"Si perdono i limiti impostati su quei prodotti. Le statistiche per prodotto restano.",
"Prodotti", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (answer != MessageBoxResult.Yes) return;
var n = WatchedProductsStore.RemoveUnwatched();
LoadProducts();
MarkWatchedProducts(_catalogAuctions);
Log($"[PRODOTTI] Tolti {n} prodotti senza stellina", LogLevel.Info);
}
/// <summary>
/// Scrive nei limiti del prodotto i valori consigliati dallo storico — minimo e
/// massimo insieme, perché è la coppia a definire la finestra in cui ha senso
/// puntare: un tetto senza pavimento lascia sprecare puntate su aste ancora lontane
/// dalla chiusura, un pavimento senza tetto non protegge da niente.
/// </summary>
private void ApplyAdviceToProduct(ProductViewModel? product)
{
if (product == null) return;
if (!product.ApplyAdvice())
{
MessageBox.Show(this,
$"Per \"{product.DisplayName}\" non ci sono abbastanza aste concluse " +
$"(ne servono almeno {PriceAdvisor.MinimumSample}) per ricavare un consiglio.",
"Limiti consigliati", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
PersistProductLimits();
Log($"[PRODOTTI] {product.DisplayName}: limiti consigliati applicati " +
$"({product.SuggestedMinPrice:F2} € {product.SuggestedMaxPrice:F2} €)",
LogLevel.Success);
if (product.AdviceWarns)
{
Log($"[PRODOTTI] {product.DisplayName}: il tetto è sotto il prezzo tipico di " +
"aggiudicazione — con questo numero di puntate l'articolo non conviene.",
LogLevel.Warning);
}
}
private void Products_ApplySuggestedClicked(object sender, RoutedEventArgs e)
{
var product = Products.SelectedProduct;
if (product == null)
{
MessageBox.Show(this, "Seleziona prima un prodotto dall'elenco.",
"Limiti consigliati", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
ApplyAdviceToProduct(product);
}
/// <summary>
/// Applica i limiti consigliati a tutto l'elenco.
///
/// <para>Chiede conferma dicendo <b>quante righe cambierebbe davvero</b>, non quante
/// ce ne sono: su un elenco dove la metà è già a posto, "applico a 40 prodotti"
/// sarebbe una risposta a una domanda che nessuno ha fatto. I prodotti senza abbastanza
/// storico restano intoccati, e quelli su cui il consiglio avverte vengono contati a
/// parte: sono i casi da guardare a mano.</para>
/// </summary>
private void Products_ApplyAllSuggestedClicked(object sender, RoutedEventArgs e)
{
var candidates = _productViewModels.Where(p => p.HasAdvice && !p.MatchesAdvice).ToList();
var senzaStorico = _productViewModels.Count(p => !p.HasAdvice);
if (candidates.Count == 0)
{
MessageBox.Show(this,
senzaStorico == _productViewModels.Count
? $"Nessun prodotto ha abbastanza aste concluse (ne servono almeno " +
$"{PriceAdvisor.MinimumSample}) per ricavare un consiglio."
: "I limiti consigliati sono già scritti su tutti i prodotti che ne hanno uno.",
"Limiti consigliati", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
var avvisi = candidates.Count(p => p.AdviceWarns);
var messaggio =
$"Scrivo i limiti consigliati su {candidates.Count} " +
(candidates.Count == 1 ? "prodotto" : "prodotti") + ".\n\n" +
"I valori scritti a mano su quei prodotti verranno sostituiti.";
if (senzaStorico > 0)
messaggio += $"\n\n{senzaStorico} prodotti restano invariati: non hanno abbastanza aste concluse.";
if (avvisi > 0)
messaggio += $"\n\nAttenzione: su {avvisi} il tetto resta sotto il prezzo tipico di " +
"aggiudicazione — con le puntate impostate quegli articoli non convengono.";
messaggio += "\n\nProcedo?";
var answer = MessageBox.Show(this, messaggio, "Limiti consigliati",
MessageBoxButton.YesNo, MessageBoxImage.Warning);
if (answer != MessageBoxResult.Yes) return;
var applicati = candidates.Count(p => p.ApplyAdvice());
PersistProductLimits();
Log($"[PRODOTTI] Limiti consigliati applicati a {applicati} prodotti" +
(avvisi > 0 ? $" ({avvisi} con avviso di convenienza)" : ""),
LogLevel.Success);
}
/// <summary>
/// Rende durevoli i limiti e li rispecchia nella scheda prodotto. Le due scritture
/// vanno sempre insieme: <c>watched-products.json</c> è la fonte di verità,
/// <c>products.json</c> è il file che si apre per capire com'è impostato un articolo,
/// e disallinearli vuol dire avere due risposte alla stessa domanda.
/// </summary>
private void PersistProductLimits()
{
WatchedProductsStore.Persist();
ProductStatsStore.SyncOptionsFromWatchList();
}
/// <summary>
/// Riscrive i limiti del prodotto sulle aste di quell'articolo già presenti nel
/// monitor. Serve la conferma perché sovrascrive anche i ritocchi fatti a mano su
/// una singola asta, che è esattamente ciò che qualcuno potrebbe non volere.
/// </summary>
private void Products_ReapplyClicked(object sender, RoutedEventArgs e)
{
var product = Products.SelectedProduct;
if (product == null)
{
MessageBox.Show(this, "Seleziona prima un prodotto dall'elenco.",
"Riapplica limiti", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
var targets = _auctionViewModels
.Where(a => product.Rule.MatchesName(a.AuctionInfo.Name))
.ToList();
if (targets.Count == 0)
{
MessageBox.Show(this,
$"Nel monitor non ci sono aste di \"{product.DisplayName}\".",
"Riapplica limiti", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
var answer = MessageBox.Show(this,
$"Riscrivo i limiti di \"{product.DisplayName}\" su {targets.Count} " +
(targets.Count == 1 ? "asta" : "aste") + " già nel monitor.\n\n" +
"Le modifiche fatte a mano su quelle aste verranno sostituite. Procedo?",
"Riapplica limiti", MessageBoxButton.YesNo, MessageBoxImage.Warning);
if (answer != MessageBoxResult.Yes) return;
var limits = ProductRuleResolver.Resolve(product.Rule, SettingsManager.Load());
foreach (var auction in targets)
{
auction.MinPrice = limits.MinPrice;
auction.MaxPrice = limits.MaxPrice;
auction.MaxClicks = limits.MaxClicks;
}
SaveAuctions();
// Il pannello dei dettagli mostra i valori dell'asta selezionata: se è fra
// quelle toccate, i campi devono cambiare sotto gli occhi.
if (_selectedAuction != null && targets.Contains(_selectedAuction))
UpdateAuctionSettingsDisplay(_selectedAuction);
Log($"[PRODOTTI] Limiti di {product.DisplayName} riapplicati a {targets.Count} aste",
LogLevel.Success);
}
}
}
+117
View File
@@ -0,0 +1,117 @@
using System;
using System.IO;
using System.Windows;
using AutoBidder.Data;
using AutoBidder.Utilities;
namespace AutoBidder
{
/// <summary>
/// L'interruttore dell'apprendimento nella barra del monitor, e le due operazioni
/// estreme delle Impostazioni: cancellare tutti i dati tranne la login, e tornare alle
/// impostazioni di fabbrica.
/// </summary>
public partial class MainWindow
{
// ── Apprendimento acceso / spento ────────────────────────────────
private bool _syncingLearningToggle;
private void AuctionMonitor_LearningToggled(object sender, RoutedEventArgs e)
{
if (_syncingLearningToggle) return;
var on = AuctionMonitor.LearningToggle.IsChecked == true;
var settings = SettingsManager.Load();
if (settings.LearningEnabled == on) return;
settings.LearningEnabled = on;
SettingsManager.Save(settings);
Log(on
? "[APPRENDIMENTO] Acceso: il motore decide con valore atteso, regime, duello e anticipo adattivo"
: "[APPRENDIMENTO] SPENTO: si punta solo entro i limiti (prezzo, puntate, budget, fascia oraria) con l'anticipo fisso",
on ? LogLevel.Success : LogLevel.Warning);
try { Learning.Refresh(); } catch { }
}
/// <summary>Allinea l'interruttore in barra a ciò che dicono le impostazioni.</summary>
private void RefreshLearningToggle()
{
try
{
_syncingLearningToggle = true;
AuctionMonitor.LearningToggle.IsChecked = SettingsManager.Load().LearningEnabled;
}
catch { }
finally { _syncingLearningToggle = false; }
}
// ── Manutenzione ─────────────────────────────────────────────────
private void Settings_WipeAllDataClicked(object sender, RoutedEventArgs e)
{
var answer = MessageBox.Show(this,
"Cancello TUTTI i dati dell'applicazione:\n\n" +
"• i due database (aste, puntate, decisioni, prodotti seguiti, aste nel monitor, modelli appresi, registri)\n" +
"• le copie di sicurezza\n" +
"• la vecchia cartella Dati delle versioni precedenti\n\n" +
"Restano la login (sessione, riferimenti del sito, browser) e le impostazioni.\n" +
"L'operazione non è reversibile e l'applicazione si riavvia subito.\n\nProcedo?",
"Elimina tutti i dati", MessageBoxButton.YesNo, MessageBoxImage.Warning);
if (answer != MessageBoxResult.Yes) return;
var conferma = MessageBox.Show(this,
"Ultima conferma: cancellare tutto tranne la login?",
"Elimina tutti i dati", MessageBoxButton.YesNo, MessageBoxImage.Stop);
if (conferma != MessageBoxResult.Yes) return;
try { _auctionMonitor.Stop(); } catch { }
var errors = AppDataWipe.WipeAllExceptLogin();
if (errors.Count > 0)
{
MessageBox.Show(this, "Alcuni file non si sono potuti cancellare:\n\n" + string.Join("\n", errors) +
"\n\nL'applicazione si riavvia comunque.", "Elimina tutti i dati", MessageBoxButton.OK, MessageBoxImage.Warning);
}
RestartApplication();
}
private void Settings_ResetSettingsClicked(object sender, RoutedEventArgs e)
{
var answer = MessageBox.Show(this,
"Riporto tutte le impostazioni ai valori di fabbrica e riavvio l'applicazione.\n\n" +
"I dati (database, prodotti, storico, apprendimento) e la login non vengono toccati.\n\nProcedo?",
"Impostazioni di fabbrica", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (answer != MessageBoxResult.Yes) return;
try { _auctionMonitor.Stop(); } catch { }
AppDataWipe.ResetSettings();
RestartApplication();
}
private void Settings_ExportAppLogClicked(object sender, RoutedEventArgs e)
{
var path = AskWhereToSave("Esporta il registro applicativo", $"registro-{DateTime.Now:yyyyMMdd-HHmm}.txt", "Testo|*.txt");
if (path == null) return;
try
{
var n = OperationalDatabase.Instance.ExportLog(OperationalDatabase.AppLog, path);
var m = OperationalDatabase.Instance.ExportLog(OperationalDatabase.FreeBidsLog,
Path.Combine(Path.GetDirectoryName(path)!, Path.GetFileNameWithoutExtension(path) + "-puntate.txt"));
Log($"[REGISTRI] Esportate {n} righe del registro applicativo e {m} del riscatto puntate in {Path.GetDirectoryName(path)}", LogLevel.Success);
OpenFolder(Path.GetDirectoryName(path)!);
}
catch (Exception ex)
{
Log($"[REGISTRI] Esportazione non riuscita: {ex.Message}", LogLevel.Error);
}
}
}
}
+64
View File
@@ -0,0 +1,64 @@
using System;
using System.ComponentModel;
using AutoBidder.Utilities;
namespace AutoBidder
{
/// <summary>
/// Chiusura ordinata dell'applicazione.
///
/// Senza, alla chiusura restavano in volo i motori delle aste, la sorveglianza dei
/// prodotti e il cerchio di aggiornamento del catalogo: il processo moriva e Windows
/// recuperava tutto, ma una puntata in corso veniva troncata a metà senza che nulla
/// lo registrasse, e la richiesta di timer a 1 ms non veniva mai restituita al sistema.
/// </summary>
public partial class MainWindow
{
private bool _shutdownDone;
protected override void OnClosing(CancelEventArgs e)
{
base.OnClosing(e);
ShutdownServices();
}
private void ShutdownServices()
{
if (_shutdownDone) return;
_shutdownDone = true;
// Ogni pezzo va spento separatamente: se uno fallisce, gli altri devono
// comunque avere la loro occasione di chiudersi bene.
TryShutdown("timer interfaccia", () =>
{
_headerTimer?.Stop();
_userBannerTimer?.Stop();
_userHtmlTimer?.Stop();
});
TryShutdown("catalogo", StopCatalogAutoRefresh);
TryShutdown("sorveglianza prodotti", () => _productWatcher?.Stop());
TryShutdown("riscatto puntate", () => _freeBidsService?.Stop());
// Salva prima di spegnere il motore: fermare le aste ne cambia lo stato.
TryShutdown("salvataggio aste", SaveAuctions);
// Ferma i motori per asta e rilascia rete e risoluzione del timer di sistema.
TryShutdown("motore aste", () => _auctionMonitor?.Dispose());
TryShutdown("browser integrato", () => Browser?.EmbeddedWebView?.Dispose());
}
private static void TryShutdown(string what, Action action)
{
try { action(); }
catch (Exception ex)
{
// In chiusura non c'è più un'interfaccia a cui riportarlo.
Console.WriteLine($"[SHUTDOWN] {what}: {ex.Message}");
}
}
}
}
+341
View File
@@ -0,0 +1,341 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using AutoBidder.Models;
using AutoBidder.Utilities;
namespace AutoBidder
{
/// <summary>
/// Sezione Statistiche: legge lo storico delle aste concluse
/// (CompletedAuctionsStore) e popola la griglia asta per asta.
/// </summary>
public partial class MainWindow
{
/// <summary>
/// Carica lo storico asta per asta.
///
/// <para>La lettura aggregata per prodotto non sta più qui: vive nella scheda
/// Prodotti, accanto ai limiti che quelle stesse cifre servono a decidere.</para>
/// </summary>
private void LoadStatistics()
{
try
{
var records = CompletedAuctionsStore.LoadAll();
StatsDetailGrid.ItemsSource = records;
StatsPillRecords.Text = records.Count == 1 ? "1 asta" : $"{records.Count} aste";
StatsPillWon.Text = $"{records.Count(r => r.WonByMe)} vinte";
// Con lo storico vuoto la griglia mostrerebbe solo intestazioni:
// meglio spiegare che si popola da sola.
var empty = records.Count == 0;
StatsEmptyState.Visibility = empty ? Visibility.Visible : Visibility.Collapsed;
StatsAuctionsPane.Visibility = empty ? Visibility.Collapsed : Visibility.Visible;
}
catch (Exception ex)
{
Log($"[STATISTICHE] Errore caricamento: {ex.Message}", LogLevel.Error);
}
}
/// <summary>Ricarica manuale delle statistiche (pulsante).</summary>
private void ReloadStatisticsButton_Click(object sender, RoutedEventArgs e)
{
LoadStatistics();
}
/// <summary>Apre la cartella dei database.</summary>
private void OpenStatsFolderButton_Click(object sender, RoutedEventArgs e)
{
try
{
var folder = AppPaths.DatabaseFolder;
if (!string.IsNullOrEmpty(folder) && System.IO.Directory.Exists(folder))
{
Process.Start(new ProcessStartInfo
{
FileName = folder,
UseShellExecute = true
});
}
}
catch (Exception ex)
{
Log($"[STATISTICHE] Impossibile aprire la cartella: {ex.Message}", LogLevel.Error);
}
}
// ── Selezione multipla ───────────────────────────────────────────
private void SelectAllStatsButton_Click(object sender, RoutedEventArgs e)
{
StatsDetailGrid.Focus();
StatsDetailGrid.SelectAll();
}
private void DeleteSelectedStatsButton_Click(object sender, RoutedEventArgs e)
=> DeleteSelectedHistoryRows();
private void StatsDetailGrid_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
if (e.Key != System.Windows.Input.Key.Delete) return;
e.Handled = true;
DeleteSelectedHistoryRows();
}
/// <summary>
/// Toglie dallo storico le righe selezionate. Prima la copia di sicurezza, poi la
/// cancellazione, poi le statistiche per prodotto ricalcolate da ciò che resta.
/// </summary>
private void DeleteSelectedHistoryRows()
{
var selected = StatsDetailGrid.SelectedItems.OfType<CompletedAuctionRecord>().ToList();
if (selected.Count == 0)
{
MessageBox.Show(this, "Seleziona prima una o più righe dello storico (Ctrl+clic, Maiusc+clic, o «Seleziona tutte»).",
"Storico", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
var answer = MessageBox.Show(this,
(selected.Count == 1
? $"Elimino dallo storico \"{selected[0].Name}\"?"
: $"Elimino dallo storico {selected.Count} aste?") +
"\n\nCon loro vanno via puntate, interrogazioni, reset e decisioni registrate. " +
"Prima salvo una copia nei backup e poi ricalcolo le statistiche per prodotto.",
"Storico", MessageBoxButton.YesNo, MessageBoxImage.Warning);
if (answer != MessageBoxResult.Yes) return;
try
{
var backup = CompletedAuctionsStore.BackupNow();
var n = CompletedAuctionsStore.Remove(selected.Select(r => r.AuctionId).ToList());
var ricostruite = ProductStatsStore.RebuildFromHistory();
LoadStatistics();
LoadProducts();
Log($"[STORICO] Eliminate {n} aste; statistiche per prodotto ricalcolate su {ricostruite} aste rimaste. Copia di sicurezza in {backup}",
LogLevel.Success);
}
catch (Exception ex)
{
Log($"[STORICO] Eliminazione non riuscita: {ex.Message}", LogLevel.Error);
}
}
/// <summary>
/// Svuota tutto lo storico in un colpo: osservazioni, decisioni, misure, contabilità
/// del rischio e statistiche per prodotto. Prodotti seguiti e apprendimento restano.
/// </summary>
private void WipeHistoryButton_Click(object sender, RoutedEventArgs e)
{
var count = CompletedAuctionsStore.LoadAll().Count;
if (count == 0)
{
MessageBox.Show(this, "Lo storico è già vuoto.", "Storico", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
var answer = MessageBox.Show(this,
$"Svuoto completamente lo storico ({count} aste)?\n\n" +
"Vanno via tutte le aste concluse con puntate, interrogazioni, reset, decisioni, misure di rete " +
"e contabilità del rischio, e le statistiche per prodotto tornano a zero.\n" +
"Prima salvo una copia nei backup. Prodotti seguiti, aste nel monitor e apprendimento restano.\n\nProcedo?",
"Storico", MessageBoxButton.YesNo, MessageBoxImage.Warning);
if (answer != MessageBoxResult.Yes) return;
try
{
var backup = CompletedAuctionsStore.BackupNow();
CompletedAuctionsStore.Clear();
ProductStatsStore.ClearStatistics();
LoadStatistics();
LoadProducts();
Log($"[STORICO] Svuotato: {count} aste tolte. Copia di sicurezza in {backup}", LogLevel.Warning);
}
catch (Exception ex)
{
Log($"[STORICO] Svuotamento non riuscito: {ex.Message}", LogLevel.Error);
}
}
/// <summary>
/// Azzera le statistiche registrate: il dialogo dice cosa c'è e quanto pesa, e
/// l'utente sceglie voce per voce. Vedi <see cref="StatsWipe"/>.
/// </summary>
private void ClearStatsButton_Click(object sender, RoutedEventArgs e)
{
var dialog = new Dialogs.WipeStatsDialog { Owner = this };
if (dialog.ShowDialog() != true || dialog.Result is not { } report) return;
LoadStatistics();
LoadProducts();
if (Learning.IsVisible) Learning.Refresh();
Log($"[STATISTICHE] Azzeramento: {report.Summary}. " +
$"{report.FilesDeleted} file tolti, {report.BytesFreed / (1024.0 * 1024.0):F1} MB liberati",
report.Errors.Count == 0 ? LogLevel.Success : LogLevel.Warning);
}
/// <summary>
/// Analizza lo storico e toglie le aste incomplete secondo le regole scelte
/// dall'utente. Il dialogo mostra il conto prima di toccare qualunque cosa.
/// </summary>
private void CleanupStatsButton_Click(object sender, RoutedEventArgs e)
{
var records = CompletedAuctionsStore.LoadAll();
if (records.Count == 0)
{
MessageBox.Show(this, "Lo storico e' vuoto: non c'e' nulla da pulire.",
"Pulizia", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
var dialog = new Dialogs.StatsCleanupDialog(records) { Owner = this };
if (dialog.ShowDialog() != true) return;
// Le statistiche per prodotto sono un archivio a parte, alimentato asta per
// asta man mano che chiudono: dopo una pulizia descriverebbero aste che non ci
// sono più — e sono loro a decidere i limiti consigliati e i tetti di puntate.
// Si ricostruiscono da zero dallo storico appena pulito, l'unica fonte rimasta.
var ricostruite = 0;
try
{
ricostruite = ProductStatsStore.RebuildFromHistory();
}
catch (Exception ex)
{
Log($"[STATISTICHE] Ricalcolo per prodotto non riuscito: {ex.Message}", LogLevel.Warning);
}
LoadStatistics();
LoadProducts();
Log($"[STORICO] Pulizia: tolte {dialog.RemovedCount} aste; statistiche per prodotto " +
$"ricalcolate su {ricostruite} aste rimaste. Copia di sicurezza in {dialog.BackupPath}",
LogLevel.Success);
}
/// <summary>
/// Chiede al server quante puntate ha speso il vincitore delle aste gia' concluse.
///
/// <para>Il dato non scade: le aste registrate prima che l'applicazione sapesse
/// leggerlo non sono perse, basta richiederle. Senza questo recupero le statistiche
/// sulle puntate ripartirebbero da zero e servirebbero mesi per averne abbastanza.</para>
/// </summary>
private async void BackfillWinnerBidsButton_Click(object sender, RoutedEventArgs e)
{
var missing = Services.WinnerBidsBackfill.FindMissing();
if (missing.Count == 0)
{
MessageBox.Show(this,
"Tutte le aste concluse hanno gia' il conteggio delle puntate del vincitore.",
"Recupero", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
var answer = MessageBox.Show(this,
$"Chiedo al server le puntate del vincitore per {missing.Count} aste concluse.\n\n" +
"Serve una richiesta per asta, e viaggia in corsia di fondo dietro alle aste " +
"in corso: misurato sul campo, circa un'asta e mezza al secondo, quindi " +
$"{Math.Max(1, (int)(missing.Count * 1.6 / 60))} minuti circa.\n\n" +
"Puoi continuare a usare l'applicazione: le aste in corso hanno la precedenza " +
"e non ne risentono.\n\nProcedo?",
"Recupera puntate dei vincitori", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (answer != MessageBoxResult.Yes) return;
var transport = _auctionMonitor?.GetApiClient()?.Transport;
if (transport == null)
{
Log("[RECUPERO] Trasporto HTTP non disponibile", LogLevel.Error);
return;
}
var button = sender as System.Windows.Controls.Button;
if (button != null) button.IsEnabled = false;
try
{
var backfill = new Services.WinnerBidsBackfill(transport);
backfill.OnLog += m => Dispatcher.Invoke(() => Log(m, LogLevel.Warning));
Log($"[RECUPERO] Avvio: {missing.Count} aste da completare", LogLevel.Info);
// La finestra di avanzamento non blocca l'applicazione: il recupero va
// piano di proposito e intanto si può fare altro.
var esito = await Dialogs.ProgressDialog.RunAsync(this, "Recupero delle puntate dei vincitori",
$"{missing.Count:N0} aste da completare, una richiesta alla volta in corsia di fondo.",
(progress, ct) =>
{
var lastReported = 0;
backfill.OnProgress += (done, total, updated) =>
{
progress.Report(new ProgressStep(done, total, $"{updated:N0} aggiornate"));
// Una riga di registro ogni cinquanta: una per asta seppellirebbe
// tutto il resto sotto migliaia di righe.
if (done - lastReported < 50 && done != total) return;
lastReported = done;
Dispatcher.Invoke(() =>
Log($"[RECUPERO] {done}/{total} aste esaminate, {updated} aggiornate", LogLevel.Info));
};
return backfill.RunAsync(0, ct);
}, modal: false);
if (esito.Error != null)
{
Log($"[RECUPERO] Non riuscito: {esito.Error.Message}", LogLevel.Error);
return;
}
if (esito.Cancelled)
{
LoadStatistics();
LoadProducts();
Log("[RECUPERO] Interrotto dall'utente: le aste già completate restano completate", LogLevel.Warning);
return;
}
var result = esito.Result!;
LoadStatistics();
LoadProducts();
Log($"[RECUPERO] Fatto: {result.Updated} aggiornate, {result.Skipped} senza dato utilizzabile, " +
$"{result.Failed} non riuscite", LogLevel.Success);
MessageBox.Show(this,
$"Aggiornate {result.Updated} aste su {result.Examined}.\n\n" +
(result.Skipped > 0
? $"{result.Skipped} senza un conteggio utilizzabile: il server non l'ha dato, " +
"oppure non superava il controllo di coerenza col prezzo finale.\n"
: "") +
(result.Failed > 0 ? $"{result.Failed} non riuscite per errori di rete.\n" : "") +
"\nLe statistiche per prodotto sono gia' aggiornate.",
"Recupero completato", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
Log($"[RECUPERO] Interrotto: {ex.Message}", LogLevel.Error);
}
finally
{
if (button != null) button.IsEnabled = true;
}
}
}
}
+71 -32
View File
@@ -1,4 +1,4 @@
using System;
using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
@@ -13,32 +13,54 @@ namespace AutoBidder
/// </summary>
public partial class MainWindow
{
/// <summary>Firma dell'ultimo log disegnato: asta, righe e ultima voce.</summary>
private (string Id, int Count, DateTime Last, int Repeat) _lastLogSignature;
/// <summary>Firma dell'ultima griglia puntatori disegnata.</summary>
private (string Id, int Count, int TotalBids) _lastBiddersSignature;
private void UpdateAuctionLog(AuctionViewModel auction)
{
try
{
var auctionInfo = auction.AuctionInfo;
var log = auctionInfo.AuctionLog;
if (log == null) return;
// Ricostruire il documento significa creare un Paragraph e un Run per ogni
// riga, fino a duecento, a ogni risposta del server. Se il log non è
// cambiato non c'è nulla da ridisegnare.
var last = log.Count > 0 ? log[^1] : null;
var signature = (auction.AuctionId, log.Count,
last?.Timestamp ?? DateTime.MinValue, last?.RepeatCount ?? 0);
if (signature == _lastLogSignature) return;
_lastLogSignature = signature;
var logBox = SelectedAuctionLog;
var doc = logBox.Document;
doc.Blocks.Clear();
foreach (var entry in auctionInfo.AuctionLog)
foreach (var entry in log)
{
var upper = entry.ToUpperInvariant();
// Color coding based on log content
Brush color;
if (upper.Contains("[ERRORE]") || upper.Contains("[FAIL]") || upper.Contains("EXCEPTION"))
color = new SolidColorBrush(Color.FromRgb(232, 17, 35)); // Red
else if (upper.Contains("[WARN]") || upper.Contains("ATTENZIONE"))
color = new SolidColorBrush(Color.FromRgb(255, 183, 0)); // Yellow/Orange
else if (upper.Contains("[OK]") || upper.Contains("SUCCESS"))
color = new SolidColorBrush(Color.FromRgb(0, 216, 0)); // Green
else
color = new SolidColorBrush(Color.FromRgb(100, 180, 255)); // Light Blue - #64B4FF (più chiaro e leggibile)
// Color coding based on structured log level
Brush color = entry.Level switch
{
Models.AuctionLogLevel.Error => new SolidColorBrush(Color.FromRgb(232, 17, 35)), // Red
Models.AuctionLogLevel.Warning => new SolidColorBrush(Color.FromRgb(255, 183, 0)), // Yellow/Orange
Models.AuctionLogLevel.Success => new SolidColorBrush(Color.FromRgb(0, 216, 0)), // Green
Models.AuctionLogLevel.Bid => new SolidColorBrush(Color.FromRgb(0, 216, 0)), // Green
Models.AuctionLogLevel.Strategy => new SolidColorBrush(Color.FromRgb(200, 160, 255)),// Purple
Models.AuctionLogLevel.Timing => new SolidColorBrush(Color.FromRgb(150, 150, 150)), // Gray
Models.AuctionLogLevel.Debug => new SolidColorBrush(Color.FromRgb(120, 120, 120)), // Dark gray
_ => new SolidColorBrush(Color.FromRgb(100, 180, 255)) // Light Blue
};
var repeatSuffix = entry.RepeatCount > 1 ? $" (x{entry.RepeatCount})" : "";
var line = $"[{entry.TimeDisplay}] [{entry.LevelLabel}] {entry.Message}{repeatSuffix}";
var p = new System.Windows.Documents.Paragraph { Margin = new Thickness(0, 2, 0, 2) };
var r = new System.Windows.Documents.Run(entry) { Foreground = color };
var r = new System.Windows.Documents.Run(line) { Foreground = color };
p.Inlines.Add(r);
doc.Blocks.Add(p);
}
@@ -59,13 +81,29 @@ namespace AutoBidder
{
try
{
var bidders = auction.AuctionInfo.BidderStats.Values
.OrderByDescending(b => b.BidCount)
.ToList();
// Copia sotto lucchetto: il motore riscrive il dizionario dal proprio thread.
var bidders = auction.AuctionInfo.SnapshotBidderStats();
SelectedAuctionBiddersGrid.ItemsSource = null;
SelectedAuctionBiddersGrid.ItemsSource = bidders;
SelectedAuctionBiddersCount.Text = $"Utenti: {bidders?.Count ?? 0}";
// La quota si calcola qui: il singolo puntatore non conosce il totale dell'asta.
var totalBids = bidders.Sum(b => b.BidCount);
foreach (var bidder in bidders)
{
bidder.SharePercent = totalBids > 0 ? bidder.BidCount * 100.0 / totalBids : 0;
}
// Riassegnare ItemsSource ricostruisce l'intera griglia e la fa lampeggiare:
// si rifà solo quando i numeri sono davvero cambiati.
var signature = (auction.AuctionId, bidders.Count, totalBids);
if (signature != _lastBiddersSignature)
{
_lastBiddersSignature = signature;
SelectedAuctionBiddersGrid.ItemsSource = null;
SelectedAuctionBiddersGrid.ItemsSource = bidders;
SelectedAuctionBiddersCount.Text = bidders.Count == 0
? "Nessun dato sui puntatori."
: $"{bidders.Count} puntatori · {totalBids} puntate osservate";
}
// ?? NUOVO: Aggiorna il contatore della storia puntate con limite configurato
var settings = SettingsManager.Load();
@@ -75,7 +113,7 @@ namespace AutoBidder
var bidHistoryCountTextBlock = AuctionMonitor.FindName("BidHistoryCount") as TextBlock;
if (bidHistoryCountTextBlock != null)
{
// Mostra "Ultime 20 puntate" se il limite è attivo
// Mostra "Ultime 20 puntate" se il limite attivo
if (maxEntries > 0)
{
bidHistoryCountTextBlock.Text = $"Ultime {maxEntries} puntate";
@@ -98,10 +136,12 @@ namespace AutoBidder
SelectedAuctionName.Text = auction.Name;
SelectedBidBeforeDeadlineMs.Text = auction.AuctionInfo.BidBeforeDeadlineMs.ToString();
SelectedCheckAuctionOpen.IsChecked = auction.AuctionInfo.CheckAuctionOpenBeforeBid;
SelectedMinPrice.Text = auction.MinPrice.ToString("F2", System.Globalization.CultureInfo.InvariantCulture);
SelectedMaxPrice.Text = auction.MaxPrice.ToString("F2", System.Globalization.CultureInfo.InvariantCulture);
SelectedMaxClicks.Text = auction.MaxClicks.ToString();
AuctionMonitor.SelectedMaxSpend.Text = auction.AuctionInfo.MaxTotalSpendEuro
.ToString("F2", System.Globalization.CultureInfo.InvariantCulture);
AuctionMonitor.SelectedStopAtBreakEven.IsChecked = auction.AuctionInfo.StopAtBreakEven;
var url = auction.AuctionInfo.OriginalUrl;
if (string.IsNullOrEmpty(url))
@@ -151,18 +191,17 @@ namespace AutoBidder
int canPauseCount = _auctionViewModels.Count(a => a.CanPause);
int canStopCount = _auctionViewModels.Count(a => a.CanStop);
// AVVIA TUTTI: abilitato se ALMENO UNA asta può essere avviata
// Scuro se NESSUNA asta può essere avviata (tutte già avviate)
// AVVIA TUTTI: abilitato se ALMENO UNA asta pu essere avviata
// Scuro se NESSUNA asta pu essere avviata (tutte gi avviate)
StartButton.IsEnabled = canStartCount > 0;
StartButton.Opacity = canStartCount > 0 ? 1.0 : 0.4;
// PAUSA TUTTI: abilitato se ALMENO UNA asta può essere messa in pausa
// Scuro se NESSUNA asta può essere messa in pausa (tutte già in pausa o ferme)
// OSSERVA TUTTE: abilitato se ALMENO UNA asta non e' gia' in sola osservazione
PauseAllButton.IsEnabled = canPauseCount > 0;
PauseAllButton.Opacity = canPauseCount > 0 ? 1.0 : 0.4;
// FERMA TUTTI: abilitato se ALMENO UNA asta può essere fermata
// Scuro se NESSUNA asta può essere fermata (tutte già ferme)
// FERMA TUTTI: abilitato se ALMENO UNA asta pu essere fermata
// Scuro se NESSUNA asta pu essere fermata (tutte gi ferme)
StopButton.IsEnabled = canStopCount > 0;
StopButton.Opacity = canStopCount > 0 ? 1.0 : 0.4;
}
@@ -233,7 +272,7 @@ namespace AutoBidder
// Resetta ai valori predefiniti dalle impostazioni
_selectedAuction.AuctionInfo.BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs;
_selectedAuction.AuctionInfo.CheckAuctionOpenBeforeBid = settings.DefaultCheckAuctionOpenBeforeBid;
_selectedAuction.AuctionInfo.BidLeadIsManual = false; // torna all'anticipo adattivo
_selectedAuction.MinPrice = settings.DefaultMinPrice;
_selectedAuction.MaxPrice = settings.DefaultMaxPrice;
_selectedAuction.MaxClicks = settings.DefaultMaxClicks;
@@ -267,7 +306,7 @@ namespace AutoBidder
}
var result = MessageBox.Show(
$"Pulire la lista degli utenti per questa asta?\n\n{_selectedAuction.Name}\n\nLa lista degli utenti che hanno puntato verrà svuotata.",
$"Pulire la lista degli utenti per questa asta?\n\n{_selectedAuction.Name}\n\nLa lista degli utenti che hanno puntato verr svuotata.",
"Conferma Pulizia",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
+178 -94
View File
@@ -12,9 +12,31 @@ namespace AutoBidder
/// </summary>
public partial class MainWindow
{
private System.Windows.Threading.DispatcherTimer _userBannerTimer;
private System.Windows.Threading.DispatcherTimer _userHtmlTimer;
private SessionService _sessionService; // NUOVO: Servizio centralizzato
// Creati in InitializeUserInfo(), non nel costruttore.
private System.Windows.Threading.DispatcherTimer? _userBannerTimer;
private System.Windows.Threading.DispatcherTimer? _userHtmlTimer;
private System.Windows.Threading.DispatcherTimer? _toConfirmTimer;
private SessionService _sessionService = null!;
/// <summary>
/// Aste vinte in attesa di conferma su Bidoo, o <c>-1</c> finché non è arrivata una
/// risposta utilizzabile. È un <c>int</c> e non un <c>int?</c> perché lo scrive la
/// rete e lo legge il battito dell'interfaccia: un annullabile sono due campi, e
/// due campi si possono leggere a metà aggiornamento.
/// </summary>
private int _auctionsToConfirmRaw = Unknown;
private const int Unknown = -1;
/// <summary>Aste da confermare, o <c>null</c> se ancora non si sa.</summary>
private int? AuctionsToConfirm
{
get
{
var value = System.Threading.Volatile.Read(ref _auctionsToConfirmRaw);
return value < 0 ? null : value;
}
}
private void InitializeUserInfoTimers()
{
@@ -29,19 +51,105 @@ namespace AutoBidder
_userBannerTimer.Interval = TimeSpan.FromMinutes(10);
_userBannerTimer.Tick += UserBannerTimer_Tick;
_userBannerTimer.Start();
// Aste da confermare: una richiesta da un byte, quindi si può chiedere spesso.
// Un minuto è il compromesso fra "il numero compare subito dopo una vincita"
// e "non si tempesta il server per un dato che cambia di rado".
_toConfirmTimer = new System.Windows.Threading.DispatcherTimer();
_toConfirmTimer.Interval = TimeSpan.FromSeconds(60);
_toConfirmTimer.Tick += (_, _) => _ = RefreshAuctionsToConfirmAsync();
_toConfirmTimer.Start();
}
/// <summary>
/// Rilegge da Bidoo quante aste vinte aspettano conferma.
///
/// <para>Una risposta non utilizzabile <b>non</b> azzera il valore noto: se la rete
/// cade, la barra continua a mostrare l'ultimo numero certo invece di far sparire
/// una vincita che esiste.</para>
/// </summary>
private async Task RefreshAuctionsToConfirmAsync()
{
try
{
var session = _auctionMonitor.GetSession();
if (session == null || string.IsNullOrEmpty(session.Username))
{
System.Threading.Volatile.Write(ref _auctionsToConfirmRaw, Unknown);
return;
}
var count = await _auctionMonitor.GetAuctionsWonToConfirmAsync().ConfigureAwait(false);
if (count.HasValue) System.Threading.Volatile.Write(ref _auctionsToConfirmRaw, count.Value);
}
catch { /* la barra non deve mai disturbare il resto */ }
}
private void InitializeSessionService()
{
// NUOVO: Inizializza SessionService
_sessionService = new SessionService(_auctionMonitor.GetApiClient());
// Event handlers
_sessionService.OnLog += (msg) => Log(msg, LogLevel.Info);
_sessionService.OnSessionChanged += (session) =>
{
Dispatcher.Invoke(() => SetUserBanner(session.Username, session.RemainingBids));
};
// La sessione salvata va ripresa, altrimenti salvarla non serve a niente.
_ = RestoreSavedSessionAsync();
}
/// <summary>
/// Riprende la sessione salvata su disco e la riattiva verso Bidoo.
///
/// Senza questo passaggio il cookie veniva scritto in session.dat e mai più letto:
/// a ogni avvio l'applicazione risultava "Non connesso" e bisognava reincollarlo,
/// pur avendone una copia valida sul disco.
///
/// Non blocca l'avvio: la validazione richiede un giro di rete, quindi la finestra
/// si apre subito e il banner si aggiorna quando la risposta arriva.
/// </summary>
private async Task RestoreSavedSessionAsync()
{
try
{
var saved = _sessionService.LoadSession();
if (saved == null || string.IsNullOrWhiteSpace(saved.CookieString)) return;
Log("[SESSION] Sessione salvata trovata: verifica in corso…", LogLevel.Info);
// Il cookie potrebbe essere scaduto da giorni: si riattiva contro il server
// invece di fidarsi del file.
var result = await _sessionService.ValidateAndActivateSessionAsync(
saved.CookieString, saved.Username);
if (result.Success && result.Session != null)
{
// Il motore deve conoscere il cookie per poter interrogare e puntare.
_auctionMonitor.InitializeSessionWithCookie(
saved.CookieString, result.Session.Username);
Dispatcher.Invoke(() =>
{
SetUserBanner(result.Session.Username, result.Session.RemainingBids);
RefreshSettingsSessionStatus();
});
Log($"[SESSION] Riconnesso come {result.Session.Username}", LogLevel.Success);
}
else
{
// Si lascia il cookie sul disco: l'utente lo vede in Impostazioni e
// decide se rinnovarlo dal browser.
Log($"[SESSION] Sessione salvata non più valida: {result.ErrorMessage}", LogLevel.Warning);
}
}
catch (Exception ex)
{
Log($"[SESSION] Ripristino non riuscito: {ex.Message}", LogLevel.Error);
}
}
private void SetUserBanner(string username, int? remainingBids)
@@ -53,26 +161,20 @@ namespace AutoBidder
if (!string.IsNullOrEmpty(username))
{
// === CONNESSO ===
// Header - Puntate + Credito
RemainingBidsText.Text = remainingBids?.ToString() ?? "0";
if (session?.ShopCredit > 0)
{
AuctionMonitor.ShopCreditText.Text = $"EUR {session.ShopCredit:F2}";
}
else
{
AuctionMonitor.ShopCreditText.Text = "EUR 0.00";
}
// Aste vinte
BannerAsteDaRiscattare.Text = "0";
// Indicatore limite puntate
var settings = Utilities.SettingsManager.Load();
UpdateMinBidsIndicator(settings.MinimumRemainingBids);
// Puntate, credito e aste da confermare li ridisegna il battito da un
// secondo (RefreshAccountPills) leggendo la sessione viva: scriverli
// anche qui non aggiungerebbe nulla e riporterebbe il rischio di due
// fonti che si contraddicono.
AuctionMonitor.UpdateAccountStatus(
remainingBids ?? session?.RemainingBids,
session is null ? null : (decimal)session.ShopCredit,
AuctionsToConfirm);
// Appena la sessione è viva si può finalmente chiedere quante vincite
// aspettano conferma: prima non si poteva sapere.
_ = RefreshAuctionsToConfirmAsync();
// === SIDEBAR - Mostra dati utente ===
SidebarUsernameText.Text = username;
SidebarUsernameText.Foreground = new System.Windows.Media.SolidColorBrush(
@@ -80,7 +182,7 @@ namespace AutoBidder
SidebarUsernameText.FontWeight = System.Windows.FontWeights.Bold;
SidebarUsernameText.ToolTip = $"Connesso come {username} - Click per disconnettere";
// Mostra dettagli (ID + Email)
// Solo l'ID: l'indirizzo di posta non aggiungeva nulla di utile qui.
if (session?.UserId > 0)
{
SidebarUserIdText.Text = $"ID: {session.UserId}";
@@ -90,31 +192,21 @@ namespace AutoBidder
{
SidebarUserIdText.Visibility = System.Windows.Visibility.Collapsed;
}
if (!string.IsNullOrEmpty(session?.Email))
{
SidebarUserEmailText.Text = session.Email;
SidebarUserEmailText.Visibility = System.Windows.Visibility.Visible;
}
else
{
SidebarUserEmailText.Visibility = System.Windows.Visibility.Collapsed;
}
SidebarUserDetailsPanel.Visibility = System.Windows.Visibility.Visible;
}
else
{
// === NON CONNESSO ===
// Reset header
RemainingBidsText.Text = "0";
AuctionMonitor.ShopCreditText.Text = "EUR 0.00";
BannerAsteDaRiscattare.Text = "0";
// Senza sessione questi numeri non esistono: un trattino lo dice,
// uno zero mentirebbe.
System.Threading.Volatile.Write(ref _auctionsToConfirmRaw, Unknown);
AuctionMonitor.UpdateAccountStatus(null, null, null);
// Nascondi indicatore limite
MinBidsLimitIndicator.Visibility = Visibility.Collapsed;
// === SIDEBAR - Mostra "Non connesso" ===
SidebarUsernameText.Text = "Non connesso";
SidebarUsernameText.Foreground = new System.Windows.Media.SolidColorBrush(
@@ -165,12 +257,12 @@ namespace AutoBidder
// Aggiorna UI con stato connesso (ottimistico)
SetUserBanner(session.Username, session.RemainingBids);
// Verifica validità cookie in background
// Verifica validit cookie in background
System.Threading.Tasks.Task.Run(async () =>
{
try
{
Log("[SESSION] Verifica validità sessione...", LogLevel.Info);
Log("[SESSION] Verifica validit sessione...", LogLevel.Info);
var success = await _auctionMonitor.UpdateUserInfoAsync();
var updatedSession = _auctionMonitor.GetSession();
@@ -184,7 +276,7 @@ namespace AutoBidder
else
{
SetUserBanner(string.Empty, 0);
Log("[SESSION] Sessione scaduta", LogLevel.Warn);
Log("[SESSION] Sessione scaduta", LogLevel.Warning);
CheckBrowserCookieAfterWebViewReady();
}
});
@@ -194,7 +286,7 @@ namespace AutoBidder
Dispatcher.Invoke(() =>
{
SetUserBanner(string.Empty, 0);
Log($"[SESSION] Errore verifica sessione: {ex.Message}", LogLevel.Warn);
Log($"[SESSION] Errore verifica sessione: {ex.Message}", LogLevel.Warning);
CheckBrowserCookieAfterWebViewReady();
});
}
@@ -231,12 +323,12 @@ namespace AutoBidder
{
await Dispatcher.InvokeAsync(() =>
{
Log("[WARN] WebView non inizializzata dopo 60 secondi", LogLevel.Warn);
Log("[WARN] WebView non inizializzata dopo 60 secondi", LogLevel.Warning);
Log("[INFO] Per accedere:", LogLevel.Info);
Log("[INFO] 1. Click su 'Non connesso' nella sidebar", LogLevel.Info);
Log("[INFO] 2. Si aprirà la scheda Browser", LogLevel.Info);
Log("[INFO] 2. Si aprir la scheda Browser", LogLevel.Info);
Log("[INFO] 3. Fai login su Bidoo", LogLevel.Info);
Log("[INFO] 4. La connessione sarà automatica", LogLevel.Info);
Log("[INFO] 4. La connessione sar automatica", LogLevel.Info);
});
return;
}
@@ -251,9 +343,9 @@ namespace AutoBidder
Log("[INFO] Nessun cookie nel browser", LogLevel.Info);
Log("[INFO] Per accedere:", LogLevel.Info);
Log("[INFO] 1. Click su 'Non connesso' nella sidebar", LogLevel.Info);
Log("[INFO] 2. Si aprirà la scheda Browser", LogLevel.Info);
Log("[INFO] 2. Si aprir la scheda Browser", LogLevel.Info);
Log("[INFO] 3. Fai login su Bidoo", LogLevel.Info);
Log("[INFO] 4. La connessione sarà automatica", LogLevel.Info);
Log("[INFO] 4. La connessione sar automatica", LogLevel.Info);
}
else
{
@@ -263,24 +355,23 @@ namespace AutoBidder
}
catch (Exception ex)
{
Log($"[WARN] Errore verifica cookie: {ex.Message}", LogLevel.Warn);
Log($"[WARN] Errore verifica cookie: {ex.Message}", LogLevel.Warning);
}
});
}
/// <summary>
/// Aggiorna immediatamente il banner delle puntate residue (chiamato dopo ogni puntata)
/// Ridisegna subito la barra del conto dopo una puntata, senza aspettare il battito.
///
/// <para>Prima aggiornava il numero solo se era maggiore di zero: finite le puntate,
/// la barra restava sull'ultimo valore positivo — cioè mentiva proprio nel momento
/// in cui contava di più.</para>
/// </summary>
public void UpdateRemainingBidsDisplay()
{
try
{
var session = _sessionService?.GetCurrentSession();
if (session != null && session.RemainingBids > 0)
{
RemainingBidsText.Text = session.RemainingBids.ToString();
Log($"[BANNER UPDATE] Puntate residue aggiornate: {session.RemainingBids}", LogLevel.Info);
}
RefreshAccountPills();
}
catch (Exception ex)
{
@@ -289,48 +380,41 @@ namespace AutoBidder
}
/// <summary>
/// ? Aggiorna l'indicatore del limite minimo puntate nel banner
/// Indicatore del limite minimo puntate, accanto al saldo.
///
/// <para>Il colore va ricalcolato anche a zero puntate: era proprio il caso in cui
/// prima restava dell'ultimo colore utile, cioè verde, mentre il conto era vuoto.</para>
/// </summary>
private void UpdateMinBidsIndicator(int minBidsLimit)
{
try
{
if (minBidsLimit > 0)
if (minBidsLimit <= 0)
{
// Mostra indicatore con solo il numero tra parentesi
MinBidsLimitIndicator.Visibility = Visibility.Visible;
MinBidsLimitIndicator.Text = $"({minBidsLimit})";
MinBidsLimitIndicator.ToolTip = $"Limite minimo puntate attivo: non scendera sotto {minBidsLimit} puntate";
// Colore basato su puntate residue
var session = _sessionService?.GetCurrentSession();
if (session != null && session.RemainingBids > 0)
{
if (session.RemainingBids <= minBidsLimit)
{
// Al limite - Rosso chiaro (più visibile su sfondo scuro)
MinBidsLimitIndicator.Foreground = new System.Windows.Media.SolidColorBrush(
System.Windows.Media.Color.FromRgb(255, 82, 82)); // #FF5252 - Rosso chiaro
}
else if (session.RemainingBids <= minBidsLimit + 10)
{
// Vicino al limite - Giallo
MinBidsLimitIndicator.Foreground = new System.Windows.Media.SolidColorBrush(
System.Windows.Media.Color.FromRgb(255, 193, 7)); // #FFC107 - Giallo
}
else
{
// Sopra il limite - Verde
MinBidsLimitIndicator.Foreground = new System.Windows.Media.SolidColorBrush(
System.Windows.Media.Color.FromRgb(0, 216, 0)); // #00D800 - Verde
}
}
}
else
{
// Nascondi indicatore
MinBidsLimitIndicator.Visibility = Visibility.Collapsed;
return;
}
MinBidsLimitIndicator.Visibility = Visibility.Visible;
MinBidsLimitIndicator.Text = $"({minBidsLimit})";
MinBidsLimitIndicator.ToolTip =
$"Limite minimo puntate attivo: il bot non scende sotto {minBidsLimit} puntate";
var session = _auctionMonitor.GetSession();
if (session == null || string.IsNullOrEmpty(session.Username))
{
MinBidsLimitIndicator.SetResourceReference(
System.Windows.Controls.TextBlock.ForegroundProperty, "Brush.TextFaint");
return;
}
var key =
session.RemainingBids <= minBidsLimit ? "Brush.Danger" :
session.RemainingBids <= minBidsLimit + 10 ? "Brush.Warning" :
"Brush.Success";
MinBidsLimitIndicator.SetResourceReference(
System.Windows.Controls.TextBlock.ForegroundProperty, key);
}
catch { }
}
+25 -14
View File
@@ -24,7 +24,7 @@ namespace AutoBidder
{
if (EmbeddedWebView == null)
{
Log("[WARN] WebView2 non disponibile", LogLevel.Warn);
Log("[WARN] WebView2 non disponibile", LogLevel.Warning);
_webViewInitCompletionSource?.TrySetResult(false);
return;
}
@@ -38,6 +38,8 @@ namespace AutoBidder
// Salva tab corrente e switcha temporaneamente a Browser
var wasVisible = Browser.Visibility == Visibility.Visible;
var currentTab = TabAsteAttive.IsChecked == true ? "AsteAttive" :
TabCerca.IsChecked == true ? "Cerca" :
TabProdotti.IsChecked == true ? "Prodotti" :
TabBrowser.IsChecked == true ? "Browser" :
TabPuntateGratis.IsChecked == true ? "PuntateGratis" :
TabDatiStatistici.IsChecked == true ? "DatiStatistici" :
@@ -85,9 +87,18 @@ namespace AutoBidder
TabAsteAttive.IsChecked = true;
AuctionMonitor.Visibility = Visibility.Visible;
break;
case "Cerca":
// "Cerca" mostra lo stesso controllo Browser, in modalità catalogo.
TabCerca.IsChecked = true;
Browser.Visibility = Visibility.Visible;
break;
case "Prodotti":
TabProdotti.IsChecked = true;
Products.Visibility = Visibility.Visible;
break;
case "PuntateGratis":
TabPuntateGratis.IsChecked = true;
PuntateGratisPanel.Visibility = Visibility.Visible;
FreeBids.Visibility = Visibility.Visible;
break;
case "DatiStatistici":
TabDatiStatistici.IsChecked = true;
@@ -113,15 +124,15 @@ namespace AutoBidder
// Registra evento per rilevare login automatico
EmbeddedWebView.CoreWebView2.NavigationCompleted += OnWebViewNavigationCompleted;
// Notifica che WebView è pronta
// Notifica che WebView pronta
_webViewInitCompletionSource?.TrySetResult(true);
// Verifica immediata se c'è già un cookie
// Verifica immediata se c' gi un cookie
await CheckAndImportCookieIfAvailable();
}
else
{
Log("[ERROR] CoreWebView2 è null dopo init", LogLevel.Error);
Log("[ERROR] CoreWebView2 null dopo init", LogLevel.Error);
_webViewInitCompletionSource?.TrySetResult(false);
}
}
@@ -160,7 +171,7 @@ namespace AutoBidder
}
catch (Exception ex)
{
Log($"[WARN] Verifica cookie fallita: {ex.Message}", LogLevel.Warn);
Log($"[WARN] Verifica cookie fallita: {ex.Message}", LogLevel.Warning);
}
}
@@ -180,7 +191,7 @@ namespace AutoBidder
if (completedTask == timeoutTask)
{
Log("[WARN] Timeout attesa inizializzazione WebView2", LogLevel.Warn);
Log("[WARN] Timeout attesa inizializzazione WebView2", LogLevel.Warning);
return false;
}
@@ -188,7 +199,7 @@ namespace AutoBidder
}
/// <summary>
/// Evento chiamato quando la navigazione nella WebView è completata
/// Evento chiamato quando la navigazione nella WebView completata
/// Rileva automaticamente se l'utente ha effettuato il login
/// </summary>
private async void OnWebViewNavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs e)
@@ -200,7 +211,7 @@ namespace AutoBidder
var url = EmbeddedWebView.CoreWebView2.Source;
// Se l'utente è sulla homepage di Bidoo (dopo login), verifica cookie
// Se l'utente sulla homepage di Bidoo (dopo login), verifica cookie
if (url.Contains("bidoo.com") && !url.Contains("login"))
{
// ? REFACTORED: Delega a CheckAndImportCookieIfAvailable
@@ -275,7 +286,7 @@ namespace AutoBidder
}
catch (Exception ex)
{
Log($"[WARN] Impossibile estrarre cookie da WebView: {ex.Message}", LogLevel.Warn);
Log($"[WARN] Impossibile estrarre cookie da WebView: {ex.Message}", LogLevel.Warning);
return null;
}
}
@@ -289,7 +300,7 @@ namespace AutoBidder
{
if (!_isWebViewInitialized || EmbeddedWebView?.CoreWebView2 == null)
{
Log("[WARN] Browser non inizializzato - attendi qualche secondo e riprova", LogLevel.Warn);
Log("[WARN] Browser non inizializzato - attendi qualche secondo e riprova", LogLevel.Warning);
return false;
}
@@ -299,11 +310,11 @@ namespace AutoBidder
if (string.IsNullOrEmpty(cookieString))
{
Log("[WARN] Nessun cookie trovato nel browser - assicurati di aver effettuato il login su bidoo.com", LogLevel.Warn);
Log("[WARN] Nessun cookie trovato nel browser - assicurati di aver effettuato il login su bidoo.com", LogLevel.Warning);
return false;
}
// ? NOTA: Non aggiorna più TextBox (rimossa) - direttamente alla validazione
// ? NOTA: Non aggiorna pi TextBox (rimossa) - direttamente alla validazione
// Valida e attiva il cookie usando SessionService
var result = await _sessionService.ValidateAndActivateSessionAsync(cookieString);
@@ -334,7 +345,7 @@ namespace AutoBidder
}
/// <summary>
/// Verifica se WebView2 è pronta per l'uso
/// Verifica se WebView2 pronta per l'uso
/// </summary>
public bool IsWebViewReady()
{
+328
View File
@@ -0,0 +1,328 @@
using System;
using System.Threading;
using AutoBidder.Utilities;
namespace AutoBidder.Data
{
/// <summary>
/// L'archivio delle osservazioni: un solo file SQLite con tutto ciò che si osserva e
/// si decide — aste, puntate, interrogazioni, reset, nostre puntate, decisioni del
/// motore, misure di rete, puntatori, sessioni, contabilità del rischio.
///
/// <para><b>Perché un database.</b> Prima c'erano un file JSON per asta (i dossier),
/// un file JSON con lo storico compatto, uno con le statistiche per prodotto: copie
/// parziali degli stessi fatti, da tenere allineate, e nessuna interrogabile. Qui ogni
/// fatto sta in una riga sola, con un indice, e una domanda come «quante puntate
/// manuali fra le 10 e le 13 sui buoni da 10 € sono rimaste senza risposta?» è una
/// riga di SQL invece di una lettura di otto gigabyte.</para>
///
/// <para>Il motore (coda di scrittura, letture in WAL) è in <see cref="SqliteDatabase"/>.
/// I dati d'esercizio e i registri stanno nell'altro file, <see cref="OperationalDatabase"/>.</para>
/// </summary>
public sealed class AuctionDatabase : SqliteDatabase
{
public const int SchemaVersion = 1;
private static readonly object InstanceSync = new();
private static AuctionDatabase? _instance;
/// <summary>L'archivio dell'applicazione, aperto al primo uso in <see cref="AppPaths.DatabaseFile"/>.</summary>
public static AuctionDatabase Instance
{
get
{
lock (InstanceSync)
{
if (_instance == null || _instance.IsDisposed)
_instance = Open(AppPaths.DatabaseFile);
return _instance;
}
}
}
/// <summary>Chiude l'archivio dell'applicazione svuotando la coda. All'uscita, o prima di spostarlo.</summary>
public static void CloseInstance()
{
lock (InstanceSync)
{
_instance?.Dispose();
_instance = null;
}
}
private long _nextDecisionId;
private long _auctionsVersion;
/// <summary>Cambia a ogni scrittura sulle aste: chi tiene una copia in memoria la confronta con questo.</summary>
public long AuctionsVersion => Interlocked.Read(ref _auctionsVersion);
private AuctionDatabase(string path) : base(path, SchemaV1, SchemaVersion, "AutoBidder.Database") { }
public static AuctionDatabase Open(string path) => new(path);
protected override void OnOpened(SqliteConnection writer)
{
// Gli id delle decisioni si assegnano qui, non da SQLite: chi registra una
// decisione deve poterla legare alla puntata senza aspettare il disco.
try { _nextDecisionId = writer.Query("SELECT COALESCE(MAX(decision_id), 0) AS n FROM bot_decisions")[0].Long("n"); }
catch { _nextDecisionId = 0; }
}
protected override void OnStatementExecuted(string sql)
{
if (sql.Contains("auctions", StringComparison.Ordinal)) Interlocked.Increment(ref _auctionsVersion);
}
/// <summary>Il prossimo id per <c>bot_decisions</c>: unico, crescente, senza aspettare la scrittura.</summary>
public long NextDecisionId() => Interlocked.Increment(ref _nextDecisionId);
/// <summary>
/// Svuota le tabelle delle osservazioni e delle decisioni. Restano i prodotti
/// (portano il valore reale scelto dall'utente) e le sessioni.
/// </summary>
public void WipeObservations()
{
EnqueueBatch(new (string, object?[])[]
{
("DELETE FROM auctions", Array.Empty<object?>()),
("DELETE FROM bot_decisions", Array.Empty<object?>()),
("DELETE FROM network_metrics", Array.Empty<object?>()),
("DELETE FROM bidders", Array.Empty<object?>()),
("DELETE FROM risk_ledger", Array.Empty<object?>())
});
Flush();
Vacuum();
}
private const string SchemaV1 = """
CREATE TABLE IF NOT EXISTS meta(
key TEXT PRIMARY KEY,
value TEXT);
-- Un prodotto: ciò che le aste hanno in comune. market_price_est è il valore
-- reale V scelto dall'utente; retail_price_bidoo è il «Compralo Ora».
CREATE TABLE IF NOT EXISTS items(
item_id INTEGER PRIMARY KEY,
product_key TEXT NOT NULL UNIQUE,
title TEXT NOT NULL DEFAULT '',
category TEXT,
retail_price_bidoo REAL,
market_price_est REAL,
market_price_source TEXT,
shipping REAL,
first_seen_at TEXT NOT NULL,
updated_at TEXT NOT NULL);
-- Un'asta: una riga sola, aggiornata dall'apertura alla chiusura. Le colonne
-- del riepilogo restano NULL finché l'asta è in corso.
CREATE TABLE IF NOT EXISTS auctions(
auction_id TEXT PRIMARY KEY,
item_id INTEGER REFERENCES items(item_id),
product_key TEXT NOT NULL DEFAULT '',
name TEXT NOT NULL DEFAULT '',
url TEXT NOT NULL DEFAULT '',
type TEXT NOT NULL DEFAULT 'normal',
status TEXT NOT NULL DEFAULT 'open',
me TEXT,
app_version TEXT,
added_at TEXT NOT NULL,
first_seen_at TEXT NOT NULL,
opened_at TEXT NOT NULL,
closed_at TEXT,
end_time TEXT,
final_price REAL,
winner_user TEXT,
won_by_me INTEGER NOT NULL DEFAULT 0,
outcome TEXT,
final_status TEXT,
bid_increment REAL NOT NULL DEFAULT 0.01,
bid_fee REAL,
shipping REAL,
transaction_fee REAL,
buy_now_price REAL,
market_value REAL,
has_win_limit INTEGER NOT NULL DEFAULT 0,
win_limit TEXT,
my_bids INTEGER NOT NULL DEFAULT 0,
winner_bids_paid INTEGER,
winner_bids_free INTEGER,
resets INTEGER NOT NULL DEFAULT 0,
distinct_bidders INTEGER NOT NULL DEFAULT 0,
total_observed_bids INTEGER NOT NULL DEFAULT 0,
top_bidder_share REAL,
observed_from_start INTEGER NOT NULL DEFAULT 0,
observed_to_end INTEGER NOT NULL DEFAULT 0,
observed_minutes REAL,
avg_ping_ms REAL,
p95_ping_ms REAL,
polls INTEGER NOT NULL DEFAULT 0,
poll_errors INTEGER NOT NULL DEFAULT 0,
configured_lead_ms INTEGER,
lead_is_manual INTEGER NOT NULL DEFAULT 0,
timer_expired INTEGER NOT NULL DEFAULT 0,
successful_bids INTEGER NOT NULL DEFAULT 0,
failed_bids INTEGER NOT NULL DEFAULT 0,
price_velocity_per_minute REAL,
price_series_json TEXT,
bids_by_user_json TEXT,
config_json TEXT,
abandon_reason TEXT,
learned_at TEXT);
CREATE INDEX IF NOT EXISTS ix_auctions_end ON auctions(end_time);
CREATE INDEX IF NOT EXISTS ix_auctions_product ON auctions(product_key);
CREATE INDEX IF NOT EXISTS ix_auctions_status ON auctions(status);
-- Ogni puntata di ogni utente, come la riporta Bidoo. Il prezzo dopo la
-- puntata la identifica: ogni puntata alza il prezzo di un centesimo esatto.
CREATE TABLE IF NOT EXISTS bids(
bid_id INTEGER PRIMARY KEY,
auction_id TEXT NOT NULL REFERENCES auctions(auction_id) ON DELETE CASCADE,
username TEXT NOT NULL,
price_after REAL NOT NULL,
bid_type TEXT NOT NULL DEFAULT '—',
is_mine INTEGER NOT NULL DEFAULT 0,
server_ts INTEGER,
seen_t REAL,
local_ts TEXT,
timer_before REAL,
rtt_ms INTEGER,
clock_offset_ms INTEGER,
UNIQUE(auction_id, price_after));
CREATE INDEX IF NOT EXISTS ix_bids_user ON bids(username);
CREATE INDEX IF NOT EXISTS ix_bids_server_ts ON bids(auction_id, server_ts);
-- Le interrogazioni, già coalescenti: una riga quando cambia qualcosa, e
-- comunque una al secondo perché il ping resti misurabile.
CREATE TABLE IF NOT EXISTS polls(
poll_id INTEGER PRIMARY KEY,
auction_id TEXT NOT NULL REFERENCES auctions(auction_id) ON DELETE CASCADE,
t REAL NOT NULL,
local_ts TEXT,
price REAL,
timer REAL,
status TEXT,
last_bidder TEXT,
is_mine INTEGER NOT NULL DEFAULT 0,
ping_ms INTEGER,
expiry_unix INTEGER,
server_unix INTEGER,
clock_offset_ms INTEGER);
CREATE INDEX IF NOT EXISTS ix_polls_auction ON polls(auction_id, t);
-- Un reset del timer: chiude un ciclo, di cui si sa fin dove è sceso il
-- cronometro e quanto è durato.
CREATE TABLE IF NOT EXISTS resets(
reset_id INTEGER PRIMARY KEY,
auction_id TEXT NOT NULL REFERENCES auctions(auction_id) ON DELETE CASCADE,
t REAL NOT NULL,
local_ts TEXT,
reset_count INTEGER,
price REAL,
bidder TEXT,
min_timer REAL,
cycle_seconds REAL);
CREATE INDEX IF NOT EXISTS ix_resets_auction ON resets(auction_id, t);
-- Le nostre puntate, con anticipo voluto e ottenuto: la coppia su cui si
-- tara il modello di latenza.
CREATE TABLE IF NOT EXISTS my_bids(
id INTEGER PRIMARY KEY,
auction_id TEXT NOT NULL REFERENCES auctions(auction_id) ON DELETE CASCADE,
t REAL NOT NULL,
local_ts TEXT,
price REAL,
planned_lead_ms INTEGER,
actual_lead_ms REAL,
ping_ms INTEGER,
rtt_ms INTEGER,
success INTEGER NOT NULL DEFAULT 0,
error TEXT,
bids_used INTEGER,
remaining_bids INTEGER,
decision_id INTEGER);
CREATE INDEX IF NOT EXISTS ix_my_bids_auction ON my_bids(auction_id, t);
-- Il registro dell'asta (strategie, avvisi, errori), riga per riga.
CREATE TABLE IF NOT EXISTS auction_log(
id INTEGER PRIMARY KEY,
auction_id TEXT NOT NULL REFERENCES auctions(auction_id) ON DELETE CASCADE,
t REAL NOT NULL,
local_ts TEXT,
level TEXT,
category TEXT,
msg TEXT);
CREATE INDEX IF NOT EXISTS ix_log_auction ON auction_log(auction_id, t);
-- Ogni decisione del motore, in Osserva (shadow) come in Attiva (live):
-- cosa avrebbe fatto o ha fatto, con che stima e per quale motivo.
CREATE TABLE IF NOT EXISTS bot_decisions(
decision_id INTEGER PRIMARY KEY,
auction_id TEXT NOT NULL,
ts TEXT NOT NULL,
t REAL,
price REAL,
timer REAL,
action TEXT NOT NULL,
p_win REAL,
ev_estimate REAL,
model_version TEXT,
policy TEXT,
regime TEXT,
mode TEXT NOT NULL,
executed INTEGER NOT NULL DEFAULT 0,
outcome TEXT,
reason_detail TEXT,
state_json TEXT,
context_key TEXT,
alt_policy TEXT,
alt_action TEXT,
alt_p REAL);
CREATE INDEX IF NOT EXISTS ix_decisions_auction ON bot_decisions(auction_id);
CREATE INDEX IF NOT EXISTS ix_decisions_ts ON bot_decisions(ts);
-- Misure di rete aggregate a finestre di dieci secondi.
CREATE TABLE IF NOT EXISTS network_metrics(
id INTEGER PRIMARY KEY,
ts TEXT NOT NULL,
rtt_ms INTEGER,
jitter_ms INTEGER,
p95_ms INTEGER,
clock_offset_ms INTEGER,
samples INTEGER,
source TEXT);
CREATE INDEX IF NOT EXISTS ix_network_ts ON network_metrics(ts);
-- Chi punta: aggregato asta per asta alla chiusura.
CREATE TABLE IF NOT EXISTS bidders(
username TEXT PRIMARY KEY,
first_seen TEXT,
last_seen TEXT,
n_auctions INTEGER NOT NULL DEFAULT 0,
n_bids INTEGER NOT NULL DEFAULT 0,
n_auto INTEGER NOT NULL DEFAULT 0,
n_manual INTEGER NOT NULL DEFAULT 0,
win_count INTEGER NOT NULL DEFAULT 0,
aggressiveness_score REAL,
profile_class TEXT);
CREATE TABLE IF NOT EXISTS sessions(
session_id INTEGER PRIMARY KEY,
started_at TEXT NOT NULL,
ended_at TEXT,
username TEXT,
app_version TEXT,
notes TEXT);
-- Uscite ed entrate in euro, per il gestore del rischio: ogni puntata
-- riuscita è una riga, ogni vincita è una riga.
CREATE TABLE IF NOT EXISTS risk_ledger(
id INTEGER PRIMARY KEY,
ts TEXT NOT NULL,
kind TEXT NOT NULL,
amount_euro REAL NOT NULL,
auction_id TEXT,
note TEXT);
CREATE INDEX IF NOT EXISTS ix_ledger_ts ON risk_ledger(ts);
""";
}
}
+521
View File
@@ -0,0 +1,521 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text.Encodings.Web;
using System.Text.Json;
using AutoBidder.Models;
using AutoBidder.Utilities;
namespace AutoBidder.Data
{
/// <summary>
/// Registra nel database tutto ciò che succede a un'asta mentre la si segue: ogni
/// interrogazione, ogni puntata di ogni utente con l'ora al millisecondo, ogni reset
/// con la profondità del ciclo, ogni nostra puntata con anticipo voluto ed effettivo,
/// ogni riga di registro, e alla fine il riepilogo.
///
/// <para>È l'erede del dossier JSON per asta, con la stessa superficie: il motore
/// chiama gli stessi metodi di prima e non sa che dietro c'è SQLite. Cambia dove
/// finiscono i dati — una riga per tabella invece di una riga per file — e il fatto
/// che da lì si possano interrogare.</para>
///
/// <para>Perché tanto dettaglio: questi dati <b>non esistono a posteriori</b>. Bidoo
/// non espone lo storico dei prezzi né i tempi delle puntate di un'asta conclusa. O li
/// si raccoglie mentre l'asta va avanti, o sono persi — e sono esattamente quelli su
/// cui il modello impara.</para>
///
/// <para>Nessuna scrittura tocca il disco sul thread che segue l'asta: ogni riga va
/// nella coda del database e prosegue.</para>
/// </summary>
public sealed class AuctionRecorder
{
private static readonly ConcurrentDictionary<string, AuctionRecorder> Open = new(StringComparer.Ordinal);
private static readonly JsonSerializerOptions Json = new()
{
WriteIndented = false,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
};
/// <summary>
/// Il nostro nome utente su Bidoo. Lo imposta il motore appena conosce la
/// sessione; finisce sull'asta, così chi rilegge sa quali puntate erano nostre.
/// </summary>
public static volatile string CurrentUsername = "";
private static AuctionDatabase Db => AuctionDatabase.Instance;
private readonly DateTime _startedAt;
private readonly List<int> _pings = new();
private readonly object _sync = new();
private long _polls;
private long _events;
private bool _closed;
// Coalescenza dei poll: si scrive quando cambia qualcosa, e comunque una volta
// al secondo. Misurato sui dossier: l'88,8% delle interrogazioni ha la stessa
// faccia della precedente.
private string? _lastPollSignature;
private DateTime _lastPollWrittenAt = DateTime.MinValue;
// Del ciclo in corso: fin dove è sceso il cronometro e quando è cominciato.
private double _cycleMinTimer = double.MaxValue;
private double _cycleStartedElapsed = -1;
private double _completedCycleMinTimer = double.NaN;
private double _completedCycleSeconds = double.NaN;
private double _lastPollPrice = double.NaN;
private const int MaxPingSamples = 50_000;
public string AuctionId { get; }
/// <summary>Righe scritte finora per quest'asta.</summary>
public long EventCount => _events;
private AuctionRecorder(string auctionId, DateTime startedAt)
{
AuctionId = auctionId;
_startedAt = startedAt;
}
// ── Apertura e chiusura ──────────────────────────────────────────
/// <summary>
/// Apre (o riapre) la registrazione di quest'asta. Riaprendo l'applicazione
/// mentre l'asta è ancora viva si <b>riprende la stessa riga</b>: un'asta seguita
/// a cavallo di un riavvio deve restare una storia sola.
/// </summary>
public static AuctionRecorder? OpenFor(AuctionInfo auction, AppSettings settings)
{
if (auction == null || !settings.RecordAuctions) return null;
if (string.IsNullOrEmpty(auction.AuctionId)) return null;
return Open.GetOrAdd(auction.AuctionId, _ => Create(auction, settings));
}
/// <summary>La registrazione già aperta per quest'asta, se c'è.</summary>
public static AuctionRecorder? For(string auctionId) =>
auctionId != null && Open.TryGetValue(auctionId, out var r) ? r : null;
/// <summary>Fa confluire nel database tutte le righe di registro delle aste. Una volta all'avvio.</summary>
public static void CaptureAuctionLogs()
{
AuctionInfo.LogSink = (auction, entry) =>
For(auction.AuctionId)?.Log(
entry.Level.ToString().ToLowerInvariant(),
entry.Category.ToString(),
entry.Message);
}
/// <summary>
/// L'asta è stata tolta dal monitor prima della fine: la riga resta, marcata
/// come interrotta, perché una serie di prezzi che si ferma a metà sembra
/// un'asta finita a quel prezzo.
/// </summary>
public static void Abandon(string auctionId, string reason)
{
if (auctionId == null) return;
if (!Open.TryRemove(auctionId, out var rec)) return;
rec.LogRow("info", "State", $"asta tolta dal monitor prima della conclusione: {reason}");
Db.Enqueue(
"UPDATE auctions SET status = CASE WHEN outcome IS NULL THEN 'abandoned' ELSE status END, " +
"abandon_reason = ?2, closed_at = COALESCE(closed_at, ?3), polls = ?4 WHERE auction_id = ?1",
auctionId, reason, AuctionDatabase.Now(), rec._polls);
}
/// <summary>Aspetta che tutto ciò che è in coda sia sul disco.</summary>
public static void FlushAll() => Db.Flush();
/// <summary>
/// Una decisione del motore, in Osserva (shadow) come in Attiva (live): cosa
/// avrebbe fatto o ha fatto, con che stima e per quale motivo. Restituisce l'id,
/// che la puntata porta con sé. L'esito si scrive alla chiusura dell'asta.
/// </summary>
public static long RecordDecision(
AuctionInfo auction, AuctionState state, string action, double? pWin, double? ev,
string? reason, string mode, string? regime, string? policy, double? leadMs,
string? contextKey = null, string? altAction = null, double? altP = null)
{
var id = Db.NextDecisionId();
var rec = For(auction.AuctionId);
var stateJson = JsonSerializer.Serialize(new
{
price = state.Price,
timer = Math.Round(state.Timer, 2),
lastBidder = string.IsNullOrEmpty(state.LastBidder) ? null : state.LastBidder,
pingMs = state.PollingLatencyMs,
bidsUsed = auction.BidsUsedOnThisAuction,
resets = auction.ResetCount,
leadMs = leadMs ?? auction.BidBeforeDeadlineMs,
duel = auction.AutoBidDuelDetected,
autoResponses = auction.AutoResponsesInARow,
recentBidders = auction.SnapshotRecentBids(10).Select(b => b.Username).Distinct(StringComparer.OrdinalIgnoreCase).Count()
}, Json);
Db.Enqueue(
"INSERT INTO bot_decisions(decision_id, auction_id, ts, t, price, timer, action, p_win, ev_estimate, model_version, policy, regime, mode, reason_detail, state_json, context_key, alt_policy, alt_action, alt_p) " +
"VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19)",
id, auction.AuctionId, AuctionDatabase.Now(), rec?.Elapsed(), state.Price, Math.Round(state.Timer, 2), action,
pWin, ev, Ml.LearningService.ModelVersion, policy, regime, mode, reason, stateJson,
contextKey, altAction == null ? null : "bandit", altAction, altP);
return id;
}
public static int OpenCount => Open.Count;
private static AuctionRecorder Create(AuctionInfo auction, AppSettings settings)
{
var rec = new AuctionRecorder(auction.AuctionId, DateTime.Now);
var now = AuctionDatabase.Now();
var productKey = ProductKeyHelper.GenerateProductKey(auction.Name);
var config = JsonSerializer.Serialize(new
{
state = auction.State.ToString(),
bidBeforeDeadlineMs = auction.BidBeforeDeadlineMs > 0 ? auction.BidBeforeDeadlineMs : settings.DefaultBidBeforeDeadlineMs,
minPrice = auction.MinPrice,
maxPrice = auction.MaxPrice,
maxBids = auction.MaxClicks,
minResets = auction.MinResets,
maxResets = auction.MaxResets,
bidLeadIsManual = auction.BidLeadIsManual,
adaptiveLead = settings.AdaptiveLeadEnabled,
leadMinMs = settings.LeadMinMs,
leadMaxMs = settings.LeadMaxMs,
valueCheckEnabled = settings.ValueCheckEnabled,
minSavingsPercentage = settings.MinSavingsPercentage,
rawPollsIncluded = settings.RecordPolls
}, Json);
var me = string.IsNullOrEmpty(CurrentUsername) ? null : CurrentUsername;
Db.EnqueueBatch(new (string, object?[])[]
{
("INSERT INTO items(product_key, title, retail_price_bidoo, shipping, first_seen_at, updated_at) " +
"VALUES(?1, ?2, ?3, ?4, ?5, ?5) " +
"ON CONFLICT(product_key) DO UPDATE SET " +
" title = CASE WHEN excluded.title != '' THEN excluded.title ELSE title END, " +
" retail_price_bidoo = COALESCE(excluded.retail_price_bidoo, retail_price_bidoo), " +
" shipping = COALESCE(excluded.shipping, shipping), updated_at = excluded.updated_at",
new object?[] { productKey, auction.Name ?? "", auction.BuyNowPrice, auction.ShippingCost, now }),
("INSERT INTO auctions(auction_id, item_id, product_key, name, url, status, me, app_version, " +
" added_at, first_seen_at, opened_at, bid_fee, shipping, buy_now_price, market_value, has_win_limit, win_limit, " +
" configured_lead_ms, lead_is_manual, config_json) " +
"VALUES(?1, (SELECT item_id FROM items WHERE product_key = ?2), ?2, ?3, ?4, 'open', ?5, ?6, ?7, ?8, ?8, ?9, ?10, ?11, " +
" (SELECT market_price_est FROM items WHERE product_key = ?2), ?12, ?13, ?14, ?15, ?16) " +
"ON CONFLICT(auction_id) DO UPDATE SET " +
" me = COALESCE(excluded.me, me), app_version = excluded.app_version, " +
" buy_now_price = COALESCE(excluded.buy_now_price, buy_now_price), " +
" shipping = COALESCE(excluded.shipping, shipping), " +
" configured_lead_ms = excluded.configured_lead_ms, lead_is_manual = excluded.lead_is_manual, " +
" config_json = excluded.config_json",
new object?[]
{
auction.AuctionId, productKey, auction.Name ?? "", auction.OriginalUrl ?? "", me, AppInfo.Version,
AuctionDatabase.Iso(auction.AddedAt), now, auction.BidCost, auction.ShippingCost, auction.BuyNowPrice,
auction.HasWinLimit, auction.WinLimitDescription,
auction.BidBeforeDeadlineMs > 0 ? auction.BidBeforeDeadlineMs : settings.DefaultBidBeforeDeadlineMs,
auction.BidLeadIsManual, config
})
});
rec.LogRow("info", "State", "registrazione aperta");
return rec;
}
/// <summary>
/// Chiude la registrazione con il riepilogo. Da qui in avanti la riga non cambia
/// più: è ciò che dice a chi analizza che la storia è completa.
/// </summary>
public static void Close(AuctionInfo auction, AuctionDetailRecord detail, AuctionState? finalState)
{
if (auction == null) return;
if (!Open.TryRemove(auction.AuctionId, out var rec)) return;
rec.WriteSummary(auction, detail, finalState);
}
private void WriteSummary(AuctionInfo auction, AuctionDetailRecord detail, AuctionState? finalState)
{
lock (_sync)
{
if (_closed) return;
_closed = true;
}
var pings = PingStats();
var series = JsonSerializer.Serialize(detail.PriceSeries.Select(p => new { t = Math.Round(p.T, 2), price = p.Price }), Json);
var byUser = JsonSerializer.Serialize(detail.BidsByUser, Json);
Db.EnqueueBatch(new (string, object?[])[]
{
("UPDATE auctions SET status = 'closed', closed_at = ?2, end_time = ?3, outcome = ?4, won_by_me = ?5, winner_user = ?6, " +
" final_price = ?7, final_status = ?8, buy_now_price = COALESCE(?9, buy_now_price), shipping = COALESCE(?10, shipping), " +
" bid_fee = ?11, my_bids = ?12, resets = ?13, distinct_bidders = ?14, total_observed_bids = ?15, top_bidder_share = ?16, " +
" observed_from_start = ?17, observed_to_end = ?18, observed_minutes = ?19, first_seen_at = ?20, " +
" avg_ping_ms = ?21, p95_ping_ms = ?22, polls = ?23, poll_errors = ?24, configured_lead_ms = ?25, lead_is_manual = ?26, " +
" timer_expired = ?27, successful_bids = ?28, failed_bids = ?29, price_velocity_per_minute = ?30, " +
" price_series_json = ?31, bids_by_user_json = ?32 " +
"WHERE auction_id = ?1",
new object?[]
{
AuctionId, AuctionDatabase.Now(), AuctionDatabase.Iso(detail.EndedAt), detail.Outcome, detail.WonByMe, detail.Winner,
detail.FinalPrice, finalState?.Status.ToString(), detail.BuyNowPrice, detail.ShippingCost,
auction.BidCost, detail.MyBids, detail.Resets, detail.DistinctBidders, detail.TotalObservedBids, Math.Round(detail.TopBidderShare, 4),
auction.ObservedFromStart, auction.ObservedToEnd, Math.Round(detail.ObservedMinutes, 2), AuctionDatabase.Iso(detail.FirstSeenAt),
pings.Avg, pings.P95, _polls, detail.PollErrors, detail.ConfiguredLeadMs, auction.BidLeadIsManual,
auction.TimerExpiredCount, auction.SuccessfulBidCount, auction.FailedBidCount, Math.Round(detail.PriceVelocityPerMinute, 4),
series, byUser
}),
// Chi ha puntato in quest'asta: aggregato una volta, alla chiusura.
("INSERT INTO bidders(username, first_seen, last_seen, n_auctions, n_bids, n_auto, n_manual, win_count) " +
"SELECT username, MIN(local_ts), MAX(local_ts), 1, COUNT(*), " +
" SUM(CASE WHEN bid_type = 'Auto' THEN 1 ELSE 0 END), SUM(CASE WHEN bid_type = 'Manuale' THEN 1 ELSE 0 END), 0 " +
"FROM bids WHERE auction_id = ?1 GROUP BY username " +
"ON CONFLICT(username) DO UPDATE SET last_seen = excluded.last_seen, first_seen = COALESCE(first_seen, excluded.first_seen), " +
" n_auctions = n_auctions + 1, n_bids = n_bids + excluded.n_bids, n_auto = n_auto + excluded.n_auto, n_manual = n_manual + excluded.n_manual",
new object?[] { AuctionId }),
("UPDATE bidders SET win_count = win_count + 1 WHERE username = ?1 AND ?1 != ''",
new object?[] { detail.Winner ?? "" }),
("UPDATE items SET retail_price_bidoo = COALESCE(?2, retail_price_bidoo), shipping = COALESCE(?3, shipping), updated_at = ?4 " +
"WHERE product_key = ?1",
new object?[] { detail.ProductKey, detail.BuyNowPrice, detail.ShippingCost, AuctionDatabase.Now() }),
// L'esito di ogni decisione, ora che si sa come è finita. Per una puntata
// partita davvero il prezzo dopo di lei era price + 0,01; per una
// decisione shadow o un NO-OP il prezzo è quello del momento. Se il prezzo
// finale non l'ha superato, nessuno ha più puntato: quella sarebbe stata
// (o è stata) la puntata vincente.
("UPDATE bot_decisions SET outcome = CASE " +
" WHEN price + 0.01 >= ?2 - 0.005 THEN 'won' ELSE 'answered' END " +
"WHERE auction_id = ?1 AND executed = 1 AND action = 'BID' AND (outcome IS NULL OR outcome = 'sent')",
new object?[] { AuctionId, detail.FinalPrice }),
("UPDATE bot_decisions SET outcome = CASE " +
" WHEN price >= ?2 - 0.005 THEN (CASE WHEN action = 'BID' THEN 'would_win' ELSE 'missed' END) " +
" ELSE (CASE WHEN action = 'BID' THEN 'would_be_answered' ELSE 'right_skip' END) END " +
"WHERE auction_id = ?1 AND executed = 0 AND outcome IS NULL",
new object?[] { AuctionId, detail.FinalPrice })
});
_events++;
}
// ── Eventi ───────────────────────────────────────────────────────
/// <summary>Una singola interrogazione a Bidoo.</summary>
public void Poll(AuctionState state, bool includeRaw)
{
_polls++;
lock (_sync)
{
if (_pings.Count < MaxPingSamples) _pings.Add(state.PollingLatencyMs);
var now = Elapsed();
if (!double.IsNaN(_lastPollPrice) && state.Price > _lastPollPrice)
{
_completedCycleMinTimer = _cycleMinTimer == double.MaxValue ? double.NaN : _cycleMinTimer;
_completedCycleSeconds = _cycleStartedElapsed >= 0 ? now - _cycleStartedElapsed : double.NaN;
_cycleMinTimer = double.MaxValue;
_cycleStartedElapsed = now;
}
else if (_cycleStartedElapsed < 0)
{
_cycleStartedElapsed = now;
}
_lastPollPrice = state.Price;
if (state.Timer > 0 && state.Timer < _cycleMinTimer) _cycleMinTimer = state.Timer;
}
NetworkSampler.Note(state);
if (!includeRaw) return;
var signature = string.Concat(
state.Price.ToString("F2"), '|', state.LastBidder, '|', state.Status, '|',
(int)state.Timer, '|', state.IsMyBid ? '1' : '0');
var wall = DateTime.Now;
if (signature == _lastPollSignature && (wall - _lastPollWrittenAt).TotalMilliseconds < 1000)
return;
_lastPollSignature = signature;
_lastPollWrittenAt = wall;
Write("INSERT INTO polls(auction_id, t, local_ts, price, timer, status, last_bidder, is_mine, ping_ms, expiry_unix, server_unix, clock_offset_ms) " +
"VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
AuctionId, Elapsed(), AuctionDatabase.Now(), state.Price, Math.Round(state.Timer, 2), state.Status.ToString(),
string.IsNullOrEmpty(state.LastBidder) ? null : state.LastBidder, state.IsMyBid, state.PollingLatencyMs,
state.ExpiryUnixSeconds, state.ServerUnixSeconds, NetworkSampler.ClockOffsetMs(state));
}
/// <summary>Una puntata altrui (o nostra, vista dallo storico), come l'ha riportata Bidoo.</summary>
public void ForeignBid(BidHistoryEntry entry, double price)
{
double? timerBefore;
lock (_sync) timerBefore = _cycleMinTimer == double.MaxValue ? null : Math.Round(_cycleMinTimer, 2);
// Il tipo dichiarato dal server vince su quello ignoto di una riga già presente;
// una puntata ricostruita da un cambio di prezzo non sovrascrive mai un tipo vero.
Write("INSERT INTO bids(auction_id, username, price_after, bid_type, is_mine, server_ts, seen_t, local_ts, timer_before) " +
"VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9) " +
"ON CONFLICT(auction_id, price_after) DO UPDATE SET " +
" bid_type = CASE WHEN excluded.bid_type != '—' THEN excluded.bid_type ELSE bid_type END, " +
" server_ts = COALESCE(NULLIF(excluded.server_ts, 0), server_ts), " +
" is_mine = MAX(is_mine, excluded.is_mine), " +
" username = CASE WHEN excluded.username != '' THEN excluded.username ELSE username END",
AuctionId, entry.Username ?? "", price, entry.BidType ?? BidHistoryEntry.TipoSconosciuto, entry.IsMyBid,
entry.Timestamp > 0 ? entry.Timestamp : (object?)null, Elapsed(), AuctionDatabase.Now(), timerBefore);
}
/// <summary>Una mia puntata, con tutto ciò che serve a giudicarla.</summary>
public void MyBid(
double price,
int plannedLeadMs,
double actualLeadMs,
int pingMs,
bool success,
string? error,
int? bidsUsed,
int? remainingBids,
int rttMs = 0,
long? decisionId = null)
{
Write("INSERT INTO my_bids(auction_id, t, local_ts, price, planned_lead_ms, actual_lead_ms, ping_ms, rtt_ms, success, error, bids_used, remaining_bids, decision_id) " +
"VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
AuctionId, Elapsed(), AuctionDatabase.Now(), price, plannedLeadMs, Math.Round(actualLeadMs, 1), pingMs, rttMs,
success, error, bidsUsed, remainingBids, decisionId);
if (decisionId is { } id)
Db.Enqueue("UPDATE bot_decisions SET executed = 1, outcome = ?2 WHERE decision_id = ?1", id, success ? "sent" : "failed:" + (error ?? "?"));
}
/// <summary>Il timer è stato azzerato da una puntata: l'asta continua.</summary>
public void Reset(int resetCount, double price, string? bidder)
{
double? minTimer, cycleSeconds;
lock (_sync)
{
minTimer = double.IsNaN(_completedCycleMinTimer) ? null : Math.Round(_completedCycleMinTimer, 2);
cycleSeconds = double.IsNaN(_completedCycleSeconds) ? null : Math.Round(_completedCycleSeconds, 2);
_completedCycleMinTimer = double.NaN;
_completedCycleSeconds = double.NaN;
}
Write("INSERT INTO resets(auction_id, t, local_ts, reset_count, price, bidder, min_timer, cycle_seconds) " +
"VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
AuctionId, Elapsed(), AuctionDatabase.Now(), resetCount, price, bidder, minTimer, cycleSeconds);
}
/// <summary>Una riga del registro dell'asta.</summary>
public void Log(string level, string category, string message)
{
// Le righe "[RESET #n]" ripetono l'evento reset che sta già nella sua tabella.
if (message != null && message.StartsWith("[RESET #", StringComparison.Ordinal)) return;
LogRow(level, category, message ?? "");
}
/// <summary>Cambio di modo: Ferma, Osserva, Attiva.</summary>
public void StateChanged(string from, string to) => LogRow("info", "State", $"{from} → {to}");
private void LogRow(string level, string category, string message)
{
Write("INSERT INTO auction_log(auction_id, t, local_ts, level, category, msg) VALUES(?1, ?2, ?3, ?4, ?5, ?6)",
AuctionId, Elapsed(), AuctionDatabase.Now(), level, category, message);
}
// ── Interno ──────────────────────────────────────────────────────
private void Write(string sql, params object?[] args)
{
if (_closed) return;
Db.Enqueue(sql, args);
_events++;
}
internal double Elapsed() => Math.Round((DateTime.Now - _startedAt).TotalSeconds, 3);
private (int Count, double Avg, int P95) PingStats()
{
int[] copy;
lock (_sync) copy = _pings.Where(p => p > 0).ToArray();
if (copy.Length == 0) return (0, 0, 0);
Array.Sort(copy);
return (copy.Length, Math.Round(copy.Average(), 1), copy[Math.Min(copy.Length - 1, (int)(copy.Length * 0.95))]);
}
}
/// <summary>
/// Misure di rete aggregate: una riga ogni dieci secondi con mediana, p95, jitter e
/// scarto dell'orologio, su tutte le interrogazioni di tutte le aste. Una riga per
/// poll sarebbero milioni di righe uguali; una ogni dieci secondi racconta la stessa
/// storia in un decimillesimo dello spazio.
/// </summary>
public static class NetworkSampler
{
private static readonly object Sync = new();
private static readonly List<int> _pings = new(512);
private static long _windowStartedTicks = Environment.TickCount64;
private static int _minOffset = int.MaxValue;
private const int WindowMs = 10_000;
/// <summary>
/// Scarto fra il nostro orologio e quello del server, in ms, per questa risposta.
/// Bidoo dichiara i secondi interi, quindi il valore è quantizzato: il minimo su
/// una finestra converge al confine reale del secondo.
/// </summary>
public static int ClockOffsetMs(AuctionState state)
{
if (state.ServerUnixSeconds <= 0) return 0;
var localMs = new DateTimeOffset(state.SnapshotTime.Kind == DateTimeKind.Utc ? state.SnapshotTime : state.SnapshotTime.ToUniversalTime())
.ToUnixTimeMilliseconds() - state.PollingLatencyMs / 2;
return (int)Math.Clamp(localMs - state.ServerUnixSeconds * 1000L, int.MinValue / 2, int.MaxValue / 2);
}
public static void Note(AuctionState state)
{
if (state.PollingLatencyMs <= 0) return;
List<int>? flush = null;
int offset = 0;
lock (Sync)
{
_pings.Add(state.PollingLatencyMs);
var o = ClockOffsetMs(state);
if (state.ServerUnixSeconds > 0 && o < _minOffset) _minOffset = o;
if (Environment.TickCount64 - _windowStartedTicks >= WindowMs)
{
flush = new List<int>(_pings);
offset = _minOffset == int.MaxValue ? 0 : _minOffset;
_pings.Clear();
_minOffset = int.MaxValue;
_windowStartedTicks = Environment.TickCount64;
}
}
if (flush == null || flush.Count == 0) return;
flush.Sort();
var p50 = flush[flush.Count / 2];
var p95 = flush[Math.Min(flush.Count - 1, (int)(flush.Count * 0.95))];
AuctionDatabase.Instance.Enqueue(
"INSERT INTO network_metrics(ts, rtt_ms, jitter_ms, p95_ms, clock_offset_ms, samples, source) VALUES(?1, ?2, ?3, ?4, ?5, ?6, 'poll')",
AuctionDatabase.Now(), p50, p95 - p50, p95, offset, flush.Count);
}
}
}
+202
View File
@@ -0,0 +1,202 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using AutoBidder.Utilities;
namespace AutoBidder.Data
{
/// <summary>
/// Il database d'esercizio: tutto ciò che l'applicazione scrive per sé e non per
/// l'analisi. Sta in un file a parte (<c>esercizio.sqlite</c>) accanto a quello delle
/// osservazioni, così l'archivio delle aste resta un archivio e questo resta piccolo.
///
/// <para><b>Documenti.</b> Le aste nel monitor, i prodotti seguiti, le statistiche per
/// prodotto, le promozioni riscattate, il modello appreso, il profilo, la latenza:
/// ognuno era un file JSON riscritto per intero a ogni modifica. Qui sono righe di una
/// tabella <c>documents</c>, chiave → JSON: la scrittura è atomica (una riga in una
/// transazione, niente file a metà), e c'è un solo file da salvare.</para>
///
/// <para><b>Registri.</b> Il registro applicativo e quello del riscatto puntate sono
/// tabelle: una riga per evento, con data e livello. Si interrogano, si esportano in
/// testo dalle Impostazioni, e si cancellano per età senza girare cartelle.</para>
/// </summary>
public sealed class OperationalDatabase : SqliteDatabase
{
public const int SchemaVersion = 1;
private static readonly object InstanceSync = new();
private static OperationalDatabase? _instance;
public static OperationalDatabase Instance
{
get
{
lock (InstanceSync)
{
if (_instance == null || _instance.IsDisposed)
_instance = Open(AppPaths.OperationalDatabaseFile);
return _instance;
}
}
}
public static void CloseInstance()
{
lock (InstanceSync)
{
_instance?.Dispose();
_instance = null;
}
}
/// <summary>Vero quando l'istanza è aperta e pronta: chi scrive nei registri lo controlla prima.</summary>
public static bool IsOpen
{
get { lock (InstanceSync) return _instance != null && !_instance.IsDisposed; }
}
private OperationalDatabase(string path) : base(path, SchemaV1, SchemaVersion, "AutoBidder.Esercizio") { }
public static OperationalDatabase Open(string path) => new(path);
private const string SchemaV1 = """
-- Un documento JSON per chiave: aste nel monitor, prodotti seguiti,
-- statistiche per prodotto, promozioni riscattate, modelli appresi.
CREATE TABLE IF NOT EXISTS documents(
key TEXT PRIMARY KEY,
json TEXT NOT NULL,
updated_at TEXT NOT NULL);
-- Il registro applicativo: una riga per evento.
CREATE TABLE IF NOT EXISTS app_log(
id INTEGER PRIMARY KEY,
ts TEXT NOT NULL,
level TEXT NOT NULL,
message TEXT NOT NULL);
CREATE INDEX IF NOT EXISTS ix_app_log_ts ON app_log(ts);
-- Il registro del riscatto puntate.
CREATE TABLE IF NOT EXISTS freebids_log(
id INTEGER PRIMARY KEY,
ts TEXT NOT NULL,
level TEXT NOT NULL,
message TEXT NOT NULL);
CREATE INDEX IF NOT EXISTS ix_freebids_log_ts ON freebids_log(ts);
""";
// ── Documenti ────────────────────────────────────────────────────
/// <summary>Il JSON del documento, o null se non c'è.</summary>
public string? GetDocument(string key)
{
Flush();
return ScalarString("SELECT json FROM documents WHERE key = ?1", key);
}
/// <summary>
/// Il documento, oppure — se manca — il contenuto del vecchio file JSON indicato,
/// che viene importato una volta e da allora vive qui. Il file resta dov'è.
/// </summary>
public string? GetDocumentOrImport(string key, string? legacyFile)
{
var json = GetDocument(key);
if (json != null) return json;
try
{
if (string.IsNullOrEmpty(legacyFile) || !File.Exists(legacyFile)) return null;
json = File.ReadAllText(legacyFile);
if (string.IsNullOrWhiteSpace(json)) return null;
SetDocument(key, json);
Log($"[DATABASE] Importato {System.IO.Path.GetFileName(legacyFile)} come documento «{key}»");
return json;
}
catch (Exception ex)
{
Log($"[DATABASE] Importazione di {legacyFile} non riuscita: {ex.Message}");
return null;
}
}
public void SetDocument(string key, string json)
{
Enqueue("INSERT INTO documents(key, json, updated_at) VALUES(?1, ?2, ?3) " +
"ON CONFLICT(key) DO UPDATE SET json = excluded.json, updated_at = excluded.updated_at",
key, json, Now());
}
public void DeleteDocument(string key) => Enqueue("DELETE FROM documents WHERE key = ?1", key);
/// <summary>Cancella tutti i documenti con quel prefisso (es. «ml/»).</summary>
public void DeleteDocuments(string prefix) => Enqueue("DELETE FROM documents WHERE key LIKE ?1", prefix + "%");
public IReadOnlyList<(string Key, long Bytes, DateTime UpdatedAt)> Documents()
{
Flush();
return Query("SELECT key, LENGTH(json) AS bytes, updated_at FROM documents ORDER BY key")
.Select(r => (r.Str("key"), r.Long("bytes"), r.Date("updated_at") ?? DateTime.MinValue))
.ToList();
}
// ── Registri ─────────────────────────────────────────────────────
public const string AppLog = "app_log";
public const string FreeBidsLog = "freebids_log";
public void WriteLog(string table, string level, string message)
{
if (table != AppLog && table != FreeBidsLog) return;
Enqueue($"INSERT INTO {table}(ts, level, message) VALUES(?1, ?2, ?3)", Now(), level, message ?? "");
}
/// <summary>Cancella le righe più vecchie di tanti giorni. Restituisce quante.</summary>
public int PurgeLogs(int days)
{
if (days <= 0) return 0;
var cutoff = Iso(DateTime.Now.AddDays(-days));
var removed = 0;
foreach (var table in new[] { AppLog, FreeBidsLog })
{
removed += (int)ScalarLong($"SELECT COUNT(*) AS n FROM {table} WHERE ts < ?1", cutoff);
Enqueue($"DELETE FROM {table} WHERE ts < ?1", cutoff);
}
Flush();
return removed;
}
/// <summary>Le ultime righe di un registro, dalla più recente.</summary>
public List<(DateTime Ts, string Level, string Message)> ReadLog(string table, int max = 2000, DateTime? since = null)
{
Flush();
var rows = since is { } s
? Query($"SELECT ts, level, message FROM {table} WHERE ts >= ?1 ORDER BY id DESC LIMIT ?2", Iso(s), max)
: Query($"SELECT ts, level, message FROM {table} ORDER BY id DESC LIMIT ?1", max);
return rows.Select(r => (r.Date("ts") ?? DateTime.MinValue, r.Str("level"), r.Str("message"))).ToList();
}
/// <summary>Scrive un registro in un file di testo leggibile (una riga per evento, ora locale).</summary>
public int ExportLog(string table, string destination, int days = 0)
{
Flush();
var rows = days > 0
? Query($"SELECT ts, level, message FROM {table} WHERE ts >= ?1 ORDER BY id", Iso(DateTime.Now.AddDays(-days)))
: Query($"SELECT ts, level, message FROM {table} ORDER BY id");
var sb = new StringBuilder();
foreach (var r in rows)
{
var ts = r.Date("ts")?.ToLocalTime() ?? DateTime.MinValue;
sb.Append(ts.ToString("yyyy-MM-dd HH:mm:ss.fff")).Append(" [").Append(r.Str("level")).Append("] ").AppendLine(r.Str("message"));
}
var folder = System.IO.Path.GetDirectoryName(destination);
if (!string.IsNullOrEmpty(folder)) Directory.CreateDirectory(folder);
File.WriteAllText(destination, sb.ToString(), new UTF8Encoding(true));
return rows.Count;
}
}
}
+495
View File
@@ -0,0 +1,495 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
namespace AutoBidder.Data
{
/// <summary>
/// SQLite senza librerie: si chiama direttamente <c>winsqlite3.dll</c>, la copia di
/// SQLite che Windows 10 e 11 tengono in <c>System32</c> per i propri componenti.
///
/// <para><b>Perché così.</b> L'applicazione non deve dipendere da pacchetti esterni.
/// Un provider ADO.NET porterebbe con sé quattro assembly e una DLL nativa; qui ci
/// sono le quindici funzioni C che servono davvero, avvolte in due classi che non
/// fanno niente di più di quello che il programma usa: aprire, preparare, legare
/// parametri, scorrere righe, chiudere.</para>
///
/// <para><b>Fili.</b> Ogni connessione è usata da un thread alla volta: il database
/// scrive da un thread suo e legge sotto un lucchetto (vedi <see cref="AuctionDatabase"/>).
/// La connessione è comunque aperta in modalità serializzata, per non dipendere dalla
/// disciplina del chiamante.</para>
/// </summary>
internal static class SqliteNative
{
private const string Lib = "winsqlite3.dll";
public const int Ok = 0;
public const int Row = 100;
public const int Done = 101;
public const int Busy = 5;
public const int Locked = 6;
public const int OpenReadWrite = 0x2;
public const int OpenCreate = 0x4;
public const int OpenFullMutex = 0x10000;
public const int TypeInteger = 1;
public const int TypeFloat = 2;
public const int TypeText = 3;
public const int TypeBlob = 4;
public const int TypeNull = 5;
/// <summary>SQLITE_TRANSIENT: SQLite copia il testo legato prima che il buffer venga liberato.</summary>
public static readonly IntPtr Transient = new(-1);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_open_v2(byte[] filename, out IntPtr db, int flags, IntPtr vfs);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_close_v2(IntPtr db);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_prepare_v2(IntPtr db, byte[] sql, int nByte, out IntPtr stmt, out IntPtr tail);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_step(IntPtr stmt);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_reset(IntPtr stmt);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_clear_bindings(IntPtr stmt);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_finalize(IntPtr stmt);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_bind_int64(IntPtr stmt, int index, long value);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_bind_double(IntPtr stmt, int index, double value);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_bind_null(IntPtr stmt, int index);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_bind_text(IntPtr stmt, int index, byte[] text, int nBytes, IntPtr destructor);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_column_count(IntPtr stmt);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_column_type(IntPtr stmt, int col);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern long sqlite3_column_int64(IntPtr stmt, int col);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern double sqlite3_column_double(IntPtr stmt, int col);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr sqlite3_column_text(IntPtr stmt, int col);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_column_bytes(IntPtr stmt, int col);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr sqlite3_column_name(IntPtr stmt, int col);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr sqlite3_errmsg(IntPtr db);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern long sqlite3_last_insert_rowid(IntPtr db);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_changes(IntPtr db);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_busy_timeout(IntPtr db, int ms);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr sqlite3_libversion();
public static byte[] Utf8Z(string s)
{
var bytes = Encoding.UTF8.GetBytes(s);
var z = new byte[bytes.Length + 1];
Buffer.BlockCopy(bytes, 0, z, 0, bytes.Length);
return z;
}
public static string? Utf8(IntPtr p) => p == IntPtr.Zero ? null : Marshal.PtrToStringUTF8(p);
}
public sealed class SqliteException : Exception
{
public int Code { get; }
public SqliteException(int code, string message) : base(message) => Code = code;
}
/// <summary>Una connessione. Da usare da un thread alla volta.</summary>
public sealed class SqliteConnection : IDisposable
{
private IntPtr _db;
private readonly Dictionary<string, SqliteStatement> _cache = new(StringComparer.Ordinal);
public string Path { get; }
public static string LibraryVersion
{
get
{
try { return SqliteNative.Utf8(SqliteNative.sqlite3_libversion()) ?? "?"; }
catch (DllNotFoundException) { return "winsqlite3.dll non trovata"; }
}
}
public SqliteConnection(string path)
{
Path = path;
var rc = SqliteNative.sqlite3_open_v2(
SqliteNative.Utf8Z(path), out _db,
SqliteNative.OpenReadWrite | SqliteNative.OpenCreate | SqliteNative.OpenFullMutex,
IntPtr.Zero);
if (rc != SqliteNative.Ok)
{
var msg = _db != IntPtr.Zero ? ErrorMessage() : $"codice {rc}";
if (_db != IntPtr.Zero) SqliteNative.sqlite3_close_v2(_db);
_db = IntPtr.Zero;
throw new SqliteException(rc, $"apertura di {path} non riuscita: {msg}");
}
// Un altro processo (o l'altra connessione) può tenere il file per qualche
// millisecondo: si aspetta invece di fallire.
SqliteNative.sqlite3_busy_timeout(_db, 5000);
}
internal IntPtr Handle => _db;
public string ErrorMessage() => SqliteNative.Utf8(SqliteNative.sqlite3_errmsg(_db)) ?? "errore sconosciuto";
public long LastInsertRowId => SqliteNative.sqlite3_last_insert_rowid(_db);
public int Changes => SqliteNative.sqlite3_changes(_db);
/// <summary>Esegue un'istruzione senza risultati (o ne scarta le righe).</summary>
public void Execute(string sql, params object?[] args)
{
using var stmt = Prepare(sql);
stmt.Bind(args);
stmt.RunToEnd();
}
/// <summary>Esegue più istruzioni separate da punto e virgola, senza parametri.</summary>
public void ExecuteScript(string script)
{
var bytes = SqliteNative.Utf8Z(script);
var offset = 0;
while (offset < bytes.Length - 1)
{
var remaining = new byte[bytes.Length - offset];
Buffer.BlockCopy(bytes, offset, remaining, 0, remaining.Length);
var rc = SqliteNative.sqlite3_prepare_v2(_db, remaining, -1, out var stmt, out var tail);
if (rc != SqliteNative.Ok) throw new SqliteException(rc, ErrorMessage());
// Il puntatore alla coda restituito da SQLite riguarda una copia nativa
// del buffer di cui non abbiamo la base: si ricava quanto è stato
// consumato scandendo fino al primo ';' fuori da apici e commenti, che è
// esattamente dove SQLite si è fermato.
var consumed = IndexAfterStatement(remaining);
if (stmt != IntPtr.Zero)
{
using var s = new SqliteStatement(this, stmt, "");
s.RunToEnd();
}
offset += Math.Max(1, consumed);
}
}
/// <summary>Fino al primo ';' fuori da apici e commenti, incluso.</summary>
private static int IndexAfterStatement(byte[] bytes)
{
var inQuote = false;
var inLineComment = false;
for (var i = 0; i < bytes.Length - 1; i++)
{
var c = (char)bytes[i];
if (inLineComment) { if (c == '\n') inLineComment = false; continue; }
if (c == '\'') inQuote = !inQuote;
else if (!inQuote && c == '-' && i + 1 < bytes.Length && bytes[i + 1] == '-') inLineComment = true;
else if (!inQuote && c == ';') return i + 1;
}
return bytes.Length - 1;
}
/// <summary>Prepara un'istruzione nuova, non condivisa. Chi la chiede la libera.</summary>
public SqliteStatement Prepare(string sql)
{
var rc = SqliteNative.sqlite3_prepare_v2(_db, SqliteNative.Utf8Z(sql), -1, out var stmt, out _);
if (rc != SqliteNative.Ok || stmt == IntPtr.Zero)
throw new SqliteException(rc, $"{ErrorMessage()} — SQL: {sql}");
return new SqliteStatement(this, stmt, sql);
}
/// <summary>
/// Un'istruzione preparata e riusata: le stesse dieci frasi vengono eseguite
/// migliaia di volte, e prepararle ogni volta costerebbe più dell'esecuzione.
/// Viene azzerata prima di ogni uso.
/// </summary>
public SqliteStatement Cached(string sql)
{
if (!_cache.TryGetValue(sql, out var stmt))
{
stmt = Prepare(sql);
stmt.Shared = true;
_cache[sql] = stmt;
}
stmt.Reset();
return stmt;
}
public List<SqliteRow> Query(string sql, params object?[] args)
{
var stmt = Cached(sql);
stmt.Bind(args);
return stmt.ReadAll();
}
public void Dispose()
{
foreach (var s in _cache.Values) s.Shared = false;
foreach (var s in _cache.Values) s.Dispose();
_cache.Clear();
if (_db != IntPtr.Zero)
{
SqliteNative.sqlite3_close_v2(_db);
_db = IntPtr.Zero;
}
}
}
/// <summary>Un'istruzione preparata.</summary>
public sealed class SqliteStatement : IDisposable
{
private readonly SqliteConnection _conn;
private IntPtr _stmt;
private string[]? _columns;
public string Sql { get; }
/// <summary>Appartiene alla cache della connessione: Dispose non la libera.</summary>
internal bool Shared { get; set; }
internal SqliteStatement(SqliteConnection conn, IntPtr stmt, string sql)
{
_conn = conn;
_stmt = stmt;
Sql = sql;
}
public void Reset()
{
SqliteNative.sqlite3_reset(_stmt);
SqliteNative.sqlite3_clear_bindings(_stmt);
}
/// <summary>Lega i parametri posizionali (?1, ?2, …) nell'ordine dato.</summary>
public void Bind(object?[] args)
{
for (var i = 0; i < args.Length; i++)
{
var idx = i + 1;
var rc = args[i] switch
{
null => SqliteNative.sqlite3_bind_null(_stmt, idx),
string s => BindText(idx, s),
bool b => SqliteNative.sqlite3_bind_int64(_stmt, idx, b ? 1 : 0),
int n => SqliteNative.sqlite3_bind_int64(_stmt, idx, n),
long l => SqliteNative.sqlite3_bind_int64(_stmt, idx, l),
double d => double.IsNaN(d) || double.IsInfinity(d)
? SqliteNative.sqlite3_bind_null(_stmt, idx)
: SqliteNative.sqlite3_bind_double(_stmt, idx, d),
float f => SqliteNative.sqlite3_bind_double(_stmt, idx, f),
decimal m => SqliteNative.sqlite3_bind_double(_stmt, idx, (double)m),
DateTime dt => BindText(idx, AuctionDatabase.Iso(dt)),
DateTimeOffset dto => BindText(idx, dto.UtcDateTime.ToString("o")),
Enum e => BindText(idx, e.ToString()),
_ => BindText(idx, args[i]!.ToString() ?? "")
};
if (rc != SqliteNative.Ok)
throw new SqliteException(rc, $"{_conn.ErrorMessage()} — parametro {idx} di: {Sql}");
}
}
private int BindText(int idx, string s)
{
var bytes = Encoding.UTF8.GetBytes(s);
return SqliteNative.sqlite3_bind_text(_stmt, idx, bytes, bytes.Length, SqliteNative.Transient);
}
/// <summary>Avanza di una riga. False alla fine.</summary>
public bool Step()
{
var rc = SqliteNative.sqlite3_step(_stmt);
if (rc == SqliteNative.Row) return true;
if (rc == SqliteNative.Done) return false;
throw new SqliteException(rc, $"{_conn.ErrorMessage()} — SQL: {Sql}");
}
public void RunToEnd()
{
while (Step()) { }
}
public string[] Columns
{
get
{
if (_columns != null) return _columns;
var n = SqliteNative.sqlite3_column_count(_stmt);
_columns = new string[n];
for (var i = 0; i < n; i++)
_columns[i] = SqliteNative.Utf8(SqliteNative.sqlite3_column_name(_stmt, i)) ?? $"c{i}";
return _columns;
}
}
public object? Value(int col)
{
switch (SqliteNative.sqlite3_column_type(_stmt, col))
{
case SqliteNative.TypeInteger: return SqliteNative.sqlite3_column_int64(_stmt, col);
case SqliteNative.TypeFloat: return SqliteNative.sqlite3_column_double(_stmt, col);
case SqliteNative.TypeText:
var p = SqliteNative.sqlite3_column_text(_stmt, col);
var n = SqliteNative.sqlite3_column_bytes(_stmt, col);
return p == IntPtr.Zero ? "" : Marshal.PtrToStringUTF8(p, n);
default: return null;
}
}
public List<SqliteRow> ReadAll()
{
var rows = new List<SqliteRow>();
var columns = Columns;
var index = SqliteRow.IndexFor(columns);
while (Step())
{
var values = new object?[columns.Length];
for (var i = 0; i < values.Length; i++) values[i] = Value(i);
rows.Add(new SqliteRow(index, values));
}
return rows;
}
public void Dispose()
{
if (Shared) return;
if (_stmt != IntPtr.Zero)
{
SqliteNative.sqlite3_finalize(_stmt);
_stmt = IntPtr.Zero;
}
}
}
/// <summary>Una riga letta, con accesso per nome di colonna e conversioni tolleranti.</summary>
public sealed class SqliteRow
{
private readonly Dictionary<string, int> _index;
private readonly object?[] _values;
private static readonly Dictionary<string, Dictionary<string, int>> IndexCache = new(StringComparer.Ordinal);
internal static Dictionary<string, int> IndexFor(string[] columns)
{
var key = string.Join("|", columns);
lock (IndexCache)
{
if (!IndexCache.TryGetValue(key, out var idx))
{
idx = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
for (var i = 0; i < columns.Length; i++) idx[columns[i]] = i;
IndexCache[key] = idx;
}
return idx;
}
}
internal SqliteRow(Dictionary<string, int> index, object?[] values)
{
_index = index;
_values = values;
}
public object? this[string column] => _index.TryGetValue(column, out var i) ? _values[i] : null;
/// <summary>Valore per posizione: per i risultati scalari.</summary>
public object? At(int i) => i >= 0 && i < _values.Length ? _values[i] : null;
public bool Has(string column) => _index.ContainsKey(column);
public string Str(string column) => this[column]?.ToString() ?? "";
public string? StrOrNull(string column) => this[column] as string;
public long Long(string column) => this[column] switch
{
long l => l,
double d => (long)d,
string s when long.TryParse(s, out var v) => v,
_ => 0
};
public int Int(string column) => (int)Long(column);
public bool Bool(string column) => Long(column) != 0;
public double Dbl(string column) => this[column] switch
{
double d => d,
long l => l,
string s when double.TryParse(s, System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture, out var v) => v,
_ => 0
};
public double? DblOrNull(string column) => this[column] == null ? null : Dbl(column);
public int? IntOrNull(string column) => this[column] == null ? null : Int(column);
public long? LongOrNull(string column) => this[column] == null ? null : Long(column);
/// <summary>Data in UTC, o null. Le date sono scritte in ISO 8601 UTC.</summary>
public DateTime? Date(string column)
{
var s = this[column] as string;
if (string.IsNullOrEmpty(s)) return null;
if (!DateTime.TryParse(s, System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.RoundtripKind, out var d))
return null;
return d.Kind switch
{
DateTimeKind.Utc => d,
DateTimeKind.Local => d.ToUniversalTime(),
_ => DateTime.SpecifyKind(d, DateTimeKind.Utc) // scritto senza fuso: era UTC
};
}
}
}
+408
View File
@@ -0,0 +1,408 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Threading;
namespace AutoBidder.Data
{
/// <summary>
/// Il motore comune dei due database dell'applicazione: un file SQLite, una coda di
/// scrittura svuotata a blocchi da un thread dedicato, una connessione di lettura a
/// parte con giornale WAL.
///
/// <para><b>Come scrive.</b> Mai sul thread che chiama. Ogni scrittura finisce in coda
/// e il thread del database la esegue in una transazione ogni qualche centinaio di
/// righe: SQLite fa migliaia di inserimenti al secondo così, e nessuno sul percorso
/// della puntata aspetta il disco. Chi deve leggere ciò che ha appena scritto chiama
/// <see cref="Flush"/>.</para>
///
/// <para><b>Come legge.</b> Su una seconda connessione, sotto un lucchetto. Con il
/// giornale WAL le letture non bloccano la scrittura e viceversa.</para>
/// </summary>
public abstract class SqliteDatabase : IDisposable
{
/// <summary>Righe di registro (errori di scrittura, migrazioni): chi si aggancia le scrive dove vuole.</summary>
public static event Action<string>? OnLog;
protected static void Log(string message) => OnLog?.Invoke(message);
private readonly SqliteConnection _writer;
private readonly SqliteConnection _reader;
private readonly object _readSync = new();
private abstract class Pending { public long Seq; }
private sealed class Statement : Pending
{
public string Sql = "";
public object?[] Args = Array.Empty<object?>();
}
private sealed class Batch : Pending
{
public List<(string Sql, object?[] Args)> Ops = new();
}
private sealed class Custom : Pending
{
public Action<SqliteConnection> Action = _ => { };
public Exception? Error;
public ManualResetEventSlim Done = new(false);
}
private readonly Queue<Pending> _queue = new();
private readonly object _queueSync = new();
private long _lastEnqueued;
private long _lastCommitted;
private readonly Thread _writerThread;
private volatile bool _disposed;
private volatile bool _stop;
private long _writesCommitted;
private long _writesFailed;
public long WritesCommitted => Interlocked.Read(ref _writesCommitted);
public long WritesFailed => Interlocked.Read(ref _writesFailed);
public int QueueLength { get { lock (_queueSync) return _queue.Count; } }
public bool IsDisposed => _disposed;
public string Path { get; }
public static string LibraryVersion => SqliteConnection.LibraryVersion;
protected SqliteDatabase(string path, string schemaScript, int schemaVersion, string threadName)
{
Path = path;
var folder = System.IO.Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(folder)) Directory.CreateDirectory(folder);
_writer = new SqliteConnection(path);
Configure(_writer);
Migrate(_writer, schemaScript, schemaVersion);
OnOpened(_writer);
_reader = new SqliteConnection(path);
Configure(_reader);
_writerThread = new Thread(WriterLoop)
{
Name = threadName,
IsBackground = true,
Priority = ThreadPriority.BelowNormal
};
_writerThread.Start();
}
/// <summary>Chiamato con la connessione di scrittura, dopo lo schema, prima del thread.</summary>
protected virtual void OnOpened(SqliteConnection writer) { }
/// <summary>Chiamato dal thread di scrittura per ogni istruzione eseguita.</summary>
protected virtual void OnStatementExecuted(string sql) { }
private static void Configure(SqliteConnection c)
{
c.Execute("PRAGMA journal_mode=WAL");
c.Execute("PRAGMA synchronous=NORMAL");
c.Execute("PRAGMA foreign_keys=ON");
c.Execute("PRAGMA temp_store=MEMORY");
}
private void Migrate(SqliteConnection c, string schema, int target)
{
var version = (int)c.Query("PRAGMA user_version")[0].Long("user_version");
if (version >= target) return;
c.ExecuteScript(schema);
c.Execute($"PRAGMA user_version={target}");
Log($"[DATABASE] Schema versione {target} in {c.Path} — SQLite {LibraryVersion}");
}
// ── Scrittura ────────────────────────────────────────────────────
public void Enqueue(string sql, params object?[] args)
{
if (_disposed) return;
Push(new Statement { Sql = sql, Args = args });
}
public void EnqueueBatch(IEnumerable<(string Sql, object?[] Args)> ops)
{
if (_disposed) return;
var batch = new Batch();
batch.Ops.AddRange(ops);
if (batch.Ops.Count == 0) return;
Push(batch);
}
public void RunOnWriter(Action<SqliteConnection> action, int timeoutMs = 60000)
{
if (_disposed) throw new ObjectDisposedException(GetType().Name);
var custom = new Custom { Action = action };
Push(custom);
if (!custom.Done.Wait(timeoutMs)) throw new TimeoutException("il database non ha risposto in tempo");
if (custom.Error != null) throw custom.Error;
}
public void ExecuteNow(string sql, params object?[] args)
{
Enqueue(sql, args);
Flush();
}
private void Push(Pending p)
{
lock (_queueSync)
{
p.Seq = ++_lastEnqueued;
_queue.Enqueue(p);
Monitor.PulseAll(_queueSync);
}
}
public bool Flush(int timeoutMs = 15000)
{
long target;
lock (_queueSync) target = _lastEnqueued;
var deadline = Environment.TickCount64 + timeoutMs;
lock (_queueSync)
{
while (Interlocked.Read(ref _lastCommitted) < target)
{
var left = deadline - Environment.TickCount64;
if (left <= 0 || _disposed) return Interlocked.Read(ref _lastCommitted) >= target;
Monitor.Wait(_queueSync, (int)Math.Min(left, 500));
}
}
return true;
}
private void WriterLoop()
{
var buffer = new List<Pending>(512);
while (!_stop)
{
buffer.Clear();
lock (_queueSync)
{
while (_queue.Count == 0 && !_stop) Monitor.Wait(_queueSync, 250);
while (_queue.Count > 0 && buffer.Count < 2000) buffer.Add(_queue.Dequeue());
}
if (buffer.Count == 0) continue;
var i = 0;
while (i < buffer.Count)
{
switch (buffer[i])
{
case Statement:
var j = i;
while (j < buffer.Count && buffer[j] is Statement) j++;
RunStatements(buffer, i, j);
i = j;
break;
case Batch b:
RunBatch(b);
i++;
break;
case Custom c:
try { c.Action(_writer); }
catch (Exception ex) { c.Error = ex; }
finally { MarkCommitted(c.Seq); c.Done.Set(); }
i++;
break;
default:
i++;
break;
}
}
}
}
private void RunStatements(List<Pending> buffer, int from, int to)
{
long lastSeq = 0;
var inTx = false;
try
{
_writer.Execute("BEGIN IMMEDIATE");
inTx = true;
for (var k = from; k < to; k++)
{
var s = (Statement)buffer[k];
lastSeq = s.Seq;
try
{
var stmt = _writer.Cached(s.Sql);
stmt.Bind(s.Args);
stmt.RunToEnd();
Interlocked.Increment(ref _writesCommitted);
OnStatementExecuted(s.Sql);
}
catch (SqliteException ex)
{
Interlocked.Increment(ref _writesFailed);
Log($"[DATABASE] Scrittura rifiutata: {ex.Message}");
}
}
_writer.Execute("COMMIT");
inTx = false;
}
catch (Exception ex)
{
Log($"[DATABASE] Transazione non riuscita: {ex.Message}");
if (inTx) { try { _writer.Execute("ROLLBACK"); } catch { } }
Interlocked.Add(ref _writesFailed, to - from);
}
finally
{
MarkCommitted(Math.Max(lastSeq, buffer[to - 1].Seq));
}
}
private void RunBatch(Batch b)
{
var inTx = false;
try
{
_writer.Execute("BEGIN IMMEDIATE");
inTx = true;
foreach (var (sql, args) in b.Ops)
{
var stmt = _writer.Cached(sql);
stmt.Bind(args);
stmt.RunToEnd();
OnStatementExecuted(sql);
}
_writer.Execute("COMMIT");
inTx = false;
Interlocked.Add(ref _writesCommitted, b.Ops.Count);
}
catch (Exception ex)
{
Log($"[DATABASE] Blocco annullato: {ex.Message}");
if (inTx) { try { _writer.Execute("ROLLBACK"); } catch { } }
Interlocked.Add(ref _writesFailed, b.Ops.Count);
}
finally
{
MarkCommitted(b.Seq);
}
}
private void MarkCommitted(long seq)
{
lock (_queueSync)
{
if (seq > _lastCommitted) Interlocked.Exchange(ref _lastCommitted, seq);
Monitor.PulseAll(_queueSync);
}
}
// ── Lettura ──────────────────────────────────────────────────────
public List<SqliteRow> Query(string sql, params object?[] args)
{
if (_disposed) return new List<SqliteRow>();
lock (_readSync) return _reader.Query(sql, args);
}
public SqliteRow? QueryOne(string sql, params object?[] args)
{
var rows = Query(sql, args);
return rows.Count > 0 ? rows[0] : null;
}
public long ScalarLong(string sql, params object?[] args)
{
var v = QueryOne(sql, args)?.At(0);
return v switch { long l => l, double d => (long)d, string s when long.TryParse(s, out var x) => x, _ => 0 };
}
public double ScalarDouble(string sql, params object?[] args)
{
var v = QueryOne(sql, args)?.At(0);
return v switch { long l => l, double d => d, string s when double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var x) => x, _ => 0 };
}
public string? ScalarString(string sql, params object?[] args) => QueryOne(sql, args)?.At(0)?.ToString();
public long Count(string table) => ScalarLong($"SELECT COUNT(*) AS n FROM {table}");
// ── Manutenzione ─────────────────────────────────────────────────
/// <summary>Copia coerente dell'intero archivio in un altro file (VACUUM INTO).</summary>
public void BackupTo(string destination)
{
Flush();
if (File.Exists(destination)) File.Delete(destination);
var folder = System.IO.Path.GetDirectoryName(destination);
if (!string.IsNullOrEmpty(folder)) Directory.CreateDirectory(folder);
RunOnWriter(c => c.Execute("VACUUM INTO ?1", destination));
}
/// <summary>Byte occupati sul disco (file principale più giornale WAL).</summary>
public long SizeBytes
{
get
{
long total = 0;
foreach (var f in new[] { Path, Path + "-wal" })
{
try { if (File.Exists(f)) total += new FileInfo(f).Length; } catch { }
}
return total;
}
}
/// <summary>Compatta il file. Fuori da ogni transazione, sul thread del database.</summary>
public void Vacuum()
{
try { RunOnWriter(c => c.Execute("VACUUM")); } catch { /* lo spazio si recupera la prossima volta */ }
}
// ── Attrezzi ─────────────────────────────────────────────────────
/// <summary>Le date si scrivono in ISO 8601 UTC: ordinano bene e non dipendono dal fuso.</summary>
public static string Iso(DateTime dt)
{
var utc = dt.Kind switch
{
DateTimeKind.Utc => dt,
DateTimeKind.Local => dt.ToUniversalTime(),
_ => DateTime.SpecifyKind(dt, DateTimeKind.Local).ToUniversalTime()
};
return utc.ToString("yyyy-MM-dd'T'HH:mm:ss.fff'Z'", CultureInfo.InvariantCulture);
}
public static string Now() => Iso(DateTime.UtcNow);
public void Dispose()
{
if (_disposed) return;
Flush(5000);
_stop = true;
lock (_queueSync) Monitor.PulseAll(_queueSync);
try { _writerThread.Join(3000); } catch { }
_disposed = true;
lock (_readSync) _reader.Dispose();
_writer.Dispose();
}
}
}
-23
View File
@@ -1,23 +0,0 @@
using Microsoft.EntityFrameworkCore;
using AutoBidder.Models;
namespace AutoBidder.Data
{
public class StatisticsContext : DbContext
{
public DbSet<ProductStat> ProductStats { get; set; }
public StatisticsContext(DbContextOptions<StatisticsContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ProductStat>()
.HasIndex(p => p.ProductKey)
.IsUnique(false);
base.OnModelCreating(modelBuilder);
}
}
}
+7 -8
View File
@@ -4,11 +4,10 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:AutoBidder.Controls"
Title="Aggiungi Asta" Height="320" Width="700"
Background="#0a0a0a" Foreground="#FFFFFF"
Background="{DynamicResource Brush.Bg}" Foreground="{DynamicResource Brush.Text}"
WindowStartupLocation="CenterOwner"
Icon="pack://application:,,,/Icon/favicon.ico"
ResizeMode="NoResize">
<Border Background="#1a1a1a" CornerRadius="8" Padding="16" Margin="8">
<Border Background="{DynamicResource Brush.Bg}" CornerRadius="8" Padding="16" Margin="8">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
@@ -17,11 +16,11 @@
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="Inserire URL dell'asta (uno o pi&#x00F9;)" Foreground="#CCCCCC" FontSize="14" Margin="0,0,0,6" />
<TextBlock Grid.Row="1" Text="Puoi aggiungere pi&#x00F9; link separandoli con 'a capo', 'spazio' o ';'" Foreground="#999999" FontSize="12" Margin="0,0,0,10" />
<TextBlock Text="Inserire URL dell'asta (uno o pi&#x00F9;)" Foreground="{DynamicResource Brush.Text}" FontSize="14" Margin="0,0,0,6" />
<TextBlock Grid.Row="1" Text="Puoi aggiungere pi&#x00F9; link separandoli con 'a capo', 'spazio' o ';'" Foreground="{DynamicResource Brush.TextMuted}" FontSize="12" Margin="0,0,0,10" />
<TextBox x:Name="AuctionUrlBox" Grid.Row="2" MinWidth="560" Margin="0,0,0,8"
Background="#181818" Foreground="#00CCFF" BorderBrush="#444" BorderThickness="1"
Background="{DynamicResource Brush.Bg}" Foreground="{DynamicResource Brush.Info}" BorderBrush="{DynamicResource Brush.Border}" BorderThickness="1"
Padding="8" FontSize="13" ToolTip="Inserisci uno o pi&#x00F9; URL/ID dell'asta. Separali con a capo, spazio o ';'"
AcceptsReturn="True" TextWrapping="Wrap" VerticalScrollBarVisibility="Auto" Height="160" />
@@ -29,9 +28,9 @@
<controls:SimpleToolbar.RightContent>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="OK" Width="110" Margin="6" Padding="10,8"
Style="{StaticResource SmallButtonStyle}" Background="#00CC66" Foreground="White" Click="OkButton_Click" />
Style="{StaticResource SmallButtonStyle}" Background="{DynamicResource Brush.Success}" Foreground="{DynamicResource Brush.TextOnAccent}" Click="OkButton_Click" />
<Button Content="Annulla" Width="110" Margin="6" Padding="10,8"
Style="{StaticResource SmallButtonStyle}" Background="#666" Foreground="White" Click="CancelButton_Click" />
Style="{StaticResource SmallButtonStyle}" Background="{DynamicResource Brush.SurfaceAlt}" Foreground="{DynamicResource Brush.Text}" Click="CancelButton_Click" />
</StackPanel>
</controls:SimpleToolbar.RightContent>
</controls:SimpleToolbar>
-89
View File
@@ -1,89 +0,0 @@
<Window x:Class="AutoBidder.Dialogs.ClosedAuctionsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Aste Chiuse - Estrazione" Height="600" Width="1000"
Background="#0a0a0a" Foreground="#FFFFFF" WindowStartupLocation="CenterOwner">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="8" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Border Background="#1a1a1a" Padding="10" CornerRadius="6" Grid.Row="0" BorderBrush="#333333" BorderThickness="1">
<DockPanel>
<TextBlock Text="Estrazione Aste Chiuse" FontSize="16" FontWeight="Bold" Foreground="#00CC66" VerticalAlignment="Center" DockPanel.Dock="Left" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" DockPanel.Dock="Right">
<Button x:Name="StartExtractButton" Content="Avvia Estrazione" Click="StartExtractButton_Click" Width="140" Height="36" Margin="8,0,0,0" Background="#00CC66" Style="{StaticResource SmallButtonStyle}"/>
<Button x:Name="ExportStatsButton" Content="Esporta Statistiche" Click="ExportStatsButton_Click" Width="160" Height="36" Margin="8,0,0,0" Background="#8B5CF6" Style="{StaticResource SmallButtonStyle}"/>
<Button x:Name="CloseButton" Content="Chiudi" Click="CloseButton_Click" Width="80" Height="36" Margin="8,0,0,0" Background="#666" Style="{StaticResource SmallButtonStyle}"/>
</StackPanel>
</DockPanel>
</Border>
<Border Grid.Row="2" Background="#1a1a1a" Padding="8" CornerRadius="6" BorderBrush="#333333" BorderThickness="1">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="8" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Products grid: styled like MainWindow -->
<DataGrid x:Name="ProductsGrid" AutoGenerateColumns="False" Grid.Column="0" Background="#1a1a1a" Foreground="#FFFFFF" BorderBrush="#333333" BorderThickness="1"
RowBackground="#1a1a1a" AlternatingRowBackground="#222222" GridLinesVisibility="Horizontal" HorizontalGridLinesBrush="#333333"
IsReadOnly="True" SelectionUnit="CellOrRowHeader" SelectionMode="Extended" ClipboardCopyMode="IncludeHeader" CanUserAddRows="False" CanUserDeleteRows="False">
<DataGrid.Resources>
<Style TargetType="DataGridColumnHeader">
<Setter Property="Background" Value="#2a2a2a" />
<Setter Property="Foreground" Value="#FFFFFF" />
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="Padding" Value="10,8" />
<Setter Property="BorderThickness" Value="0,0,0,2" />
<Setter Property="BorderBrush" Value="#00CC66" />
</Style>
<Style TargetType="DataGridRow">
<Setter Property="Height" Value="36" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="#0099FF" />
<Setter Property="Foreground" Value="White" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="DataGridCell">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="10,6" />
</Style>
</DataGrid.Resources>
<DataGrid.ContextMenu>
<ContextMenu>
<MenuItem Command="ApplicationCommands.Copy" Header="Copia" />
</ContextMenu>
</DataGrid.ContextMenu>
<DataGrid.Columns>
<DataGridTextColumn Header="Asta URL" Binding="{Binding AuctionUrl}" Width="2*" />
<DataGridTextColumn Header="Nome" Binding="{Binding ProductName}" Width="3*"/>
<DataGridTextColumn Header="Prezzo" Binding="{Binding FinalPrice}" Width="80"/>
<DataGridTextColumn Header="Vincitore" Binding="{Binding Winner}" Width="120"/>
<DataGridTextColumn Header="Puntate Usate" Binding="{Binding BidsUsed}" Width="100"/>
<DataGridTextColumn Header="Scraped At" Binding="{Binding ScrapedAt}" Width="140"/>
</DataGrid.Columns>
</DataGrid>
<GridSplitter Grid.Column="1" Width="8" />
<!-- Log area styled like main window log -->
<Border Grid.Column="2" Background="#0f0f0f" Padding="8" CornerRadius="6" BorderBrush="#333333" BorderThickness="1">
<DockPanel>
<TextBlock Text="Log Operazioni" FontWeight="Bold" Foreground="#00CC66" DockPanel.Dock="Top" Margin="0,0,0,8" />
<RichTextBox x:Name="ExtractLogBox" IsReadOnly="True" VerticalScrollBarVisibility="Auto" FontFamily="Consolas" FontSize="11" Background="#0f0f0f" Foreground="#CCC" BorderBrush="#333333" BorderThickness="1" />
</DockPanel>
</Border>
</Grid>
</Border>
</Grid>
</Window>
@@ -1,140 +0,0 @@
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Documents;
using Microsoft.Win32;
using AutoBidder.Models;
using AutoBidder.Services;
using AutoBidder.Data;
using Microsoft.EntityFrameworkCore;
namespace AutoBidder.Dialogs
{
public partial class ClosedAuctionsWindow : Window
{
private ObservableCollection<ClosedAuctionRecord> _products = new();
private bool _isRunning = false;
// StatsService using local DB. Create context with default sqlite file in app folder
private readonly StatsService _statsService;
public ClosedAuctionsWindow()
{
InitializeComponent();
ProductsGrid.ItemsSource = _products;
var optionsBuilder = new DbContextOptionsBuilder<StatisticsContext>();
var dbPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "stats.db");
optionsBuilder.UseSqlite($"Data Source={dbPath}");
var ctx = new StatisticsContext(optionsBuilder.Options);
_statsService = new StatsService(ctx);
Log("Finestra pronta");
}
private void Log(string message)
{
try
{
var para = new Paragraph(new Run($"{DateTime.Now:HH:mm} - {message}"));
ExtractLogBox.Document.Blocks.Add(para);
// keep size manageable
while (ExtractLogBox.Document.Blocks.Count > 500)
ExtractLogBox.Document.Blocks.Remove(ExtractLogBox.Document.Blocks.FirstBlock);
ExtractLogBox.ScrollToEnd();
}
catch { }
}
private async void StartExtractButton_Click(object sender, RoutedEventArgs e)
{
if (_isRunning)
{
Log("Estrazione già in corso");
return;
}
_isRunning = true;
StartExtractButton.IsEnabled = false;
Log("Avvio procedura di estrazione da closed_auctions.php...");
try
{
var scraper = new ClosedAuctionsScraper(null, _statsService, Log);
var closedUrl = "https://it.bidoo.com/closed_auctions.php";
Log($"Scarico: {closedUrl}");
int count = 0;
await foreach (var rec in scraper.ScrapeYieldAsync(closedUrl))
{
// Filter out records without bids info (user requested)
if (!rec.BidsUsed.HasValue)
{
Log($"Scartata asta (mancano puntate): {rec.AuctionUrl} - '{rec.ProductName ?? "?"}'");
continue;
}
// Add and log incrementally so user sees progress
_products.Add(rec);
count++;
Log($"[{count}] {rec.ProductName} | Prezzo: {(rec.FinalPrice.HasValue?rec.FinalPrice.Value.ToString("F2")+"":"--")} | Vincitore: {rec.Winner ?? "--"} | Puntate: {rec.BidsUsed.Value} | URL: {rec.AuctionUrl}");
}
Log($"Estrazione completata: {count} record aggiunti.");
}
catch (Exception ex)
{
Log($"[ERRORE] Estrattore: {ex.Message}");
}
finally
{
_isRunning = false;
StartExtractButton.IsEnabled = true;
}
}
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
this.Close();
}
private async void ExportStatsButton_Click(object sender, RoutedEventArgs e)
{
try
{
Log("Preparazione esportazione statistiche...");
var stats = await _statsService.GetAllStatsAsync();
if (stats == null || stats.Count == 0)
{
Log("Nessuna statistica disponibile da esportare.");
MessageBox.Show(this, "Nessuna statistica disponibile.", "Esporta Statistiche", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
var dlg = new SaveFileDialog() { Filter = "CSV files|*.csv|All files|*.*", FileName = "auction_stats.csv" };
if (dlg.ShowDialog(this) != true) return;
using var sw = new StreamWriter(dlg.FileName, false, System.Text.Encoding.UTF8);
sw.WriteLine("ProductKey,ProductName,TotalAuctions,AverageBidsUsed,AverageFinalPrice,LastSeen");
foreach (var s in stats)
{
var line = $"\"{s.ProductKey}\",\"{s.ProductName}\",{s.TotalAuctions},{s.AverageBidsUsed:F2},{s.AverageFinalPrice:F2},{s.LastSeen:O}";
sw.WriteLine(line);
}
Log($"Statistiche esportate su: {dlg.FileName}");
MessageBox.Show(this, "Statistiche esportate con successo.", "Esporta Statistiche", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
Log($"[ERRORE] Esporta: {ex.Message}");
MessageBox.Show(this, "Errore durante esportazione: " + ex.Message, "Esporta Statistiche", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
}
+33
View File
@@ -0,0 +1,33 @@
<Window x:Class="AutoBidder.Dialogs.ProgressDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Operazione in corso"
Width="480" SizeToContent="Height"
WindowStartupLocation="CenterOwner"
ShowInTaskbar="False" ResizeMode="NoResize"
Background="{DynamicResource Brush.Bg}">
<StackPanel Margin="22,18">
<TextBlock x:Name="TitleText"
FontSize="{StaticResource Font.Size.Lg}" FontWeight="SemiBold"
Foreground="{DynamicResource Brush.Text}"/>
<TextBlock x:Name="MessageText" Style="{StaticResource Hint}" Margin="0,6,0,14"
TextWrapping="Wrap"/>
<ProgressBar x:Name="Bar" Height="6" Minimum="0" Maximum="100" IsIndeterminate="True"/>
<Grid Margin="0,10,0,0">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock x:Name="CountText" Style="{StaticResource Hint}" Text=""/>
<TextBlock x:Name="ElapsedText" Style="{StaticResource Hint}" Margin="12,0,0,0" Text=""/>
</StackPanel>
<Button x:Name="CancelButton" Content="Annulla" HorizontalAlignment="Right"
Style="{StaticResource SmallRoundedButton}"
Background="{DynamicResource Brush.SurfaceAlt}"
Foreground="{DynamicResource Brush.Text}"
Padding="18,5" Click="CancelButton_Click"/>
</Grid>
</StackPanel>
</Window>
+144
View File
@@ -0,0 +1,144 @@
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using AutoBidder.Utilities;
namespace AutoBidder.Dialogs
{
/// <summary>
/// La finestra di avanzamento delle operazioni lunghe: titolo, una riga che dice a
/// che punto si è, la barra, il conto fatto/totale, il tempo passato e Annulla.
///
/// <para>Si usa da <see cref="RunAsync{T}"/>: si passa il lavoro da fare, che riceve
/// un <see cref="IProgress{T}"/> e un token di annullamento; la finestra si apre, segue
/// l'avanzamento e si chiude da sola. In modalità «modale» la finestra principale
/// resta disabilitata finché il lavoro non finisce; altrimenti si può continuare a
/// usare l'applicazione (le aste in corso non si fermano mai, in nessun caso).</para>
/// </summary>
public partial class ProgressDialog : Window
{
private readonly CancellationTokenSource _cts = new();
private readonly Stopwatch _elapsed = Stopwatch.StartNew();
private readonly DispatcherTimer _clock;
private bool _finished;
public CancellationToken Token => _cts.Token;
public ProgressDialog(string title, string message, bool cancellable)
{
InitializeComponent();
Title = title;
TitleText.Text = title;
MessageText.Text = message;
CancelButton.Visibility = cancellable ? Visibility.Visible : Visibility.Collapsed;
_clock = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
_clock.Tick += (_, _) => ElapsedText.Text = _elapsed.Elapsed.TotalSeconds < 60
? $"{_elapsed.Elapsed.TotalSeconds:N0} s"
: $"{(int)_elapsed.Elapsed.TotalMinutes} min {_elapsed.Elapsed.Seconds:00} s";
_clock.Start();
}
/// <summary>Aggiorna barra e testi. Va chiamato sul thread dell'interfaccia.</summary>
public void Report(ProgressStep step)
{
if (_finished) return;
if (step.Total > 0)
{
Bar.IsIndeterminate = false;
Bar.Maximum = step.Total;
Bar.Value = Math.Clamp(step.Done, 0, step.Total);
CountText.Text = $"{step.Done:N0} / {step.Total:N0}";
}
else
{
Bar.IsIndeterminate = true;
CountText.Text = step.Done > 0 ? step.Done.ToString("N0") : "";
}
if (!string.IsNullOrEmpty(step.Message)) MessageText.Text = step.Message;
}
private void CancelButton_Click(object sender, RoutedEventArgs e) => RequestCancel();
private void RequestCancel()
{
if (_cts.IsCancellationRequested) return;
_cts.Cancel();
CancelButton.IsEnabled = false;
MessageText.Text = "Annullamento in corso: aspetto che il passo in corso finisca…";
}
/// <summary>
/// La X della finestra non chiude niente a metà: chiede l'annullamento, se
/// l'operazione lo ammette, e la finestra se ne va quando il lavoro è finito.
/// </summary>
protected override void OnClosing(CancelEventArgs e)
{
if (!_finished)
{
e.Cancel = true;
if (CancelButton.Visibility == Visibility.Visible) RequestCancel();
}
base.OnClosing(e);
}
private void Finish()
{
_finished = true;
_clock.Stop();
Close();
}
/// <summary>
/// Esegue <paramref name="work"/> mostrando la finestra di avanzamento sopra
/// <paramref name="owner"/>. Restituisce l'esito: risultato, annullata, o errore.
/// </summary>
/// <param name="modal">Vero: la finestra principale è disabilitata finché non si finisce.</param>
/// <param name="cancellable">Vero: c'è il pulsante Annulla e il token viene annullato.</param>
public static async Task<ProgressOutcome<T>> RunAsync<T>(
Window? owner, string title, string message,
Func<IProgress<ProgressStep>, CancellationToken, Task<T>> work,
bool modal = true, bool cancellable = true)
{
var dialog = new ProgressDialog(title, message, cancellable);
if (owner != null && owner.IsLoaded) dialog.Owner = owner;
// Progress<T> cattura il contesto di sincronizzazione di chi lo crea: da qui,
// il thread dell'interfaccia. I rapporti arrivano già sul thread giusto.
var progress = new Progress<ProgressStep>(dialog.Report);
if (modal && owner != null) owner.IsEnabled = false;
dialog.Show();
try
{
var result = await work(progress, dialog.Token);
return dialog.Token.IsCancellationRequested
? ProgressOutcome<T>.CancelledByUser()
: ProgressOutcome<T>.Ok(result);
}
catch (OperationCanceledException)
{
return ProgressOutcome<T>.CancelledByUser();
}
catch (Exception ex)
{
return ProgressOutcome<T>.Failed(ex);
}
finally
{
if (modal && owner != null) owner.IsEnabled = true;
dialog.Finish();
owner?.Activate();
}
}
}
}
+8 -9
View File
@@ -2,11 +2,10 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Configura Sessione" Height="440" Width="700"
Background="#0a0a0a" Foreground="#FFFFFF"
Background="{DynamicResource Brush.Bg}" Foreground="{DynamicResource Brush.Text}"
WindowStartupLocation="CenterOwner"
Icon="pack://application:,,,/Icon/favicon.ico"
ResizeMode="NoResize">
<Border Background="#1a1a1a" CornerRadius="8" Padding="16" Margin="8">
<Border Background="{DynamicResource Brush.Bg}" CornerRadius="8" Padding="16" Margin="8">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
@@ -15,9 +14,9 @@
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="Inserisci Cookie di sessione" FontSize="16" FontWeight="SemiBold" Foreground="#00CC66" Margin="0,0,0,12" />
<TextBlock Grid.Row="1" Text="Cookie di sessione (copia dal menu delle Impostazioni/Applicazioni di Chrome)" Foreground="#CCCCCC" FontSize="13" />
<TextBlock Grid.Row="2" Foreground="#888" FontSize="12" TextWrapping="Wrap" Margin="0,4,0,10">
<TextBlock Text="Inserisci Cookie di sessione" FontSize="16" FontWeight="SemiBold" Foreground="{DynamicResource Brush.Success}" Margin="0,0,0,12" />
<TextBlock Grid.Row="1" Text="Cookie di sessione (copia dal menu delle Impostazioni/Applicazioni di Chrome)" Foreground="{DynamicResource Brush.Text}" FontSize="13" />
<TextBlock Grid.Row="2" Foreground="{DynamicResource Brush.TextMuted}" FontSize="12" TextWrapping="Wrap" Margin="0,4,0,10">
<Run Text="Come trovare i cookie di sessione in Chrome:" />
<LineBreak/>
<Run Text="1. Apri Chrome e premi F12 (Windows) o Cmd+Option+I (Mac) per aprire gli Strumenti per sviluppatori." />
@@ -29,12 +28,12 @@
<Run Text="4. Scegli il sito desiderato e copia il valore del cookie di sessione." />
</TextBlock>
<TextBox x:Name="CookieBox" Grid.Row="3" MinWidth="320" MinHeight="120" MaxHeight="220" VerticalScrollBarVisibility="Auto"
Background="#181818" Foreground="#00CCFF" BorderBrush="#444" BorderThickness="1" Padding="8" FontSize="14" AcceptsReturn="True" TextWrapping="Wrap" />
Background="{DynamicResource Brush.Bg}" Foreground="{DynamicResource Brush.Info}" BorderBrush="{DynamicResource Brush.Border}" BorderThickness="1" Padding="8" FontSize="14" AcceptsReturn="True" TextWrapping="Wrap" />
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
<Button x:Name="OkButton" Content="OK" Width="110" Margin="6" Padding="10,8"
Style="{StaticResource SmallButtonStyle}" Background="#00CC66" Foreground="White" Click="OkButton_Click" />
Style="{StaticResource SmallButtonStyle}" Background="{DynamicResource Brush.Success}" Foreground="{DynamicResource Brush.TextOnAccent}" Click="OkButton_Click" />
<Button x:Name="CancelButton" Content="Annulla" Width="110" Margin="6" Padding="10,8"
Style="{StaticResource SmallButtonStyle}" Background="#666" Foreground="White" Click="CancelButton_Click" />
Style="{StaticResource SmallButtonStyle}" Background="{DynamicResource Brush.SurfaceAlt}" Foreground="{DynamicResource Brush.Text}" Click="CancelButton_Click" />
</StackPanel>
</Grid>
</Border>
+138
View File
@@ -0,0 +1,138 @@
<Window x:Class="AutoBidder.Dialogs.StatsCleanupDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Pulizia dello storico"
Width="620" SizeToContent="Height"
WindowStartupLocation="CenterOwner"
ShowInTaskbar="False"
Background="{DynamicResource Brush.Bg}">
<StackPanel Margin="22,18">
<TextBlock Text="Cosa considerare incompleto"
FontSize="{StaticResource Font.Size.Lg}" FontWeight="SemiBold"
Foreground="{DynamicResource Brush.Text}"/>
<TextBlock Style="{StaticResource Hint}" Margin="0,6,0,14"
Text="Il conto si aggiorna mentre scegli. Niente viene toccato finché non premi Pulisci, e viene salvata una copia dello storico prima di procedere."/>
<CheckBox x:Name="ChkNoPrice" Margin="0,4" IsChecked="True"
Content="Aste senza prezzo finale"
ToolTip="Non dicono nulla e sporcano ogni media: il prezzo è il dato su cui si regge tutto il resto."
Checked="Option_Changed" Unchecked="Option_Changed"/>
<CheckBox x:Name="ChkBadBids" Margin="0,4" IsChecked="True"
Content="Aste con puntate del vincitore incoerenti col prezzo"
ToolTip="Ogni puntata alza il prezzo di un centesimo, quindi il prezzo in centesimi è il totale delle puntate dell'asta. Un vincitore che ne dichiara di più — o zero — porta un dato che non può essere vero."
Checked="Option_Changed" Unchecked="Option_Changed"/>
<CheckBox x:Name="ChkDuplicates" Margin="0,4" IsChecked="True"
Content="Doppioni con lo stesso identificativo (tiene il più recente)"
ToolTip="La stessa asta può essere registrata due volte: quando il motore ne vede la fine e quando viene tolta dal monitor. Si tiene la versione più recente."
Checked="Option_Changed" Unchecked="Option_Changed"/>
<CheckBox x:Name="ChkNoWinnerBids" Margin="0,4"
Content="Aste di cui non si conoscono le puntate del vincitore"
ToolTip="Severo: toglie tutte le aste su cui non si può calcolare il costo reale. Prima di usarlo, prova «Recupera puntate dei vincitori» nella scheda Storico — il dato si può quasi sempre riprendere dal server."
Checked="Option_Changed" Unchecked="Option_Changed"/>
<CheckBox x:Name="ChkNoWinner" Margin="0,4"
Content="Aste senza vincitore"
ToolTip="Aste scadute senza offerte, oppure registrate a metà."
Checked="Option_Changed" Unchecked="Option_Changed"/>
<CheckBox x:Name="ChkNoValue" Margin="0,4"
Content="Aste senza valore del prodotto"
ToolTip="Senza il «Compra Subito» non si può calcolare il risparmio, ma il prezzo e le puntate restano validi."
Checked="Option_Changed" Unchecked="Option_Changed"/>
<CheckBox x:Name="ChkObservedOnly" Margin="0,4"
Content="Aste solo osservate (senza mie puntate)"
ToolTip="Tiene le aste su cui hai puntato — sono le uniche con il costo reale dentro — e toglie tutte le altre. Utile per uno storico compatto delle tue partite."
Checked="Option_Changed" Unchecked="Option_Changed"/>
<CheckBox x:Name="ChkWithMyBids" Margin="0,4"
Content="Aste su cui ho puntato"
ToolTip="Il verso opposto: tiene solo il mercato. Un'asta in cui sei intervenuto ha un prezzo finale che hai contribuito a fare, e per stimare cosa succede senza di te va tolta."
Checked="Option_Changed" Unchecked="Option_Changed"/>
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
<CheckBox x:Name="ChkPriceRatio" VerticalAlignment="Center"
Content="Aste chiuse oltre il"
ToolTip="Una chiusura al 60% del valore su un buono da 100 € non descrive il mercato: descrive due persone che si sono incaponite. Tenerla sposta le medie e i limiti consigliati verso l'alto per tutti.&#10;&#10;Sul tuo storico la chiusura mediana sta fra il 5% e il 6% del valore, e tre aste su quattro chiudono sotto il 15%."
Checked="Option_Changed" Unchecked="Option_Changed"/>
<TextBox x:Name="PriceRatioBox" Text="40" Width="50" Margin="8,0"
VerticalAlignment="Center" TextChanged="Option_Changed"/>
<TextBlock Text="% del valore del prodotto" VerticalAlignment="Center"
Foreground="{DynamicResource Brush.Text}"/>
</StackPanel>
<!-- Per prodotto: i prodotti presenti nello storico, col numero di aste -->
<Expander x:Name="ProductsExpander" Margin="0,10,0,0" IsExpanded="False"
Foreground="{DynamicResource Brush.Text}">
<Expander.Header>
<TextBlock x:Name="ProductsHeader" Text="Per prodotto: nessuno scelto"
Foreground="{DynamicResource Brush.Text}"/>
</Expander.Header>
<ListBox x:Name="ProductsList" MaxHeight="170" Margin="0,6,0,0"
Background="{DynamicResource Brush.SurfaceAlt}"
BorderBrush="{DynamicResource Brush.Border}"
ScrollViewer.VerticalScrollBarVisibility="Auto">
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding Label}"
IsChecked="{Binding Selected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Foreground="{DynamicResource Brush.Text}"
Checked="Option_Changed" Unchecked="Option_Changed"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Expander>
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
<CheckBox x:Name="ChkCoverage" VerticalAlignment="Center"
Content="Aste seguite per meno del"
ToolTip="Toglie le aste agganciate a metà. Sono il caso peggiore: i numeri sono veri ma parziali, quindi sembrano buone e abbassano le medie senza dare segnali.&#10;&#10;Quanta asta si è vista si ricava dal prezzo finale, senza bisogno di alcun dato in più: ogni puntata vale un centesimo, quindi il prezzo in centesimi è il totale delle puntate dell'asta, e i reset osservati dicono quanti se ne sono visti.&#10;&#10;Sul tuo storico, su 5876 aste: al 75% ne resterebbero circa 78 su 100, al 90% circa 64, al 95% circa 54, al 99% circa 34."
Checked="Option_Changed" Unchecked="Option_Changed"/>
<TextBox x:Name="CoverageBox" Text="90" Width="50" Margin="8,0"
VerticalAlignment="Center" TextChanged="Option_Changed"/>
<TextBlock Text="% della loro durata" VerticalAlignment="Center"
Foreground="{DynamicResource Brush.Text}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
<CheckBox x:Name="ChkOlderThan" VerticalAlignment="Center"
Content="Aste chiuse da più di"
ToolTip="Le aste vecchie descrivono un mercato che può essere cambiato."
Checked="Option_Changed" Unchecked="Option_Changed"/>
<TextBox x:Name="DaysBox" Text="180" Width="60" Margin="8,0"
VerticalAlignment="Center" TextChanged="Option_Changed"/>
<TextBlock Text="giorni" VerticalAlignment="Center"
Foreground="{DynamicResource Brush.Text}"/>
</StackPanel>
<Border Style="{StaticResource InfoBox}" Margin="0,16,0,0">
<StackPanel>
<TextBlock x:Name="SummaryText" TextWrapping="Wrap"
Foreground="{DynamicResource Brush.Text}"
FontSize="{StaticResource Font.Size.Sm}" LineHeight="18"
Text="Calcolo…"/>
<TextBlock x:Name="DetailText" TextWrapping="Wrap" Margin="0,8,0,0"
Foreground="{DynamicResource Brush.TextMuted}"
FontSize="{StaticResource Font.Size.Sm}" LineHeight="17"/>
</StackPanel>
</Border>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,18,0,0">
<Button x:Name="CleanButton" Content="Pulisci" Padding="26,7" Margin="0,0,10,0"
Style="{StaticResource ModernButton}"
Background="{DynamicResource Brush.Danger}"
Foreground="{DynamicResource Brush.TextOnAccent}"
Click="Clean_Click"/>
<Button Content="Annulla" Padding="26,7"
Style="{StaticResource ModernButton}"
Background="{DynamicResource Brush.SurfaceAlt}"
IsCancel="True"/>
</StackPanel>
</StackPanel>
</Window>
+175
View File
@@ -0,0 +1,175 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using AutoBidder.Models;
using AutoBidder.Utilities;
namespace AutoBidder.Dialogs
{
/// <summary>
/// Sceglie cosa considerare incompleto nello storico e lo toglie.
///
/// <para>Il conto si aggiorna a ogni spunta, <b>prima</b> di toccare qualunque cosa: una
/// pulizia è irreversibile, e l'unico modo di renderla una decisione informata è mostrare
/// quante righe cadrebbero e per quale motivo mentre si sceglie. La copia di sicurezza
/// prima di scrivere è la seconda rete: le statistiche sono mesi di raccolta che nessun
/// pulsante ha il diritto di far sparire per un clic distratto.</para>
/// </summary>
public partial class StatsCleanupDialog : Window
{
private readonly List<CompletedAuctionRecord> _records;
/// <summary>Una riga dell'elenco prodotti: chiave, etichetta con il conteggio, spunta.</summary>
private sealed class ProductRow
{
public string Key { get; init; } = "";
public string Label { get; init; } = "";
public bool Selected { get; set; }
}
private readonly List<ProductRow> _products;
public StatsCleanupDialog(List<CompletedAuctionRecord> records)
{
InitializeComponent();
_records = records ?? new List<CompletedAuctionRecord>();
// I prodotti presenti nello storico, dal più numeroso: è l'ordine in cui
// uno li cerca quando vuole fare spazio.
_products = _records
.Where(r => !string.IsNullOrWhiteSpace(r.ProductKey))
.GroupBy(r => r.ProductKey, StringComparer.OrdinalIgnoreCase)
.Select(g => new ProductRow
{
Key = g.Key,
Label = $"{g.Select(r => r.Name).FirstOrDefault(n => !string.IsNullOrWhiteSpace(n)) ?? g.Key} ({g.Count()})"
})
.OrderByDescending(p => p.Label.Length > 0 ? _records.Count(r => string.Equals(r.ProductKey, p.Key, StringComparison.OrdinalIgnoreCase)) : 0)
.ToList();
ProductsList.ItemsSource = _products;
Loaded += (_, _) => Refresh();
}
/// <summary>Righe rimosse, dopo la conferma. 0 se l'utente ha annullato.</summary>
public int RemovedCount { get; private set; }
/// <summary>Dove è finita la copia di sicurezza.</summary>
public string? BackupPath { get; private set; }
private StatsMaintenance.CleanupOptions ReadOptions()
{
var options = new StatsMaintenance.CleanupOptions
{
RemoveWithoutPrice = ChkNoPrice.IsChecked == true,
RemoveInconsistentBids = ChkBadBids.IsChecked == true,
RemoveDuplicates = ChkDuplicates.IsChecked == true,
RemoveWithoutWinnerBids = ChkNoWinnerBids.IsChecked == true,
RemoveWithoutWinner = ChkNoWinner.IsChecked == true,
RemoveWithoutValue = ChkNoValue.IsChecked == true
};
// Una soglia illeggibile o fuori scala non deve diventare "togli tutto":
// il filtro resta spento finché il numero non ha senso.
if (ChkCoverage.IsChecked == true &&
double.TryParse(CoverageBox.Text.Trim().Replace(',', '.'),
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture, out var percento) &&
percento is > 0 and <= 100)
{
options.MinObservedCoverage = percento / 100.0;
}
if (ChkOlderThan.IsChecked == true &&
int.TryParse(DaysBox.Text.Trim(), out var days) && days > 0)
{
options.OlderThan = DateTime.Now.AddDays(-days);
}
options.RemoveObservedOnly = ChkObservedOnly.IsChecked == true;
options.RemoveWithMyBids = ChkWithMyBids.IsChecked == true;
if (ChkPriceRatio.IsChecked == true &&
double.TryParse(PriceRatioBox.Text.Trim().Replace(',', '.'),
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture, out var ratio) &&
ratio > 0)
{
options.MaxPriceRatioPercent = ratio;
}
var scelti = _products.Where(p => p.Selected).Select(p => p.Key).ToList();
if (scelti.Count > 0)
options.RemoveProductKeys = new HashSet<string>(scelti, StringComparer.OrdinalIgnoreCase);
return options;
}
private void Option_Changed(object sender, RoutedEventArgs e) => Refresh();
private void Refresh()
{
if (SummaryText == null) return;
if (ProductsHeader != null)
{
var n = _products.Count(p => p.Selected);
ProductsHeader.Text = n == 0
? $"Per prodotto: nessuno scelto ({_products.Count} nello storico)"
: $"Per prodotto: {n} scelti";
}
var report = StatsMaintenance.Preview(_records, ReadOptions());
SummaryText.Text = report.Removed == 0
? $"Nessuna delle {report.Total} aste verrebbe tolta."
: $"Verrebbero tolte {report.Removed} aste su {report.Total}. " +
$"Ne resterebbero {report.Kept}.";
DetailText.Text = report.ByReason.Count == 0
? ""
: string.Join("\n", report.ByReason
.OrderByDescending(kv => kv.Value)
.Select(kv => $"• {kv.Value} {kv.Key}"));
CleanButton.IsEnabled = report.Removed > 0;
}
private void Clean_Click(object sender, RoutedEventArgs e)
{
var options = ReadOptions();
var report = StatsMaintenance.Preview(_records, options);
if (report.Removed == 0) return;
var answer = MessageBox.Show(this,
$"Tolgo {report.Removed} aste su {report.Total}.\n\n" +
"L'operazione non è reversibile, ma prima viene salvata una copia " +
"dello storico nella cartella dei backup.\n\nProcedo?",
"Pulizia dello storico", MessageBoxButton.YesNo, MessageBoxImage.Warning);
if (answer != MessageBoxResult.Yes) return;
try
{
BackupPath = CompletedAuctionsStore.BackupNow();
// La stessa risposta usata per il conto: contare con una regola e
// cancellare con un'altra è il modo classico di far sparire dati che
// nessuno voleva perdere.
var kept = StatsMaintenance.Apply(_records, options);
CompletedAuctionsStore.ReplaceAll(kept);
RemovedCount = report.Removed;
DialogResult = true;
}
catch (Exception ex)
{
MessageBox.Show(this, $"Pulizia non riuscita: {ex.Message}",
"Pulizia dello storico", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
}
+64
View File
@@ -0,0 +1,64 @@
<Window x:Class="AutoBidder.Dialogs.WipeStatsDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Azzera le statistiche"
Width="600" SizeToContent="Height"
WindowStartupLocation="CenterOwner"
ShowInTaskbar="False"
Background="{DynamicResource Brush.Bg}">
<StackPanel Margin="22,18">
<TextBlock Text="Cosa azzerare"
FontSize="{StaticResource Font.Size.Lg}" FontWeight="SemiBold"
Foreground="{DynamicResource Brush.Text}"/>
<TextBlock Style="{StaticResource Hint}" Margin="0,6,0,14"
Text="Accanto a ogni voce c'è quanto spazio occupa adesso. Niente viene toccato finché non premi Azzera. Il database viene copiato per intero nella cartella dei backup prima di essere svuotato; il resto no."/>
<CheckBox x:Name="ChkHistory" Margin="0,4" IsChecked="True"
Foreground="{DynamicResource Brush.Text}"
ToolTip="Tutto ciò che il database ha osservato e deciso: aste, puntate di ogni utente, interrogazioni, reset, tue puntate, decisioni del motore, misure di rete, puntatori, contabilità del rischio. I prodotti con il loro valore reale restano. Prima viene fatta una copia completa del database nei backup."
Checked="Option_Changed" Unchecked="Option_Changed"/>
<CheckBox x:Name="ChkProducts" Margin="0,4" IsChecked="True"
Foreground="{DynamicResource Brush.Text}"
ToolTip="I totali per prodotto (aste viste, prezzi medi, puntate del vincitore). Le opzioni scelte nella scheda Prodotti restano: si azzerano i numeri, non le scelte."
Checked="Option_Changed" Unchecked="Option_Changed"/>
<CheckBox x:Name="ChkLegacy" Margin="0,4" IsChecked="False"
Foreground="{DynamicResource Brush.Text}"
ToolTip="La vecchia cartella Dati delle versioni precedenti: i file JSON già importati nel database, le statistiche, i registri di testo e i dossier delle aste (possono essere gigabyte). L'applicazione non li legge più."
Checked="Option_Changed" Unchecked="Option_Changed"/>
<CheckBox x:Name="ChkLearning" Margin="0,4" IsChecked="False"
Foreground="{DynamicResource Brush.Text}"
ToolTip="Il modello appreso, il profilo per prodotto e ora, lo sfidante, il bandit, il modello di latenza e il contatore delle aste già studiate. Non si perde niente di irrecuperabile: al prossimo avvio l'applicazione ristudia da capo tutte le aste chiuse del database (qualche minuto in sottofondo)."
Checked="Option_Changed" Unchecked="Option_Changed"/>
<Border Style="{StaticResource InfoBox}" Margin="0,14,0,0">
<StackPanel>
<TextBlock x:Name="SummaryText" TextWrapping="Wrap"
Foreground="{DynamicResource Brush.Text}"
FontSize="{StaticResource Font.Size.Sm}" LineHeight="18"
Text="Calcolo…"/>
<TextBlock TextWrapping="Wrap" Margin="0,8,0,0"
Foreground="{DynamicResource Brush.TextMuted}"
FontSize="{StaticResource Font.Size.Sm}" LineHeight="17"
Text="Per cancellare tutto — database, copie di sicurezza, cartella vecchia — c'è «Elimina tutti i dati» nelle Impostazioni."/>
</StackPanel>
</Border>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,18,0,0">
<Button x:Name="WipeButton" Content="Azzera" Padding="26,7" Margin="0,0,10,0"
Style="{StaticResource ModernButton}"
Background="{DynamicResource Brush.Danger}"
Foreground="{DynamicResource Brush.TextOnAccent}"
Click="Wipe_Click"/>
<Button Content="Annulla" Padding="20,7" IsCancel="True"
Style="{StaticResource ModernButton}"
Background="{DynamicResource Brush.SurfaceAlt}"
Foreground="{DynamicResource Brush.Text}"/>
</StackPanel>
</StackPanel>
</Window>
+93
View File
@@ -0,0 +1,93 @@
using System;
using System.Windows;
using AutoBidder.Utilities;
namespace AutoBidder.Dialogs
{
/// <summary>
/// Sceglie quali statistiche azzerare e le azzera.
///
/// <para>Accanto a ogni voce c'è quanto pesa adesso: cancellare è irreversibile, e
/// l'unico modo di renderlo una decisione informata è dire cosa sparisce e quanto è.
/// Lo storico delle aste concluse è l'unica voce con copia di sicurezza automatica:
/// è il solo archivio che non si ricostruisce da nient'altro.</para>
/// </summary>
public partial class WipeStatsDialog : Window
{
private StatsWipe.Sizes _sizes;
public WipeStatsDialog()
{
InitializeComponent();
Loaded += (_, _) => { _sizes = StatsWipe.Measure(); Refresh(); };
}
/// <summary>Il rapporto dell'azzeramento, dopo la conferma. Null se annullato.</summary>
public StatsWipe.Report? Result { get; private set; }
private StatsWipe.Options ReadOptions() => new()
{
History = ChkHistory.IsChecked == true,
ProductStats = ChkProducts.IsChecked == true,
LegacyArchives = ChkLegacy.IsChecked == true,
Learning = ChkLearning.IsChecked == true
};
private void Option_Changed(object sender, RoutedEventArgs e) => Refresh();
private void Refresh()
{
if (!IsLoaded) return;
ChkHistory.Content = $"Osservazioni nel database: aste, puntate, interrogazioni, decisioni ({_sizes.HistoryAuctions} aste, {Fmt(_sizes.HistoryBytes)})";
ChkProducts.Content = $"Statistiche per prodotto ({Fmt(_sizes.ProductsBytes)})";
ChkLegacy.Content = $"Vecchia cartella Dati delle versioni precedenti ({_sizes.LegacyFiles} file, {Fmt(_sizes.LegacyBytes)})";
ChkLearning.Content = $"Apprendimento: modello, profilo, latenza ({Fmt(_sizes.LearningBytes)})";
var o = ReadOptions();
long totale = 0;
if (o.History) totale += _sizes.HistoryBytes;
if (o.ProductStats) totale += _sizes.ProductsBytes;
if (o.LegacyArchives) totale += _sizes.LegacyBytes;
if (o.Learning) totale += _sizes.LearningBytes;
SummaryText.Text = o.Nothing
? "Nessuna voce scelta."
: $"Si liberano circa {Fmt(totale)}." +
(o.History ? " Lo storico viene prima copiato nei backup." : "") +
(o.Learning ? " L'apprendimento ripartirà da zero e ristudierà le aste al prossimo avvio." : "");
WipeButton.IsEnabled = !o.Nothing;
}
private void Wipe_Click(object sender, RoutedEventArgs e)
{
var o = ReadOptions();
if (o.Nothing) return;
var answer = MessageBox.Show(this,
"Le voci scelte vengono cancellate. L'operazione non è reversibile" +
(o.History ? ", salvo la copia dello storico nei backup" : "") + ".\n\nProcedo?",
"Azzera le statistiche", MessageBoxButton.YesNo, MessageBoxImage.Warning);
if (answer != MessageBoxResult.Yes) return;
try
{
Result = StatsWipe.Run(o);
DialogResult = true;
}
catch (Exception ex)
{
MessageBox.Show(this, $"Azzeramento non riuscito: {ex.Message}",
"Azzera le statistiche", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private static string Fmt(long bytes) =>
bytes >= 1L << 30 ? $"{bytes / (double)(1L << 30):F2} GB"
: bytes >= 1L << 20 ? $"{bytes / (double)(1L << 20):F1} MB"
: bytes >= 1L << 10 ? $"{bytes / (double)(1L << 10):F0} KB"
: $"{bytes} B";
}
}
@@ -1,357 +0,0 @@
# AutoBidder v4.0 - Architettura Completa
## ?? Diagramma Architettura
```
???????????????????????????????????????????????????????????????????????
? MainWindow.xaml ?
? (TabControl Principale) ?
???????????????????????????????????????????????????????????????????????
? ?
? ????????????? ????????????? ???????????????? ???????????????? ?
? ? ?? Monitor? ? ?? Browser? ? ?? Statistiche? ? ?? Impostazioni? ?
? ? Aste ? ? ? ? ? ? ? ?
? ????????????? ????????????? ???????????????? ???????????????? ?
? ? ? ? ? ?
? ? ? ? ? ?
? ??????????????????????????????????????????????????????????????? ?
? ? UserControls (4 controlli modulari) ? ?
? ??????????????????????????????????????????????????????????????? ?
? ?
?????????????????????????????????????????????????????????????????????
?
? Events & Data Binding
?
?
???????????????????????????????????????????????????????????????????????
? MainWindow Code-Behind ?
? (Partial Classes - 13 file) ?
???????????????????????????????????????????????????????????????????????
? ?
? MainWindow.xaml.cs ? Core & Initialization ?
? MainWindow.ControlEvents.cs ? NEW: Event Routing ?
? MainWindow.Commands.cs ? Command Pattern ?
? MainWindow.AuctionManagement.cs ? CRUD Aste ?
? MainWindow.EventHandlers.Browser.cs ? Browser Logic ?
? MainWindow.EventHandlers.Export.cs ? Export Features ?
? MainWindow.EventHandlers.Settings.cs ? Settings Management ?
? MainWindow.EventHandlers.Stats.cs ? Statistics Analysis ?
? MainWindow.Logging.cs ? Logging System ?
? MainWindow.UIUpdates.cs ? UI Refresh ?
? MainWindow.UrlParsing.cs ? URL Utilities ?
? MainWindow.UserInfo.cs ? User Session ?
? MainWindow.ButtonHandlers.cs ? Button Events ?
? ?
?????????????????????????????????????????????????????????????????????
?
? Service Layer
?
?
???????????????????????????????????????????????????????????????????????
? Services Layer ?
???????????????????????????????????????????????????????????????????????
? ?
? AuctionMonitor ? Core monitoring service ?
? BidooApiClient ? HTTP API client ?
? SessionManager ? Session persistence ?
? StatsService ? Statistics engine ?
? ClosedAuctionsScraper ? Data scraping ?
? ?
?????????????????????????????????????????????????????????????????????
?
? Data Access
?
?
???????????????????????????????????????????????????????????????????????
? Models & Data Layer ?
???????????????????????????????????????????????????????????????????????
? ?
? Models/ ?
? ??? AuctionInfo ? Dati asta ?
? ??? AuctionState ? Stato runtime ?
? ??? BidResult ? Risultato puntata ?
? ??? BidHistory ? Storico ?
? ??? BidderInfo ? Info utenti ?
? ??? ... ?
? ?
? ViewModels/ ?
? ??? AuctionViewModel ? MVVM pattern ?
? ?
? Data/ ?
? ??? StatisticsContext ? EF Core DbContext ?
? ?
? Utilities/ ?
? ??? PersistenceManager ? Salvataggio JSON ?
? ??? SettingsManager ? App settings ?
? ??? CsvExporter ? Export utilities ?
? ??? ... ?
? ?
???????????????????????????????????????????????????????????????????????
```
## ?? Flusso Dati
### 1. User Interaction Flow
```
User Click
?
UserControl (XAML)
?
UserControl.xaml.cs (Routed Event)
?
MainWindow.ControlEvents.cs (Event Router)
?
MainWindow.[Feature].cs (Business Logic)
?
Service Layer (AuctionMonitor, ApiClient, etc.)
?
Models/Data Update
?
Property Change Notification
?
UI Update (Data Binding)
```
### 2. Auction Monitoring Flow
```
AuctionMonitor.Start()
?
Polling Loop (async)
?
BidooApiClient.PollAuctionStateAsync()
?
HTTP Request to Bidoo API
?
Parse JSON Response
?
Update AuctionState
?
Fire OnAuctionUpdated Event
?
MainWindow.AuctionMonitor_OnAuctionUpdated()
?
Update AuctionViewModel
?
DataGrid Auto-Refresh (INotifyPropertyChanged)
```
### 3. Export Flow
```
User Click "Esporta"
?
MainWindow.EventHandlers.Export.cs
?
Load Export Settings
?
Filter Auctions (Open/Closed/Unknown)
?
For Each Auction:
?? Generate File (CSV/JSON/XML)
?? CsvExporter / JsonSerializer / XDocument
?? Save to Disk
?
Optional: Remove Exported Auctions
?
Show Completion Message
```
## ?? Componenti Chiave
### UserControls
```
Controls/
??? AuctionMonitorControl [430 lines XAML]
? ??? Header (Toolbar)
? ??? MainContent (Grid + Details)
? ??? Footer (Global Log)
?
??? BrowserControl [120 lines XAML]
? ??? Navigation Toolbar
? ??? WebView2 Embedded
?
??? StatisticsControl [80 lines XAML]
? ??? Header (Load Button)
? ??? DataGrid (Stats)
? ??? Footer (Progress)
?
??? SettingsControl [200 lines XAML]
??? Session Config
??? Export Settings
??? Auction Defaults
```
### Partial Classes
```
MainWindow/
??? xaml.cs [150 lines] Core
??? ControlEvents.cs [150 lines] NEW: Event routing
??? Commands.cs [80 lines] Commands
??? AuctionManagement.cs [200 lines] CRUD
??? EventHandlers.*.cs [600 lines] Events (4 files)
??? Logging.cs [50 lines] Log system
??? UIUpdates.cs [120 lines] UI refresh
??? UrlParsing.cs [80 lines] URL utils
??? UserInfo.cs [140 lines] Session
??? ButtonHandlers.cs [200 lines] Buttons
```
## ?? Design Patterns Utilizzati
### 1. **MVVM (Model-View-ViewModel)**
- `Model`: AuctionInfo, BidHistory, etc.
- `View`: XAML files (MainWindow, UserControls)
- `ViewModel`: AuctionViewModel (INotifyPropertyChanged)
### 2. **Service Layer**
- `AuctionMonitor`: Orchestrazione monitoring
- `BidooApiClient`: HTTP communication
- `SessionManager`: Persistenza sessione
### 3. **Repository Pattern**
- `PersistenceManager`: Load/Save aste
- `SettingsManager`: Load/Save settings
### 4. **Observer Pattern**
- Events: `OnAuctionUpdated`, `OnBidExecuted`, `OnLog`
- Data Binding: `INotifyPropertyChanged`
### 5. **Command Pattern**
- `RelayCommand`: WPF ICommand implementation
- Grid commands: Start, Pause, Stop, Bid
### 6. **Composite Pattern**
- UserControls compongono il MainWindow
- Ogni controllo è autonomo ma collabora
### 7. **Strategy Pattern**
- Export formats: CSV, JSON, XML
- Diversi scraper per HTML parsing
## ?? Sicurezza & Best Practices
### ? Implementate
- [x] Cookie encryption (future enhancement)
- [x] Input validation (URL, prezzi, etc.)
- [x] Error handling robusto
- [x] Logging strutturato
- [x] Thread safety (lock su collections)
### ?? Raccomandazioni Future
- [ ] Secure credential storage (Windows Credential Manager)
- [ ] Rate limiting per API calls
- [ ] Retry policy con exponential backoff
- [ ] Circuit breaker pattern per resilienza
- [ ] Telemetry & monitoring
## ?? Metriche Codebase
| Metrica | Prima | Dopo | Delta |
|---------|-------|------|-------|
| File XAML | 1 (1000 lines) | 5 (100+4×150) | +4 files |
| File C# (MainWindow) | 2 | 14 | +12 files |
| Dimensione media file | 500 lines | 120 lines | -76% |
| Linee per classe | 1000+ | 50-200 | -80% |
| Complessità ciclomatica | Alta | Bassa | ?? |
| Testabilità | 30% | 85% | +55% |
| Riutilizzabilità | 10% | 90% | +80% |
## ?? Performance
### Ottimizzazioni
1. **Lazy Loading**: Tab caricati on-demand
2. **Virtual Scrolling**: DataGrid virtualizzato
3. **Async Operations**: Tutte le IO sono async
4. **Caching**: Stati asta cachati in memoria
5. **Debouncing**: TextBox changes debounced
### Benchmarks Stimati
- Startup time: ~2s (cold), ~0.5s (warm)
- UI responsiveness: <16ms per frame (60fps)
- Memory footprint: ~100MB base + 10MB per 100 aste
- API polling: ~50-200ms latency media
## ?? Documentazione
### File Documentazione Creati
1. `REFACTORING_SUMMARY.md` - Code-behind refactoring
2. `XAML_REFACTORING_SUMMARY.md` - XAML refactoring
3. `ARCHITECTURE_OVERVIEW.md` - Questo file
### XML Comments
Tutte le classi public hanno XML documentation:
```csharp
/// <summary>
/// Descrizione classe
/// </summary>
/// <param name="param">Descrizione parametro</param>
/// <returns>Descrizione return</returns>
```
## ?? Getting Started
### Per Sviluppatori
1. **Clona il repository**
```bash
git clone https://192.168.30.23/Alby96/Mimante
cd Mimante/Mimante
```
2. **Apri in Visual Studio 2022**
- Apri `AutoBidder.csproj`
- Restore NuGet packages
- Build Solution
3. **Struttura Progetto**
- `/Controls/` - UserControls modulari
- `/Services/` - Business logic
- `/Models/` - Data models
- `/ViewModels/` - MVVM ViewModels
- `/Utilities/` - Helper utilities
4. **Workflow Sviluppo**
- Modifica UI ? Edit UserControl XAML
- Modifica logic ? Edit MainWindow partial classes
- Aggiungi feature ? Create new service/model
- Test ? Build & Run
### Per Utenti Finali
1. **Primo Avvio**
- Tab "Impostazioni" ? Configura sessione (cookie)
- Tab "Impostazioni" ? Imposta percorso export
2. **Monitoraggio Aste**
- Tab "Monitor Aste" ? Aggiungi URL/ID asta
- Clicca "Avvia" per iniziare il monitoring
- Configura parametri asta nel pannello dettagli
3. **Statistiche**
- Tab "Statistiche" ? Carica aste chiuse
- Analizza medie prezzi e click
## ?? Troubleshooting
### Problemi Comuni
**Problema**: Cookie non valido
- **Soluzione**: Vai su bidoo.com, F12 > Application > Cookies > Copia __stattrb
**Problema**: WebView2 non si carica
- **Soluzione**: Installa WebView2 Runtime da microsoft.com
**Problema**: Export fallisce
- **Soluzione**: Verifica permessi cartella e spazio disco
**Problema**: Asta non viene monitorata
- **Soluzione**: Verifica che sia attiva (checkbox) e non in pausa
## ?? Support
- **Issues**: GitHub Issues
- **Docs**: `/docs` folder
- **Wiki**: Project Wiki
---
**AutoBidder v4.0** - Architettura modulare e scalabile per il monitoraggio automatizzato delle aste Bidoo.com ??
-344
View File
@@ -1,344 +0,0 @@
# Changelog
Tutte le modifiche importanti a questo progetto saranno documentate in questo file.
Il formato è basato su [Keep a Changelog](https://keepachangelog.com/it/1.0.0/),
e questo progetto aderisce a [Semantic Versioning](https://semver.org/lang/it/).
## [4.0.0] - 2024
### 🎉 Maggiori Cambiamenti
#### Refactoring Architettura
- **Partial Classes**: MainWindow diviso in 13 file partial per responsabilità specifiche
- **UserControls Modulari**: Creati 5 UserControls riutilizzabili (AuctionMonitor, Browser, Settings, Statistics, SimpleToolbar)
- **Struttura a Cartelle**: Riorganizzazione completa del progetto in cartelle logiche
#### Nuovo Layout UI
- **Dashboard Moderna**: Layout a griglia con panel ridimensionabili
- **GridSplitters**: 4 splitter per personalizzazione completa del workspace
- **Design Dark Theme**: Palette colori consistente (#1E1E1E, #252526, #2D2D30)
- **Card-Style Panels**: Tutti i pannelli con bordi arrotondati e ombre
### ✨ Nuove Funzionalità
#### Sistema di Logging Avanzato
- Log colorati per severity (Info, Success, Warn, Error)
- Timestamp automatici
- Auto-scroll intelligente
- Log globale + log per singola asta
#### Monitoraggio Aste
- Monitoraggio simultaneo di più aste
- Polling HTTP API-based (no Selenium)
- Tracking real-time timer, prezzo, offerenti
- Statistiche dettagliate per asta
#### Browser Integrato
- WebView2 Microsoft Edge
- Navigazione completa su Bidoo
- Aggiunta rapida aste da URL
- Context menu personalizzato
#### Export Dati
- Supporto formati: CSV, JSON, XML
- Export massivo o per singola asta
- Opzioni configurabili (logs, bidders, metadata)
- Auto-rimozione dopo export
### 🔧 Miglioramenti
#### Performance
- Ridotto uso memoria con lazy loading UserControls
- Ottimizzazione rendering DataGrid con virtualizzazione
- Async/await per tutte le operazioni I/O
- Throttling polling API
#### UX/UI
- Icone emoji per maggiore leggibilità
- Tooltip informativi su bottoni disabilitati
- Feedback visivo per azioni utente
- Messaggi di errore user-friendly
#### Code Quality
- Riduzione complessità ciclomatica
- Separazione concerns (SoC)
- Eliminazione codice duplicato
- XML documentation per API pubbliche
### 📦 Dipendenze
#### Aggiunte
- `Microsoft.EntityFrameworkCore.Sqlite` v8.0.0
- `Microsoft.Web.WebView2` v1.0.1343.22
- `Microsoft.Windows.SDK.BuildTools` v10.0.26100.6584
#### Rimosse
- ~~Selenium.WebDriver~~ (sostituito con HTTP API)
- ~~Selenium.WebDriver.ChromeDriver~~ (non più necessario)
### 🐛 Bug Fix
#### Critici
- Fix memory leak in AuctionMonitor polling loop
- Fix race condition in bid execution
- Fix crash quando WebView2 non inizializzato
- Fix parsing URL con caratteri speciali
#### Minori
- Fix auto-scroll log quando raggiunge bottom
- Fix selezione asta dopo rimozione
- Fix salvataggio impostazioni con valori nulli
- Fix export XML con caratteri escape
### 🔒 Sicurezza
- Cookie session storage cifrato
- Validazione input URL
- Sanitizzazione dati prima di export
- Protezione contro injection in log
### 📝 Documentazione
#### Nuovi File
- `README.md` - Panoramica progetto e setup
- `REFACTORING_SUMMARY.md` - Dettagli refactoring code-behind
- `XAML_REFACTORING_SUMMARY.md` - Dettagli refactoring XAML
- `ARCHITECTURE_OVERVIEW.md` - Overview architettura software
- `XAML_REFACTORING_CHECKLIST.md` - Checklist implementazione
- `CHANGELOG.md` - Questo file
#### Guide
- Guida importazione cookie da browser
- Best practices per configurazione aste
- FAQ troubleshooting comuni
### 🗂️ Struttura Progetto
```
Prima:
AutoBidder/
├── MainWindow.xaml/cs (2000+ righe)
├── Models/
├── Services/
└── Utilities/
Dopo:
AutoBidder/
├── Core/
│ ├── MainWindow files (13 partial classes)
│ └── EventHandlers/
├── Controls/ (5 UserControls)
├── Dialogs/
├── Models/
├── Services/
├── ViewModels/
├── Utilities/
├── Data/
└── Documentation/
```
### 📊 Metriche
| Metrica | Prima | Dopo | Miglioramento |
|---------|-------|------|---------------|
| LOC MainWindow.xaml | 1000+ | 100 | -90% |
| LOC MainWindow.xaml.cs | 2000+ | 180 | -91% |
| File partial classes | 1 | 13 | +1200% |
| Complessità ciclomatica | 85 | 12 | -86% |
| Test coverage | 0% | 45% | +45% |
| Manutenibilità | 35 | 82 | +134% |
### ⚠️ Breaking Changes
- **Namespace Changes**: Alcuni namespace sono stati riorganizzati
- **API Changes**: `AuctionMonitor` ha nuova signature per eventi
- **Config Format**: Formato file `app_settings.json` modificato
- **Database Schema**: Aggiunto campo `PollingLatencyMs` a statistiche
### 🔄 Migrazioni
#### Da v3.x a v4.0
1. **Cookie Session**:
```json
// Vecchio formato
{ "cookie": "..." }
// Nuovo formato
{ "authCookie": "...", "userId": "...", "expiryDate": "..." }
```
2. **Aste Salvate**:
- Percorso spostato da `auctions.json` → `saved_auctions.json`
- Eseguire script migrazione: `dotnet run --migrate`
3. **Database SQLite**:
- Nuova tabella `AuctionStatistics`
- Eseguire: `dotnet ef database update`
### 🎯 Roadmap Futura
#### v4.1 (Q1 2025)
- [ ] Sistema notifiche desktop
- [ ] Multi-account support
- [ ] Temi personalizzabili
- [ ] Backup cloud automatico
#### v4.2 (Q2 2025)
- [ ] Machine Learning per bid prediction
- [ ] Analytics dashboard avanzato
- [ ] Plugin system
- [ ] REST API per integrazioni
#### v5.0 (Q3 2025)
- [ ] Architettura microservizi
- [ ] Web version (Blazor)
- [ ] Mobile app (MAUI)
- [ ] Multi-piattaforma (Linux, macOS)
### 🙏 Ringraziamenti
- **Microsoft**: Per .NET 8 e WPF
- **WebView2 Team**: Per il fantastico browser embedded
- **EF Core Team**: Per l'ORM potente e leggero
- **Bidoo**: Per la piattaforma aste (non ufficialmente affiliati)
---
**Legenda Emoji**:
- 🎉 Maggiori cambiamenti
- ✨ Nuove funzionalità
- 🔧 Miglioramenti
- 🐛 Bug fix
- 🔒 Sicurezza
- 📝 Documentazione
- 🗂️ Struttura
- 📊 Metriche
- ⚠️ Breaking changes
- 🔄 Migrazioni
- 🎯 Roadmap
- 🙏 Ringraziamenti
## v4.1 - UI Modernizzata (2024-01-XX)
### 🎨 Miglioramenti UI
- ✅ **Header semplificato**: Info utente spostate in basso a sinistra
- ✅ **Pannello utente** elegante con:
- Username + ID utente
- Email
- Design card moderno con bordi arrotondati
- Visibilità automatica (appare solo quando loggato)
- ✅ **Header compatto** con statistiche chiave:
- Puntate residue (verde #00D800)
- Credito Shop (verde #00D800)
- Aste vinte (giallo #FFB700)
- ✅ **Layout pulito** stile moderno con separatori verticali
### ⚙️ Performance
- ✅ **Aggiornamento ogni 5 minuti** (era 1 minuto)
- Timer HTML principale: 5 minuti
- Timer API fallback: 10 minuti
- Ridotto carico rete del 80%
- ✅ Pannello utente nascosto di default (meno distrazione)
### 📊 Posizionamento Info
```
┌─────────────────────────────────────────────┐
│ Puntate: 199 | Credito: EUR 15.00 | Aste: 0│ [Pulsanti]
├─────────────────────────────────────────────┤
│ │
│ GRIGLIA ASTE + LOG │
│ │
├─────────────────────────────────────────────┤
│ IMPOSTAZIONI | UTENTI | LOG │
│ │
└─────────────────────────────────────────────┘
┌────────────────────┐
│ sirbietole23 │ ← Pannello utente
│ (ID: 6707664) │ in basso a sx
│ email@email.com │
└────────────────────┘
```
---
## v4.0 - Sistema di Timing Avanzato
### ⚡ Nuovo Sistema di Timing
- ✅ Sostituito "Timer Click (secondi)" con "Anticipo (ms)"
- ✅ Precisione al millisecondo invece dei secondi
- ✅ Polling adattivo 10-1000ms basato su timer rimanente
- ✅ Cooldown 800ms tra puntate consecutive
- ✅ Rilevamento puntate recenti altri utenti (500ms)
- ✅ Checkbox opzionale "Verifica stato asta prima di puntare"
### 🐛 Bug Fix
- ✅ Fix persistenza valori modificati per singola asta
- ✅ Fix visualizzazione username e puntate rimanenti
- ✅ Conferma richiesta prima di cancellare asta (pulsante + tasto Canc)
- ✅ Ottimizzazione logging per miglior performance
- ✅ Fix stato pulsanti globali all'avvio
- ✅ **Fix tasto Canc**: Ora elimina correttamente l'asta selezionata
- Cambiato da `KeyDown` a `PreviewKeyDown` (priorità più alta)
- Migliorata gestione focus keyboard sul DataGrid
- Aggiunto messaggio di conferma migliorato
- Aggiunto logging dettagliato per debug
- **Fix messaggio duplicato**: Rimosso secondo messaggio di conferma (ora ne appare solo uno)
- ✅ **Fix avvio singola asta**: Ora il pulsante "Avvia" sulla griglia funziona senza "Avvia Tutti"
- Auto-start del monitoraggio quando si avvia la prima asta
- Auto-stop del monitoraggio quando si ferma l'ultima asta
- Logging dettagliato con `[AUTO-START]` e `[AUTO-STOP]`
- Comportamento più intuitivo e flessibile
- ✅ **Fix persistenza impostazioni predefinite**: Le impostazioni ora vengono applicate e persistono correttamente
- Nuove aste usano valori dalle impostazioni salvate invece di hardcoded
- Impostazioni predefinite vengono caricate all'avvio
- Logging dettagliato quando si salvano/applicano defaults
- File settings.json in %LocalAppData%\AutoBidder
- ✅ **Fix puntata se già vincitore**: Sistema ora evita di puntare quando l'utente è già il vincitore corrente
- Controllo `IsMyBid` in `ShouldBid()` come prima condizione
- Logging chiaro: `[STRATEGIA] SKIP: Sono già il vincitore corrente`
- Elimina errori "Asta chiusa" quando già vincitore
- Risparmia puntate e chiamate API inutili
- Punta solo quando serve riprendersi l'asta
- ✅ **Fix campo URL browser**: URL sempre visibile e campo non editabile
- Campo URL ora `IsReadOnly="True"` (non modificabile)
- URL si aggiorna automaticamente ad ogni navigazione
- Rimosso pulsante "Vai" non funzionale
- Cursore freccia + tooltip esplicativo
- UX più chiara e coerente
- ✅ **Navigazione con frecce direzionali**: Naviga tra le aste con i tasti Su e Giù
- Comportamento nativo WPF della DataGrid
- Aggiornamento automatico pannello dettagli asta
- Scroll automatico per seguire la selezione
- Navigazione rapida senza usare il mouse
- ✅ **Riordinamento manuale aste**: Pulsanti per cambiare l'ordine delle aste nella lista
- Pulsante "↑ Sposta Su" per spostare verso l'alto
- Pulsante "↓ Sposta Giù" per spostare verso il basso
- Ordine salvato automaticamente su disco
- Gestione intelligente casi limite (cima/fondo)
- Logging dettagliato: `[MOVE UP]` / `[MOVE DOWN]`
- Permette di organizzare le aste per priorità o categoria
- ✅ **Navigazione con frecce direzionali**: Naviga tra le aste con i tasti Su e Giù
- Gestione esplicita in PreviewKeyDown con e.Handled = true
- Fix conflitto con GridSplitter (non modifica più altezza pannelli)
- Aggiornamento automatico pannello dettagli asta
- Scroll automatico per seguire la selezione
- Navigazione rapida senza usare il mouse
- ✅ **Riordinamento manuale aste**: Pulsanti per cambiare l'ordine delle aste nella lista
- Pulsanti "Sposta Su" e "Sposta Giù" (senza emoji per migliore compatibilità)
- Ordine salvato automaticamente su disco
- Gestione intelligente casi limite (cima/fondo)
- Logging dettagliato: `[MOVE UP]` / `[MOVE DOWN]`
- Permette di organizzare le aste per priorità o categoria
- ✅ **Validazione robusta campi numerici**: Impedisce inserimento caratteri non validi
- Solo numeri accettati in tutti i campi numerici dell'applicazione
- Campi interi: Anticipo (ms), Max Clicks, limiti log
- Campi decimali: Min/Max EUR con supporto sia punto che virgola
- Campo vuoto → ripristinato automaticamente a 0 (interi) o 0.00 (decimali)
- Blocco paste di testo non valido
- Normalizzazione automatica formato decimali (virgola → punto, 2 decimali)
- Nessun errore di parsing possibile
- 13 campi validati in tutta l'applicazione
- Helper riusabile: `Utilities\NumericTextBoxHelper.cs`
- **Nota**: Cancellare completamente un campo lo imposta a zero (modo rapido per resettare)
@@ -1,261 +0,0 @@
# ?? Debug: Cookie Detection Non Funziona
## ?? Problema
Dopo 60 secondi dall'avvio, rimane "Non connesso" anche se browser ha cookie valido.
## ? Logging Dettagliato Aggiunto
Ho aggiunto **logging completo** per diagnosticare il problema. Ora ogni step è tracciato.
### Punti di Log Aggiunti
#### 1. InitializeWebView2()
```csharp
[DEBUG] Chiamata EnsureCoreWebView2Async...
[DEBUG] EnsureCoreWebView2Async completata
[DEBUG] CoreWebView2 disponibile, navigating...
[DEBUG] Notifica WebView pronta (TrySetResult)
[DEBUG] Inizio CheckAndImportCookieIfAvailable
```
#### 2. CheckAndImportCookieIfAvailable()
```csharp
[DEBUG] CheckAndImportCookieIfAvailable - inizio
[DEBUG] Delay 1000ms completato, chiamo GetCookieFromWebView
[DEBUG] GetCookieFromWebView ritornato, cookie presente: True/False
[DEBUG] Cookie già presente in sessione corrente, skip import
[DEBUG] Nessun cookie trovato nel browser
```
#### 3. WaitForWebViewInitAsync()
```csharp
[DEBUG] WaitForWebViewInitAsync - inizio (timeout: 60s)
[DEBUG] WebView già inizializzata, ritorno true immediato
[DEBUG] Creazione TaskCompletionSource
[DEBUG] WaitForWebViewInitAsync completato, result: true/false
```
#### 4. CheckBrowserCookieAfterWebViewReady()
```csharp
[DEBUG] CheckBrowserCookieAfterWebViewReady - avviato Task.Run
[DEBUG] Attesa inizializzazione WebView per verifica cookie...
[DEBUG] WaitForWebViewInitAsync completato, ready: true/false
[DEBUG] WebView pronta, procedo con verifica cookie
[DEBUG] Dispatcher.InvokeAsync - chiamo GetCookieFromWebView
[DEBUG] GetCookieFromWebView ritornato, cookie: PRESENTE/VUOTO
```
---
## ?? Istruzioni per Test e Debug
### Step 1: Pulisci e Riavvia
```powershell
# Pulisci sessione salvata
Remove-Item "$env:LOCALAPPDATA\AutoBidder\session.dat" -ErrorAction SilentlyContinue
# Riavvia app
```
### Step 2: Osserva Log Completo
Dopo l'avvio, il log dovrebbe mostrare **tutta la sequenza**:
#### Sequenza Attesa (WebView OK + Cookie Trovato)
```
[17:30:53] [SESSION] Nessuna sessione salvata
[17:30:53] [BROWSER] Inizializzazione WebView2 in background...
[17:30:53] [DEBUG] CheckBrowserCookieAfterWebViewReady - avviato Task.Run
[17:30:53] [DEBUG] Attesa inizializzazione WebView per verifica cookie...
[17:30:53] [DEBUG] WaitForWebViewInitAsync - inizio (timeout: 60s)
[17:30:53] [DEBUG] Creazione TaskCompletionSource
[17:30:54] [DEBUG] Chiamata EnsureCoreWebView2Async...
... [attesa 40-50 secondi] ...
[17:31:43] [DEBUG] EnsureCoreWebView2Async completata
[17:31:43] [DEBUG] CoreWebView2 disponibile, navigating...
[17:31:43] [BROWSER] WebView2 inizializzato e pre-caricato
[17:31:43] [DEBUG] Notifica WebView pronta (TrySetResult)
[17:31:43] [DEBUG] Inizio CheckAndImportCookieIfAvailable
[17:31:43] [DEBUG] CheckAndImportCookieIfAvailable - inizio
[17:31:43] [DEBUG] WaitForWebViewInitAsync completato, result: true
[17:31:43] [DEBUG] WebView pronta, procedo con verifica cookie
[17:31:43] [DEBUG] Dispatcher.InvokeAsync - chiamo GetCookieFromWebView
[17:31:44] [DEBUG] Delay 1000ms completato, chiamo GetCookieFromWebView
[17:31:45] [DEBUG] GetCookieFromWebView ritornato, cookie presente: True
[17:31:45] [DEBUG] GetCookieFromWebView ritornato, cookie: PRESENTE
[17:31:45] [BROWSER] Cookie rilevato nel browser - importazione automatica...
[17:31:45] [DEBUG] Chiamata AutoImportCookieFromWebView
[17:31:45] [SESSION OK] Validata e attiva: username, XX puntate
[17:31:45] [DEBUG] AutoImportCookieFromWebView completata
```
---
### Step 3: Identifica Punto di Fallimento
Confronta il tuo log con la sequenza sopra. **Dove si ferma?**
#### Scenario A: WebView Non Si Inizializza ?
**Log**:
```
[17:30:53] [DEBUG] Chiamata EnsureCoreWebView2Async...
[17:31:53] [WARN] Timeout attesa inizializzazione WebView2
```
**Causa**: `EnsureCoreWebView2Async` si blocca per 60 secondi e va in timeout
**Soluzione**:
1. Verifica WebView2 Runtime installato:
```powershell
Get-ItemProperty -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" -Name pv
```
2. Se mancante, scarica da: https://developer.microsoft.com/en-us/microsoft-edge/webview2/
---
#### Scenario B: WebView OK ma Cookie Non Trovato ?
**Log**:
```
[17:31:43] [BROWSER] WebView2 inizializzato e pre-caricato
[17:31:45] [DEBUG] GetCookieFromWebView ritornato, cookie presente: False
[17:31:45] [DEBUG] Nessun cookie trovato nel browser
[17:31:45] [INFO] Nessun cookie nel browser
[17:31:45] [INFO] Per accedere:
```
**Causa**: WebView pronta ma nessun cookie `__stattrb` trovato
**Verifica**:
1. Apri app
2. Click tab "Browser"
3. Vai su https://it.bidoo.com
4. Apri DevTools (F12) ? Application ? Cookies
5. Cerca cookie `__stattrb`
**Soluzioni**:
- Se cookie assente: Fai login su Bidoo manualmente
- Se cookie presente ma non rilevato: Bug in `GetCookieFromWebView()`, devo fixare
---
#### Scenario C: Cookie Trovato ma Importazione Fallisce ?
**Log**:
```
[17:31:45] [DEBUG] GetCookieFromWebView ritornato, cookie presente: True
[17:31:45] [BROWSER] Cookie rilevato - importazione automatica...
[17:31:45] [DEBUG] Chiamata AutoImportCookieFromWebView
[17:31:46] [SESSION ERROR] Cookie importato ma non valido: [errore]
```
**Causa**: Cookie trovato ma validazione fallita
**Possibili Cause**:
1. Cookie scaduto
2. API Bidoo cambiata
3. Errore di rete
**Soluzione**: Controlla log dettagliato errore, potrei dover fixare `ValidateAndActivateSessionAsync`
---
#### Scenario D: Tutto OK ma UI Non Aggiorna ?
**Log**:
```
[17:31:45] [SESSION OK] Validata e attiva: username, XX puntate
[17:31:45] [DEBUG] AutoImportCookieFromWebView completata
```
**Ma sidebar ancora "Non connesso"**
**Causa**: `SetUserBanner()` non chiamato o chiamato con parametri sbagliati
**Soluzione**: Controlla se c'è chiamata a `SetUserBanner()` dopo l'import
---
### Step 4: Inviami il Log
**Copia TUTTO il log** dal momento dell'avvio fino a 60 secondi dopo, e inviamelo.
Cercherò specificamente questi pattern:
1. ? `[DEBUG] EnsureCoreWebView2Async completata` ? WebView init OK
2. ? `[DEBUG] GetCookieFromWebView ritornato, cookie presente: True` ? Cookie trovato
3. ? `[SESSION OK] Validata e attiva` ? Validazione OK
4. ? Qualsiasi `[ERROR]` o `[WARN]` ? Problema specifico
---
## ?? Quick Fixes Comuni
### Fix 1: WebView2 Runtime Mancante
```powershell
# Download installer
$url = "https://go.microsoft.com/fwlink/p/?LinkId=2124703"
Invoke-WebRequest -Uri $url -OutFile "MicrosoftEdgeWebview2Setup.exe"
# Installa
.\MicrosoftEdgeWebview2Setup.exe /silent /install
```
### Fix 2: Cookie Browser Assente
1. Apri app
2. Tab "Browser"
3. Vai su https://it.bidoo.com
4. Login manuale:
- Username: `sirbietole23`
- Password: [tua password]
5. Verifica login riuscito (homepage Bidoo)
6. Riavvia app
### Fix 3: Firewall/Antivirus Blocca WebView
Aggiungi eccezione per:
- `AutoBidder.exe`
- `msedgewebview2.exe`
---
## ?? Checklist Diagnostica
Prima di inviare log, verifica:
- [ ] WebView2 Runtime installato?
- [ ] Browser ha cookie `__stattrb`?
- [ ] Sei loggato su Bidoo nel browser integrato?
- [ ] Firewall/antivirus non blocca app?
- [ ] Hai riavviato app dopo aver fatto login?
- [ ] Log mostra "[DEBUG]" lines? (se no, build non aggiornata)
---
## ?? Prossimi Passi
1. ? Avvia app con logging dettagliato
2. ? Aspetta 60 secondi
3. ? Copia TUTTO il log
4. ? Inviami il log completo
5. ? Identificherò il punto esatto di fallimento
6. ? Fornirò fix mirato
---
**File Modificati**:
- `Core\MainWindow.WebView.cs` - Logging dettagliato init + cookie check
- `Core\MainWindow.UserInfo.cs` - Logging dettagliato attesa WebView
**Build**: ? Compilazione riuscita
**Pronto per Debug**: ? Sì
**Azione Richiesta**: Riavvia app e inviami log completo dei primi 60 secondi
@@ -1,148 +0,0 @@
# ?? Diagnostica Recupero Dati Utente
## Cosa è cambiato
**NON ho modificato** la procedura di recupero dati utente nelle ultime modifiche.
Il codice esistente è lo stesso di prima, ma ho aggiunto **logging dettagliato** per capire cosa sta andando storto.
## Come funziona il recupero dati
Il sistema usa **2 strategie parallele** (ridondanza per affidabilità):
### 1?? **METODO PRINCIPALE**: HTML Scraping (Timer 5 minuti)
- **URL**: `https://it.bidoo.com/bids_history.php`
- **Estrae**: Username, Puntate residue
- **Pattern cercati**:
```regex
<a class="pers_lnk"[^>]*>([^<]+)</a> # Username
<span id="divSaldoBidBottom"[^>]*>(\d+)</span> # Puntate
```
### 2?? **METODO FALLBACK**: API (Timer 10 minuti)
- **URL**: `https://it.bidoo.com/buy_bids.php`
- **Estrae**: Username, Email, ID, Telefono, Puntate, Credito Shop
- **Pattern cercati**:
```regex
BidooCnf.userObj.username = 'username';
BidooCnf.userObj.email = 'email@example.com';
BidooCnf.userObj.id = '123456';
<span id="divSaldoBidMobile">206</span>
<span class="cbstotal">15.00</span>
```
## ?? Possibili Cause dell'Errore
### 1. **Cookie Scaduto o Non Valido**
Il cookie `__stattrb` potrebbe essere scaduto o non più valido.
**Come verificare**:
1. Apri il browser e vai su `https://it.bidoo.com`
2. Apri DevTools (F12) ? Applicazione ? Cookie
3. Controlla se il cookie `__stattrb` esiste
4. Copia il nuovo valore e inseriscilo nelle Impostazioni
### 2. **Sito Bidoo ha Cambiato Struttura HTML**
Bidoo potrebbe aver modificato la struttura delle pagine.
**Come verificare**:
1. Guarda i log dettagliati (ora disponibili dopo le modifiche)
2. Cerca messaggi tipo:
- `[USER HTML ERROR] Username NON trovato nell'HTML`
- `[USER HTML DEBUG] Snippet HTML: ...`
3. Confronta lo snippet con i pattern regex
### 3. **Problema di Rete o Firewall**
Il server potrebbe bloccare le richieste.
**Come verificare**:
1. Cerca nei log:
- `[USER HTML ERROR] HTTP 403` ? Bloccato
- `[USER HTML ERROR] HTTP 401` ? Non autorizzato
- `[USER HTML ERROR] HTTP 500` ? Errore server
### 4. **Redirect o Risposta Non HTML**
Il server potrebbe fare redirect o rispondere con JSON/testo.
**Come verificare**:
1. Cerca nei log:
- `[USER HTML ERROR] Risposta non contiene HTML valido`
- `Body length: <100` ? Risposta troppo corta
## ?? Nuovo Logging Disponibile
Ho aggiunto logging **molto dettagliato** per diagnosticare:
### Log nel Console Output
```
[INFO] Tentativo recupero dati utente da HTML...
[USER HTML REQUEST] GET https://it.bidoo.com/bids_history.php
[USER HTML RESPONSE] Status: 200 OK
[USER HTML RESPONSE] Body length: 45233 chars
[USER HTML PARSED] Username trovato: sirbietole23
[USER HTML PARSED] Puntate residue trovate: 206
[USER HTML SUCCESS] Dati estratti: sirbietole23, 206 puntate
[OK] Dati utente aggiornati via HTML: sirbietole23, 206 puntate
```
### Se Fallisce
```
[USER HTML RESPONSE] Status: 200 OK
[USER HTML RESPONSE] Body length: 45233 chars
[USER HTML ERROR] Username NON trovato nell'HTML
[USER HTML DEBUG] Snippet HTML: <!DOCTYPE html><html lang="it">...
[USER HTML ERROR] Puntate residue NON trovate nell'HTML
[USER HTML FAILED] Impossibile estrarre dati utente dall'HTML
[WARN] HTML scraping non ha restituito dati validi - verifica cookie nelle Impostazioni
```
## ?? Come Risolvere
### Soluzione 1: Aggiorna Cookie
1. Vai su **Impostazioni**
2. Clicca **Configura Sessione**
3. Inserisci il cookie `__stattrb` aggiornato dal browser
4. Clicca **Salva**
5. Controlla i log
### Soluzione 2: Verifica Log Dettagliati
1. **Riavvia l'applicazione**
2. Aspetta 5-10 secondi (timer automatico parte)
3. Guarda il **Log Principale** in basso
4. Cerca i messaggi `[USER HTML...]` e `[USER INFO...]`
5. Inviami lo snippet HTML se vedi errori
### Soluzione 3: Test Manuale
1. Apri browser e vai su `https://it.bidoo.com/bids_history.php`
2. Verifica se sei loggato (vedi username in alto)
3. Se non sei loggato ? Cookie scaduto
4. Se sei loggato ? Mandami screenshot della pagina
## ?? Test di Verifica
Dopo aver seguito le soluzioni, verifica che nei log appaia:
? **SUCCESSO**:
```
[OK] Dati utente aggiornati via HTML: tuousername, X puntate
```
? **ANCORA ERRORE**:
```
[ERROR] Impossibile aggiornare info utente - verifica cookie nelle Impostazioni
```
Se ancora non funziona, **inviami i log completi** dal primo avvio fino all'errore.
## ?? Supporto
Se il problema persiste:
1. Copia **tutti i log** dal pannello principale
2. Invia screenshot della **scheda Impostazioni** (censura cookie se vuoi)
3. Dimmi se hai aggiornato il cookie recentemente
4. Dimmi se funzionava prima (quando?)
---
**Data**: 2025
**Versione**: 4.0+
@@ -1,437 +0,0 @@
# ? Feature: Pulsanti Apertura Asta Riorganizzati e Funzionanti
## ?? Obiettivo
Riorganizzare i pulsanti per l'asta selezionata e aggiungere funzionalità complete per:
1. **Aprire l'asta nel browser interno** (integrato nell'applicazione)
2. **Aprire l'asta nel browser esterno** (browser predefinito di sistema)
3. **Copiare URL** negli appunti
4. **Esportare asta** (singola)
---
## ?? Problema Prima
- ? **Un solo pulsante "Apri"** senza funzionalità
- ? **Nessun modo** di aprire nel browser interno
- ? **Nessun modo** di aprire nel browser esterno
- ? **Layout confuso** con pulsanti non ben organizzati
---
## ? Soluzione Implementata
### 1?? Nuova Organizzazione Pulsanti
**Layout Precedente**:
```
[Apri] [Copia] [Esporta]
```
**Nuovo Layout (2x2)**:
```
??????????????????????????????????????????
? ?? Browser Interno | ?? Browser Esterno ?
??????????????????????????????????????????
? ?? Copia URL | ?? Esporta ?
??????????????????????????????????????????
```
### 2?? Pulsanti Implementati
#### ?? Browser Interno
- **Testo**: "?? Browser Interno"
- **Colore**: `#007ACC` (Blu Azure)
- **Tooltip**: "Apri asta nel browser integrato"
- **Funzionalità**:
- Passa alla tab "Browser"
- Carica l'asta nel WebView2 integrato
- Log: `[BROWSER] Apertura asta nel browser interno`
#### ?? Browser Esterno
- **Testo**: "?? Browser Esterno"
- **Colore**: `#0078D7` (Blu più chiaro)
- **Tooltip**: "Apri asta nel browser predefinito di sistema"
- **Funzionalità**:
- Apre l'URL nel browser predefinito del sistema
- Utilizza `Process.Start` con `UseShellExecute = true`
- Log: `[BROWSER] Apertura asta nel browser esterno`
#### ?? Copia URL
- **Testo**: "?? Copia URL"
- **Colore**: `#9B4F96` (Viola)
- **Tooltip**: "Copia URL negli appunti"
- **Funzionalità**: (già esistente, riorganizzato)
- Copia l'URL negli appunti
- Log: `URL copiato negli appunti`
#### ?? Esporta
- **Testo**: "?? Esporta"
- **Colore**: `#106EBE` (Blu scuro)
- **Tooltip**: "Esporta dati asta"
- **Funzionalità**:
- Mostra messaggio "Funzionalità in sviluppo"
- Log: `[INFO] Richiesto export singolo`
---
## ?? File Modificati
### 1. `Controls/AuctionMonitorControl.xaml`
**Modifiche**:
- Rimosso layout a 3 colonne `UniformGrid Columns="3"`
- Aggiunto `Grid 2x2` per layout organizzato
- Creati 4 pulsanti ben definiti con emoji e tooltip
**Prima**:
```xaml
<UniformGrid Columns="3" Margin="0,0,0,15">
<Button Content="Apri" /> <!-- Non funzionante -->
<Button x:Name="CopyAuctionUrlButton" Content="Copia" />
<Button Content="Esporta" /> <!-- Non funzionante -->
</UniformGrid>
```
**Dopo**:
```xaml
<Grid Margin="0,0,0,15">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Riga 1: Browser -->
<Button Grid.Row="0" Grid.Column="0"
x:Name="OpenAuctionInternalButton"
Content="?? Browser Interno"
Background="#007ACC"
ToolTip="Apri asta nel browser integrato"
Click="OpenAuctionInternalButton_Click"/>
<Button Grid.Row="0" Grid.Column="1"
x:Name="OpenAuctionExternalButton"
Content="?? Browser Esterno"
Background="#0078D7"
ToolTip="Apri asta nel browser predefinito di sistema"
Click="OpenAuctionExternalButton_Click"/>
<!-- Riga 2: Azioni -->
<Button Grid.Row="1" Grid.Column="0"
x:Name="CopyAuctionUrlButton"
Content="?? Copia URL"
Click="CopyAuctionUrlButton_Click"/>
<Button Grid.Row="1" Grid.Column="1"
x:Name="ExportAuctionButton"
Content="?? Esporta"
Click="ExportAuctionButton_Click"/>
</Grid>
```
### 2. `Controls/AuctionMonitorControl.xaml.cs`
**Aggiunti gestori**:
```csharp
private void OpenAuctionInternalButton_Click(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(OpenAuctionInternalClickedEvent, this));
}
private void OpenAuctionExternalButton_Click(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(OpenAuctionExternalClickedEvent, this));
}
private void ExportAuctionButton_Click(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(ExportAuctionClickedEvent, this));
}
```
**Aggiunti RoutedEvent**:
```csharp
public static readonly RoutedEvent OpenAuctionInternalClickedEvent = ...
public static readonly RoutedEvent OpenAuctionExternalClickedEvent = ...
public static readonly RoutedEvent ExportAuctionClickedEvent = ...
```
### 3. `MainWindow.xaml`
**Aggiunti binding**:
```xaml
<controls:AuctionMonitorControl
...
OpenAuctionInternalClicked="AuctionMonitor_OpenAuctionInternalClicked"
OpenAuctionExternalClicked="AuctionMonitor_OpenAuctionExternalClicked"
ExportAuctionClicked="AuctionMonitor_ExportAuctionClicked"
.../>
```
### 4. `Core/MainWindow.ControlEvents.cs`
**Aggiunti routing eventi**:
```csharp
private void AuctionMonitor_OpenAuctionInternalClicked(object sender, RoutedEventArgs e)
{
OpenAuctionInternalButton_Click(sender, e);
}
private void AuctionMonitor_OpenAuctionExternalClicked(object sender, RoutedEventArgs e)
{
OpenAuctionExternalButton_Click(sender, e);
}
private void AuctionMonitor_ExportAuctionClicked(object sender, RoutedEventArgs e)
{
ExportAuctionButton_Click(sender, e);
}
```
### 5. `Core/MainWindow.ButtonHandlers.cs`
**Implementate funzionalità**:
```csharp
private void OpenAuctionInternalButton_Click(object sender, RoutedEventArgs e)
{
// Passa alla tab Browser
TabBrowser.IsChecked = true;
// Naviga all'URL
if (EmbeddedWebView?.CoreWebView2 != null)
{
EmbeddedWebView.CoreWebView2.Navigate(url);
}
}
private void OpenAuctionExternalButton_Click(object sender, RoutedEventArgs e)
{
System.Diagnostics.Process.Start(new ProcessStartInfo
{
FileName = url,
UseShellExecute = true
});
}
private void ExportAuctionButton_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("Funzionalità in sviluppo...");
}
```
---
## ?? Comportamento
### Scenario 1: Apri nel Browser Interno
**Azioni**:
1. Seleziona un'asta nella griglia
2. Clicca **"?? Browser Interno"**
**Risultato**:
- ? **Tab "Browser"** si attiva automaticamente
- ? **WebView2** carica l'URL dell'asta
- ? **Log**: `[BROWSER] Apertura asta nel browser interno: Nome Asta`
- ? **URL visibile** nella barra del browser interno
**Se browser non pronto**:
- ?? Mostra avviso: "Il browser interno non è ancora pronto. Riprova tra qualche secondo."
---
### Scenario 2: Apri nel Browser Esterno
**Azioni**:
1. Seleziona un'asta nella griglia
2. Clicca **"?? Browser Esterno"**
**Risultato**:
- ? **Browser predefinito** (Chrome/Firefox/Edge) si apre
- ? **URL dell'asta** viene caricato nel browser esterno
- ? **Log**: `[BROWSER] Apertura asta nel browser esterno: Nome Asta`
---
### Scenario 3: Copia URL
**Azioni**:
1. Seleziona un'asta
2. Clicca **"?? Copia URL"**
**Risultato**:
- ? **URL negli appunti**
- ? **Log**: `URL copiato negli appunti`
- ? Puoi incollare con `Ctrl+V`
---
### Scenario 4: Esporta Asta
**Azioni**:
1. Seleziona un'asta
2. Clicca **"?? Esporta"**
**Risultato**:
- ?? **Messaggio**: "Funzionalità in sviluppo"
- ? **Log**: `[INFO] Richiesto export singolo per asta: Nome Asta`
---
## ?? Vantaggi
### Prima:
- ? **Pulsante "Apri" non funzionante**
- ? **Nessuna distinzione** browser interno/esterno
- ? **Layout poco chiaro**
### Dopo:
- ? **Due pulsanti distinti** per browser interno ed esterno
- ? **Emoji intuitive** (?? ?? ?? ??)
- ? **Tooltip esplicativi** su ogni pulsante
- ? **Layout organizzato** 2x2
- ? **Funzionalità complete** e testate
- ? **Gestione errori** appropriata
- ? **Logging dettagliato**
---
## ?? Come Testare
### Test 1: Browser Interno
1. Aggiungi un'asta
2. Selezionala nella griglia
3. Clicca **"?? Browser Interno"**
4. ? **Verifica**:
- Tab "Browser" si attiva
- Asta si apre nel WebView2
- URL visibile nella barra
### Test 2: Browser Esterno
1. Aggiungi un'asta
2. Selezionala
3. Clicca **"?? Browser Esterno"**
4. ? **Verifica**:
- Browser predefinito si apre
- URL corretto caricato
### Test 3: Nessuna Selezione
1. Non selezionare nessuna asta
2. Clicca un pulsante qualsiasi
3. ? **Verifica**: Messaggio "Seleziona un'asta dalla griglia"
### Test 4: Copia URL
1. Seleziona asta
2. Clicca **"?? Copia URL"**
3. Apri Notepad
4. `Ctrl+V`
5. ? **Verifica**: URL dell'asta incollato
---
## ?? Layout Visivo
```
???????????????????????? IMPOSTAZIONI ???????????????????????
? ?
? Nome Asta: iPhone 15 Pro ?
? https://it.bidoo.com/auction.php?a=asta_12345 ?
? ?
? ??????????????????????????????????????????????? ?
? ? ?? Browser Interno ? ?? Browser Esterno ? ?
? ??????????????????????????????????????????????? ?
? ? ?? Copia URL ? ?? Esporta ? ?
? ??????????????????????????????????????????????? ?
? ?
? Anticipo (ms): [200] Min EUR: [0] ?
? Max EUR: [0] Max Clicks: [0] ?
? ? Verifica stato asta prima di puntare ?
? ?
? [Reset] ?
??????????????????????????????????????????????????????????????
```
---
## ?? Log Esempi
### Apertura Browser Interno
```
[BROWSER] Apertura asta nel browser interno: iPhone 15 Pro
```
### Apertura Browser Esterno
```
[BROWSER] Apertura asta nel browser esterno: iPhone 15 Pro
```
### Copia URL
```
URL copiato negli appunti
```
### Export (in sviluppo)
```
[INFO] Richiesto export singolo per asta: iPhone 15 Pro (funzionalità in sviluppo)
```
### Errore
```
[ERRORE] Apertura nel browser interno: Object reference not set to an instance of an object
```
---
## ? Checklist Verifica
- [x] Pulsanti riorganizzati in layout 2x2
- [x] Emoji intuitive su ogni pulsante
- [x] Tooltip esplicativi
- [x] Browser interno funzionante
- [x] Browser esterno funzionante
- [x] Copia URL funzionante
- [x] Export mostra messaggio appropriato
- [x] Gestione errori per asta non selezionata
- [x] Gestione errori per browser non pronto
- [x] Logging dettagliato
- [x] Build compila senza errori
---
**Data Feature**: 2025-01-23
**Versione**: 4.1+
**Feature**: Pulsanti apertura asta riorganizzati e funzionanti
**Status**: ? IMPLEMENTATA
---
## ?? Riepilogo
### Prima:
- ? 1 pulsante "Apri" non funzionante
- ? Nessuna distinzione browser interno/esterno
- ? Layout confuso
### Dopo:
- ? **4 pulsanti** ben organizzati (2x2)
- ? **Browser interno** + **Browser esterno**
- ? **Emoji intuitive** ?? ?? ?? ??
- ? **Tutto funzionante** e testato
- ? **Gestione errori** completa
- ? **Logging dettagliato**
### Layout:
```
?? Browser Interno | ?? Browser Esterno
?? Copia URL | ?? Esporta
```
?? **Pulsanti riorganizzati e completamente funzionanti!**
@@ -1,340 +0,0 @@
# Feature: Navigazione e Riordinamento Aste
## Descrizione
Questa feature aggiunge due funzionalità per migliorare la gestione delle aste nella lista:
1. **Navigazione con frecce direzionali** ????
2. **Riordinamento manuale** con pulsanti ????
## Funzionalità Implementate
### 1?? Navigazione con Frecce Direzionali
Puoi navigare tra le aste usando le **frecce Su e Giù** sulla tastiera.
#### Come Usare
1. Clicca su un'asta nella griglia per selezionarla (assicurati che la griglia abbia il focus)
2. Usa le **frecce ?? Su** e **?? Giù** per spostarti tra le aste
3. Il pannello "Impostazioni" si aggiorna automaticamente mostrando i dettagli dell'asta selezionata
#### Comportamento
- **Gestione esplicita**: Le frecce cambiano la selezione nella DataGrid
- **Prevenzione conflitti**: L'evento viene marcato come `Handled` per evitare che i GridSplitter intercettino le frecce
- Lo **scroll automatico** segue la selezione
- L'evento `SelectionChanged` aggiorna i dettagli dell'asta
#### Vantaggi
- ? Navigazione rapida senza mouse
- ? Scorrimento fluido della lista
- ? Aggiornamento immediato dei dettagli
- ? Non interferisce con i GridSplitter
---
### 2?? Riordinamento Manuale Aste
Puoi **cambiare l'ordine** delle aste nella lista usando i pulsanti dedicati.
#### Come Usare
**Pulsanti nella Toolbar:**
- **Sposta Su**: Sposta l'asta selezionata verso l'alto
- **Sposta Giù**: Sposta l'asta selezionata verso il basso
**Posizione dei Pulsanti:**
```
???????????????????????????????????????????????????????????????
? Aste monitorate: 5 ?
? [Aggiungi] [Sposta Su] [Sposta Giù] [Rimuovi] [Rimuovi Tutte] ?
???????????????????????????????????????????????????????????????
```
#### Funzionamento
1. **Seleziona** un'asta dalla griglia
2. Clicca su **"Sposta Su"** per spostarla verso l'alto
3. Clicca su **"Sposta Giù"** per spostarla verso il basso
4. L'ordine viene **salvato automaticamente** su disco
#### Comportamento
- **In cima**: Se l'asta è già in cima, il pulsante "Sposta Su" non fa nulla
- **In fondo**: Se l'asta è già in fondo, il pulsante "Sposta Giù" non fa nulla
- **Selezione mantenuta**: L'asta rimane selezionata dopo lo spostamento
- **Auto-scroll**: La vista scorre automaticamente per mostrare l'asta
#### Logging
```
[MOVE UP] Asta spostata verso l'alto: Nome Asta
[MOVE DOWN] Asta spostata verso il basso: Nome Asta
[MOVE] L'asta è già in cima alla lista
[MOVE] L'asta è già in fondo alla lista
```
---
## Design UI
### Pulsanti Riordinamento
- **Colore**: Viola `#9B4F96` (stesso colore del pulsante "Punta")
- **Testo**: Semplice "Sposta Su" / "Sposta Giù" (senza emoji)
- **Stile**: Arrotondati con padding compatto
- **Dimensione**: Piccola (`SmallRoundedButton`)
### Tooltip
- **"Sposta Su"**: "Sposta l'asta selezionata verso l'alto"
- **"Sposta Giù"**: "Sposta l'asta selezionata verso il basso"
---
## Implementazione Tecnica
### File Modificati
#### 1. `Controls\AuctionMonitorControl.xaml`
```xml
<Button Content="Sposta Su"
x:Name="MoveUpButton"
Background="#9B4F96"
Style="{StaticResource SmallRoundedButton}"
Click="MoveUpButton_Click"
ToolTip="Sposta l'asta selezionata verso l'alto"/>
<Button Content="Sposta Giù"
x:Name="MoveDownButton"
Background="#9B4F96"
Style="{StaticResource SmallRoundedButton}"
Click="MoveDownButton_Click"
ToolTip="Sposta l'asta selezionata verso il basso"/>
```
#### 2. `Controls\AuctionMonitorControl.xaml.cs`
```csharp
// Gestione esplicita frecce Su/Giù
private void MultiAuctionsGrid_PreviewKeyDown(object sender, KeyEventArgs e)
{
// ... gestione Delete ...
// Gestione frecce Su/Giù
else if (e.Key == Key.Up && MultiAuctionsGrid.Items.Count > 0)
{
int currentIndex = MultiAuctionsGrid.SelectedIndex;
if (currentIndex > 0)
{
MultiAuctionsGrid.SelectedIndex = currentIndex - 1;
MultiAuctionsGrid.ScrollIntoView(MultiAuctionsGrid.SelectedItem);
e.Handled = true; // Previeni ridimensionamento pannelli
}
}
else if (e.Key == Key.Down && MultiAuctionsGrid.Items.Count > 0)
{
int currentIndex = MultiAuctionsGrid.SelectedIndex;
if (currentIndex < MultiAuctionsGrid.Items.Count - 1)
{
MultiAuctionsGrid.SelectedIndex = currentIndex + 1;
MultiAuctionsGrid.ScrollIntoView(MultiAuctionsGrid.SelectedItem);
e.Handled = true; // Previeni ridimensionamento pannelli
}
}
}
```
#### 3. `MainWindow.xaml`
```xml
<controls:AuctionMonitorControl
MoveUpClicked="AuctionMonitor_MoveUpClicked"
MoveDownClicked="AuctionMonitor_MoveDownClicked"
... />
```
#### 4. `Core\MainWindow.ControlEvents.cs`
```csharp
private void AuctionMonitor_MoveUpClicked(object sender, RoutedEventArgs e)
{
MoveUpButton_Click(sender, e);
}
private void AuctionMonitor_MoveDownClicked(object sender, RoutedEventArgs e)
{
MoveDownButton_Click(sender, e);
}
```
#### 5. `Core\MainWindow.ButtonHandlers.cs`
```csharp
private void MoveUpButton_Click(object sender, RoutedEventArgs e)
{
// Sposta verso l'alto usando ObservableCollection.Move()
var currentIndex = _auctionViewModels.IndexOf(_selectedAuction);
if (currentIndex > 0)
{
_auctionViewModels.Move(currentIndex, currentIndex - 1);
SaveAuctions(); // Persiste l'ordine
}
}
private void MoveDownButton_Click(object sender, RoutedEventArgs e)
{
// Sposta verso il basso usando ObservableCollection.Move()
var currentIndex = _auctionViewModels.IndexOf(_selectedAuction);
if (currentIndex < _auctionViewModels.Count - 1)
{
_auctionViewModels.Move(currentIndex, currentIndex + 1);
SaveAuctions(); // Persiste l'ordine
}
}
```
---
## Fix Problema Frecce e GridSplitter
### Problema Originale
Le frecce Su/Giù modificavano l'altezza dei pannelli invece di navigare tra le aste, perché i `GridSplitter` intercettavano gli eventi prima della DataGrid.
### Soluzione Implementata
1. **Gestione esplicita** delle frecce in `PreviewKeyDown`
2. **e.Handled = true** per bloccare la propagazione dell'evento
3. **Cambio manuale** dell'indice selezionato nella DataGrid
4. **ScrollIntoView** per mantenere l'asta selezionata visibile
### Risultato
? Le frecce Su/Giù ora navigano correttamente tra le aste
? Non interferiscono più con i GridSplitter
? L'evento SelectionChanged viene correttamente sollevato
---
## Come Testare
### Test Navigazione con Frecce
1. Avvia l'applicazione
2. Aggiungi almeno **3 aste**
3. Clicca sulla **prima asta** nella griglia
4. Premi **freccia Giù** ?? ? La selezione si sposta sulla seconda asta
5. Premi **freccia Su** ?? ? La selezione torna alla prima asta
6. Verifica che:
- ? Il pannello "Impostazioni" si aggiorna
- ? L'altezza dei pannelli **NON cambia**
- ? Lo scroll segue la selezione
### Test Riordinamento Manuale
1. Avvia l'applicazione
2. Aggiungi almeno **3 aste** (es. Asta A, Asta B, Asta C)
3. Seleziona **Asta B** (quella in mezzo)
4. Clicca su **"Sposta Su"**
- ? Asta B si sposta sopra Asta A
- ? Ordine diventa: B, A, C
5. Clicca su **"Sposta Giù"** (con B ancora selezionata)
- ? Asta B torna nella posizione originale
- ? Ordine diventa: A, B, C
6. Chiudi e riapri l'applicazione
- ? L'ordine è **persistito** correttamente
### Test Casi Limite
1. **In cima**: Seleziona la prima asta e clicca "Sposta Su"
- ? Nessuna azione, log: "L'asta è già in cima"
2. **In fondo**: Seleziona l'ultima asta e clicca "Sposta Giù"
- ? Nessuna azione, log: "L'asta è già in fondo"
3. **Nessuna selezione**: Clicca "Sposta Su" senza selezionare
- ? Messaggio: "Seleziona un'asta dalla griglia"
4. **Freccia Su in cima**: Premi freccia Su sulla prima asta
- ? Nessun movimento, rimane sulla prima
5. **Freccia Giù in fondo**: Premi freccia Giù sull'ultima asta
- ? Nessun movimento, rimane sull'ultima
---
## Casi d'Uso
### Scenario 1: Priorità Aste
**Problema**: Hai 10 aste ma alcune sono più importanti
**Soluzione**: Sposta le aste prioritarie **in cima** alla lista
### Scenario 2: Organizzazione per Categoria
**Problema**: Vuoi raggruppare aste simili (es. Shop, Buoni, Elettronica)
**Soluzione**: Riordina manualmente per categoria
### Scenario 3: Navigazione Rapida
**Problema**: Devi controllare rapidamente tutte le aste
**Soluzione**: Usa le **frecce Su/Giù** per scorrere velocemente
---
## Vantaggi
| Funzionalità | Vantaggio | Prima | Dopo |
|--------------|-----------|-------|------|
| **Navigazione Frecce** | Controllo rapido da tastiera | Solo mouse | ?? Frecce |
| **Riordinamento** | Lista personalizzata | Ordine fisso | ?? Riordinabile |
| **Persistenza** | Ordine salvato | N/A | ?? Auto-save |
| **UX** | Interfaccia intuitiva | N/A | ? Pulsanti chiari |
| **No Conflitti** | Frecce non alterano layout | Ridimensionava | ? Solo navigazione |
---
## Metriche
- **Frecce direzionali**: Gestione custom con e.Handled = true
- **Riordinamento**: O(1) - `ObservableCollection.Move()`
- **Salvataggio**: Automatico dopo ogni spostamento
- **UI Responsiveness**: Nessun lag o blocco
- **Conflitti**: Zero conflitti con GridSplitter
---
## Possibili Miglioramenti Futuri
- [ ] **Drag & Drop**: Trascina le aste con il mouse
- [ ] **Scorciatoie da tastiera**: `Ctrl+Up` e `Ctrl+Down` per spostare
- [ ] **Selezione multipla**: Sposta più aste contemporaneamente
- [ ] **Ordinamento automatico**: Per nome, prezzo, timer, ecc.
- [ ] **Gruppi/Cartelle**: Organizza aste in categorie
---
## Note di Sviluppo
### Perché Gestione Esplicita delle Frecce?
- ? **Previene conflitti** con GridSplitter
- ? **Controllo totale** sul comportamento
- ? **e.Handled = true** blocca propagazione
- ? **Compatibile** con altri componenti WPF
### Perché ObservableCollection.Move()?
- ? **Thread-safe** con UI binding
- ? **Notifica automatica** alla DataGrid
- ? **Performante** (O(1) complexity)
- ? **Built-in WPF** - nessuna dipendenza esterna
### Perché Pulsanti Senza Emoji?
- ?? **Compatibilità**: Funziona su tutti i sistemi
- ?? **Leggibilità**: Testo chiaro e immediato
- ?? **Professionalità**: Interfaccia pulita
- ?? **Accessibilità**: Migliore supporto screen reader
---
## Checklist Completamento
- [x] Navigazione con frecce Su/Giù
- [x] Fix conflitto GridSplitter
- [x] Pulsanti "Sposta Su" e "Sposta Giù"
- [x] Rimozione emoji dai pulsanti
- [x] Gestione casi limite (cima/fondo)
- [x] Salvataggio automatico ordine
- [x] Logging dettagliato
- [x] Messaggi utente chiari
- [x] Tooltip informativi
- [x] Compilazione senza errori
- [x] Documentazione completa
---
## Conclusioni
Questa feature migliora significativamente l'**usabilità** dell'applicazione, permettendo agli utenti di:
- Navigare rapidamente tra le aste con la **tastiera** senza conflitti con i GridSplitter
- Personalizzare l'**ordine** delle aste secondo le proprie preferenze
- Mantenere l'ordine **persistente** tra le sessioni
Il tutto con un'implementazione **pulita**, **performante**, **senza conflitti UI** e **ben documentata**! ??
@@ -1,341 +0,0 @@
# ? Feature: Focus Automatico su Asta Successiva dopo Cancellazione
## ?? Obiettivo
Permettere la **cancellazione rapida di più aste** spostando automaticamente il focus sulla riga successiva dopo ogni cancellazione, così l'utente può:
1. Selezionare un'asta
2. Premere `Canc` (o cliccare "Rimuovi")
3. Confermare la rimozione
4. **Il focus si sposta automaticamente sulla riga successiva**
5. Premere di nuovo `Canc` per rimuovere l'asta successiva
6. Ripetere rapidamente
---
## ? Implementazione
### File Modificato: `Core/MainWindow.ButtonHandlers.cs`
**Metodo**: `RemoveUrlButton_Click`
### Logica Implementata
```csharp
// 1?? Salva l'indice corrente PRIMA di rimuovere
var currentIndex = _auctionViewModels.IndexOf(_selectedAuction);
// 2?? ... rimuove l'asta ...
// 3?? Calcola quale asta selezionare dopo
if (_auctionViewModels.Count > 0)
{
int newIndex;
if (currentIndex >= _auctionViewModels.Count)
{
// L'asta rimossa era l'ultima ? seleziona la nuova ultima
newIndex = _auctionViewModels.Count - 1;
}
else
{
// Seleziona l'asta che ora si trova nella stessa posizione
newIndex = currentIndex;
}
// 4?? Seleziona l'asta
MultiAuctionsGrid.SelectedIndex = newIndex;
_selectedAuction = _auctionViewModels[newIndex];
// 5?? Forza il focus sulla griglia (con delay per permettere UI update)
Dispatcher.BeginInvoke(new Action(() =>
{
MultiAuctionsGrid.Focus();
// Scroll fino alla riga selezionata
if (MultiAuctionsGrid.SelectedItem != null)
{
MultiAuctionsGrid.ScrollIntoView(MultiAuctionsGrid.SelectedItem);
}
Log($"[FOCUS] Focus spostato su: {_selectedAuction.Name}", LogLevel.Info);
}), System.Windows.Threading.DispatcherPriority.Background);
}
else
{
// Nessuna asta rimasta
_selectedAuction = null;
Log($"[REMOVE] Nessuna asta rimasta nella lista", LogLevel.Info);
}
```
---
## ?? Comportamento
### Scenario 1: Rimuovi Asta in Mezzo alla Lista
**Lista iniziale**:
```
1. Asta A
2. Asta B ? SELEZIONATA
3. Asta C
4. Asta D
```
**Azioni**:
1. Premi `Canc` su "Asta B"
2. Confermi la rimozione
**Risultato**:
```
1. Asta A
2. Asta C ? FOCUS AUTOMATICO (era in posizione 3, ora in posizione 2)
3. Asta D
```
? **Focus su "Asta C"** (riga successiva)
---
### Scenario 2: Rimuovi Ultima Asta
**Lista iniziale**:
```
1. Asta A
2. Asta B
3. Asta C
4. Asta D ? SELEZIONATA
```
**Azioni**:
1. Premi `Canc` su "Asta D"
2. Confermi la rimozione
**Risultato**:
```
1. Asta A
2. Asta B
3. Asta C ? FOCUS AUTOMATICO (nuova ultima asta)
```
? **Focus su "Asta C"** (nuova ultima asta)
---
### Scenario 3: Rimuovi Prima Asta
**Lista iniziale**:
```
1. Asta A ? SELEZIONATA
2. Asta B
3. Asta C
4. Asta D
```
**Azioni**:
1. Premi `Canc` su "Asta A"
2. Confermi la rimozione
**Risultato**:
```
1. Asta B ? FOCUS AUTOMATICO (era in posizione 2, ora in posizione 1)
2. Asta C
3. Asta D
```
? **Focus su "Asta B"** (nuova prima asta)
---
### Scenario 4: Rimuovi Tutte le Aste Rapidamente
**Lista iniziale**:
```
1. Asta A ? SELEZIONATA
2. Asta B
3. Asta C
```
**Azioni rapide**:
1. `Canc` ? Conferma ? Focus su "Asta B"
2. `Canc` ? Conferma ? Focus su "Asta C"
3. `Canc` ? Conferma ? **Nessuna asta rimasta**
**Risultato**:
```
(lista vuota)
```
? **Puoi cancellare tutte le aste premendo solo `Canc` + `Invio` ripetutamente!**
---
## ?? Vantaggi
### ? Cancellazione Rapidissima
**Prima**:
1. Seleziona asta 1
2. Premi `Canc`
3. Conferma
4. ? **Focus perso** - devi cliccare di nuovo sulla lista
5. Seleziona asta 2
6. Premi `Canc`
7. ...
**Dopo**:
1. Seleziona asta 1
2. Premi `Canc` + `Invio` (conferma)
3. ? **Focus automaticamente su asta 2**
4. Premi `Canc` + `Invio`
5. ? **Focus automaticamente su asta 3**
6. Premi `Canc` + `Invio`
7. ...
### ?? Workflow Migliorato
- ? **Non serve più usare il mouse** dopo la prima selezione
- ? **Cancellazione sequenziale rapidissima** con solo tastiera
- ? **Scroll automatico** alla riga selezionata (sempre visibile)
- ? **Log dettagliato** del focus spostato
---
## ?? Come Testare
### Test 1: Cancellazione Singola
1. Aggiungi 5 aste
2. Seleziona l'asta in posizione 3
3. Premi `Canc`
4. Conferma con `Invio`
5. ? **Verifica**: Focus automaticamente sull'asta che era in posizione 4 (ora posizione 3)
### Test 2: Cancellazione Rapida Multiple
1. Aggiungi 10 aste
2. Seleziona la prima asta
3. Premi rapidamente: `Canc` ? `Invio` ? `Canc` ? `Invio` ? `Canc` ? `Invio`
4. ? **Verifica**: Cancellate 3 aste senza mai perdere il focus
### Test 3: Cancellazione Ultima Asta
1. Aggiungi 3 aste
2. Seleziona l'ultima asta
3. Premi `Canc` + `Invio`
4. ? **Verifica**: Focus sulla nuova ultima asta (era la penultima)
### Test 4: Cancellazione Tutte le Aste
1. Aggiungi 5 aste
2. Seleziona la prima
3. Premi `Canc` + `Invio` per 5 volte di seguito
4. ? **Verifica**: Lista vuota, nessun errore
### Test 5: Scroll Automatico
1. Aggiungi 20 aste (scrollable)
2. Scrolla in fondo
3. Seleziona un'asta in fondo
4. Premi `Canc` + `Invio`
5. ? **Verifica**: La vista scrolla per mostrare la nuova asta selezionata
---
## ?? Log di Debug
Dopo ogni cancellazione, nel log appare:
```
[REMOVE] Asta rimossa: Balenciaga Collana (ID: 82746448)
[FOCUS] Focus spostato su: iPhone 15 Pro
```
Se rimuovi l'ultima asta:
```
[REMOVE] Asta rimossa: Ultima Asta (ID: 12345)
[REMOVE] Nessuna asta rimasta nella lista
```
---
## ?? Dettagli Tecnici
### Uso di `Dispatcher.BeginInvoke`
```csharp
Dispatcher.BeginInvoke(new Action(() =>
{
MultiAuctionsGrid.Focus();
MultiAuctionsGrid.ScrollIntoView(MultiAuctionsGrid.SelectedItem);
Log($"[FOCUS] Focus spostato su: {_selectedAuction.Name}", LogLevel.Info);
}), System.Windows.Threading.DispatcherPriority.Background);
```
**Perché?**
- Il focus va dato **DOPO** che la UI ha completato il rendering della rimozione
- `DispatcherPriority.Background` assicura che l'operazione avvenga quando la UI è pronta
- Senza questo delay, il focus potrebbe essere perso o applicato alla riga sbagliata
### Gestione Indici
**Caso 1**: Rimuovi asta in mezzo
```csharp
currentIndex = 2 // Asta B
// Dopo rimozione, Count = 3
newIndex = currentIndex = 2 // Ora punta a Asta C
```
**Caso 2**: Rimuovi ultima asta
```csharp
currentIndex = 4 // Asta D (ultima)
// Dopo rimozione, Count = 3
currentIndex >= Count // true
newIndex = Count - 1 = 2 // Asta C (nuova ultima)
```
---
## ? Checklist Verifica
- [x] Focus si sposta automaticamente dopo cancellazione
- [x] Funziona con asta in mezzo alla lista
- [x] Funziona con ultima asta
- [x] Funziona con prima asta
- [x] Funziona con lista vuota
- [x] Scroll automatico alla riga selezionata
- [x] Log dettagliato del focus
- [x] Nessun errore se lista vuota
- [x] Cancellazione rapida con solo tastiera funziona
- [x] Build compila senza errori
---
**Data Feature**: 2025-01-23
**Versione**: 4.1+
**Feature**: Auto-focus su asta successiva dopo cancellazione
**Status**: ? IMPLEMENTATA
---
## ?? Riepilogo
### Prima:
- ? Focus perso dopo cancellazione
- ? Serve cliccare di nuovo sulla lista
- ? Cancellazione multipla lenta
### Dopo:
- ? Focus **automatico** sulla riga successiva
- ? Cancellazione **rapidissima** con solo tastiera
- ? Workflow **fluido** e **intuitivo**
- ? Scroll **automatico** per visibilità
- ? Log **dettagliato** per debugging
### Shortcut Rapido:
```
Seleziona asta ? Canc ? Invio ? Canc ? Invio ? Canc ? Invio ? ...
```
?? **Cancellazione ultra-rapida di multiple aste!**
@@ -1,515 +0,0 @@
# ?? Feature: Storia Puntate in Tempo Reale
## ?? Obiettivo
Aggiungere una nuova scheda "Storia Puntate" accanto alla scheda "Utenti" nel pannello asta selezionata, che mostra le ultime N puntate effettuate sull'asta in tempo reale.
---
## ?? Formato Dati API
### Risposta da `data.php?ALL=83110253`
```
1764068206*[83110253;ON;1764068216;42;fedekikka2323;3,42;fedekikka2323;1764068204;3|41;chamorro1984;1764068194;3|40;fedekikka2323;1764068184;3|...]
```
**Struttura**:
- `1764068206` = Server timestamp
- `*` = Separatore
- `[...]` = Dati asta tra parentesi quadre
- Dati principali: `83110253;ON;1764068216;42;fedekikka2323`
- `|` = Separatore storia puntate
- Storia: `42;fedekikka2323;1764068204;3|41;chamorro1984;1764068194;3|...`
### Formato Storia Puntate
Ogni record separato da `|`:
```
priceIndex;username;timestamp;bidType
```
**Esempio**:
- `42;fedekikka2323;1764068204;3`
- Prezzo: 42 (= €0.42)
- Username: fedekikka2323
- Timestamp: 1764068204 (Unix timestamp)
- Tipo: 3 (Auto) / 1 (Manuale)
---
## ? Implementazione Completata
### 1?? Model - `BidHistoryEntry.cs`
```csharp
namespace AutoBidder.Models
{
public class BidHistoryEntry
{
public decimal Price { get; set; }
public string BidType { get; set; } // "Auto" o "Manuale"
public long Timestamp { get; set; }
public string Username { get; set; }
// Proprietà calcolate
public string TimeFormatted => DateTimeOffset.FromUnixTimeSeconds(Timestamp)
.ToLocalTime().ToString("HH:mm:ss");
public string PriceFormatted => Price.ToString("0.00");
public bool IsMyBid { get; set; } // True se è la mia puntata
}
}
```
### 2?? AuctionInfo - Lista Storia
```csharp
// In Models/AuctionInfo.cs
/// <summary>
/// Storia delle ultime puntate effettuate sull'asta (da API)
/// </summary>
[JsonIgnore]
public List<BidHistoryEntry> RecentBids { get; set; } = new List<BidHistoryEntry>();
```
### 3?? AuctionState - Passaggio Dati
```csharp
// In Models/AuctionState.cs
/// <summary>
/// Storia delle ultime puntate (dal polling API)
/// </summary>
public List<BidHistoryEntry>? RecentBidsHistory { get; set; }
```
### 4?? Parsing API - `BidooApiClient.cs`
```csharp
private AuctionState? ParsePollingResponse(string auctionId, string response, int latency)
{
// ...existing parsing...
// ? Parse storia puntate
if (!string.IsNullOrEmpty(historyData))
{
state.RecentBidsHistory = ParseBidHistory(historyData, fields[3]);
}
return state;
}
private List<BidHistoryEntry>? ParseBidHistory(string historyData, string currentPriceStr)
{
var entries = new List<BidHistoryEntry>();
var records = historyData.Split('|');
foreach (var record in records)
{
if (string.IsNullOrWhiteSpace(record)) continue;
var parts = record.Split(';');
if (parts.Length < 4) continue;
// priceIndex;username;timestamp;bidType
if (!int.TryParse(parts[0], out var priceIndex)) continue;
var username = parts[1].Trim();
if (!long.TryParse(parts[2], out var timestamp)) continue;
var bidTypeCode = parts.Length > 3 ? parts[3].Trim() : "0";
string bidType = bidTypeCode switch
{
"3" => "Auto",
"1" => "Manuale",
_ => "Auto"
};
var entry = new BidHistoryEntry
{
Price = priceIndex * 0.01m,
BidType = bidType,
Timestamp = timestamp,
Username = username,
IsMyBid = username.Equals(_session.Username, StringComparison.OrdinalIgnoreCase)
};
entries.Add(entry);
}
return entries.Count > 0 ? entries : null;
}
```
### 5?? Propagazione - `AuctionMonitor.cs`
```csharp
private async Task PollAndProcessAuction(AuctionInfo auction, CancellationToken token)
{
var state = await _apiClient.PollAuctionStateAsync(...);
// ? Aggiorna storia puntate
if (state.RecentBidsHistory != null && state.RecentBidsHistory.Count > 0)
{
auction.RecentBids = state.RecentBidsHistory;
}
// ...rest of processing...
}
```
---
## ?? Vista XAML - DA IMPLEMENTARE
### Struttura Layout
```xml
<!-- In Controls/AuctionMonitorControl.xaml -->
<!-- Sostituisci TabControl esistente con questo: -->
<TabControl Grid.Row="4" Background="#2D2D30" BorderThickness="0">
<!-- Tab Utenti (esistente) -->
<TabItem Header="Utenti" Foreground="#CCCCCC">
<DataGrid x:Name="SelectedAuctionBiddersGrid"
ItemsSource="{Binding RecentBids}"
...>
<!-- Columns esistenti -->
</DataGrid>
</TabItem>
<!-- ? NUOVA Tab Storia Puntate -->
<TabItem Header="Storia Puntate" Foreground="#CCCCCC">
<DataGrid x:Name="BidHistoryGrid"
ItemsSource="{Binding BidHistoryEntries}"
AutoGenerateColumns="False"
IsReadOnly="True"
CanUserAddRows="False"
CanUserDeleteRows="False"
CanUserResizeRows="False"
HeadersVisibility="Column"
GridLinesVisibility="Horizontal"
HorizontalGridLinesBrush="#3E3E42"
Background="#1E1E1E"
Foreground="#CCCCCC"
BorderThickness="0"
RowHeight="32">
<DataGrid.Columns>
<!-- Colonna Prezzo -->
<DataGridTextColumn Header="PREZZO"
Binding="{Binding PriceFormatted}"
Width="80">
<DataGridTextColumn.ElementStyle>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="#00D800"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
<!-- Colonna Modalità -->
<DataGridTextColumn Header="MODALITÀ"
Binding="{Binding BidType}"
Width="90">
<DataGridTextColumn.ElementStyle>
<Style TargetType="TextBlock">
<Setter Property="HorizontalAlignment" Value="Center"/>
<Style.Triggers>
<DataTrigger Binding="{Binding BidType}" Value="Auto">
<Setter Property="Foreground" Value="#FFC107"/>
</DataTrigger>
<DataTrigger Binding="{Binding BidType}" Value="Manuale">
<Setter Property="Foreground" Value="#03A9F4"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
<!-- Colonna Orario -->
<DataGridTextColumn Header="ORARIO"
Binding="{Binding TimeFormatted}"
Width="90">
<DataGridTextColumn.ElementStyle>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="#9E9E9E"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
<!-- Colonna Utente -->
<DataGridTextColumn Header="UTENTE"
Binding="{Binding Username}"
Width="*">
<DataGridTextColumn.ElementStyle>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="#CCCCCC"/>
<Setter Property="Margin" Value="8,0,0,0"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsMyBid}" Value="True">
<Setter Property="Foreground" Value="#00D800"/>
<Setter Property="FontWeight" Value="Bold"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
</DataGrid.Columns>
<!-- Stili righe -->
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Setter Property="Background" Value="#2D2D30"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#3E3E42"/>
</Trigger>
<DataTrigger Binding="{Binding IsMyBid}" Value="True">
<Setter Property="Background" Value="#1A4D1A"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
<!-- Stile header -->
<DataGrid.ColumnHeaderStyle>
<Style TargetType="DataGridColumnHeader">
<Setter Property="Background" Value="#252526"/>
<Setter Property="Foreground" Value="#CCCCCC"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="FontSize" Value="11"/>
<Setter Property="Padding" Value="8,6"/>
<Setter Property="BorderThickness" Value="0,0,1,1"/>
<Setter Property="BorderBrush" Value="#3E3E42"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
</Style>
</DataGrid.ColumnHeaderStyle>
</DataGrid>
</TabItem>
</TabControl>
```
### Colori e Stile
| Elemento | Colore | Descrizione |
|----------|--------|-------------|
| **Prezzo** | `#00D800` | Verde brillante |
| **Auto** | `#FFC107` | Giallo/Arancio |
| **Manuale** | `#03A9F4` | Azzurro |
| **Orario** | `#9E9E9E` | Grigio chiaro |
| **Utente** | `#CCCCCC` | Bianco/Grigio |
| **Mia Puntata** | `#00D800` | Verde (bold) + sfondo `#1A4D1A` |
---
## ?? Preview Visivo
```
??????????????????????????????????????????????
? [Utenti] [Storia Puntate] ? ? Tabs
??????????????????????????????????????????????
? PREZZO ? MODALITÀ ? ORARIO ? UTENTE ? ? Header
?????????????????????????????????????????????
? 0.42 ? Auto ? 11:54:41 ? chamorro ? ? Riga normale
? 0.41 ? Auto ? 11:54:31 ? makrucco39 ?
? 0.40 ? Manuale ? 11:54:20 ? chamorro ?
? 0.39 ? Auto ? 11:54:10 ? sirbiet... ? ? Mia puntata (verde)
? 0.38 ? Manuale ? 11:54:00 ? chamorro ?
??????????????????????????????????????????????
```
---
## ?? Aggiornamento UI - DA IMPLEMENTARE
### ViewModel Binding
Aggiungi proprietà al `AuctionViewModel`:
```csharp
// In ViewModels/AuctionViewModel.cs
public ObservableCollection<BidHistoryEntry> BidHistoryEntries { get; }
= new ObservableCollection<BidHistoryEntry>();
public void RefreshBidHistory()
{
Dispatcher.Invoke(() =>
{
BidHistoryEntries.Clear();
if (_auctionInfo.RecentBids != null)
{
foreach (var bid in _auctionInfo.RecentBids)
{
BidHistoryEntries.Add(bid);
}
}
});
}
```
### Update on Poll
```csharp
// In MainWindow.xaml.cs - evento OnAuctionUpdated
private void AuctionMonitor_OnAuctionUpdated(AuctionState state)
{
Dispatcher.BeginInvoke(() =>
{
var vm = _auctionViewModels.FirstOrDefault(a => a.AuctionId == state.AuctionId);
if (vm != null)
{
// ...existing updates...
// ? NUOVO: Aggiorna storia puntate
vm.RefreshBidHistory();
}
});
}
```
---
## ?? Utilizzo Dati
### Informazioni Fornite
1. **Prezzo Puntata**: Mostra progressione prezzo asta
2. **Modalità**: Distingue puntate automatiche da manuali
3. **Orario**: Timestamp preciso ogni puntata
4. **Utente**: Chi ha puntato (evidenzia tue puntate)
### Benefici per l'Utente
? **Visione Real-Time**: Vedi chi sta puntando ora
? **Pattern Recognition**: Identifica utenti aggressivi
? **Strategia**: Decide quando puntare basandosi su attività
? **Trasparenza**: Visibilità completa sulle ultime puntate
? **Tracciabilità**: Log permanente ultime azioni
---
## ?? Sincronizzazione con Tab Utenti
### Doppia Funzione
**Tab Utenti** (esistente):
- Statistiche aggregate per utente
- Totale puntate per utente
- Ordinamento per conteggio
**Tab Storia Puntate** (nuova):
- Cronologia temporale
- Dettaglio singola puntata
- Mostra ultime N azioni
### Aggiornamento Contatori
La storia puntate può **aggiornare** le statistiche utenti:
```csharp
// Quando arriva nuova storia, aggiorna BidderStats
foreach (var bid in state.RecentBidsHistory)
{
if (!auction.BidderStats.ContainsKey(bid.Username))
{
auction.BidderStats[bid.Username] = new BidderInfo
{
Username = bid.Username,
BidCount = 0
};
}
// Aggiorna se timestamp più recente
var existing = auction.BidderStats[bid.Username];
if (bid.Timestamp > existing.LastBidTimestamp)
{
existing.LastBidTime = DateTimeOffset.FromUnixTimeSeconds(bid.Timestamp).DateTime;
existing.LastBidTimestamp = bid.Timestamp;
}
}
```
---
## ? Checklist Implementazione
### Completato
- [x] Model `BidHistoryEntry`
- [x] Aggiunta `RecentBids` a `AuctionInfo`
- [x] Aggiunta `RecentBidsHistory` a `AuctionState`
- [x] Parsing storia in `BidooApiClient.ParseBidHistory()`
- [x] Propagazione in `AuctionMonitor.PollAndProcessAuction()`
- [x] Build compila senza errori
### Da Fare
- [ ] Aggiungere TabControl con nuova tab in XAML
- [ ] Creare `BidHistoryEntries` ObservableCollection in ViewModel
- [ ] Implementare `RefreshBidHistory()` in ViewModel
- [ ] Binding DataGrid a `BidHistoryEntries`
- [ ] Chiamare `RefreshBidHistory()` in `OnAuctionUpdated`
- [ ] Test con aste reali
---
## ?? Prossimi Passi
1. **Modifica XAML**: Aggiungi TabItem "Storia Puntate"
2. **Aggiorna ViewModel**: Aggiungi `BidHistoryEntries` + `RefreshBidHistory()`
3. **Wire Update Event**: Chiama `RefreshBidHistory()` su poll
4. **Test**: Verifica con aste attive
5. **Opzionale**: Limita a ultime N puntate (es. 20)
---
## ?? Note Implementazione
### Performance
- **Storia limitata**: API restituisce solo ultime ~10 puntate
- **Update frequente**: Ogni polling (10ms-1s) aggiorna lista
- **ObservableCollection**: Usa binding WPF per update automatico
### Sincronizzazione
- **Tab Utenti**: Statistiche aggregate (contatori)
- **Tab Storia**: Cronologia temporale (dettaglio)
- **Entrambe aggiornate**: Da stesso polling API
### Edge Cases
- **Asta appena iniziata**: Storia vuota ? mostra messaggio
- **Parsing fallito**: Storia null ? non crasha, tab vuota
- **Username lungo**: Troncato con ellipsis
---
**Data Feature**: 2025
**Versione**: 7.5+
**Status**: ? BACKEND COMPLETO | ? FRONTEND DA IMPLEMENTARE
---
## ?? Conclusione
Il backend è **100% completo e testato**. La storia puntate viene:
1. ? Estratta dall'API
2. ? Parsata correttamente
3. ? Propagata ad `AuctionInfo`
4. ? Aggiornata ad ogni polling
Serve solo:
- Aggiungere tab XAML
- Fare binding dati
- Chiamare refresh UI
**Pronto per frontend!** ??
@@ -1,410 +0,0 @@
# ? Feature: Limiti Log Configurabili dall'Utente
## ?? Obiettivo
Permettere all'utente di **configurare i limiti massimi dei log** tramite l'interfaccia delle impostazioni, invece di usare valori hardcoded nel codice.
---
## ? Implementazione
### 1?? Nuovi Parametri in `AppSettings`
**File**: `Utilities/SettingsManager.cs`
Aggiunte due nuove proprietà:
```csharp
/// <summary>
/// Numero massimo di righe di log da mantenere per ogni singola asta (default: 500)
/// </summary>
public int MaxLogLinesPerAuction { get; set; } = 500;
/// <summary>
/// Numero massimo di righe di log da mantenere nel log globale (default: 1000)
/// </summary>
public int MaxGlobalLogLines { get; set; } = 1000;
```
---
### 2?? Interfaccia Utente - Nuova Sezione
**File**: `Controls/SettingsControl.xaml`
Aggiunta sezione "Limiti Log" con:
- **TextBox** per configurare max righe log per asta
- **TextBox** per configurare max righe log globale
- **Info Box** con spiegazione e valori raccomandati
```xaml
<!-- SEZIONE 4: Limiti Log -->
<Border Background="#252526">
<StackPanel>
<TextBlock Text="Limiti Log" Style="{StaticResource SectionHeader}"/>
<Grid>
<TextBlock Text="Max Righe Log per Asta" />
<TextBox x:Name="MaxLogLinesPerAuctionTextBox" Text="500" />
<TextBlock Text="Max Righe Log Globale" />
<TextBox x:Name="MaxGlobalLogLinesTextBox" Text="1000" />
</Grid>
<Border Style="{StaticResource InfoBox}">
<TextBlock Text="Valori consigliati: 500-1000 per asta, 1000-2000 per log globale."/>
</Border>
</StackPanel>
</Border>
```
---
### 3?? Salvataggio e Caricamento
**File**: `Core/EventHandlers/MainWindow.EventHandlers.Settings.cs`
#### Caricamento Impostazioni
```csharp
private void LoadDefaultSettings()
{
var settings = SettingsManager.Load();
// Carica limiti log
Settings.MaxLogLinesPerAuction.Text = settings.MaxLogLinesPerAuction.ToString();
Settings.MaxGlobalLogLines.Text = settings.MaxGlobalLogLines.ToString();
Log($"[OK] Impostazioni caricate: Log Asta={settings.MaxLogLinesPerAuction}, Log Globale={settings.MaxGlobalLogLines}");
}
```
#### Salvataggio Impostazioni
```csharp
private void SaveDefaultsButton_Click(object sender, RoutedEventArgs e)
{
var settings = SettingsManager.Load();
// Salva limiti log
if (int.TryParse(Settings.MaxLogLinesPerAuction.Text, out var maxLogPerAuction) && maxLogPerAuction > 0)
{
settings.MaxLogLinesPerAuction = maxLogPerAuction;
}
if (int.TryParse(Settings.MaxGlobalLogLines.Text, out var maxGlobalLog) && maxGlobalLog > 0)
{
settings.MaxGlobalLogLines = maxGlobalLog;
}
SettingsManager.Save(settings);
Log($"[OK] Limiti log salvati: Asta={settings.MaxLogLinesPerAuction}, Globale={settings.MaxGlobalLogLines}");
}
```
---
### 4?? Utilizzo dei Parametri
#### Log Globale
**File**: `Core/MainWindow.Logging.cs`
```csharp
private void Log(string message, LogLevel level = LogLevel.Info)
{
// Carica limite dalle impostazioni
var settings = SettingsManager.Load();
int maxLogLines = settings.MaxGlobalLogLines;
// Aggiungi log...
// Rimuovi righe eccedenti
if (LogBox.Document.Blocks.Count > maxLogLines)
{
int excessCount = LogBox.Document.Blocks.Count - maxLogLines;
for (int i = 0; i < excessCount; i++)
{
LogBox.Document.Blocks.Remove(LogBox.Document.Blocks.FirstBlock);
}
}
}
```
#### Log per Asta
**File**: `Models/AuctionInfo.cs`
```csharp
public void AddLog(string message, int maxLines = 500)
{
var entry = $"{DateTime.Now:HH:mm:ss.fff} - {message}";
AuctionLog.Add(entry);
// Mantieni solo gli ultimi maxLines log
if (AuctionLog.Count > maxLines)
{
int excessCount = AuctionLog.Count - maxLines;
AuctionLog.RemoveRange(0, excessCount);
}
}
```
**Nota**: Per il log per asta, viene usato il parametro opzionale `maxLines` con default 500. L'utente può configurare il limite ma richiede un riavvio dell'applicazione per applicarlo.
---
## ?? Interfaccia Utente
### Screenshot Concettuale
```
???????????????????????????????????????????????????
? LIMITI LOG ?
???????????????????????????????????????????????????
? ?
? Configura il numero massimo di righe di log da ?
? mantenere in memoria per ottimizzare le ?
? performance. ?
? ?
? Max Righe Log per Asta: [ 500 ] ?
? Max Righe Log Globale: [ 1000 ] ?
? ?
? ??????????????????????????????????????????????? ?
? ? ?? Informazioni ? ?
? ? ? ?
? ? • I log più vecchi verranno rimossi ? ?
? ? automaticamente ? ?
? ? • Valori più bassi = meno memoria ? ?
? ? • Valori più alti = più storico ? ?
? ? • Raccomandati: 500-1000 asta, 1000-2000 ? ?
? ? globale ? ?
? ??????????????????????????????????????????????? ?
? ?
???????????????????????????????????????????????????
[Salva] [Annulla]
```
---
## ?? Configurazione
| Parametro | Impostazione | Valore Default | Range Raccomandato |
|-----------|--------------|----------------|-------------------|
| **Log per Asta** | `MaxLogLinesPerAuction` | 500 | 500-1000 |
| **Log Globale** | `MaxGlobalLogLines` | 1000 | 1000-2000 |
---
## ?? Workflow Utente
### Modifica Limiti
1. Apri **Impostazioni**
2. Scorri fino a "**Limiti Log**"
3. Modifica i valori:
- **Max Righe Log per Asta**: es. 1000
- **Max Righe Log Globale**: es. 2000
4. Clicca **Salva**
5. ? **Log globale**: applicato immediatamente
6. ?? **Log per asta**: applicato alle nuove righe
### Valori Suggeriti
#### Uso Leggero (< 5 aste)
```
Log per Asta: 300
Log Globale: 500
Memoria: ~100 KB
```
#### Uso Normale (5-15 aste)
```
Log per Asta: 500 ? Default
Log Globale: 1000 ? Default
Memoria: ~200 KB
```
#### Uso Intensivo (15+ aste)
```
Log per Asta: 1000
Log Globale: 2000
Memoria: ~400 KB
```
---
## ?? Persistenza
Le impostazioni vengono salvate in:
```
%LocalAppData%\AutoBidder\settings.json
```
Esempio file:
```json
{
"MaxLogLinesPerAuction": 500,
"MaxGlobalLogLines": 1000,
"DefaultBidBeforeDeadlineMs": 200,
"ExportPath": "C:\\Exports",
...
}
```
---
## ?? Applicazione Modifiche
### Log Globale
- ? **Applicato immediatamente** alla prossima chiamata `Log()`
- Nessun riavvio necessario
### Log per Asta
- ?? **Usato per nuove righe** dopo il salvataggio
- I log esistenti non vengono troncati
- Per applicare a log esistenti: pulisci log manualmente
---
## ?? Come Testare
### Test 1: Modifica Limiti
1. Vai in **Impostazioni**
2. Imposta "Max Righe Log Globale" = **100**
3. Clicca **Salva**
4. Genera 150+ righe di log
5. ? **Verifica**: Log contiene max 100 righe
6. ? **Verifica**: Le righe più vecchie sono state rimosse
### Test 2: Valori Molto Bassi
1. Imposta "Max Righe Log Globale" = **10**
2. Salva
3. Genera 50 righe di log
4. ? **Verifica**: Log contiene esattamente 10 righe
### Test 3: Valori Molto Alti
1. Imposta "Max Righe Log Globale" = **5000**
2. Salva
3. Monitora aste per 1 ora
4. ? **Verifica**: Log cresce fino a 5000 righe e poi si stabilizza
### Test 4: Persistenza
1. Modifica limiti (es. 200/400)
2. Salva
3. Chiudi applicazione
4. Riapri applicazione
5. ? **Verifica**: Valori nelle impostazioni sono 200/400
---
## ?? Log di Debug
Quando salvi le impostazioni, vedi:
```
[OK] Limiti log salvati: Asta=500, Globale=1000
```
Quando carichi le impostazioni:
```
[OK] Impostazioni caricate: Log Asta=500, Log Globale=1000
```
---
## ?? Troubleshooting
### Problema: Modifiche Non Applicate
**Sintomo**: Cambio i valori ma i log continuano ad accumularsi
**Soluzione**:
1. Verifica di aver cliccato **Salva**
2. Controlla il log per conferma salvataggio
3. Per log per asta: genera nuovi log per vedere l'effetto
### Problema: Valori Non Validi
**Sintomo**: Inserisco 0 o valori negativi
**Soluzione**:
- Il codice ignora valori ? 0
- Usa valori > 0 (minimo raccomandato: 100)
### Problema: Troppa Memoria
**Sintomo**: Uso memoria ancora alto
**Soluzione**:
1. Riduci i limiti (es. 300/500)
2. Salva
3. Pulisci log manualmente (pulsante "Pulisci Log")
---
## ?? File Modificati
| File | Modifiche |
|------|-----------|
| `Utilities/SettingsManager.cs` | ? Aggiunte proprietà `MaxLogLinesPerAuction` e `MaxGlobalLogLines` |
| `Controls/SettingsControl.xaml` | ? Aggiunta sezione UI "Limiti Log" |
| `Core/EventHandlers/MainWindow.EventHandlers.Settings.cs` | ?? Salvataggio/caricamento limiti log |
| `Core/MainWindow.Logging.cs` | ?? Usa `settings.MaxGlobalLogLines` invece di costante |
| `Models/AuctionInfo.cs` | ?? Parametro opzionale `maxLines` in `AddLog()` |
---
## ? Checklist Verifica
- [x] Nuove proprietà in `AppSettings`
- [x] Sezione UI "Limiti Log" nelle impostazioni
- [x] Salvataggio limiti funzionante
- [x] Caricamento limiti funzionante
- [x] Log globale usa impostazioni
- [x] Log per asta ha parametro configurabile
- [x] Info box con spiegazione
- [x] Persistenza in `settings.json`
- [x] Valori default ragionevoli (500/1000)
- [x] Build compila senza errori
---
**Data Feature**: 2025-01-23
**Versione**: 4.1+
**Feature**: Limiti log configurabili dall'utente
**Status**: ? IMPLEMENTATA
---
## ?? Riepilogo
### Prima:
- ? Limiti **hardcoded** nel codice
- ? Utente non può modificarli
- ? Serviva ricompilare per cambiare limiti
### Dopo:
- ? Limiti **configurabili** dalle impostazioni
- ? **Interfaccia grafica** semplice
- ? **Valori default** ragionevoli (500/1000)
- ? **Info box** con raccomandazioni
- ? **Persistenza** automatica
- ? **Applicazione immediata** per log globale
### Vantaggi:
```
Flessibilità: Utente controlla limiti ?
Facilità: UI intuitiva ?
Performance: Ottimizzabili al volo ?
Persistenza: Salvato automaticamente ?
```
?? **Utente ha pieno controllo sui limiti log!**
@@ -1,444 +0,0 @@
# ? Sistema Centralizzato di Gestione HTTP - Implementazione Completa
## ?? Obiettivo
Implementare un sistema centralizzato per tutte le richieste HTTP nell'applicazione con:
- **Cache HTML** - Evita richieste duplicate
- **Rate Limiting** - Max 5 richieste/secondo
- **Request Queue** - Max 3 richieste concorrenti
- **Retry automatico** - Max 2 tentativi per richiesta
- **Timeout configurabile** - 15 secondi per richiesta
---
## ??? Architettura
### Nuovo Servizio: `HtmlCacheService`
**File**: `Services/HtmlCacheService.cs`
**Responsabilità**:
1. ? Gestione centralizzata di tutte le richieste HTTP
2. ? Cache in memoria con expiration automatica (5 minuti)
3. ? Rate limiting (5 req/s) per non sovraccaricare il server
4. ? Concorrenza limitata (max 3 richieste parallele)
5. ? Retry automatico con exponential backoff
6. ? Logging dettagliato di tutte le operazioni
---
## ?? Configurazione
### Parametri Ottimizzati
```csharp
_htmlCacheService = new HtmlCacheService(
maxConcurrentRequests: 3, // Max 3 richieste parallele
requestsPerSecond: 5, // Max 5 richieste al secondo
cacheExpiration: TimeSpan.FromMinutes(5), // Cache valida 5 minuti
maxRetries: 2 // Max 2 tentativi per richiesta
);
```
### Timeout HTTP
- **15 secondi** per richiesta (aumentato da 10s)
- **Retry automatico** dopo timeout con delay incrementale
---
## ?? Funzionalità Principali
### 1?? **Cache Intelligente**
```csharp
// Prima richiesta - fetcha da server
var response1 = await _htmlCacheService.GetHtmlAsync(url);
// response1.FromCache = false
// Seconda richiesta entro 5 minuti - usa cache
var response2 = await _htmlCacheService.GetHtmlAsync(url);
// response2.FromCache = true ?
```
**Vantaggi**:
- ? Riduce drasticamente le richieste HTTP
- ? Risposta istantanea per URL già visitati
- ? Risparmio bandwidth
- ? Minor carico sul server Bidoo
### 2?? **Rate Limiting Automatico**
```csharp
// Richiesta 1: Parte immediatamente
await GetHtmlAsync("url1");
// Richiesta 2: Parte dopo 200ms (1/5 secondo)
await GetHtmlAsync("url2");
// Richiesta 3: Parte dopo altri 200ms
await GetHtmlAsync("url3");
```
**Log**:
```
[RATE LIMIT] Delay di 200ms
[HTML FETCH] Success: ...auction.php (12453 chars)
```
### 3?? **Retry Automatico**
```csharp
// Tentativo 1: Timeout
[HTML RETRY] Timeout tentativo 1/2: ...auction.php
// Delay: 1 secondo
// Tentativo 2: Success
[HTML RETRY] Success al tentativo 2: ...auction.php
```
**Exponential Backoff**:
- Tentativo 1: Immediato
- Tentativo 2: Dopo 1 secondo
- Tentativo 3: Dopo 2 secondi (se configurato)
### 4?? **Gestione Concorrenza**
```csharp
// Max 3 richieste parallele tramite SemaphoreSlim
private readonly SemaphoreSlim _rateLimiter;
```
**Scenario**:
- Richiesta 1, 2, 3: Partono immediatamente
- Richiesta 4: Aspetta che una delle prime 3 completi
- Quando 1 finisce ? 4 parte automaticamente
---
## ?? Metodi Modificati
### 1. `FetchAuctionNameInBackgroundAsync()`
**Prima** ?:
```csharp
using var httpClient = new HttpClient();
httpClient.Timeout = TimeSpan.FromSeconds(15);
var html = await httpClient.GetStringAsync(url);
```
**Dopo** ?:
```csharp
var response = await _htmlCacheService.GetHtmlAsync(
auction.OriginalUrl,
RequestPriority.Normal,
bypassCache: false
);
if (response.Success)
{
// Usa response.Html
// response.FromCache indica se era cached
}
```
**Benefici**:
- ? Cache automatica (nomi già recuperati non vengono ri-scaricati)
- ? Rate limiting (non sovraccarica server)
- ? Retry automatico (meno fallimenti)
- ? Logging centralizzato
---
### 2. `LoadProductInfoInBackgroundAsync()`
**Prima** ?:
```csharp
using var httpClient = new HttpClient();
httpClient.Timeout = TimeSpan.FromSeconds(10);
var html = await httpClient.GetStringAsync(auction.OriginalUrl);
```
**Dopo** ?:
```csharp
var response = await _htmlCacheService.GetHtmlAsync(
auction.OriginalUrl,
RequestPriority.High, // ? Priorità alta per info prodotto
bypassCache: false
);
```
**Benefici**:
- ? **Priority High** = ottiene slot prima di richieste normali
- ? Cache = se già scaricato per nome, usa stessa risposta
- ? Logging mostra se usa cache
---
### 3. `AddAuctionFromUrl()`
**Prima** ?:
```csharp
using var httpClient = new HttpClient();
var html = await httpClient.GetStringAsync(url);
```
**Dopo** ?:
```csharp
var response = await _htmlCacheService.GetHtmlAsync(url, RequestPriority.Normal);
if (response.Success)
{
// Estrai nome dal HTML
}
```
---
## ?? Vantaggi dell'Implementazione
### Performance
| Metrica | Prima ? | Dopo ? | Miglioramento |
|---------|---------|---------|---------------|
| **Richieste duplicate** | Tutte eseguite | Cached (0 req) | ?% |
| **Timeout per richiesta** | 10s fisso | 15s + 2 retry | +50% |
| **Richieste/secondo** | Illimitate | Max 5 | Controllato |
| **Richieste concorrenti** | Illimitate | Max 3 | Controllato |
| **Cache hit ratio** | 0% | ~40-60% | Dipende dall'uso |
### Affidabilità
1. ? **Meno errori timeout** - 15s + retry
2. ? **Nessun sovraccarico server** - rate limiting
3. ? **Resilienza** - retry automatico
4. ? **Logging completo** - tracciabilità
### User Experience
1. ? **Nomi caricati più velocemente** - cache
2. ? **Meno "Asta XXXX"** - retry automatico
3. ? **Info prodotto istantanee** - se cached
4. ? **Sistema più responsive** - concorrenza limitata
---
## ?? Logging Dettagliato
### Cache Hit
```
[HTML CACHE] Hit per: ...auction.php?a=asta_83111759
[NAME] Nome recuperato per asta 83111759: 150€ Bidoo Shop + 150 pt (cached)
```
### Nuova Richiesta
```
[RATE LIMIT] Delay di 200ms
[HTML FETCH] Success: ...auction.php?a=asta_83111760 (12453 chars)
```
### Retry per Timeout
```
[HTML RETRY] Timeout tentativo 1/2: ...auction.php?a=asta_83111761
[HTML RETRY] Success al tentativo 2: ...auction.php?a=asta_83111761
```
### Pulizia Cache
```
[HTML CACHE] Pulite 15 entry scadute
```
---
## ?? Scenari d'Uso
### Scenario 1: Aggiunta 12 Aste Simultanee
**Prima** ?:
```
T=0s: 12 richieste HTTP partono tutte insieme
? Server sovraccarico
? 3-4 timeout
? Aste con "Asta XXXX"
```
**Dopo** ?:
```
T=0s: 3 richieste partono (slot disponibili)
T=0.2s: 3 richieste seguenti (rate limit)
T=0.4s: 3 richieste seguenti
T=0.6s: 3 richieste finali
? Tutte completano con successo
? Timeout? ? Retry automatico
? 11/12 nomi recuperati
```
### Scenario 2: Ri-selezione Asta
**Prima** ?:
```
1. Selezioni asta ? Scarica HTML per nome
2. Clicki su altra asta
3. Ri-clicki sulla prima asta ? Ri-scarica HTML per info prodotto
(2 richieste per stessa asta)
```
**Dopo** ?:
```
1. Selezioni asta ? Scarica HTML per nome
2. Clicki su altra asta
3. Ri-clicki sulla prima asta ? USA CACHE per info prodotto ?
[HTML CACHE] Hit per: ...auction.php
[PRODUCT INFO] Valore=18.90€ (cached)
```
### Scenario 3: Aggiunta Aste Duplicate
**Prima** ?:
```
1. Aggiungi asta 83111759 ? Scarica HTML
2. Provi ad aggiungere di nuovo ? Duplicato rilevato
3. Ma HTML già scaricato (spreco bandwidth)
```
**Dopo** ?:
```
1. Aggiungi asta 83111759 ? Scarica HTML + salva in cache
2. Provi ad aggiungere di nuovo ? Duplicato rilevato
3. Se aggiungi altra asta con stesso URL ? USA CACHE ?
```
---
## ?? API Pubblica
### `GetHtmlAsync()`
```csharp
public async Task<HtmlResponse> GetHtmlAsync(
string url,
RequestPriority priority = RequestPriority.Normal,
bool bypassCache = false
)
```
**Parametri**:
- `url`: URL da scaricare
- `priority`: `Low`, `Normal`, `High`, `Critical` (per future implementazioni)
- `bypassCache`: Se `true`, ignora cache e forza download
**Ritorna**: `HtmlResponse`
```csharp
public class HtmlResponse
{
public bool Success { get; set; }
public string Html { get; set; }
public string Error { get; set; }
public bool FromCache { get; set; } // ? Indica se era cached
public string Url { get; set; }
}
```
### `CleanExpiredCache()`
```csharp
public void CleanExpiredCache()
```
**Uso**: Rimuove entry cache scadute (> 5 minuti)
**Chiamato automaticamente**: Ogni 10 minuti via timer
### `ClearCache()`
```csharp
public void ClearCache()
```
**Uso**: Pulisce tutta la cache manualmente
### `GetStats()`
```csharp
public CacheStats GetStats()
```
**Ritorna**: Statistiche cache
```csharp
public class CacheStats
{
public int TotalEntries { get; set; } // Entry in cache
public int AvailableSlots { get; set; } // Slot liberi per richieste
public int MaxConcurrent { get; set; } // Max richieste parallele
}
```
---
## ?? Risultati
### Build Status
```
========== Compilazione: 1 completato/i ==========
? Build Successful
?? Warning non critici (XAML - NumericTextBoxBehavior)
? 0 Errors
```
### Test Scenario
**Aggiunta 12 aste**:
- ? Tutte le richieste gestite dal servizio centralizzato
- ? Rate limiting applicato (200ms delay tra richieste)
- ? 3 richieste parallele massimo
- ? Retry automatico per timeout
- ? 11/12 nomi recuperati (1 timeout anche dopo retry)
- ? Retry automatico dopo 30 secondi recupera l'ultimo
---
## ?? File Modificati
| File | Modifiche |
|------|-----------|
| **Nuovo:** `Services/HtmlCacheService.cs` | ? Servizio completo (400+ righe) |
| `MainWindow.xaml.cs` | ? Aggiunto campo `_htmlCacheService` |
| | ? Inizializzazione nel costruttore |
| | ? Timer pulizia cache automatica |
| `Core/MainWindow.AuctionManagement.cs` | ? `FetchAuctionNameInBackgroundAsync()` usa servizio |
| | ? `LoadProductInfoInBackgroundAsync()` usa servizio |
| | ? `AddAuctionFromUrl()` usa servizio |
| | ? Aggiunto using `AutoBidder.Services` |
---
## ?? Prossimi Passi Consigliati
### 1. Estendi ad Altri Componenti
**File da modificare**:
- `Services/AuctionMonitor.cs` - Polling stato aste
- `Core/MainWindow.UserInfo.cs` - Recupero info utente
- `Services/ClosedAuctionsScraper.cs` - Scraping aste chiuse
### 2. Monitoring & Statistiche
Aggiungi dashboard con:
- Cache hit ratio (es: 45% requests cached)
- Request throughput (es: 3.2 req/s media)
- Average response time
- Retry success rate
### 3. Configurazione Avanzata
Permetti all'utente di configurare:
- Durata cache (default: 5min)
- Max concurrent requests (default: 3)
- Requests per second (default: 5)
- Max retries (default: 2)
---
**Data Implementazione**: 2025
**Versione**: 5.0+
**Status**: ? IMPLEMENTATO E TESTATO
**Benefici**: Riduzione richieste HTTP ~40-60%, maggiore affidabilità, migliore UX
@@ -1,397 +0,0 @@
# ?? Feature: Stato Iniziale Aste Configurabile
## ?? Descrizione
Questa feature permette di configurare lo stato iniziale delle aste in due scenari:
1. **All'apertura dell'applicazione**: decidere se le aste salvate devono essere caricate ferme, in pausa o attive
2. **All'aggiunta di una nuova asta**: decidere se una nuova asta deve essere fermata, in pausa o attiva
## ?? Problema Risolto
Prima di questa feature:
- ? Le aste venivano sempre caricate in stato "fermato"
- ? Le nuove aste venivano sempre aggiunte in stato "fermato"
- ? Era necessario avviare manualmente ogni asta o tutte le aste ogni volta
Dopo questa feature:
- ? Puoi configurare il comportamento predefinito per le aste al caricamento
- ? Puoi configurare il comportamento predefinito per le nuove aste
- ? Puoi avviare automaticamente le aste all'apertura dell'applicazione
- ? Puoi aggiungere nuove aste già attive senza intervento manuale
## ?? Dove Trovare le Impostazioni
1. Apri l'applicazione
2. Vai alla tab **"Impostazioni"**
3. Scorri fino alla sezione **"Stato Iniziale Aste"**
## ?? Opzioni Disponibili
### 1?? Stato Aste al Caricamento dell'Applicazione
Determina come devono essere caricate le aste salvate quando apri l'applicazione.
| Opzione | Comportamento | Quando Usare |
|---------|--------------|--------------|
| **Fermata** | Le aste vengono caricate ma non monitorate fino all'avvio manuale | Default sicuro - decidi tu quali avviare |
| **In Pausa** | Le aste sono caricate e pronte, ma non puntano automaticamente | Prepara le aste senza avviarle subito |
| **Attiva** | Le aste vengono monitorate e puntano automaticamente | Avvio automatico - uso avanzato |
### 2?? Stato Iniziale di una Nuova Asta Aggiunta
Determina lo stato di una nuova asta quando la aggiungi tramite "Aggiungi Asta".
| Opzione | Comportamento | Quando Usare |
|---------|--------------|--------------|
| **Fermata** | La nuova asta viene aggiunta ma non monitorata | Default sicuro - controlli tu quando avviarla |
| **In Pausa** | La nuova asta è pronta ma non punta automaticamente | Prepara la configurazione prima di attivare |
| **Attiva** | La nuova asta viene monitorata e punta automaticamente | Aggiunta rapida - parte subito |
## ?? Stati delle Aste Spiegati
### ?? Fermata (Stopped)
- **IsActive = false**
- **IsPaused = false**
- L'asta **non viene monitorata**
- Il timer non viene aggiornato
- Non vengono effettuate puntate
- Pulsante "Avvia" abilitato
### ?? In Pausa (Paused)
- **IsActive = true**
- **IsPaused = true**
- L'asta **viene monitorata** (timer aggiornato)
- Le informazioni vengono scaricate
- **Non vengono effettuate puntate automatiche**
- Utile per osservare senza puntare
- Pulsante "Riprendi" abilitato
### ?? Attiva (Active)
- **IsActive = true**
- **IsPaused = false**
- L'asta viene **completamente monitorata**
- Le informazioni vengono scaricate
- **Vengono effettuate puntate automatiche**
- Pulsante "Pausa" abilitato
## ?? Comportamento Auto-Start/Auto-Stop
### Auto-Start del Monitoraggio
Il monitoraggio (`AuctionMonitor`) viene avviato automaticamente quando:
1. **Caricamento aste con stato "Active"**
```
[AUTO-START] Monitoraggio avviato automaticamente per 3 aste caricate in stato attivo
```
2. **Aggiunta nuova asta con stato "Active"**
```
[AUTO-START] Monitoraggio avviato automaticamente per nuova asta attiva: Asta 12345
```
### Auto-Stop del Monitoraggio
Il monitoraggio viene fermato automaticamente quando:
- Non ci sono più aste attive (tutte fermate)
- L'ultima asta attiva viene fermata manualmente
```
[AUTO-STOP] Monitoraggio fermato: nessuna asta attiva
```
## ?? Scenari d'Uso
### ?? Scenario 1: Uso Controllato (Consigliato)
**Configurazione:**
- Caricamento: **Fermata**
- Nuova asta: **Fermata**
**Vantaggi:**
- ? Massimo controllo
- ? Decidi tu quando avviare ogni asta
- ? Eviti avvii accidentali
- ? Ideale per principianti
**Workflow:**
1. Apri l'applicazione ? tutte le aste ferme
2. Aggiungi una nuova asta ? fermata
3. Configuri prezzo min/max, clicks
4. Avvii manualmente solo le aste che vuoi
---
### ?? Scenario 2: Preparazione Rapida
**Configurazione:**
- Caricamento: **In Pausa**
- Nuova asta: **In Pausa**
**Vantaggi:**
- ? Le aste sono pronte ma non puntano
- ? Puoi osservare i timer e le informazioni
- ? Configuri con calma prima di attivare
- ? Utile per monitorare senza puntare
**Workflow:**
1. Apri l'applicazione ? tutte le aste in pausa
2. Timer e info aggiornate
3. Configuri prezzo min/max
4. Riprendi solo le aste che vuoi far puntare
---
### ?? Scenario 3: Avvio Automatico (Avanzato)
**Configurazione:**
- Caricamento: **Attiva**
- Nuova asta: **Attiva**
**Vantaggi:**
- ? Zero intervento manuale
- ? Le aste partono automaticamente
- ? Ideale per aste ben configurate
- ? Massima automazione
**Attenzione:**
- ?? Assicurati che tutte le aste abbiano configurazioni corrette (prezzo min/max, clicks)
- ?? Le puntate inizieranno immediatamente all'apertura
- ?? Usa solo se hai esperienza
**Workflow:**
1. Apri l'applicazione ? tutte le aste partono
2. Aggiungi nuova asta ? parte subito
3. Monitoraggio completamente automatico
---
### ?? Scenario 4: Mix Personalizzato
**Configurazione:**
- Caricamento: **Fermata**
- Nuova asta: **Attiva**
**Vantaggi:**
- ? Aste esistenti controllate manualmente
- ? Nuove aste partono subito
- ? Flessibilità massima
**Quando usarlo:**
- Hai già aste configurate che vuoi controllare
- Aggiungi rapidamente nuove aste che devono partire subito
---
## ?? Implementazione Tecnica
### ?? File Modificati
1. **`Utilities\SettingsManager.cs`**
- Aggiunte proprietà `DefaultStartAuctionsOnLoad` e `DefaultNewAuctionState`
- Default: `"Stopped"` per entrambe
2. **`Controls\SettingsControl.xaml`**
- Aggiunta nuova sezione "Stato Iniziale Aste"
- 6 RadioButton per le due configurazioni
- Info box con spiegazioni
3. **`Core\EventHandlers\MainWindow.EventHandlers.Settings.cs`**
- Metodo `LoadDefaultSettings()` carica gli stati dai settings
- Metodo `SaveDefaultsButton_Click()` salva gli stati selezionati
4. **`Core\MainWindow.AuctionManagement.cs`**
- `LoadSavedAuctions()` applica lo stato configurato alle aste caricate
- `AddAuctionById()` applica lo stato configurato alle nuove aste
- `AddAuctionFromUrl()` applica lo stato configurato alle nuove aste
- Auto-start del monitoraggio quando necessario
### ?? Flusso Logico
#### Caricamento Aste
```csharp
var settings = SettingsManager.Load();
var loadState = settings.DefaultStartAuctionsOnLoad; // "Active", "Paused", "Stopped"
foreach (var auction in auctions)
{
switch (loadState)
{
case "Active":
auction.IsActive = true;
auction.IsPaused = false;
break;
case "Paused":
auction.IsActive = true;
auction.IsPaused = true;
break;
case "Stopped":
default:
auction.IsActive = false;
auction.IsPaused = false;
break;
}
}
// Se loadState == "Active", avvia monitoraggio
if (loadState == "Active" && auctions.Count > 0)
{
_auctionMonitor.Start();
_isAutomationActive = true;
}
```
#### Aggiunta Nuova Asta
```csharp
var settings = SettingsManager.Load();
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;
}
// Crea asta con stato configurato
var auction = new AuctionInfo
{
IsActive = isActive,
IsPaused = isPaused,
// ... altre proprietà
};
// Se Active, avvia monitoraggio se non già attivo
if (isActive && !isPaused && !_isAutomationActive)
{
_auctionMonitor.Start();
_isAutomationActive = true;
}
```
## ?? Logging
### Caricamento Aste
```
[LOAD] 5 aste caricate con stato iniziale: Active
[AUTO-START] Monitoraggio avviato automaticamente per 5 aste caricate in stato attivo
```
### Aggiunta Nuova Asta
```
[ADD] Asta aggiunta con stato=Active, Anticipo=200ms
[AUTO-START] Monitoraggio avviato automaticamente per nuova asta attiva: Asta 12345
```
### Salvataggio Impostazioni
```
[OK] Impostazioni salvate: Anticipo=200ms, MinPrice=€0.00, MaxPrice=€0.00, MaxClicks=0, LogAsta=500, LogGlobale=1000, LoadState=Active, NewState=Stopped
```
## ?? Note Importanti
### 1. Compatibilità con Aste Esistenti
- ? Le impostazioni vengono applicate **solo al caricamento**
- ? Non modificano lo stato delle aste già in memoria
- ? Riavvia l'applicazione per applicare le nuove impostazioni al caricamento
### 2. Persistenza degli Stati
- ? Lo stato attuale delle aste **non viene salvato** tra sessioni
- ? All'apertura, tutte le aste prendono lo stato configurato
- ?? Se vuoi che alcune aste siano sempre attive, usa "Active" come stato al caricamento
### 3. Sicurezza
- ?? Con "Active" al caricamento, le puntate iniziano **immediatamente**
- ?? Assicurati che **tutte le aste** abbiano configurazioni corrette
- ?? Controlla il saldo puntate prima di usare "Active"
### 4. Monitoraggio Automatico
- ? Il monitoraggio si avvia/ferma automaticamente quando necessario
- ? Non serve cliccare "Avvia Tutti" se aggiungi un'asta in stato "Active"
- ? Il monitoraggio si ferma quando non ci sono più aste attive
## ?? Test di Verifica
- [x] Caricamento aste con stato "Stopped" ? tutte ferme
- [x] Caricamento aste con stato "Paused" ? tutte in pausa
- [x] Caricamento aste con stato "Active" ? tutte attive + monitoraggio avviato
- [x] Aggiunta asta con stato "Stopped" ? fermata
- [x] Aggiunta asta con stato "Paused" ? in pausa
- [x] Aggiunta asta con stato "Active" ? attiva + monitoraggio avviato se necessario
- [x] Salvataggio impostazioni ? persiste tra riavvii
- [x] Logging corretto per tutti gli scenari
- [x] Auto-start del monitoraggio quando necessario
- [x] Pulsanti globali aggiornati correttamente
## ?? Esempio Completo
### Setup Iniziale
1. Vai su **Impostazioni** ? **Stato Iniziale Aste**
2. Imposta:
- Caricamento: **Fermata**
- Nuova asta: **Attiva**
3. Clicca **Salva**
### Uso
1. **Riavvia l'applicazione**
- Log: `[LOAD] 3 aste caricate con stato iniziale: Stopped`
- Tutte le aste esistenti sono ferme
2. **Aggiungi una nuova asta** (es. asta_12345)
- Log: `[ADD] Asta aggiunta con stato=Active, Anticipo=200ms`
- Log: `[AUTO-START] Monitoraggio avviato automaticamente per nuova asta attiva: Asta 12345`
- La nuova asta parte subito
- Il monitoraggio è attivo
3. **Avvia manualmente le aste esistenti**
- Clicca "Avvia" su ogni asta che vuoi monitorare
- Oppure clicca "Avvia Tutti"
## ?? Best Practices
### ? Raccomandazioni
1. **Per principianti:**
- Usa sempre "Fermata" per entrambe le opzioni
- Configura bene ogni asta prima di avviarla
- Avvia manualmente solo quando sei pronto
2. **Per utenti intermedi:**
- Usa "In Pausa" per preparare le aste
- Osserva i timer prima di attivare
- Riprendi manualmente quando decidi
3. **Per utenti avanzati:**
- Usa "Active" solo se tutte le aste sono ben configurate
- Controlla sempre i log all'avvio
- Verifica il saldo puntate prima di aprire l'app
### ? Errori da Evitare
1. ? **Non** usare "Active" al caricamento se hai aste non configurate
2. ? **Non** dimenticare di configurare prezzo min/max prima di usare "Active"
3. ? **Non** usare "Active" per nuove aste se vuoi prima verificare le info
4. ? **Non** confondere "In Pausa" con "Fermata" (pausa comunque monitora)
---
**Data Implementazione**: 2025
**Versione**: 5.0+
**Status**: ? IMPLEMENTATO
**Compatibilità**: Tutte le versioni successive
## ?? Riferimenti
- Vedi anche: `Documentation\FIX_SINGLE_AUCTION_START.md` per auto-start/stop del monitoraggio
- Vedi anche: `Documentation\FIX_DEFAULT_SETTINGS_PERSISTENCE.md` per impostazioni predefinite
@@ -1,368 +0,0 @@
# ? Feature: Limite Massimo Righe Log
## ?? Obiettivo
Prevenire l'**accumulo eccessivo di log in memoria** impostando limiti massimi per:
1. **Log per singola asta** (ogni asta ha il suo log separato)
2. **Log globale** (log principale dell'applicazione)
Senza questi limiti, durante sessioni lunghe di monitoraggio la memoria potrebbe crescere indefinitamente e causare rallentamenti o crash.
---
## ?? Problema Prima delle Modifiche
### Log per Asta
- ? **Aveva già** un limite di 500 righe
- ? Usava `RemoveAt(0)` singolarmente invece di `RemoveRange()` (inefficiente)
### Log Globale
- ? **Nessun limite** - accumulava log indefinitamente
- ? Memoria cresceva continuamente durante sessioni lunghe
- ? Potenziali rallentamenti dopo ore di utilizzo
---
## ? Soluzione Implementata
### 1?? Log per Asta - Ottimizzato
**File**: `Models/AuctionInfo.cs`
**Modifiche**:
- ? Aggiunta costante `MAX_LOG_LINES = 500`
- ? Ottimizzato per rimuovere più righe in blocco con `RemoveRange()`
- ? Commento esplicativo per chiarezza
```csharp
/// <summary>
/// Numero massimo di righe di log da mantenere per ogni asta
/// </summary>
private const int MAX_LOG_LINES = 500;
/// <summary>
/// Aggiunge una voce al log dell'asta con limite automatico di righe
/// </summary>
public void AddLog(string message)
{
var entry = $"{DateTime.Now:HH:mm:ss.fff} - {message}";
AuctionLog.Add(entry);
// Mantieni solo gli ultimi MAX_LOG_LINES log
if (AuctionLog.Count > MAX_LOG_LINES)
{
// Rimuovi i log più vecchi per mantenere la dimensione sotto controllo
int excessCount = AuctionLog.Count - MAX_LOG_LINES;
AuctionLog.RemoveRange(0, excessCount);
}
}
```
**Vantaggi**:
- ? **Performance**: `RemoveRange()` è più efficiente di cicli `RemoveAt()`
- ? **Costante**: Facile modificare il limite in futuro
- ? **Documentazione**: Commenti esplicativi
---
### 2?? Log Globale - Nuovo Limite
**File**: `Core/MainWindow.Logging.cs`
**Modifiche**:
- ? Aggiunta costante `MAX_GLOBAL_LOG_PARAGRAPHS = 1000`
- ? Rimozione automatica dei paragrafi più vecchi quando si supera il limite
- ? Ottimizzato per non rallentare la UI
```csharp
/// <summary>
/// Numero massimo di paragrafi (righe) nel log globale prima di rimuovere i più vecchi
/// </summary>
private const int MAX_GLOBAL_LOG_PARAGRAPHS = 1000;
private void Log(string message, LogLevel level = LogLevel.Info)
{
Dispatcher.BeginInvoke(() =>
{
try
{
// ... creazione paragraph ...
LogBox.Document.Blocks.Add(p);
// ? NUOVO: Mantieni solo gli ultimi MAX_GLOBAL_LOG_PARAGRAPHS paragrafi
if (LogBox.Document.Blocks.Count > MAX_GLOBAL_LOG_PARAGRAPHS)
{
// Rimuovi i paragrafi più vecchi (primi inseriti)
int excessCount = LogBox.Document.Blocks.Count - MAX_GLOBAL_LOG_PARAGRAPHS;
for (int i = 0; i < excessCount; i++)
{
if (LogBox.Document.Blocks.FirstBlock != null)
{
LogBox.Document.Blocks.Remove(LogBox.Document.Blocks.FirstBlock);
}
}
}
// ... auto-scroll ...
}
catch { }
});
}
```
**Vantaggi**:
- ? **Memoria controllata**: Max 1000 righe nel log globale
- ? **FIFO (First In First Out)**: Rimuove i log più vecchi
- ? **Trasparente**: L'utente non si accorge della rimozione (avviene in background)
---
## ?? Limiti Configurati
| Tipo Log | Limite Righe | File | Costante |
|----------|--------------|------|----------|
| **Log Asta** | 500 | `Models/AuctionInfo.cs` | `MAX_LOG_LINES` |
| **Log Globale** | 1000 | `Core/MainWindow.Logging.cs` | `MAX_GLOBAL_LOG_PARAGRAPHS` |
---
## ?? Comportamento
### Scenario 1: Log Asta Supera 500 Righe
**Situazione**:
- Asta monitorata per ore
- Log asta arriva a 520 righe
**Comportamento**:
```
Prima: [01:00:00] Log riga 1
[01:00:01] Log riga 2
...
[05:00:00] Log riga 520
Dopo AddLog():
[01:00:21] Log riga 21 ? I primi 20 log vengono rimossi
[01:00:22] Log riga 22
...
[05:00:00] Log riga 520
Righe mantenute: 500 (ultimi)
```
? **Log più vecchi rimossi automaticamente**
---
### Scenario 2: Log Globale Supera 1000 Righe
**Situazione**:
- Applicazione in uso per diverse ore
- Log globale arriva a 1050 paragrafi
**Comportamento**:
```
Prima: [01:00:00] [INFO] Applicazione avviata
[01:00:01] [OK] Asta aggiunta
...
[06:00:00] [SUCCESS] Puntata riuscita (riga 1050)
Dopo nuovo log:
[01:00:51] [OK] Asta aggiunta ? I primi 50 paragrafi rimossi
[01:00:52] [INFO] Polling avviato
...
[06:00:00] [SUCCESS] Puntata riuscita
[06:00:01] [INFO] Nuovo log ? Aggiunto
Paragrafi mantenuti: 1000 (ultimi)
```
? **Paragrafi più vecchi rimossi automaticamente**
---
## ?? Risparmio Memoria
### Prima delle Modifiche
**Sessione 8 ore**:
- **Log Asta**: ~500 righe/asta (già limitato)
- **Log Globale**: ~10,000+ righe (NESSUN LIMITE ?)
- **Memoria occupata**: ~2-5 MB per il solo log globale
- **Rallentamenti**: Possibili dopo diverse ore
### Dopo le Modifiche
**Sessione 8 ore**:
- **Log Asta**: ~500 righe/asta (ottimizzato ?)
- **Log Globale**: MAX 1000 righe (NUOVO LIMITE ?)
- **Memoria occupata**: ~200 KB per log globale
- **Rallentamenti**: ELIMINATI ?
**Risparmio memoria**: **~90%** sul log globale
---
## ?? Come Modificare i Limiti
Se in futuro vuoi cambiare i limiti, modifica le costanti:
### Log per Asta
```csharp
// File: Models/AuctionInfo.cs
// Cambia questo valore:
private const int MAX_LOG_LINES = 500; // ? es. 1000 per più log
```
### Log Globale
```csharp
// File: Core/MainWindow.Logging.cs
// Cambia questo valore:
private const int MAX_GLOBAL_LOG_PARAGRAPHS = 1000; // ? es. 2000 per più log
```
**Raccomandazioni**:
- ? **Log Asta**: 500-1000 righe (sufficiente per debugging)
- ? **Log Globale**: 1000-2000 righe (bilanciamento memoria/utilità)
- ?? **Non esagerare**: Valori troppo alti annullano il beneficio
---
## ?? Come Testare
### Test 1: Log Asta Raggiunge Limite
1. Aggiungi un'asta
2. Avvia monitoraggio
3. Aspetta che vengano generati >500 log
4. **Verifica**: Controlla che il log dell'asta non superi 500 righe
5. **Verifica**: I log più vecchi vengono rimossi automaticamente
### Test 2: Log Globale Raggiunge Limite
1. Avvia applicazione
2. Genera molti log (aggiungi/rimuovi aste, avvia/ferma, ecc.)
3. Quando arrivi a ~1000+ righe nel log globale
4. **Verifica**: Il log non cresce oltre 1000 paragrafi
5. **Verifica**: I paragrafi più vecchi vengono rimossi
### Test 3: Performance Durante Sessione Lunga
1. Avvia applicazione
2. Monitora 5-10 aste per 4-8 ore
3. **Verifica**: Nessun rallentamento visibile
4. **Verifica**: Uso memoria stabile (non cresce indefinitamente)
### Test 4: Log Dopo Pulizia Manuale
1. Genera 1000+ righe nel log globale
2. Clicca "Pulisci Log Globale"
3. **Verifica**: Log pulito correttamente
4. Genera nuovi log
5. **Verifica**: Limite si applica di nuovo correttamente
---
## ?? Log di Debug
Non ci sono log specifici per la rimozione automatica (avviene in modo trasparente).
Puoi verificare che funzioni:
- **Log Asta**: Controlla `AuctionLog.Count` in debug
- **Log Globale**: Controlla `LogBox.Document.Blocks.Count` in debug
---
## ?? Troubleshooting
### Problema: Log Troppo Corti
**Sintomo**: I log vengono eliminati troppo velocemente
**Soluzione**: Aumenta le costanti:
```csharp
// Log Asta
private const int MAX_LOG_LINES = 1000; // Da 500 a 1000
// Log Globale
private const int MAX_GLOBAL_LOG_PARAGRAPHS = 2000; // Da 1000 a 2000
```
### Problema: Memoria Ancora Alta
**Sintomo**: Uso memoria elevato anche con limiti
**Causa**: Potrebbero essere altre strutture dati (BidHistory, BidderStats, ecc.)
**Soluzione**: Implementare limiti anche per:
- `BidHistory` (storico puntate)
- `BidderStats` (statistiche utenti)
---
## ?? File Modificati
| File | Modifiche |
|------|-----------|
| `Models/AuctionInfo.cs` | ? Aggiunta costante `MAX_LOG_LINES` |
| `Models/AuctionInfo.cs` | ?? Ottimizzato `AddLog()` con `RemoveRange()` |
| `Core/MainWindow.Logging.cs` | ? Aggiunta costante `MAX_GLOBAL_LOG_PARAGRAPHS` |
| `Core/MainWindow.Logging.cs` | ?? Limite automatico nel metodo `Log()` |
---
## ? Checklist Verifica
- [x] Costante `MAX_LOG_LINES = 500` in `AuctionInfo`
- [x] Costante `MAX_GLOBAL_LOG_PARAGRAPHS = 1000` in `MainWindow.Logging`
- [x] `RemoveRange()` usato invece di loop `RemoveAt()`
- [x] Log asta limitato a 500 righe
- [x] Log globale limitato a 1000 paragrafi
- [x] Rimozione automatica dei log più vecchi (FIFO)
- [x] Nessun rallentamento durante rimozione
- [x] Build compila senza errori
- [x] Codice documentato con commenti
---
**Data Feature**: 2025-01-23
**Versione**: 4.1+
**Feature**: Limite massimo righe log
**Status**: ? IMPLEMENTATA
---
## ?? Riepilogo
### Prima:
- ? Log globale **senza limite** (crescita indefinita)
- ?? Log asta con limite ma codice inefficiente
- ? Potenziali problemi di memoria/performance
### Dopo:
- ? **Log asta**: MAX 500 righe (ottimizzato)
- ? **Log globale**: MAX 1000 righe (NUOVO)
- ? **Rimozione automatica** log più vecchi (FIFO)
- ? **Memoria controllata** (~90% risparmio)
- ? **Performance stabili** anche dopo ore di utilizzo
- ? **Facile configurazione** tramite costanti
### Benefici:
```
Memoria Log Globale:
Prima: [????????????????????] 5 MB (dopo 8h)
Dopo: [???] 200 KB (sempre)
Risparmio: ~96% ??
```
### Limiti Configurati:
```
?? Log Asta: 500 righe per asta
?? Log Globale: 1000 righe totali
```
?? **Memoria ottimizzata e performance garantite!**
@@ -1,469 +0,0 @@
# ?? Feature: Limite Minimo Puntate Residue
## ?? Descrizione
Aggiunge un'opzione per **impedire che il numero di puntate dell'account scenda sotto una soglia minima** configurabile dall'utente, con indicatore visivo nella schermata principale.
---
## ? Implementazione
### 1?? Impostazione in AppSettings
**File**: `Utilities\SettingsManager.cs` ? **GIÀ IMPLEMENTATO**
```csharp
/// <summary>
/// Numero minimo di puntate residue da mantenere sull'account.
/// Se impostato > 0, il sistema non punterà se le puntate residue scenderebbero sotto questa soglia.
/// Default: 0 (nessun limite)
/// </summary>
public int MinimumRemainingBids { get; set; } = 0;
```
---
### 2?? UI nelle Impostazioni
**File**: `Controls\SettingsControl.xaml`
**Posizione**: Dopo "Max Righe Log Globale" nella SEZIONE 5: Limiti Log
```xaml
<!-- Dopo MaxGlobalLogLinesTextBox, riga 383 -->
<TextBlock Grid.Row="2" Grid.Column="0"
Text="Puntate Minime da Mantenere"
Foreground="#CCCCCC"
Margin="0,10"
VerticalAlignment="Center"
ToolTip="Numero minimo di puntate residue da mantenere sull'account. Se impostato > 0, non punterà se scende sotto questa soglia (0 = nessun limite)"/>
<TextBox Grid.Row="2" Grid.Column="1"
x:Name="MinimumRemainingBidsTextBox"
Text="0"
Margin="10,10"/>
```
**Modifiche necessarie**:
1. Cambiare Grid.RowDefinitions da 2 a 3 righe
2. Aggiungere la terza riga (TextBlock + TextBox)
**Layout finale Sezione Limiti Log**:
```
Max Righe Log per Asta: [500]
Max Righe Log Globale: [1000]
Puntate Minime da Mantenere: [0] ? NUOVO
```
---
### 3?? Banner Principale - Indicatore Visivo
**File**: `Controls\AuctionMonitorControl.xaml`
**Posizione**: Nel banner puntate residue (riga ~80-90)
**Prima**:
```xaml
<TextBlock Text="Puntate:" ... />
<TextBlock x:Name="RemainingBidsText" Text="0" ... />
```
**Dopo**:
```xaml
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="Puntate:" ... />
<TextBlock x:Name="RemainingBidsText" Text="0" ... />
<!-- ? NUOVO: Indicatore limite attivo -->
<TextBlock x:Name="MinBidsLimitIndicator"
Text="???"
FontSize="16"
Margin="8,0,0,0"
VerticalAlignment="Center"
Visibility="Collapsed"
ToolTip="Limite minimo puntate attivo: non scenderà sotto X puntate"/>
</StackPanel>
```
**Caratteristiche indicatore**:
- ??? Emoji scudo per indicare "protezione"
- Visibile solo quando `MinimumRemainingBids > 0`
- Tooltip dinamico: "Limite minimo puntate attivo: non scenderà sotto X puntate"
- Colore: Verde (#00D800) quando sopra il limite
---
### 4?? Salvataggio/Caricamento Impostazione
**File**: `Core\EventHandlers\MainWindow.EventHandlers.Settings.cs`
#### Caricamento
```csharp
private void LoadDefaultSettings()
{
var settings = SettingsManager.Load();
// ...existing code...
// ? NUOVO: Carica limite minimo puntate
Settings.MinimumRemainingBidsTextBox.Text = settings.MinimumRemainingBids.ToString();
// Aggiorna indicatore visivo
UpdateMinBidsIndicator(settings.MinimumRemainingBids);
}
```
#### Salvataggio
```csharp
private void SaveDefaultsButton_Click(object sender, RoutedEventArgs e)
{
var settings = SettingsManager.Load() ?? new AppSettings();
// ...existing code...
// ? NUOVO: Salva limite minimo puntate
if (int.TryParse(Settings.MinimumRemainingBidsTextBox.Text, out var minBids) && minBids >= 0)
{
settings.MinimumRemainingBids = minBids;
// Aggiorna indicatore visivo
UpdateMinBidsIndicator(minBids);
if (minBids > 0)
{
Log($"[LIMIT] Impostato limite minimo puntate: {minBids}", LogLevel.Info);
}
}
SettingsManager.Save(settings);
}
```
---
### 5?? Logica di Controllo - ShouldBid
**File**: `Services\AuctionMonitor.cs`
**Metodo**: `ShouldBid(AuctionInfo auction, AuctionState state)`
```csharp
private bool ShouldBid(AuctionInfo auction, AuctionState state)
{
// ? NUOVO: Controllo limite minimo puntate residue
var settings = Utilities.SettingsManager.Load();
if (settings.MinimumRemainingBids > 0)
{
// Ottieni puntate residue dalla sessione
var session = _apiClient.GetSession();
if (session != null && session.RemainingBids <= settings.MinimumRemainingBids)
{
auction.AddLog($"[LIMIT] Puntata bloccata: puntate residue ({session.RemainingBids}) al limite minimo ({settings.MinimumRemainingBids})");
return false;
}
}
// ? NUOVO: Non puntare se sono già il vincitore corrente
if (state.IsMyBid)
{
return false;
}
// ...existing checks...
}
```
---
### 6?? Aggiornamento Indicatore Visivo
**File**: `Core\MainWindow.UserInfo.cs`
**Nuovo metodo**:
```csharp
/// <summary>
/// Aggiorna l'indicatore del limite minimo puntate nel banner
/// </summary>
private void UpdateMinBidsIndicator(int minBidsLimit)
{
try
{
if (minBidsLimit > 0)
{
// Mostra indicatore
AuctionMonitor.MinBidsLimitIndicator.Visibility = Visibility.Visible;
AuctionMonitor.MinBidsLimitIndicator.ToolTip = $"Limite minimo puntate attivo: non scenderà sotto {minBidsLimit} puntate";
// Colore basato su puntate residue
var session = _sessionService?.GetCurrentSession();
if (session != null && session.RemainingBids <= minBidsLimit + 10)
{
// Vicino al limite - Giallo avviso
AuctionMonitor.MinBidsLimitIndicator.Foreground = new SolidColorBrush(Color.FromRgb(255, 193, 7));
}
else
{
// Sopra il limite - Verde
AuctionMonitor.MinBidsLimitIndicator.Foreground = new SolidColorBrush(Color.FromRgb(0, 216, 0));
}
}
else
{
// Nascondi indicatore
AuctionMonitor.MinBidsLimitIndicator.Visibility = Visibility.Collapsed;
}
}
catch { }
}
```
**Chiamare questo metodo**:
1. In `LoadSavedSession()` dopo aver caricato la sessione
2. In `SetUserBanner()` quando aggiorna le puntate residue
3. In `SaveDefaultsButton_Click()` dopo aver salvato il limite
---
## ?? UI Mockup
### Banner Principale
```
???????????????????????????????????????????????????????????????????
? ?? AutoBidder Puntate: 50 ??? EUR 15.00 ?
???????????????????????????????????????????????????????????????????
?
Indicatore limite attivo
```
**Stati indicatore**:
- **Nascosto**: Quando `MinimumRemainingBids = 0` (nessun limite)
- **Verde ???**: Quando `RemainingBids > MinimumRemainingBids + 10`
- **Giallo ??**: Quando `RemainingBids <= MinimumRemainingBids + 10` (vicino al limite)
- **Rosso ??**: Quando `RemainingBids <= MinimumRemainingBids` (al limite, non punterà)
### Impostazioni - Sezione Limiti Log
```
???????????????????????????????????????????????????????
? ?? Limiti Log ?
? ?
? Max Righe Log per Asta: [500 ] ?
? Max Righe Log Globale: [1000 ] ?
? Puntate Minime da Mantenere: [10 ] ? NUOVO?
? ?
? ?? Informazioni ?
? • Se impostato > 0, il sistema non punterà ?
? se le puntate residue scendono sotto questa ?
? soglia. ?
? • Usa questa opzione per mantenere sempre ?
? un "cuscinetto" di puntate sull'account. ?
? • Valore 0 = nessun limite (comportamento default)?
???????????????????????????????????????????????????????
```
---
## ?? Scenari d'Uso
### Scenario 1: Nessun Limite (Default)
**Config**:
- `MinimumRemainingBids = 0`
**Comportamento**:
- ? Sistema punta normalmente
- ? Nessun indicatore visibile
- ? Può usare tutte le puntate disponibili
---
### Scenario 2: Limite Conservativo
**Config**:
- `MinimumRemainingBids = 20`
- `RemainingBids = 50`
**Comportamento**:
- ? Sistema punta normalmente (50 > 20)
- ? Indicatore verde ??? visibile
- ? Può scendere fino a 21 puntate
**Log**:
```
[OK] Click su Asta 12345: 150ms
...
[LIMIT] Puntata bloccata: puntate residue (20) al limite minimo (20)
```
---
### Scenario 3: Vicino al Limite
**Config**:
- `MinimumRemainingBids = 20`
- `RemainingBids = 25`
**Comportamento**:
- ? Sistema punta normalmente (25 > 20)
- ?? Indicatore giallo ?? visibile
- ? Può scendere fino a 21 puntate
- ?? Avviso visivo che si sta avvicinando al limite
---
### Scenario 4: Al Limite
**Config**:
- `MinimumRemainingBids = 20`
- `RemainingBids = 20`
**Comportamento**:
- ? Sistema NON punta più
- ?? Indicatore rosso ?? visibile
- ? Tutte le puntate bloccate
**Log**:
```
[LIMIT] Puntata bloccata: puntate residue (20) al limite minimo (20)
[LIMIT] Puntata bloccata: puntate residue (20) al limite minimo (20)
...
```
---
## ?? Modifiche File - Riepilogo
### File da Modificare
1. **`Utilities\SettingsManager.cs`** ? GIÀ FATTO
- Aggiunto campo `MinimumRemainingBids`
2. **`Controls\SettingsControl.xaml`** ?? TODO
- Aggiungere TextBox "Puntate Minime da Mantenere"
- Modificare Grid.RowDefinitions da 2 a 3 righe
3. **`Controls\AuctionMonitorControl.xaml`** ?? TODO
- Aggiungere TextBlock `MinBidsLimitIndicator` nel banner
4. **`Core\EventHandlers\MainWindow.EventHandlers.Settings.cs`** ?? TODO
- Aggiungere caricamento/salvataggio `MinimumRemainingBids`
5. **`Core\MainWindow.UserInfo.cs`** ?? TODO
- Aggiungere metodo `UpdateMinBidsIndicator()`
- Chiamare nei punti appropriati
6. **`Services\AuctionMonitor.cs`** ?? TODO
- Aggiungere controllo in `ShouldBid()`
---
## ?? Test di Verifica
### Test 1: Impostazione Limite ?
**Steps**:
1. Vai su Impostazioni
2. Imposta "Puntate Minime da Mantenere" = 20
3. Clicca "Salva"
4. Verifica log: `[LIMIT] Impostato limite minimo puntate: 20`
5. Verifica indicatore ??? appare nel banner
**Risultato atteso**: ? Limite salvato e indicatore visibile
---
### Test 2: Blocco Puntata al Limite ?
**Steps**:
1. Imposta limite = 20
2. Simula puntate residue = 20
3. Avvia monitoraggio
4. Verifica log: `[LIMIT] Puntata bloccata: puntate residue (20) al limite minimo (20)`
5. Verifica indicatore ?? rosso
**Risultato atteso**: ? Nessuna puntata eseguita
---
### Test 3: Puntata Permessa Sopra Limite ?
**Steps**:
1. Imposta limite = 20
2. Puntate residue = 50
3. Avvia monitoraggio
4. Verifica puntata eseguita: `[OK] Click su Asta...`
5. Verifica indicatore ??? verde
**Risultato atteso**: ? Puntata eseguita normalmente
---
### Test 4: Nessun Limite (Default) ?
**Steps**:
1. Imposta limite = 0
2. Puntate residue = 5
3. Avvia monitoraggio
4. Verifica puntata eseguita: `[OK] Click su Asta...`
5. Verifica indicatore nascosto
**Risultato atteso**: ? Puntata eseguita, nessun indicatore
---
## ?? Best Practices
### ?? Valori Consigliati
| Strategia | Limite Consigliato | Motivo |
|-----------|-------------------|--------|
| **Aggressiva** | 0-10 | Usa quasi tutte le puntate disponibili |
| **Bilanciata** | 20-50 | Mantiene cuscinetto sicurezza |
| **Conservativa** | 100+ | Riserva ampia per imprevisti |
### ?? Avvisi
1. **Non impostare troppo alto**: Rischi di non puntare mai
2. **Monitorare puntate**: Ricaricare prima di raggiungere il limite
3. **Avviso giallo**: Segnala quando sei vicino (10 puntate dal limite)
---
## ?? Vantaggi della Feature
### ? Sicurezza
- ??? **Protezione account**: Non finisci mai le puntate completamente
- ?? **Cuscinetto emergenze**: Mantieni sempre puntate per aste importanti
### ? Controllo
- ?? **Visibilità immediata**: Indicatore sempre visibile
- ?? **Avvisi proattivi**: Colori cambiano vicino al limite
- ?? **Log dettagliati**: Traccia quando il limite blocca puntate
### ? Flessibilità
- ?? **Configurabile**: Ogni utente sceglie il proprio limite
- ?? **Disattivabile**: Imposta 0 per disabilitare
- ?? **Persistente**: Salva automaticamente le preferenze
---
## ?? Riferimenti
- Pattern: Safety Limits in Automated Systems
- Similar Feature: Trading Stop-Loss Mechanisms
- UX Pattern: Visual Status Indicators with Color Coding
---
**Data Feature**: 2025
**Versione**: 5.7+
**Priorità**: Alta (Safety Feature)
**Status**: ?? DOCUMENTATO - Pronto per implementazione
**Complessità**: ?? Media (6 file da modificare)
**Impatto**: ????? Alto (Protezione utente)
@@ -1,399 +0,0 @@
# ?? Feature: Validazione Campi Numerici
## ?? Descrizione
Implementazione di una validazione robusta per tutti i campi numerici dell'applicazione che impedisce l'inserimento di caratteri non validi e gestisce intelligentemente i campi vuoti.
## ? Problema Risolto
### Prima
- ? Possibile inserire lettere e caratteri speciali nei campi numerici
- ? Campi vuoti causavano errori di parsing
- ? Nessuna standardizzazione del formato decimale (punto vs virgola)
- ? Comportamento inconsistente tra campi diversi
- ? Errori runtime quando si tentava di salvare valori non validi
### Dopo
- ? Solo numeri accettati (nessun carattere non valido)
- ? Campo vuoto ? ripristinato automaticamente a valore predefinito
- ? Formato decimale standardizzato (accetta sia punto che virgola)
- ? Comportamento consistente in tutta l'applicazione
- ? Nessun errore di parsing possibile
---
## ? Funzionalità Implementate
### 1?? Validazione Input Interi
**Campi Interessati:**
- Anticipo (ms) - Impostazioni asta
- Max Clicks - Impostazioni asta
- Puntate Minime da Mantenere - Protezione account
- Max Righe Log per Asta
- Max Righe Log Globale
- Max Puntate da Visualizzare
**Comportamento:**
```
Digitazione: Solo cifre 0-9 permesse
Incolla: Solo testo numerico accettato
Spazio: Ignorato
Canc/Backspace: Se campo vuoto ? ripristina a 0 al LostFocus
```
**Esempio:**
```
Input: "abc123def" ? Bloccato, nessun carattere inserito
Input: "123" ? Accettato ?
Campo vuoto + Tab ? Ripristinato a "0" ?
```
---
### 2?? Validazione Input Decimali
**Campi Interessati:**
- Min EUR - Impostazioni asta
- Max EUR - Impostazioni asta
- Prezzo Minimo (€) - Defaults
- Prezzo Massimo (€) - Defaults
**Comportamento:**
```
Digitazione: Solo cifre 0-9, punto (.) e virgola (,)
Separatore: Accetta sia . che , (un solo separatore permesso)
Incolla: Solo numeri decimali validi
Normalizzazione: Converte virgola in punto e formatta a 2 decimali
Campo vuoto + Tab: Ripristinato a "0.00" al LostFocus
```
**Esempio:**
```
Input: "12,50" ? Salvato come "12.50" ?
Input: "12.5" ? Salvato come "12.50" ?
Input: "12" ? Salvato come "12.00" ?
Input: "12.5.6" ? Secondo punto bloccato ?
Campo vuoto + Tab ? Ripristinato a "0.00" ?
```
---
## ?? Implementazione Tecnica
### Classe Helper: `NumericTextBoxHelper`
Posizione: `Utilities\NumericTextBoxHelper.cs`
```csharp
public static class NumericTextBoxHelper
{
// Setup per campi interi
public static void SetupIntegerInput(TextBox textBox, int defaultValue = 0)
// Setup per campi decimali
public static void SetupDecimalInput(TextBox textBox, double defaultValue = 0.00, bool allowNegative = false)
// Recupero valori con fallback
public static int GetIntegerValue(TextBox textBox, int defaultValue = 0)
public static double GetDecimalValue(TextBox textBox, double defaultValue = 0.00)
}
```
### Eventi Gestiti
1. **PreviewTextInput**: Blocca caratteri non validi durante la digitazione
2. **Pasting**: Blocca incolla di testo non valido
3. **LostFocus**: Ripristina valore predefinito se campo vuoto
4. **KeyDown**: Blocca tasto spazio
---
## ?? Campi Validati
### Auction Monitor - Impostazioni Asta
| Campo | Tipo | Default | Descrizione |
|-------|------|---------|-------------|
| Anticipo (ms) | Intero | 200 | Millisecondi di anticipo |
| Min EUR | Decimale | 0.00 | Prezzo minimo |
| Max EUR | Decimale | 0.00 | Prezzo massimo |
| Max Clicks | Intero | 0 | Numero massimo click |
### Settings - Impostazioni Predefinite
| Campo | Tipo | Default | Descrizione |
|-------|------|---------|-------------|
| Anticipo Puntata (ms) | Intero | 200 | Default per nuove aste |
| Prezzo Minimo (€) | Decimale | 0.00 | Default prezzo minimo |
| Prezzo Massimo (€) | Decimale | 0.00 | Default prezzo massimo |
| Max Click | Intero | 0 | Default max click |
### Settings - Protezione Account
| Campo | Tipo | Default | Descrizione |
|-------|------|---------|-------------|
| Puntate Minime da Mantenere | Intero | 0 | Soglia protezione puntate |
### Settings - Limiti Log
| Campo | Tipo | Default | Descrizione |
|-------|------|---------|-------------|
| Max Righe Log per Asta | Intero | 500 | Limite righe log asta |
| Max Righe Log Globale | Intero | 1000 | Limite righe log globale |
| Max Puntate da Visualizzare | Intero | 20 | Limite storia puntate |
---
## ?? Test di Verifica
### Test 1: Blocco Caratteri Non Validi
**Steps:**
1. Apri impostazioni asta
2. Clicca sul campo "Max Clicks"
3. Prova a digitare: `"abc123def"`
4. ? **Verifica**: Solo `"123"` appare nel campo
### Test 2: Gestione Campo Vuoto
**Steps:**
1. Apri impostazioni asta
2. Svuota completamente il campo "Max EUR" (seleziona tutto e cancella)
3. Premi Tab (o clicca fuori dal campo)
4. ? **Verifica**: Campo ripristinato a `"0.00"` (non al valore predefinito precedente)
**Nota Importante**:
- Il campo vuoto viene **sempre** ripristinato a **0** (o **0.00** per decimali)
- **NON** viene ripristinato al valore predefinito configurato
- Questo permette di "resettare" facilmente un campo cancellando tutto
### Test 3: Formato Decimale
**Steps:**
1. Apri impostazioni predefinite
2. Campo "Prezzo Massimo": digita `"12,5"`
3. Premi Tab
4. ? **Verifica**: Valore normalizzato a `"12.50"`
### Test 4: Incolla Testo Non Valido
**Steps:**
1. Copia testo: `"abc123xyz"`
2. Prova a incollare in "Max Clicks"
3. ? **Verifica**: Incolla bloccato (o solo numeri estratti)
### Test 5: Doppio Separatore Decimale
**Steps:**
1. Campo "Max EUR": digita `"12.5"`
2. Prova a digitare un altro punto: `"."`
3. ? **Verifica**: Secondo punto bloccato
---
## ?? Casi d'Uso
### Scenario 1: Utente Inesperto
**Problema**: Utente prova a inserire "100 euro" nel campo Max EUR
**Comportamento:**
```
Input: "100 euro"
Risultato: Solo "100" inserito (lettere bloccate)
Al LostFocus: Formattato come "100.00"
```
### Scenario 2: Copia/Incolla da Excel
**Problema**: Utente copia valore da Excel con formato locale (es. `"12,50 €"`)
**Comportamento:**
```
Incolla: "12,50 €"
Risultato: Solo "12,50" accettato (simbolo € rimosso)
Al LostFocus: Normalizzato a "12.50"
```
### Scenario 3: Cancellazione Completa
**Problema**: Utente cancella tutto il campo per "resettarlo a zero"
**Comportamento:**
```
Input: [Canc][Canc][Canc]... fino a campo vuoto
Durante digitazione: Campo rimane vuoto
Al LostFocus: Ripristinato a "0" (interi) o "0.00" (decimali)
```
**? Vantaggio**: Cancellare tutto il campo è il modo più veloce per impostare il valore a zero!
---
## ?? Vantaggi
| Aspetto | Prima | Dopo |
|---------|-------|------|
| **Errori Runtime** | Frequenti | Impossibili ? |
| **UX** | Confusa | Chiara ? |
| **Validazione** | Manuale | Automatica ? |
| **Consistenza** | Bassa | Alta ? |
| **Formato** | Variabile | Standardizzato ? |
| **Errori Utente** | Possibili | Prevenuti ? |
---
## ?? Flusso di Validazione
### Input Intero
```
1. Utente digita carattere
?
2. PreviewTextInput: È una cifra?
?? Sì ? Permetti
?? No ? Blocca (e.Handled = true)
?
3. Utente finisce di digitare
?
4. LostFocus: Campo vuoto?
?? Sì ? Imposta "0"
?? No ? Mantieni valore
```
### Input Decimale
```
1. Utente digita carattere
?
2. PreviewTextInput: Cifra, . o , ?
?? Cifra ? Permetti
?? . o , ? C'è già un separatore?
? ?? Sì ? Blocca
? ?? No ? Permetti
?? Altro ? Blocca
?
3. LostFocus:
?? Campo vuoto ? Imposta "0.00"
?? Campo pieno ? Normalizza formato
?? Sostituisci , con .
?? Formatta a 2 decimali (F2)
```
---
## ?? Note Implementative
### Perché Non Usare `InputMask` o Behavior?
? **Scelta Fatta**: Event handlers diretti
**Vantaggi:**
- ? Massimo controllo sul comportamento
- ? Nessuna dipendenza esterna
- ? Facile da debuggare
- ? Performante
- ? Compatibile con tutti i controlli WPF
**Alternative Scartate:**
- ? InputMask: Rigido, meno flessibile
- ? Behavior XAML: Dipendenza extra, più complesso
- ? Converter: Solo per visualizzazione, non per input
### Gestione Cross-Platform (Virgola vs Punto)
La soluzione accetta **sia punto che virgola** come separatore decimale:
```csharp
// Accetta entrambi durante input
if (e.Text == "." || e.Text == ",") { ... }
// Normalizza al salvataggio
string text = textBox.Text.Replace(",", ".");
double.Parse(text, CultureInfo.InvariantCulture);
```
**Vantaggi:**
- ? Funziona con tastiere italiane (virgola)
- ? Funziona con tastiere internazionali (punto)
- ? Formato salvato sempre consistente (punto)
---
## ?? Risoluzione Problemi
### Problema: Campo Accetta Ancora Lettere
**Causa**: Validazione non inizializzata
**Soluzione**:
```csharp
// Verifica che InitializeNumericInputValidation() sia chiamato nel constructor
public MainWindow()
{
InitializeComponent();
InitializeNumericInputValidation(); // ? Deve essere presente
}
```
### Problema: Campo Non Si Svuota
**Causa**: LostFocus ripristina immediatamente
**Comportamento Corretto**: È intenzionale! Previene campi vuoti invalidi.
**Quando Cancelli Tutto**:
- ? Durante digitazione: Campo rimane vuoto
- ? Al LostFocus: Ripristinato a "0" o "0.00"
**Questo è utile!** Cancellare tutto il campo è il modo più rapido per impostarlo a zero.
### Problema: Decimali Non Formattati
**Causa**: TextChanged handlers custom interferiscono
**Soluzione**: Rimuovi handler TextChanged custom, usa NumericTextBoxHelper
---
## ? Checklist Completamento
- [x] Classe NumericTextBoxHelper creata
- [x] Setup interi implementato
- [x] Setup decimali implementato
- [x] Gestione campo vuoto
- [x] Normalizzazione formato decimale
- [x] Blocco caratteri non validi
- [x] Blocco incolla non valido
- [x] Gestione virgola/punto
- [x] Tutti i campi numerici validati
- [x] Compilazione senza errori
- [x] Documentazione completa
---
## ?? Metriche
| Metrica | Valore |
|---------|--------|
| **Campi Validati** | 13 |
| **Tipi Validazione** | 2 (Int, Decimal) |
| **Eventi Gestiti** | 4 per campo |
| **Errori Prevenuti** | ? (impossibili) |
| **Codice Riusabile** | 100% |
| **Dipendenze Esterne** | 0 |
---
## ?? Conclusioni
Questa feature migliora significativamente la **robustezza** e l'**usabilità** dell'applicazione:
? **Zero errori** di parsing possibili
? **UX consistente** in tutta l'app
? **Codice riusabile** e mantenibile
? **Nessuna dipendenza** esterna
? **Cross-platform** (punto/virgola)
Gli utenti possono ora inserire valori numerici senza preoccuparsi di errori di formato! ??
@@ -1,590 +0,0 @@
# ?? Feature: Informazioni Prodotto e Calcolatore Valore
## ?? Obiettivo
Creare una sezione che mostra le **informazioni complete del prodotto** in asta e un **calcolatore intelligente** che stima:
1. Quante puntate potrebbero servire per vincere
2. Quale potrebbe essere il prezzo finale
3. Se conviene partecipare all'asta
## ?? Informazioni da Estrarre dall'HTML
### Dati Disponibili nell'HTML di Bidoo
```html
<span class="reserved-price col-xs-12 text-center">
<span>Valore:</span> 20,00 €
</span>
<div class="buynow-btn col-xs-6">
<a class="buy-now" href="buy_your_product.php?a=...">
<div class="btn-rapid buy-rapid-now">
<i class="fas fa-shopping-cart"></i>
20,00 €
</div>
</a>
</div>
```
**Informazioni Estraibili**:
- ? **Valore di mercato** (€20.00)
- ? **Prezzo Compra Subito** (€20.00)
- ? **Nome prodotto** (dal titolo pagina)
- ? **ID prodotto** (dal data-id-product)
- ? **Limite vincite** (dai tooltip/attributi)
- ? **Spese spedizione** (se presenti nell'HTML)
---
## ??? Architettura Soluzione
### 1?? Nuovo Model: `ProductInfo`
```csharp
public class ProductInfo
{
// Dati base
public string ProductId { get; set; }
public string ProductName { get; set; }
public string ProductUrl { get; set; }
// Prezzi
public decimal RetailPrice { get; set; } // Valore di mercato
public decimal BuyNowPrice { get; set; } // Prezzo Compra Subito
public decimal ShippingCost { get; set; } // Spese di spedizione
// Limiti
public int? WinLimit { get; set; } // 1 volta ogni X giorni
public bool HasWinLimit { get; set; }
// Metadata
public DateTime ScrapedAt { get; set; }
public bool IsDataValid { get; set; }
}
```
### 2?? Nuovo Service: `ProductInfoScraper`
```csharp
public class ProductInfoScraper
{
public async Task<ProductInfo> ScrapeProductInfoAsync(string auctionUrl);
private decimal ExtractRetailPrice(string html);
private decimal ExtractBuyNowPrice(string html);
private decimal ExtractShippingCost(string html);
private (bool hasLimit, int? days) ExtractWinLimit(string html);
}
```
### 3?? Nuovo Model: `ValueCalculation`
```csharp
public class ValueCalculation
{
// Input
public decimal RetailPrice { get; set; }
public decimal BuyNowPrice { get; set; }
public decimal ShippingCost { get; set; }
// Stime
public int EstimatedBidsNeeded { get; set; } // Puntate stimate
public decimal EstimatedFinalPrice { get; set; } // Prezzo finale stimato
public decimal EstimatedTotalCost { get; set; } // Costo totale (prezzo + puntate)
public decimal EstimatedSavings { get; set; } // Risparmio vs BuyNow
public bool IsWorthIt { get; set; } // Conviene partecipare?
// Confidence
public int ConfidenceLevel { get; set; } // 0-100%
public string ConfidenceReason { get; set; }
// Raccomandazioni
public int RecommendedMaxBids { get; set; }
public decimal RecommendedMaxPrice { get; set; }
public string Recommendation { get; set; }
}
```
### 4?? Nuovo Service: `ValueCalculator`
```csharp
public class ValueCalculator
{
// Costo una puntata (€0.75)
private const decimal BID_COST = 0.75m;
public ValueCalculation Calculate(ProductInfo product, ProductInsights? insights = null);
// Algoritmo di stima basato su:
// - Valore prodotto
// - Statistiche storiche (se disponibili)
// - Pattern comuni di Bidoo
}
```
---
## ?? Algoritmo di Calcolo Valore
### Formula Base
```csharp
// Stima puntate necessarie
EstimatedBidsNeeded = EstimateFromHistoryOrHeuristic();
// Costo puntate
decimal bidsCost = EstimatedBidsNeeded * 0.75m;
// Prezzo finale stimato (2-5% del valore retail)
EstimatedFinalPrice = RetailPrice * 0.035m; // Media 3.5%
// Costo totale
EstimatedTotalCost = EstimatedFinalPrice + bidsCost + ShippingCost;
// Risparmio
EstimatedSavings = BuyNowPrice - EstimatedTotalCost;
// Conviene?
IsWorthIt = EstimatedSavings > 0;
```
### Euristica Intelligente
```csharp
private int EstimateBidsFromProductValue(decimal retailPrice)
{
// Prodotti economici: più competizione relativa
if (retailPrice < 20m)
return (int)(retailPrice * 4); // ~40-80 puntate
// Prodotti medi: competizione media
if (retailPrice < 100m)
return (int)(retailPrice * 3); // ~60-300 puntate
// Prodotti costosi: competizione alta ma meno partecipanti
if (retailPrice < 500m)
return (int)(retailPrice * 2.5); // ~250-1250 puntate
// Prodotti molto costosi
return (int)(retailPrice * 2); // ~1000+ puntate
}
```
### Integrazione con Statistiche Storiche
```csharp
if (insights != null && insights.TotalAuctions > 5)
{
// Usa dati reali se disponibili
EstimatedBidsNeeded = (int)insights.AverageBidsUsed;
EstimatedFinalPrice = (decimal)insights.AverageFinalPrice;
ConfidenceLevel = insights.ConfidenceScore;
}
else
{
// Usa euristica
EstimatedBidsNeeded = EstimateBidsFromProductValue(RetailPrice);
ConfidenceLevel = 30; // Basso senza dati storici
}
```
---
## ?? UI: Nuova Sezione "Info Prodotto"
### Opzione 1: Nuova Tab nella Sidebar (Raccomandato)
```
Sidebar:
?? Aste Attive
?? Browser
?? Puntate Gratis
?? Dati Statistici
?? Info Prodotto ? NUOVO
?? Impostazioni
```
### Opzione 2: Pannello Espandibile in "Impostazioni Asta"
```
[Impostazioni] (selezione asta)
?? Nome Asta + URL
?? [Browser Interno] [Browser Esterno]
?? [Copia URL] [Esporta]
?
?? [? Info Prodotto] ? Espandibile
? ?? Valore: €45.00
? ?? Compra Subito: €45.00
? ?? Spedizione: €4.90
? ?? Limite: 1 volta/30gg
? ?
? ?? [?? CALCOLA VALORE]
? ?
? ?? [Risultati Calcolo]
? ?? Puntate stimate: ~120
? ?? Prezzo finale: ~€1.57
? ?? Costo puntate: ~€90.00
? ?? Costo totale: ~€96.47
? ?? Risparmio: -€51.47 ?
? ?? Raccomandazione: "Non conviene"
?
?? Anticipo (ms): [200]
?? Min EUR / Max EUR / Max Clicks
?? [Reset]
```
---
## ?? Layout UI Dettagliato
### Sezione Info Prodotto (Espandibile)
```xaml
<Expander Header="?? Informazioni Prodotto" IsExpanded="False">
<StackPanel Margin="10">
<!-- Dati Prodotto -->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Valore:" FontWeight="Bold"/>
<TextBlock Grid.Row="0" Grid.Column="1" Text="€45.00" Foreground="#00D800"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Compra Subito:" FontWeight="Bold"/>
<TextBlock Grid.Row="1" Grid.Column="1" Text="€45.00" Foreground="#007ACC"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="Spedizione:" FontWeight="Bold"/>
<TextBlock Grid.Row="2" Grid.Column="1" Text="€4.90" Foreground="#FFB700"/>
<TextBlock Grid.Row="3" Grid.Column="0" Text="Limite:" FontWeight="Bold"/>
<TextBlock Grid.Row="3" Grid.Column="1" Text="1 volta ogni 30 giorni"/>
</Grid>
<!-- Pulsante Calcola -->
<Button Content="?? Calcola Valore"
Background="#007ACC"
Click="CalculateValue_Click"
Margin="0,15,0,10"/>
<!-- Risultati Calcolo -->
<Border BorderBrush="#3E3E42" BorderThickness="1"
Background="#2D2D30" Padding="10"
Visibility="{Binding HasCalculation}">
<StackPanel>
<TextBlock Text="?? Analisi Valore"
FontWeight="Bold"
FontSize="14"
Margin="0,0,0,10"/>
<!-- Stime -->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Puntate stimate:"/>
<TextBlock Grid.Row="0" Grid.Column="1" Text="~120" FontWeight="Bold"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Prezzo finale:"/>
<TextBlock Grid.Row="1" Grid.Column="1" Text="~€1.57" FontWeight="Bold"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="Costo puntate:"/>
<TextBlock Grid.Row="2" Grid.Column="1" Text="~€90.00" FontWeight="Bold"/>
<TextBlock Grid.Row="3" Grid.Column="0" Text="Costo totale:"/>
<TextBlock Grid.Row="3" Grid.Column="1" Text="~€96.47" FontWeight="Bold"/>
<TextBlock Grid.Row="4" Grid.Column="0" Text="Risparmio:"/>
<TextBlock Grid.Row="4" Grid.Column="1"
Text="-€51.47"
FontWeight="Bold"
Foreground="#E81123"/>
<TextBlock Grid.Row="5" Grid.Column="0" Text="Conviene:"/>
<TextBlock Grid.Row="5" Grid.Column="1"
Text="? NO"
FontWeight="Bold"
Foreground="#E81123"/>
</Grid>
<!-- Raccomandazione -->
<Border Background="#3E3E42"
Padding="8"
Margin="0,10,0,0"
CornerRadius="4">
<StackPanel>
<TextBlock Text="?? Raccomandazione:"
FontWeight="Bold"
Margin="0,0,0,5"/>
<TextBlock Text="Non conviene partecipare. Il costo stimato supera il prezzo 'Compra Subito'."
TextWrapping="Wrap"
Foreground="#CCCCCC"/>
</StackPanel>
</Border>
<!-- Pulsante Applica Limiti -->
<Button Content="? Applica come Limiti Asta"
Background="#00D800"
Margin="0,10,0,0"
ToolTip="Imposta Max Clicks e Max Price consigliati"/>
</StackPanel>
</Border>
<!-- Confidence -->
<TextBlock Text="?? Confidence: 45% - Dati insufficienti"
Foreground="#FFB700"
FontSize="11"
Margin="0,10,0,0"/>
</StackPanel>
</Expander>
```
---
## ?? Funzionalità "Applica come Limiti"
Quando clicchi **"Applica come Limiti Asta"**:
1. **Max Clicks** viene impostato al valore raccomandato (es. 120)
2. **Max Price** viene impostato al prezzo finale stimato (es. €1.57)
3. **Log**: `[VALUE] Limiti applicati: Max Clicks=120, Max Price=€1.57`
```csharp
private void ApplyCalculatedLimits_Click(object sender, RoutedEventArgs e)
{
if (_selectedAuction == null || _calculation == null) return;
_selectedAuction.MaxClicks = _calculation.RecommendedMaxBids;
_selectedAuction.MaxPrice = (double)_calculation.RecommendedMaxPrice;
UpdateSelectedAuctionDetails(_selectedAuction);
SaveAuctions();
Log($"[VALUE] Limiti applicati: Max Clicks={_calculation.RecommendedMaxBids}, " +
$"Max Price=€{_calculation.RecommendedMaxPrice:F2}", LogLevel.Success);
MessageBox.Show(
$"Limiti applicati con successo!\n\n" +
$"Max Clicks: {_calculation.RecommendedMaxBids}\n" +
$"Max Price: €{_calculation.RecommendedMaxPrice:F2}",
"Limiti Applicati",
MessageBoxButton.OK,
MessageBoxImage.Information);
}
```
---
## ?? Implementazione Scraper
### Estrazione Valore Retail
```csharp
private decimal ExtractRetailPrice(string html)
{
try
{
// Cerca: <span>Valore:</span> 20,00 €
var match = Regex.Match(html,
@"<span>Valore:<\/span>\s*([\d,]+)\s*€",
RegexOptions.IgnoreCase);
if (match.Success)
{
var priceText = match.Groups[1].Value.Replace(",", ".");
if (decimal.TryParse(priceText, NumberStyles.Any, CultureInfo.InvariantCulture, out var price))
{
return price;
}
}
}
catch (Exception ex)
{
Debug.WriteLine($"[SCRAPER ERROR] ExtractRetailPrice: {ex.Message}");
}
return 0m;
}
```
### Estrazione Prezzo Compra Subito
```csharp
private decimal ExtractBuyNowPrice(string html)
{
try
{
// Cerca: <div class="btn-rapid buy-rapid-now">...20,00 €...</div>
var match = Regex.Match(html,
@"buy-rapid-now[^>]*>.*?([\d,]+)\s*€",
RegexOptions.IgnoreCase | RegexOptions.Singleline);
if (match.Success)
{
var priceText = match.Groups[1].Value.Replace(",", ".");
if (decimal.TryParse(priceText, NumberStyles.Any, CultureInfo.InvariantCulture, out var price))
{
return price;
}
}
}
catch (Exception ex)
{
Debug.WriteLine($"[SCRAPER ERROR] ExtractBuyNowPrice: {ex.Message}");
}
return 0m;
}
```
---
## ?? Esempio Output
### Prodotto: Trapunta Matrimoniale €45
```
?? Informazioni Prodotto
Valore: €45.00
Compra Subito: €45.00
Spedizione: €4.90
Limite: 1 volta ogni 30 giorni
[?? Calcola Valore]
?? Analisi Valore
?????????????????????????????
Puntate stimate: ~120
Prezzo finale: ~€1.57
Costo puntate: ~€90.00
Spedizione: €4.90
?????????????????????????????
Costo totale: ~€96.47
Risparmio: -€51.47 ?
Conviene: NO ?
?? Raccomandazione:
Non conviene partecipare. Il costo stimato (€96.47)
supera il prezzo 'Compra Subito' (€45.00).
Confidence: 30% - Senza dati storici
[? Applica come Limiti Asta]
```
### Prodotto: 47 Puntate €9.40
```
?? Informazioni Prodotto
Valore: €9.40
Compra Subito: €9.40
Spedizione: €0.00 (Digitale)
Limite: No
[?? Calcola Valore]
?? Analisi Valore
?????????????????????????????
Puntate stimate: ~30
Prezzo finale: ~€0.33
Costo puntate: ~€22.50
Spedizione: €0.00
?????????????????????????????
Costo totale: ~€22.83
Risparmio: +€13.43 ?
Conviene: NO ?
?? Raccomandazione:
Non conviene molto. Comprare direttamente costa meno.
Le puntate digitali sono utili solo se ne hai bisogno
urgente a costo ridotto.
Confidence: 40% - Euristica base
[? Applica come Limiti Asta]
```
---
## ? Vantaggi Funzionalità
### Per l'Utente
1. **Decisione Informata**: Sa in anticipo se conviene partecipare
2. **Stime Realistiche**: Vede costo stimato vs prezzo retail
3. **Limiti Automatici**: Può applicare limiti consigliati con 1 click
4. **Trasparenza**: Capisce quanto potrebbe spendere realmente
### Per il Sistema
1. **Integrazione Storico**: Usa `ProductInsights` se disponibili
2. **Fallback Intelligente**: Euristica quando mancano dati
3. **Persistenza**: Info prodotto salvate con l'asta
4. **Scalabile**: Facile aggiungere nuovi fattori di calcolo
---
## ??? File da Creare/Modificare
### Nuovi File
- ? `Models/ProductInfo.cs`
- ? `Models/ValueCalculation.cs`
- ? `Services/ProductInfoScraper.cs`
- ? `Services/ValueCalculator.cs`
### File da Modificare
- ? `Models/AuctionInfo.cs` - Aggiungere `ProductInfo`
- ? `Controls/AuctionMonitorControl.xaml` - Aggiungere Expander "Info Prodotto"
- ? `Controls/AuctionMonitorControl.xaml.cs` - Gestori eventi
- ? `Core/MainWindow.ButtonHandlers.cs` - Handler "Calcola Valore" e "Applica Limiti"
---
## ?? Implementazione a Step
### Step 1: Models
1. Creare `ProductInfo.cs`
2. Creare `ValueCalculation.cs`
### Step 2: Services
1. Creare `ProductInfoScraper.cs`
2. Creare `ValueCalculator.cs`
### Step 3: Integration
1. Aggiungere `ProductInfo` a `AuctionInfo`
2. Creare metodo `ScrapeAndCalculate()`
### Step 4: UI
1. Aggiungere Expander in AuctionMonitorControl
2. Aggiungere pulsanti e handlers
### Step 5: Testing
1. Test scraping varie aste
2. Test calcolo con/senza storici
3. Test applicazione limiti
---
**Vuoi che proceda con l'implementazione?**
@@ -1,192 +0,0 @@
# Feature: Calcolo Valore Prodotto
## Descrizione
Sistema per calcolare e visualizzare il valore reale di un prodotto all'asta, considerando tutti i costi effettivi e il risparmio rispetto al prezzo "Compra Subito".
## Implementazione
### Data: 20 Novembre 2025
### Modifiche Effettuate
#### 1. `Models/AuctionInfo.cs`
- Aggiunte proprietà per le informazioni del prodotto:
- `BuyNowPrice`: Prezzo "Compra Subito" del prodotto
- `ShippingCost`: Spese di spedizione
- `HasWinLimit`: Indica se c'è un limite di vincita
- `WinLimitDescription`: Descrizione del limite (es: "1 volta ogni 30 giorni")
- `BidCost`: Costo per puntata (default 0.20€)
- `CalculatedValue`: Ultimo valore calcolato
- Aggiunta classe `ProductValue` per rappresentare il valore calcolato:
- `CurrentPrice`: Prezzo attuale dell'asta
- `TotalBids`: Numero totale di puntate
- `MyBids`: Numero di puntate dell'utente
- `MyBidsCost`: Costo delle puntate dell'utente
- `TotalCostIfWin`: Costo totale se si vince (prezzo + puntate + spedizione)
- `BuyNowPrice`, `ShippingCost`: Riferimenti al prodotto
- `Savings`: Risparmio rispetto al "Compra Subito"
- `SavingsPercentage`: Percentuale di risparmio
- `IsWorthIt`: Indica se conviene continuare
- `Summary`: Messaggio riassuntivo
#### 2. `Utilities/ProductValueCalculator.cs`
Nuova classe helper con metodi statici:
- `Calculate()`: Calcola il valore del prodotto basandosi sullo stato corrente
- Input: AuctionInfo, prezzo corrente, numero totale puntate
- Output: Oggetto ProductValue con tutti i calcoli
- `ExtractProductInfo()`: Estrae informazioni dal HTML della pagina dell'asta
- Cerca il prezzo "Compra Subito" con regex
- Cerca il limite di vincita
- Aggiorna l'oggetto AuctionInfo
- `FormatValueMessage()`: Formatta un messaggio colorato per il log
- ? se conveniente
- ? se non conveniente
- ?? se non c'è prezzo di riferimento
#### 3. `ViewModels/AuctionViewModel.cs`
- Aggiunte proprietà per il binding nella UI:
- `TotalCostDisplay`: Costo totale formattato
- `SavingsDisplay`: Risparmio formattato con percentuale
- `WorthItDisplay`: Icona ? o ?
- `BuyNowPriceDisplay`: Prezzo "Compra Subito"
- `MyBidsCostDisplay`: Costo delle mie puntate
- Aggiunto metodo `RefreshProductValue()` per notificare aggiornamenti
## Funzionamento
### Calcolo del Valore
Il valore viene calcolato considerando:
1. **Prezzo Attuale**: Prezzo corrente dell'asta in euro
2. **Costo Puntate**: Numero puntate utente × 0.20€ (configurabile)
3. **Spese Spedizione**: Se disponibili
4. **Totale**: Prezzo + Puntate + Spedizione
5. **Risparmio**: (Compra Subito + Spedizione) - Totale
### Formula
```
Costo Puntate = Numero Puntate Utente × 0.20€
Totale = Prezzo Attuale + Costo Puntate + Spese Spedizione
Risparmio = (Compra Subito + Spedizione) - Totale
Percentuale = (Risparmio / (Compra Subito + Spedizione)) × 100
```
### Esempio
- Prezzo attuale: 2.50€
- Puntate utente: 10 (= 2.00€)
- Spedizione: 5.00€
- **Totale: 9.50€**
- Compra Subito: 20.00€
- **Risparmio: 15.50€ (62.0%)**
## Estrazione Informazioni HTML
Il sistema cerca automaticamente nell'HTML:
1. **Prezzo "Compra Subito"**:
- Pattern: `buy-rapid-now`
- Pattern alternativo: `buy-now`
- Format: "€ 20,00" o "20,00 €"
2. **Valore Prodotto** (fallback):
- Pattern: `reserved-price`
- Format: "Valore: 20,00 €"
3. **Limite Vincita**:
- Pattern: `bi-limit-win`
- Attributo: `title="Puoi vincere questo prodotto 1 volta ogni X giorni"`
- Classe `hidden` indica nessun limite
## Integrazione con AuctionMonitor
Per integrare il calcolo del valore nel monitoraggio delle aste:
1. **All'avvio del monitor**: Estrarre info prodotto dall'HTML
```csharp
ProductValueCalculator.ExtractProductInfo(html, auctionInfo);
```
2. **Ad ogni aggiornamento stato**: Calcolare valore corrente
```csharp
var value = ProductValueCalculator.Calculate(
auctionInfo,
currentPrice,
totalBidsCount
);
auctionInfo.CalculatedValue = value;
viewModel.RefreshProductValue();
```
3. **Nel log**: Mostrare messaggio formattato
```csharp
var message = ProductValueCalculator.FormatValueMessage(value);
auctionInfo.AddLog(message);
```
## Configurazione
### Costo per Puntata
Il costo per puntata può essere configurato per ogni asta:
```csharp
auctionInfo.BidCost = 0.20; // Default
auctionInfo.BidCost = 0.15; // Con sconto
auctionInfo.BidCost = 0.10; // Puntate vinte
```
### Spese di Spedizione
Se note, possono essere impostate manualmente:
```csharp
auctionInfo.ShippingCost = 5.00;
```
## UI - Colonne da Aggiungere
Per visualizzare le informazioni nella griglia aste, aggiungere queste colonne:
1. **Totale**: `TotalCostDisplay` - Costo totale se si vince
2. **Risparmio**: `SavingsDisplay` - Risparmio vs Compra Subito
3. **?/?**: `WorthItDisplay` - Indicatore convenienza
4. **Compra Subito**: `BuyNowPriceDisplay` - Prezzo riferimento
5. **Costo Puntate**: `MyBidsCostDisplay` - Quanto speso in puntate
## Limitazioni Attuali
1. **Spese Spedizione**: Non estratte automaticamente dall'HTML
- Possono essere su pagina separata
- Richiedono autenticazione
- Variano per utente/località
2. **Crediti Puntate**: Il costo 0.20€ è una stima massima
- Puntate con sconto costano meno
- Puntate vinte sono gratuite
- Non si tiene conto dei pacchetti promozionali
3. **Valore Reale**: Non considera altri fattori
- Valore di mercato effettivo del prodotto
- Condizioni del prodotto (nuovo/usato)
- Garanzie e resi
## TODO Futuro
- [ ] Estrazione automatica spese spedizione
- [ ] Tracciamento costo reale delle puntate (distinguere puntate comprate/vinte)
- [ ] Storico valori per analisi trend
- [ ] Soglia di convenienza configurabile
- [ ] Alert quando non conviene più puntare
- [ ] Calcolo ROI (Return on Investment) per statistiche
- [ ] Export dati valore per analisi
## Note Tecniche
- Le regex per l'estrazione sono case-insensitive
- Il parsing dei prezzi gestisce sia virgola che punto decimale
- I calcoli usano `double` per precisione sufficiente (massimo 2 decimali)
- Thread-safe: il calcolo è stateless, gli aggiornamenti sono sincronizzati
@@ -1,815 +0,0 @@
# ?? Feature: Pre-caricamento WebView2 e Estrazione Cookie Automatica
## ?? Descrizione
Implementazione di due feature complementari per migliorare l'esperienza utente con il browser integrato:
1. **Pre-caricamento WebView2**: Il browser si inizializza in background all'avvio dell'applicazione
2. **Estrazione Cookie Automatica**: Possibilità di importare automaticamente il cookie di sessione dal browser integrato
---
## ?? Problemi Risolti
### Problema 1: Browser Lento al Primo Utilizzo ?
**Prima**:
```
1. Avvio applicazione
2. Click su tab "Browser"
3. ? Attesa inizializzazione WebView2 (~3-5 secondi)
4. ? Attesa caricamento pagina Bidoo (~2-3 secondi)
5. ?? Utente può finalmente usare il browser
```
**Dopo** ?:
```
1. Avvio applicazione
? (in background)
? WebView2 si inizializza
? Bidoo.com si pre-carica
2. Click su tab "Browser"
3. ?? Browser immediatamente disponibile
4. ?? Utente può usarlo subito
```
### Problema 2: Cookie Manuale Complesso ?
**Prima**:
- Utente deve aprire DevTools (F12)
- Navigare in Application ? Cookies
- Copiare manualmente tutti i cookie
- Incollare nella TextBox Impostazioni
- Formato complesso e facile da sbagliare
**Dopo** ?:
- Utente fa login nel browser integrato
- Click su "Importa da Browser"
- Cookie estratto e validato automaticamente
- Sessione salvata automaticamente
---
## ?? Implementazione
### 1?? Pre-caricamento WebView2
**File**: `Core\MainWindow.WebView.cs` (NUOVO)
#### Metodo: `InitializeWebView2()`
```csharp
/// <summary>
/// Inizializza WebView2 in background all'avvio per pre-caricare il browser
/// </summary>
private async void InitializeWebView2()
{
try
{
if (EmbeddedWebView == null)
{
Log("[WARN] WebView2 non disponibile", LogLevel.Warn);
return;
}
Log("[BROWSER] Inizializzazione WebView2 in background...", LogLevel.Info);
// Aspetta che CoreWebView2 sia inizializzato
await EmbeddedWebView.EnsureCoreWebView2Async(null);
if (EmbeddedWebView.CoreWebView2 != null)
{
_isWebViewInitialized = true;
// Pre-carica la pagina di Bidoo in background
// Questo rende il browser immediatamente utilizzabile
EmbeddedWebView.CoreWebView2.Navigate("https://it.bidoo.com");
Log("[BROWSER] WebView2 inizializzato e pre-caricato", LogLevel.Success);
// Registra evento per rilevare login automatico
EmbeddedWebView.CoreWebView2.NavigationCompleted += OnWebViewNavigationCompleted;
}
}
catch (Exception ex)
{
Log($"[WARN] Inizializzazione WebView2 fallita: {ex.Message}", LogLevel.Warn);
}
}
```
**Caratteristiche**:
- ?? **Asincrono**: Non blocca l'avvio dell'applicazione
- ?? **Background**: Si esegue mentre l'utente vede la schermata principale
- ?? **Pre-navigazione**: Carica direttamente `it.bidoo.com`
- ?? **Event handler**: Rileva automaticamente quando l'utente fa login
#### Chiamata nel Constructor
**File**: `MainWindow.xaml.cs`
```csharp
public MainWindow()
{
InitializeComponent();
// ...altre inizializzazioni...
// ? NUOVO: Pre-carica WebView2 in background
InitializeWebView2();
// ...resto del constructor...
}
```
---
### 2?? Rilevamento Automatico Login
**File**: `Core\MainWindow.WebView.cs`
#### Metodo: `OnWebViewNavigationCompleted()`
```csharp
/// <summary>
/// Evento chiamato quando la navigazione nella WebView è completata
/// Rileva automaticamente se l'utente ha effettuato il login
/// </summary>
private async void OnWebViewNavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs e)
{
try
{
if (!e.IsSuccess || EmbeddedWebView?.CoreWebView2 == null)
return;
var url = EmbeddedWebView.CoreWebView2.Source;
// Se l'utente è sulla homepage di Bidoo
if (url.Contains("bidoo.com") && !url.Contains("login"))
{
// Tenta di estrarre il cookie __stattrb
var cookie = await GetCookieFromWebView();
if (!string.IsNullOrEmpty(cookie))
{
// Verifica se è diverso da quello già salvato
var currentSession = _sessionService?.GetCurrentSession();
if (currentSession == null || string.IsNullOrEmpty(currentSession.CookieString) ||
!currentSession.CookieString.Contains(cookie))
{
// Notifica l'utente che può importare il cookie
Log("[BROWSER] Rilevato cookie di sessione nel browser - usa 'Importa da Browser' per utilizzarlo", LogLevel.Info);
}
}
}
}
catch { }
}
```
**Logica**:
1. ? Attende navigazione completata con successo
2. ?? Verifica se siamo su Bidoo (non pagina login)
3. ?? Estrae cookie dalla WebView
4. ?? Confronta con cookie salvato
5. ?? Notifica utente se cookie è nuovo o diverso
---
### 3?? Estrazione Cookie dalla WebView
**File**: `Core\MainWindow.WebView.cs`
#### Metodo: `GetCookieFromWebView()`
```csharp
/// <summary>
/// Estrae il cookie __stattrb dalla WebView2
/// </summary>
/// <returns>Cookie completo o null se non trovato</returns>
private async Task<string?> GetCookieFromWebView()
{
try
{
if (EmbeddedWebView?.CoreWebView2 == null)
return null;
// Ottieni tutti i cookie di bidoo.com
var cookies = await EmbeddedWebView.CoreWebView2.CookieManager.GetCookiesAsync("https://it.bidoo.com");
if (cookies == null || cookies.Count == 0)
return null;
// Cerca il cookie __stattrb (cookie di sessione principale)
var stattrb = cookies.FirstOrDefault(c => c.Name == "__stattrb");
if (stattrb == null)
return null;
// Costruisci la stringa cookie completa con tutti i cookie necessari
var cookieStrings = cookies
.Where(c => !string.IsNullOrEmpty(c.Value))
.Select(c => $"{c.Name}={c.Value}")
.ToList();
return string.Join("; ", cookieStrings);
}
catch (Exception ex)
{
Log($"[WARN] Impossibile estrarre cookie da WebView: {ex.Message}", LogLevel.Warn);
return null;
}
}
```
**Processo**:
1. ?? Ottiene TUTTI i cookie di `it.bidoo.com`
2. ?? Cerca il cookie principale `__stattrb`
3. ?? Costruisce stringa cookie completa (formato API-ready)
4. ? Ritorna stringa nel formato: `"cookie1=value1; cookie2=value2; ..."`
**Vantaggi**:
- ?? **Formato corretto**: Già nel formato usato dalle API
- ?? **Cookie completi**: Include tutti i cookie necessari (non solo `__stattrb`)
- ??? **Sicuro**: Gestisce errori e cookie mancanti
---
### 4?? Importazione Cookie con Validazione
**File**: `Core\MainWindow.WebView.cs`
#### Metodo: `ImportCookieFromWebView()`
```csharp
/// <summary>
/// Importa il cookie dalla WebView e lo salva per l'uso nelle API
/// </summary>
public async Task<bool> ImportCookieFromWebView()
{
try
{
if (!_isWebViewInitialized || EmbeddedWebView?.CoreWebView2 == null)
{
Log("[WARN] Browser non inizializzato - attendi qualche secondo e riprova", LogLevel.Warn);
return false;
}
Log("[BROWSER] Estrazione cookie dal browser...", LogLevel.Info);
var cookieString = await GetCookieFromWebView();
if (string.IsNullOrEmpty(cookieString))
{
Log("[WARN] Nessun cookie trovato nel browser - assicurati di aver effettuato il login su bidoo.com", LogLevel.Warn);
return false;
}
// Aggiorna la TextBox nelle impostazioni
SettingsCookieTextBox.Text = cookieString;
// Valida e attiva il cookie usando SessionService
var result = await _sessionService.ValidateAndActivateSessionAsync(cookieString);
if (result.Success && result.Session != null)
{
// Salva automaticamente la sessione
_sessionService.SaveSession(result.Session);
// Aggiorna il banner
SetUserBanner(result.Session.Username, result.Session.RemainingBids);
Log($"[OK] Cookie importato e validato - Utente: {result.Session.Username}, Puntate: {result.Session.RemainingBids}", LogLevel.Success);
return true;
}
else
{
Log($"[ERRORE] Cookie importato ma non valido: {result.ErrorMessage}", LogLevel.Error);
return false;
}
}
catch (Exception ex)
{
Log($"[ERRORE] Importazione cookie: {ex.Message}", LogLevel.Error);
return false;
}
}
```
**Processo completo**:
1. ? Verifica WebView inizializzata
2. ?? Estrae cookie dalla WebView
3. ?? Aggiorna TextBox Impostazioni
4. ?? **Valida cookie** tramite SessionService (chiamata API)
5. ?? **Salva automaticamente** se valido
6. ?? **Aggiorna banner** con dati utente
7. ? Ritorna true/false per feedback UI
---
### 5?? Aggiornamento Event Handler
**File**: `Core\EventHandlers\MainWindow.EventHandlers.Settings.cs`
```csharp
private async void ImportCookieFromBrowserButton_Click(object sender, RoutedEventArgs e)
{
try
{
// ? NUOVO: Usa il metodo migliorato di estrazione cookie
var success = await ImportCookieFromWebView();
if (success)
{
MessageBox.Show(this,
"Cookie importato e validato con successo!\nLa sessione è stata salvata automaticamente.",
"Importa Cookie",
MessageBoxButton.OK,
MessageBoxImage.Information);
}
else
{
MessageBox.Show(this,
"Impossibile importare il cookie.\n\n" +
"Assicurati di:\n" +
"1. Aver effettuato il login su bidoo.com nella scheda Browser\n" +
"2. Attendere che il browser sia completamente inizializzato\n" +
"3. Verificare di essere sulla homepage di Bidoo\n\n" +
"Controlla il log per maggiori dettagli.",
"Cookie Non Trovato",
MessageBoxButton.OK,
MessageBoxImage.Warning);
}
}
catch (Exception ex)
{
Log($"[ERRORE] Importazione cookie: {ex.Message}", LogLevel.Error);
MessageBox.Show(this,
"Errore durante importazione cookie: " + ex.Message,
"Errore",
MessageBoxButton.OK,
MessageBoxImage.Error);
}
}
```
**UI Feedback**:
- ? **Successo**: MessageBox conferma + sessione salvata
- ?? **Fallimento**: MessageBox con istruzioni chiare
- ? **Errore**: MessageBox con dettagli errore
---
## ?? Flussi Operativi
### Flusso 1: Avvio Applicazione con Pre-caricamento
```
1. MainWindow() Constructor
?
2. InitializeComponent()
?
3. InitializeWebView2() [Background, Async]
?
4. EnsureCoreWebView2Async()
? WebView2 si inizializza (~2-3 secondi)
?
5. CoreWebView2.Navigate("https://it.bidoo.com")
? Pagina si carica (~1-2 secondi)
?
6. _isWebViewInitialized = true ?
?
7. OnWebViewNavigationCompleted registrato ?
?
[Nel frattempo utente vede schermata principale]
?
8. Utente clicca tab "Browser"
?
9. ?? Browser già caricato e pronto!
```
**Tempo risparmiato**: ~4-6 secondi ?
---
### Flusso 2: Importazione Cookie da Browser
```
1. Utente va su tab "Browser"
?
2. Naviga su https://it.bidoo.com
?
3. Effettua login con username/password
?
4. OnWebViewNavigationCompleted() rileva login ?
?
5. Log: "[BROWSER] Rilevato cookie di sessione..."
?
6. Utente va su tab "Impostazioni"
?
7. Click "Importa da Browser"
?
8. ImportCookieFromWebView()
?? Estrae cookie completo dalla WebView ?
?? Aggiorna TextBox ?
?? Valida tramite SessionService ?
?? Salva automaticamente ?
?? Aggiorna banner utente ?
?
9. MessageBox: "Cookie importato e validato!"
?
10. ? Sessione attiva e salvata
```
**Vantaggi**:
- ?? **No DevTools**: Non serve aprire F12
- ?? **No copia/incolla**: Tutto automatico
- ? **Validazione immediata**: Cookie verificato subito
- ? **Salvataggio automatico**: Nessun passo extra
---
### Flusso 3: Rilevamento Automatico Nuovo Login
```
1. Utente ha già una sessione salvata (scaduta)
?
2. Va su tab "Browser"
?
3. Fa login su Bidoo
?
4. OnWebViewNavigationCompleted()
?? Estrae cookie dalla WebView ?
?? Confronta con cookie salvato ??
?? Cookie è diverso/nuovo ?
?
5. Log: "[BROWSER] Rilevato cookie di sessione..."
?
6. ?? Utente vede notifica nel log
?
7. Va su Impostazioni
?
8. Click "Importa da Browser"
?
9. ? Nuova sessione attiva
```
**Scenario d'uso**:
- Cookie scaduto
- Cambio account
- Nuova sessione dopo logout
---
## ?? Vantaggi della Soluzione
### 1. Performance ?
| Operazione | Prima | Dopo | Risparmio |
|-----------|-------|------|-----------|
| **Primo accesso Browser** | ~5-7s | ~0s | **~5-7s** |
| **Importazione Cookie** | Manuale (3-5 min) | Automatica (5s) | **~3-5 min** |
| **Setup completo** | ~10 min | ~2 min | **~8 min** |
### 2. Usabilità ??
**Prima** ?:
- Attesa inizializzazione browser
- Procedura manuale cookie complessa
- Possibili errori formato
**Dopo** ?:
- Browser immediatamente disponibile
- Click singolo per importare cookie
- Validazione automatica
### 3. Affidabilità ???
**Caratteristiche**:
- ? **Validazione automatica**: Cookie verificato prima del salvataggio
- ? **Formato garantito**: Estrazione programmatica (no errori umani)
- ? **Cookie completi**: Include tutti i cookie necessari
- ? **Rilevamento automatico**: Notifica quando disponibile nuovo cookie
### 4. Esperienza Utente ??
**Miglioramenti**:
- ?? **Startup più veloce**: Browser pronto prima che utente lo apra
- ?? **Notifiche intelligenti**: Sistema avvisa quando può importare cookie
- ?? **Sincronizzazione automatica**: Browser integrato e API usano stesso cookie
- ?? **Workflow semplificato**: Login browser ? Click importa ? Fatto
---
## ?? Test di Verifica
### Test 1: Pre-caricamento WebView ?
**Steps**:
1. Chiudi completamente applicazione
2. Riavvia applicazione
3. **Attendi 3 secondi** (tempo init WebView)
4. Click tab "Browser"
5. **Verifica**: Pagina Bidoo già caricata (no spinner, no attesa)
**Log attesi**:
```
[OK] AutoBidder v4.0 avviato
[BROWSER] Inizializzazione WebView2 in background...
[BROWSER] WebView2 inizializzato e pre-caricato
```
**Risultato atteso**: ? Browser immediatamente utilizzabile
---
### Test 2: Importazione Cookie con Successo ?
**Steps**:
1. Tab "Browser" ? Vai su https://it.bidoo.com
2. Effettua login (username + password)
3. Attendi homepage (dopo login)
4. Tab "Impostazioni"
5. Click "Importa da Browser"
6. **Verifica**:
- MessageBox: "Cookie importato e validato!"
- Banner mostra username e puntate
- TextBox cookie popolata
**Log attesi**:
```
[BROWSER] Rilevato cookie di sessione nel browser - usa 'Importa da Browser'
[BROWSER] Estrazione cookie dal browser...
[OK] Cookie importato e validato - Utente: username, Puntate: XX
```
**Risultato atteso**: ? Sessione attiva e salvata automaticamente
---
### Test 3: Importazione Senza Login ??
**Steps**:
1. Tab "Browser" ? Vai su https://it.bidoo.com (NO login)
2. Tab "Impostazioni"
3. Click "Importa da Browser"
4. **Verifica**:
- MessageBox di avviso
- Istruzioni chiare
**Log attesi**:
```
[BROWSER] Estrazione cookie dal browser...
[WARN] Nessun cookie trovato nel browser - assicurati di aver effettuato il login
```
**Risultato atteso**: ?? Messaggio chiaro con istruzioni
---
### Test 4: Browser Non Inizializzato ??
**Steps**:
1. Avvia applicazione
2. **Immediatamente** vai su tab "Impostazioni" (senza aspettare)
3. Click "Importa da Browser"
4. **Verifica**: Messaggio di attesa
**Log attesi**:
```
[WARN] Browser non inizializzato - attendi qualche secondo e riprova
```
**Risultato atteso**: ?? Messaggio indica di aspettare
---
### Test 5: Rilevamento Automatico Login ?
**Steps**:
1. Avvia applicazione (con WebView pre-caricata)
2. Tab "Browser"
3. Effettua login su Bidoo
4. **Verifica log**: Notifica automatica
**Log attesi**:
```
[BROWSER] Rilevato cookie di sessione nel browser - usa 'Importa da Browser' per utilizzarlo
```
**Risultato atteso**: ? Sistema rileva login e notifica utente
---
## ?? Architettura File
```
AutoBidder/
??? MainWindow.xaml.cs
? ??? Constructor: InitializeWebView2() chiamato
?
??? Core/
? ??? MainWindow.WebView.cs ? NUOVO FILE
? ? ??? InitializeWebView2()
? ? ??? OnWebViewNavigationCompleted()
? ? ??? GetCookieFromWebView()
? ? ??? ImportCookieFromWebView()
? ? ??? IsWebViewReady()
? ?
? ??? EventHandlers/
? ??? MainWindow.EventHandlers.Settings.cs
? ??? ImportCookieFromBrowserButton_Click() [AGGIORNATO]
?
??? Controls/
??? BrowserControl.xaml
??? EmbeddedWebView (WebView2)
```
---
## ?? Dettagli Tecnici
### WebView2 Runtime Requirements
**Prerequisiti**:
- ? WebView2 Runtime installato (automatico su Windows 11)
- ? Package NuGet: `Microsoft.Web.WebView2` (già presente)
### Cookie Manager API
```csharp
// API WebView2 per gestione cookie
var cookieManager = webView.CoreWebView2.CookieManager;
// Ottieni cookie per dominio
var cookies = await cookieManager.GetCookiesAsync("https://it.bidoo.com");
// Accedi a singolo cookie
var cookie = cookies.FirstOrDefault(c => c.Name == "__stattrb");
string name = cookie.Name;
string value = cookie.Value;
string domain = cookie.Domain;
string path = cookie.Path;
```
### Sincronizzazione Cookie
**Problema risolto**:
- WebView2 e HttpClient usano store cookie **separati**
- Cookie in WebView2 NON automaticamente disponibile per HttpClient
- Soluzione: Estrazione programmatica + init manuale HttpClient
**Implementazione**:
```csharp
// 1. Estrai da WebView
var cookieString = await GetCookieFromWebView();
// 2. Passa a SessionService
var result = await _sessionService.ValidateAndActivateSessionAsync(cookieString);
// 3. SessionService inizializza HttpClient con cookie
_apiClient.InitializeSessionWithCookie(cookieString, username);
```
---
## ?? Limitazioni e Note
### Limitazioni Conosciute
1. **WebView2 Runtime Required**
- ?? Utenti Windows 10 vecchi potrebbero non avere WebView2
- ? Gestito gracefully (log warning se non disponibile)
2. **Timing Init WebView**
- ?? Init richiede ~2-3 secondi
- ?? "Importa da Browser" disponibile solo dopo init
- ? Messaggio chiaro se cliccato troppo presto
3. **Cookie Security**
- ?? Cookie __stattrb è HttpOnly (non accessibile da JS)
- ? WebView2 CookieManager bypassa questa restrizione (API nativa)
- ? Cookie estratti in modo sicuro
### Best Practices
1. **Attendi Init Completa**
```csharp
if (!IsWebViewReady())
{
Log("[WARN] Attendi inizializzazione WebView...");
return;
}
```
2. **Gestisci Errori Gracefully**
```csharp
try
{
var cookie = await GetCookieFromWebView();
}
catch (Exception ex)
{
Log($"[WARN] Errore estrazione: {ex.Message}");
// Continue without cookie
}
```
3. **Valida Sempre Cookie Estratti**
```csharp
// Non assumere mai che cookie sia valido
var result = await _sessionService.ValidateAndActivateSessionAsync(cookie);
if (!result.Success)
{
// Handle invalid cookie
}
```
---
## ?? Vantaggi Architetturali
### 1. Separazione Concerns
| Responsabilità | File |
|----------------|------|
| **Pre-caricamento** | `MainWindow.WebView.cs` |
| **Estrazione cookie** | `MainWindow.WebView.cs` |
| **Validazione cookie** | `SessionService.cs` |
| **UI Event handlers** | `MainWindow.EventHandlers.Settings.cs` |
| **Storage cookie** | `SessionManager.cs` |
### 2. Riusabilità
```csharp
// Metodi pubblici riutilizzabili
public async Task<bool> ImportCookieFromWebView()
public bool IsWebViewReady()
```
### 3. Testabilità
```csharp
// Logica isolata, facile da testare
private async Task<string?> GetCookieFromWebView()
{
// Pura logica di estrazione
// No side effects
// Facile da unit test
}
```
---
## ? Conclusione
### Feature Implementate
? **Pre-caricamento WebView2**
- Browser inizializzato in background all'avvio
- Pagina Bidoo pre-caricata
- Tempo risparmiato: ~5-7 secondi
? **Estrazione Cookie Automatica**
- Click singolo per importare cookie
- Validazione automatica
- Salvataggio automatico
- Tempo risparmiato: ~3-5 minuti
? **Rilevamento Login Automatico**
- Sistema rileva quando utente fa login
- Notifica disponibilità cookie
- Workflow semplificato
### Build Status
? **Compilazione riuscita**
- Tutti i file compilano correttamente
- Nessun warning
- Tutte le dipendenze soddisfatte
### Impatto Utente
**Miglioramenti quantificabili**:
- ? **67% più veloce**: Primo accesso browser (5s ? 0s)
- ? **90% più veloce**: Setup cookie (5min ? 30s)
- ?? **100% più semplice**: No procedura manuale DevTools
- ?? **0 errori**: Cookie sempre nel formato corretto
---
**Data Implementazione**: 2025
**Versione**: 5.7+
**Feature 1**: Pre-caricamento WebView2 ?
**Feature 2**: Estrazione Cookie Automatica ?
**Status**: ? IMPLEMENTATO E TESTATO
## ?? Riferimenti
- `Core\MainWindow.WebView.cs` - Logica WebView e cookie
- `MainWindow.xaml.cs` - Init pre-caricamento
- `Core\EventHandlers\MainWindow.EventHandlers.Settings.cs` - UI handlers
- `Services\SessionService.cs` - Validazione cookie
- [WebView2 API Documentation](https://learn.microsoft.com/en-us/microsoft-edge/webview2/)
@@ -1,126 +0,0 @@
# ?? Fix: Colore Log Asta Schiarito
## ?? Problema
**Log asta singola** (pannello "Log Asta" in basso a destra) usava **blu scuro** (#007ACC) difficile da leggere su sfondo scuro (#1E1E1E).
## ? Soluzione
Cambiato colore da **#007ACC** (blu scuro) a **#64B4FF** (blu chiaro) per migliore contrasto e leggibilità.
---
## ?? Confronto
| Aspetto | Prima | Dopo |
|---------|-------|------|
| **Colore Hex** | #007ACC | #64B4FF |
| **RGB** | 0, 122, 204 | 100, 180, 255 |
| **Contrasto su #1E1E1E** | 3.2:1 (Passabile) | 5.8:1 (Buono) |
| **WCAG AA Compliance** | ? No (< 4.5:1) | ? Sì (> 4.5:1) |
| **Leggibilità** | Difficile | Facile ? |
---
## ?? File Modificato
**File**: `Core\MainWindow.UIUpdates.cs`
**Metodo**: `UpdateAuctionLog(AuctionViewModel auction)`
**Linea**: 26-27
### Prima ?
```csharp
else
color = new SolidColorBrush(Color.FromRgb(0, 122, 204)); // Blue (info)
```
### Dopo ?
```csharp
else
color = new SolidColorBrush(Color.FromRgb(100, 180, 255)); // Light Blue - #64B4FF (più chiaro e leggibile)
```
---
## ?? Palette Completa Log Asta
Ora **entrambi i log** (Globale + Asta) usano gli **stessi colori coerenti**:
| Tipo Log | Colore | Hex | RGB | Uso |
|----------|--------|-----|-----|-----|
| **Info** | Blu Chiaro | #64B4FF | 100, 180, 255 | Messaggi normali |
| **Success** | Verde | #00D800 | 0, 216, 0 | Operazioni riuscite |
| **Warn** | Giallo/Arancio | #FFB700 | 255, 183, 0 | Avvisi |
| **Error** | Rosso | #E81123 | 232, 17, 35 | Errori |
---
## ?? Esempio Visivo
### Prima ?
```
Log Asta (sfondo #1E1E1E):
--------------------
17:23:45 - [INFO] Polling asta... ? Blu scuro, difficile da leggere
17:23:46 - [OK] Prezzo aggiornato ? Verde, OK
17:23:47 - [WARN] Vicino al limite ? Giallo, OK
17:23:48 - [ERRORE] Connessione fallita ? Rosso, OK
```
### Dopo ?
```
Log Asta (sfondo #1E1E1E):
--------------------
17:23:45 - [INFO] Polling asta... ? Blu chiaro, facile da leggere ?
17:23:46 - [OK] Prezzo aggiornato ? Verde, OK
17:23:47 - [WARN] Vicino al limite ? Giallo, OK
17:23:48 - [ERRORE] Connessione fallita ? Rosso, OK
```
---
## ?? Coerenza UI
Ora **tutti i log** nell'applicazione usano lo **stesso colore blu chiaro** (#64B4FF):
1. ? **Log Globale** (pannello in alto a destra)
2. ? **Log Asta** (pannello in basso a destra)
**Benefici**:
- Aspetto coerente in tutta l'app
- Migliore leggibilità su sfondo scuro
- Rispetto standard WCAG AA per contrasto testo
---
## ?? Test Visivo
**Come testare**:
1. Avvia app
2. Aggiungi un'asta
3. Seleziona l'asta
4. Guarda pannello "Log Asta" in basso a destra
5. Verifica che i messaggi info siano **blu chiaro** e **facilmente leggibili**
**Confronta con**:
- Log Globale (in alto a destra) ? Stesso colore blu ?
- Messaggi Success/Warn/Error ? Colori invariati ?
---
**Data Fix**: 2025
**Versione**: 6.3+
**Issue**: Log asta con blu scuro poco leggibile
**Soluzione**: Cambiato a blu chiaro #64B4FF
**Status**: ? RISOLTO
## ?? File Coinvolti
- `Core\MainWindow.UIUpdates.cs` - UpdateAuctionLog (log asta)
- `Core\MainWindow.Logging.cs` - Log (log globale)
Entrambi ora usano lo stesso colore blu chiaro per coerenza UI.
@@ -1,388 +0,0 @@
# ? Fix Conteggio Puntate da Risposta Server
## ?? Problema Rilevato
Il sistema **contava manualmente** le puntate guardando quante volte il nome dell'utente compariva nella `BidHistory`, invece di usare i **dati ufficiali** che il server restituisce quando punti.
### ? Comportamento Precedente
```csharp
// Conta quante volte "Tu" appare nella history
public int MyClicks
{
get
{
var history = _auctionInfo.BidHistory;
return history.Count(h => h.EventType == BidEventType.MyBid);
}
}
```
**Problemi**:
- ? Non usa i dati ufficiali dal server
- ? Potrebbe essere impreciso se la history non è sincronizzata
- ? Non mostra le puntate residue totali
- ? Non tiene traccia delle puntate usate per asta specifica
---
## ?? Cosa Restituisce il Server
Quando punti con successo, il server Bidoo risponde con **9 campi** separati da `|`:
```
ok|<remainingBids>|<campo3>|<campo4>|<bidsUsedOnThisAuction>|<campo6>|<campo7>|<campo8>|<campo9>
```
**Esempio risposta reale**:
```
ok|47|xxx|xxx|1|xxx|xxx|xxx|xxx
```
**Campi importanti**:
- ? **Campo 1** (indice 0): "ok" - Conferma successo
- ?? **Campo 2** (indice 1): **Puntate residue totali** (es. 47)
- ?? **Campo 5** (indice 4): **Puntate usate su questa asta** (es. 1)
---
## ? Soluzione Implementata
### 1?? Aggiornato `BidResult` per Catturare i Dati
**File**: `Models/BidResult.cs`
Aggiunte proprietà per memorizzare le informazioni dal server:
```csharp
/// <summary>
/// Puntate residue totali dell'utente (da risposta server)
/// </summary>
public int? RemainingBids { get; set; }
/// <summary>
/// Puntate usate su questa specifica asta (da risposta server)
/// </summary>
public int? BidsUsedOnThisAuction { get; set; }
```
### 2?? Aggiornato `AuctionInfo` per Salvare i Dati
**File**: `Models/AuctionInfo.cs`
Aggiunte proprietà per tracciare:
```csharp
/// <summary>
/// Puntate residue totali dell'utente (aggiornate dopo ogni puntata su questa asta)
/// </summary>
[JsonPropertyName("RemainingBids")]
public int? RemainingBids { get; set; }
/// <summary>
/// Puntate usate specificamente su questa asta (da risposta server)
/// </summary>
[JsonPropertyName("BidsUsedOnThisAuction")]
public int? BidsUsedOnThisAuction { get; set; }
```
### 3?? Parsing della Risposta Server - CORRETTO
**File**: `Services/BidooApiClient.cs`
Modificato `PlaceBidAsync` per leggere i campi corretti:
```csharp
if (responseText.StartsWith("ok", StringComparison.OrdinalIgnoreCase))
{
result.Success = true;
var parts = responseText.Split('|');
Log($"[BID PARSE] Risposta completa: {responseText}", auctionId);
Log($"[BID PARSE] Numero totale campi: {parts.Length}", auctionId);
// ? Campo 2 (indice 1): Puntate residue totali
if (parts.Length > 1 && int.TryParse(parts[1], out var remaining))
{
result.RemainingBids = remaining;
_session.RemainingBids = remaining;
Log($"[BID SUCCESS] ? Puntate residue totali: {remaining}", auctionId);
}
// ? Campo 5 (indice 4): Puntate usate su questa asta
if (parts.Length > 4 && int.TryParse(parts[4], out var usedOnAuction))
{
result.BidsUsedOnThisAuction = usedOnAuction;
Log($"[BID SUCCESS] ? Puntate usate su questa asta: {usedOnAuction}", auctionId);
}
// Log tutti i campi per debugging
Log($"[BID PARSE DEBUG] Tutti i campi della risposta:", auctionId);
for (int i = 0; i < parts.Length; i++)
{
Log($" Campo {i+1} (indice {i}): '{parts[i]}'", auctionId);
}
}
```
### 4?? Aggiornamento dopo Puntata Automatica
**File**: `Services/AuctionMonitor.cs`
Modificato `ExecuteBid` per salvare i dati in `AuctionInfo`:
```csharp
// Esegui la puntata
var result = await _apiClient.PlaceBidAsync(auction.AuctionId, auction.OriginalUrl);
auction.LastClickAt = DateTime.UtcNow;
// Aggiorna dati puntate da risposta server
if (result.Success)
{
if (result.RemainingBids.HasValue)
{
auction.RemainingBids = result.RemainingBids.Value;
}
if (result.BidsUsedOnThisAuction.HasValue)
{
auction.BidsUsedOnThisAuction = result.BidsUsedOnThisAuction.Value;
}
}
```
### 5?? Aggiornamento dopo Puntata Manuale
**File**: `Core/MainWindow.Commands.cs`
Modificato `ExecuteGridBidAsync` per salvare i dati anche dalle puntate manuali:
```csharp
var result = await _auctionMonitor.PlaceManualBidAsync(vm.AuctionInfo);
// Aggiorna dati puntate da risposta server per puntata manuale
if (result.Success)
{
if (result.RemainingBids.HasValue)
{
vm.AuctionInfo.RemainingBids = result.RemainingBids.Value;
}
if (result.BidsUsedOnThisAuction.HasValue)
{
vm.AuctionInfo.BidsUsedOnThisAuction = result.BidsUsedOnThisAuction.Value;
}
// Notifica aggiornamento contatori per aggiornare la UI
vm.RefreshCounters();
}
```
### 6?? Aggiornato `AuctionViewModel.MyClicks`
**File**: `ViewModels/AuctionViewModel.cs`
Modificato per **prioritizzare i dati ufficiali del server** con fallback al conteggio manuale:
```csharp
// My clicks: priorità a dati ufficiali dal server, fallback a conteggio manuale
public int MyClicks
{
get
{
// ? Se disponibile, usa il dato ufficiale dal server (puntate usate su questa asta)
if (_auctionInfo.BidsUsedOnThisAuction.HasValue)
{
return _auctionInfo.BidsUsedOnThisAuction.Value;
}
// ?? Fallback: conta manualmente dalla history (comportamento precedente)
var history = _auctionInfo.BidHistory;
if (history == null) return 0;
BidHistory[] snapshot;
lock (history)
{
snapshot = history.ToArray();
}
return snapshot.Count(h => h != null && h.EventType == BidEventType.MyBid);
}
}
```
---
## ?? Comportamento Atteso
### ? Scenario 1: Prima Puntata
**Situazione**:
- Asta nuova, nessuna puntata ancora
**Azioni**:
1. Clicchi "Punta" (manuale) o la strategia punta automaticamente
2. Server risponde: `ok|150|199|1`
**Risultato**:
- ?? Prezzo: €1.50
- ?? Puntate residue totali: **199**
- ?? Puntate usate su questa asta: **1**
- ?? La colonna "Puntate" nella griglia mostra: **1**
### ? Scenario 2: Seconda Puntata
**Situazione**:
- Hai già puntato una volta
**Azioni**:
1. Punti di nuovo
2. Server risponde: `ok|175|198|2`
**Risultato**:
- ?? Prezzo: €1.75
- ?? Puntate residue totali: **198** (decrementato)
- ?? Puntate usate su questa asta: **2** (incrementato)
- ?? La colonna "Puntate" nella griglia mostra: **2**
### ? Scenario 3: Asta Salvata e Ricaricata
**Situazione**:
- Hai puntato 5 volte
- Chiudi l'applicazione
- Riapri l'applicazione
**Risultato**:
- ? La colonna "Puntate" mostra: **5** (salvato nel file JSON)
- ? Non serve ricontare dalla history
---
## ?? Vantaggi della Soluzione
### ?? 1. Dati Ufficiali e Precisi
- ? **Usa i dati direttamente dal server** (fonte di verità)
- ? Sempre sincronizzato con il server
- ? Nessun rischio di conteggio errato
### ?? 2. Persistenza Corretta
- ? I dati vengono salvati nel file JSON
- ? Ricaricando l'asta, i contatori sono corretti
- ? Non serve ricalcolare dalla history
### ?? 3. Aggiornamento Real-Time
- ? Aggiornamento immediato dopo ogni puntata
- ? Funziona per puntate automatiche E manuali
- ? La UI si aggiorna automaticamente con `RefreshCounters()`
### ?? 4. Monitoraggio Puntate Residue
- ? Puoi vedere quante puntate ti rimangono in totale
- ? Puoi vedere quante puntate hai usato per asta specifica
- ? Dati sempre aggiornati dopo ogni puntata
### ??? 5. Fallback Intelligente
- ? Se i dati del server non sono disponibili (vecchie aste), usa il conteggio manuale
- ? Compatibilità con aste salvate prima dell'aggiornamento
---
## ?? Log Migliorati
### Prima (solo conferma puntata):
```
[BID SUCCESS] Puntata piazzata
```
### Dopo (con dettagli):
```
[BID SUCCESS] Puntata piazzata - Puntate residue totali: 199
[BID SUCCESS] Puntate usate su questa asta: 5
```
---
## ?? Come Testare
### Test 1: Puntata Manuale
1. Aggiungi un'asta
2. Clicca "Punta" nella griglia
3. ? Verifica che la colonna "Puntate" si aggiorni immediatamente
4. ? Controlla il log per vedere: `Puntate usate su questa asta: X`
### Test 2: Puntata Automatica
1. Configura strategia (es. Anticipo = 200ms)
2. Avvia l'asta
3. Aspetta che la strategia punti automaticamente
4. ? Verifica che la colonna "Puntate" si aggiorni
5. ? Controlla il log per i dettagli
### Test 3: Puntate Multiple
1. Punta manualmente 5 volte
2. ? Verifica che il contatore passi da 1 ? 2 ? 3 ? 4 ? 5
3. ? Ogni volta controlla il log per conferma
### Test 4: Persistenza
1. Punta 3 volte
2. Chiudi l'applicazione
3. Riapri l'applicazione
4. ? Verifica che la colonna "Puntate" mostri ancora **3**
### Test 5: Puntate Residue Totali
1. Nota le tue puntate residue totali (es. 200)
2. Punta su un'asta
3. ? Nel log dovresti vedere: `Puntate residue totali: 199`
4. Punta di nuovo
5. ? Nel log dovresti vedere: `Puntate residue totali: 198`
---
## ?? File Modificati
| File | Modifiche |
|------|-----------|
| `Models/BidResult.cs` | ? Aggiunte proprietà `RemainingBids` e `BidsUsedOnThisAuction` |
| `Models/AuctionInfo.cs` | ? Aggiunte proprietà `RemainingBids` e `BidsUsedOnThisAuction` con serializzazione JSON |
| `Services/BidooApiClient.cs` | ?? Parsing risposta server per estrarre puntate residue e usate |
| `Services/AuctionMonitor.cs` | ?? Aggiornamento `AuctionInfo` dopo puntata automatica |
| `Core/MainWindow.Commands.cs` | ?? Aggiornamento `AuctionInfo` dopo puntata manuale + `RefreshCounters()` |
| `ViewModels/AuctionViewModel.cs` | ?? `MyClicks` ora usa dati server con fallback a conteggio manuale |
---
## ? Test di Verifica
- [x] Parsing risposta server funziona correttamente
- [x] Dati vengono salvati in `AuctionInfo` dopo puntata
- [x] `MyClicks` mostra il valore corretto dalla risposta server
- [x] Fallback a conteggio manuale per aste senza dati server
- [x] Puntate manuali aggiornano i contatori
- [x] Puntate automatiche aggiornano i contatori
- [x] `RefreshCounters()` aggiorna la UI immediatamente
- [x] Dati persistono dopo chiusura/riapertura app
- [x] Log mostrano informazioni dettagliate
- [x] Build compila senza errori
---
**Data Fix**: 2025
**Versione**: 4.1+
**Issue**: Conteggio puntate manuale invece di usare dati server
**Status**: ? RISOLTO
---
## ?? Riepilogo
### Prima:
- ? Conteggio manuale dalla `BidHistory`
- ? Non usa dati ufficiali dal server
- ? Possibili imprecisioni
### Dopo:
- ? Usa **dati ufficiali** dalla risposta server
- ? Mostra **puntate residue totali**
- ? Mostra **puntate usate per asta**
- ? Aggiornamento **real-time**
- ? **Persistenza** corretta
- ? **Fallback intelligente** per retrocompatibilità
@@ -1,387 +0,0 @@
# ?? Fix: Persistenza Storia Puntate (v2 - Aggiornato)
## ? Problema Rilevato
Il sistema **perdeva le puntate più vecchie** quando l'API restituiva solo le ultime ~10 puntate. Ad ogni polling, la lista `RecentBids` veniva **sostituita completamente** con le nuove puntate, perdendo quelle precedenti.
### ?? Comportamento Precedente
```csharp
// In AuctionMonitor.cs - PollAndProcessAuction()
if (state.RecentBidsHistory != null && state.RecentBidsHistory.Count > 0)
{
auction.RecentBids = state.RecentBidsHistory; // ?? SOSTITUISCE completamente!
}
```
**Problemi**:
- ? **Perdita dati**: Le puntate più vecchie non più presenti nell'API vengono perse
- ? **Storico incompleto**: L'utente vede solo le ultime ~10 puntate
- ? **Nessun confronto**: Non verifica se le puntate sono già presenti
- ? **Ordine sbagliato**: Puntate più vecchie in cima invece delle più recenti
- ? **BidderStats disconnesso**: Contatori utenti non sincronizzati con RecentBids
- ? **Nessuna persistenza**: Chiudendo/riaprendo si perdeva tutto
---
## ? Soluzione Implementata (v2)
### 1?? Ordine Inverso - Più Recenti in Cima
Le puntate sono ora ordinate in **ordine decrescente per timestamp**:
```csharp
// Ordina per timestamp DECRESCENTE (più recenti in cima)
auction.RecentBids = auction.RecentBids
.OrderByDescending(b => b.Timestamp)
.ToList();
```
**Risultato UI**:
```
??????????????????????????????????????????????
? STORIA PUNTATE (20/20) ?
??????????????????????????????????????????????
? 0.42 ? Auto ? 12:00:20 ? chamorro ? ? ULTIMA (più recente)
? 0.41 ? Auto ? 12:00:18 ? makrucco39 ?
? 0.40 ? Manuale ? 12:00:16 ? fedekikka... ?
? ... ? ... ? ... ? ... ?
? 0.23 ? Auto ? 11:59:40 ? sirbiet... ? ? PRIMA (più vecchia)
??????????????????????????????????????????????
```
### 2?? BidderStats Basato su RecentBids (Fonte Ufficiale)
**File**: `Services/AuctionMonitor.cs` - Nuovo metodo `UpdateBidderStatsFromRecentBids()`
```csharp
/// <summary>
/// Aggiorna le statistiche dei bidder basandosi sulla lista RecentBids (fonte ufficiale).
/// Raggruppa le puntate per utente e conta il numero di puntate per ciascuno.
/// </summary>
private void UpdateBidderStatsFromRecentBids(AuctionInfo auction)
{
// Raggruppa puntate per username
var bidsByUser = auction.RecentBids
.GroupBy(b => b.Username, StringComparer.OrdinalIgnoreCase)
.ToDictionary(
g => g.Key,
g => new
{
Count = g.Count(),
LastBidTime = DateTimeOffset.FromUnixTimeSeconds(g.Max(b => b.Timestamp)).DateTime
},
StringComparer.OrdinalIgnoreCase
);
// Aggiorna o crea BidderInfo per ogni utente
foreach (var kvp in bidsByUser)
{
var username = kvp.Key;
var stats = kvp.Value;
if (!auction.BidderStats.ContainsKey(username))
{
auction.BidderStats[username] = new BidderInfo
{
Username = username,
BidCount = stats.Count,
LastBidTime = stats.LastBidTime
};
}
else
{
existing.BidCount = stats.Count;
existing.LastBidTime = stats.LastBidTime;
}
}
// Rimuovi bidder che non sono più in RecentBids
var usersInRecentBids = new HashSet<string>(
auction.RecentBids.Select(b => b.Username),
StringComparer.OrdinalIgnoreCase
);
var usersToRemove = auction.BidderStats.Keys
.Where(u => !usersInRecentBids.Contains(u))
.ToList();
foreach (var user in usersToRemove)
{
auction.BidderStats.Remove(user);
}
}
```
**Chiamato dopo ogni merge**:
```csharp
// Aggiorna statistiche bidder basandosi su RecentBids
UpdateBidderStatsFromRecentBids(auction);
```
### 3?? Persistenza Completa
**File**: `Models/BidHistoryEntry.cs` - Serializzazione JSON
```csharp
[JsonPropertyName("Price")]
public decimal Price { get; set; }
[JsonPropertyName("BidType")]
public string BidType { get; set; }
[JsonPropertyName("Timestamp")]
public long Timestamp { get; set; }
[JsonPropertyName("Username")]
public string Username { get; set; }
// Proprietà calcolate non serializzate
[JsonIgnore]
public string TimeFormatted { get; }
[JsonIgnore]
public string PriceFormatted { get; }
[JsonIgnore]
public bool IsMyBid { get; set; } // Ripristinato al caricamento
```
**File**: `Models/AuctionInfo.cs` - RecentBids ora serializzato
```csharp
/// <summary>
/// Storia delle ultime puntate effettuate sull'asta (da API)
/// Questa è la fonte UFFICIALE per il conteggio puntate per utente
/// </summary>
[JsonPropertyName("RecentBids")]
public List<BidHistoryEntry> RecentBids { get; set; } = new List<BidHistoryEntry>();
```
### 4?? Ripristino IsMyBid al Caricamento
**File**: `Core/MainWindow.AuctionManagement.cs` - Metodo `LoadSavedAuctions()`
```csharp
// Ottieni username corrente dalla sessione per ripristinare IsMyBid
var session = _auctionMonitor.GetSession();
var currentUsername = session?.Username ?? string.Empty;
var auctions = Utilities.PersistenceManager.LoadAuctions();
foreach (var auction in auctions)
{
// ? NUOVO: Ripristina IsMyBid per tutte le puntate in RecentBids
if (auction.RecentBids != null && auction.RecentBids.Count > 0 && !string.IsNullOrEmpty(currentUsername))
{
foreach (var bid in auction.RecentBids)
{
bid.IsMyBid = bid.Username.Equals(currentUsername, StringComparison.OrdinalIgnoreCase);
}
}
// ...resto del caricamento...
}
```
---
## ?? Comportamento Completo
### ? Scenario 1: Prima Sessione (Asta Appena Avviata)
**Polling 1** (12:00:00):
- API restituisce: `[#100, #101, ..., #110]` (10 puntate)
- `RecentBids` = `[#110 ? #100]` (ordine decrescente, più recenti in cima)
- `BidderStats` = 3 utenti con conteggio aggiornato
**Polling 2** (12:00:10):
- API restituisce: `[#105, #106, ..., #115]` (10 puntate)
- **Merge**: Identifica #111-#115 come nuove
- `RecentBids` = `[#115 ? #100]` (15 puntate totali)
- `BidderStats` = Aggiornato automaticamente da RecentBids
**Polling 3** (12:00:20):
- API restituisce: `[#110, #111, ..., #120]` (10 puntate)
- **Merge**: Identifica #116-#120 come nuove
- `RecentBids` = `[#120 ? #100]` (20 puntate, limite raggiunto)
- `BidderStats` = Sincronizzato perfettamente
---
### ? Scenario 2: Chiusura e Riapertura Programma
**Stato Salvataggio**:
```json
{
"RecentBids": [
{"Price": 0.42, "BidType": "Auto", "Timestamp": 1764068204, "Username": "fedekikka2323"},
{"Price": 0.41, "BidType": "Auto", "Timestamp": 1764068194, "Username": "chamorro1984"},
...
]
}
```
**Al Riavvio**:
1. ? `RecentBids` viene caricato dal JSON
2. ? `IsMyBid` viene ripristinato per ogni puntata confrontando con username sessione
3. ? `BidderStats` viene **ricalcolato** da `RecentBids` al primo merge
4. ? **Tutto riprende** esattamente da dove era rimasto
---
## ?? Tab "Utenti" vs Tab "Storia Puntate"
### Tab "Utenti" (BidderStats)
**Fonte Dati**: `BidderStats` (aggiornato da `RecentBids`)
```
??????????????????????????????????????
? UTENTE ? PUNTATE ? ULTIMO ?
??????????????????????????????????????
? fedekikka23 ? 12 ? 12:00:20 ?
? chamorro1984 ? 8 ? 12:00:18 ?
? sirbiet... ? 5 ? 12:00:10 ?
??????????????????????????????????????
```
- **Aggregato**: Conta totale puntate per utente
- **Ordinabile**: Per nome, numero puntate, ultimo orario
- **Basato su**: `RecentBids` (fonte ufficiale)
### Tab "Storia Puntate" (RecentBids)
**Fonte Dati**: `RecentBids` (direttamente)
```
??????????????????????????????????????????????
? PREZZO ? MODALITÀ ? ORARIO ? UTENTE ?
?????????????????????????????????????????????
? 0.42 ? Auto ? 12:00:20 ? fedekikka ? ? Ultima
? 0.41 ? Auto ? 12:00:18 ? chamorro ?
? 0.40 ? Manuale ? 12:00:16 ? fedekikka ?
? 0.39 ? Auto ? 12:00:14 ? sirbiet... ?
??????????????????????????????????????????????
```
- **Cronologico**: Ordine temporale (più recenti in cima)
- **Dettagliato**: Prezzo, tipo, orario esatto
- **Evidenzia**: Tue puntate in verde
---
## ?? Sincronizzazione Perfetta
```
???????????????????????????????????????????
? API POLLING ?
? (Ultime ~10 puntate) ?
???????????????????????????????????????????
?
?
???????????????????????????????????????????
? MergeBidHistory() ?
? • Confronta con esistenti ?
? • Aggiunge solo nuove ?
? • Ordina DECRESCENTE ?
? • Limita a MaxBidHistoryEntries ?
???????????????????????????????????????????
?
?
???????????????????????????????????????????
? RecentBids ?
? [Puntata#120, Puntata#119, ..., #100] ? ? Fonte UFFICIALE
???????????????????????????????????????????
?
????????????????
? ?
? ?
????????????????????? ?????????????????????
? BidderStats ? ? UI Storia ?
? (Tab Utenti) ? ? (Tab Storia) ?
? ? ? ?
? • Conteggi ? ? • Cronologia ?
? • Ultimo orario ? ? • Dettagli ?
? • Sincronizzato ? ? • Evidenziato ?
????????????????????? ?????????????????????
```
---
## ?? Persistenza File JSON
### Esempio Salvataggio
```json
{
"AuctionId": "83110253",
"Name": "Apple iPhone 14",
"RecentBids": [
{
"Price": 0.42,
"BidType": "Auto",
"Timestamp": 1764068204,
"Username": "fedekikka2323"
},
{
"Price": 0.41,
"BidType": "Auto",
"Timestamp": 1764068194,
"Username": "chamorro1984"
}
]
}
```
### Al Caricamento
1. ? Deserializza `RecentBids` dal JSON
2. ? Ripristina `IsMyBid` confrontando username
3. ? `BidderStats` viene ricalcolato automaticamente al primo polling
---
## ?? Vantaggi Completi
| Vantaggio | Descrizione |
|-----------|-------------|
| ? **Storico Persistente** | Le puntate sopravvivono a chiusura/riapertura app |
| ? **Ordine Corretto** | Ultime puntate in cima (UI intuitiva) |
| ? **Fonte Ufficiale Unica** | `RecentBids` è l'unica fonte di verità |
| ? **Sincronizzazione Perfetta** | `BidderStats` sempre allineato con `RecentBids` |
| ? **Nessuna Perdita Dati** | Merge intelligente mantiene puntate vecchie |
| ? **Limite Configurabile** | `MaxBidHistoryEntries` nelle impostazioni |
| ? **Performance** | HashSet O(1) per deduplicazione |
| ? **IsMyBid Ripristinato** | Evidenziazione corretta dopo riavvio |
---
## ?? File Modificati
| File | Modifiche |
|------|-----------|
| `Models/BidHistoryEntry.cs` | ? Aggiunta serializzazione JSON |
| `Models/AuctionInfo.cs` | ? `RecentBids` ora serializzato |
| `Services/AuctionMonitor.cs` | ? Ordinamento DECRESCENTE |
| | ? Nuovo metodo `UpdateBidderStatsFromRecentBids()` |
| `Core/MainWindow.AuctionManagement.cs` | ? Ripristino `IsMyBid` al caricamento |
---
**Data Fix**: 2025
**Versione**: 7.7+
**Issue**: Storia puntate non persistente + ordine sbagliato + BidderStats disconnesso
**Status**: ? RISOLTO COMPLETAMENTE
---
## ?? Conclusione
Sistema **completo e robusto**:
1. ? **Persistenza**: Tutto salvato e ricaricato perfettamente
2. ? **Ordine**: Puntate più recenti in cima
3. ? **Sincronizzazione**: `BidderStats` basato su `RecentBids`
4. ? **Ripristino**: `IsMyBid` corretto dopo riavvio
5. ? **Performance**: Ottimizzato con HashSet
**Pronto per l'uso!** ??
@@ -1,288 +0,0 @@
# ?? Fix URL Browser - Campo Non Editabile
## Problema Rilevato
Nella scheda **Browser**:
1. ? L'**indirizzo URL** della pagina corrente **non era sempre visibile** nel campo in alto
2. ? Il campo era **editabile**, permettendo di inserire URL personalizzati (funzionalità non ancora implementata)
3. ? Il pulsante **"Vai"** era presente ma non funzionale
## Causa del Problema
Il `TextBox` `BrowserAddress` era configurato come campo editabile standard:
```xaml
<!-- ? PRIMA -->
<TextBox x:Name="BrowserAddress"
VerticalAlignment="Center"
BorderThickness="0"
Background="Transparent"
Foreground="#CCCCCC"
Padding="10,0"
FontSize="13"/>
<!-- Mancava IsReadOnly="True" -->
```
L'URL veniva aggiornato correttamente negli eventi `NavigationStarting` e `NavigationCompleted`, ma:
- Il campo era modificabile dall'utente
- Il pulsante "Vai" suggeriva una funzionalità non implementata
## Soluzione Implementata
### ? 1. Campo URL Non Editabile
Aggiunto `IsReadOnly="True"` al TextBox:
```xaml
<!-- ? DOPO -->
<TextBox x:Name="BrowserAddress"
VerticalAlignment="Center"
BorderThickness="0"
Background="Transparent"
Foreground="#CCCCCC"
Padding="10,0"
FontSize="13"
IsReadOnly="True"
Cursor="Arrow"
ToolTip="Indirizzo della pagina corrente (non editabile)"/>
```
**Caratteristiche**:
- ? `IsReadOnly="True"` - Non modificabile
- ? `Cursor="Arrow"` - Mostra cursore normale (non testo)
- ? `ToolTip` - Spiega che il campo è solo visualizzazione
### ? 2. Rimosso Pulsante "Vai"
Eliminato il pulsante "Vai" non necessario:
**Prima**:
```xaml
<Button x:Name="BrowserGoButton"
Content="Vai"
Click="BrowserGoButton_Click"/>
```
**Dopo**: Pulsante rimosso ?
### ? 3. Mantenuto Aggiornamento Automatico
L'URL viene ancora aggiornato automaticamente in `MainWindow.EventHandlers.Browser.cs`:
```csharp
private void EmbeddedWebView_NavigationStarting(...)
{
BrowserAddress.Text = e.Uri ?? string.Empty;
// ...
}
private void EmbeddedWebView_NavigationCompleted(...)
{
var uri = EmbeddedWebView?.Source?.ToString() ?? BrowserAddress.Text;
BrowserAddress.Text = uri;
// ...
}
```
## Comportamento Atteso
### ? Scenario 1: Navigazione Normale
1. Apri scheda **Browser**
2. Vai su `https://it.bidoo.com`
3. ? URL appare nel campo in alto: `https://it.bidoo.com/`
4. Clicca link in pagina ? Vai a `https://it.bidoo.com/auction.php?a=asta_12345`
5. ? URL si aggiorna automaticamente nel campo
### ? Scenario 2: Campo Non Editabile
1. Apri scheda **Browser**
2. Prova a cliccare nel campo URL
3. ? **Non puoi modificare** il testo
4. ? Cursore rimane freccia (non diventa testo)
5. ? Tooltip mostra: "Indirizzo della pagina corrente (non editabile)"
### ? Scenario 3: Navigazione con Pulsanti
1. Usa **"Indietro"** / **"Avanti"** / **"Ricarica"** / **"Home"**
2. ? URL si aggiorna automaticamente
3. ? Campo mostra sempre l'indirizzo corrente
### ? Scenario 4: Aggiunta Asta
1. Naviga su un'asta: `https://it.bidoo.com/auction.php?a=asta_12345`
2. ? URL visibile nel campo
3. Clicca **"Aggiungi Asta"**
4. ? L'URL dal campo viene usato per aggiungere l'asta
## Vantaggi della Soluzione
### ?? 1. UX Chiara
- ? **Prima**: Campo editabile ma funzionalità non implementata
- ? **Dopo**: Campo read-only, comportamento chiaro
### ?? 2. Nessuna Confusione
- ? **Prima**: Pulsante "Vai" che non faceva nulla
- ? **Dopo**: Solo funzionalità implementate visibili
### ?? 3. Visualizzazione Sempre Aggiornata
- ? URL aggiornato automaticamente ad ogni navigazione
- ? Sincronizzato con WebView2
### ?? 4. Preparato per Futuro
Se in futuro si implementa la navigazione manuale:
- Basta rimuovere `IsReadOnly="True"`
- Ri-aggiungere pulsante "Vai"
- Tutto il resto già funziona
## File Modificati
### 1. ? `Controls\BrowserControl.xaml`
**Modifiche**:
- Aggiunto `IsReadOnly="True"` a `BrowserAddress`
- Aggiunto `Cursor="Arrow"` per UX migliore
- Aggiunto `ToolTip` esplicativo
- Rimosso pulsante "Vai" (BrowserGoButton)
**Prima**:
```xaml
<TextBox x:Name="BrowserAddress" ... />
<Button x:Name="BrowserGoButton" Content="Vai" Click="BrowserGoButton_Click"/>
<Button x:Name="BrowserAddAuctionButton" Content="Aggiungi Asta" .../>
```
**Dopo**:
```xaml
<TextBox x:Name="BrowserAddress" IsReadOnly="True" Cursor="Arrow" ToolTip="..." />
<Button x:Name="BrowserAddAuctionButton" Content="Aggiungi Asta" .../>
```
### 2. ? `Controls\BrowserControl.xaml.cs`
**Modifiche**:
- Rimosso metodo `BrowserGoButton_Click`
- Evento `BrowserGoClickedEvent` lasciato per compatibilità (non usato)
### 3. ? `Core\EventHandlers\MainWindow.EventHandlers.Browser.cs`
**Modifiche**:
- Rimosso gestore `BrowserGoButton_Click`
- Mantenuti gestori `NavigationStarting` e `NavigationCompleted`
### 4. ? `MainWindow.xaml`
**Modifiche**:
- Rimosso binding `BrowserGoClicked="Browser_BrowserGoClicked"`
## Layout Browser
### Toolbar Nuovo
```
??????????????????????????????????????????????????????????????
? [Indietro] [Avanti] [Ricarica] [Home] ?URL? [Aggiungi] ?
??????????????????????????????????????????????????????????????
```
**Prima**:
```
[Indietro] [Avanti] [Ricarica] [Home] [URL editabile] [Vai] [Aggiungi]
```
**Dopo**:
```
[Indietro] [Avanti] [Ricarica] [Home] [URL read-only] [Aggiungi Asta]
```
## Note Tecniche
### Perché `IsReadOnly` invece di Disabilitato?
| Proprietà | Effetto | Pro | Contro |
|-----------|---------|-----|--------|
| `IsEnabled="False"` | ? Disabilitato | Chiaro che non è usabile | Testo grigio, difficile da leggere |
| `IsReadOnly="True"` | ? Read-only | Testo leggibile, copiabile | Potrebbe sembrare editabile |
**Scelta**: `IsReadOnly="True"` + `Cursor="Arrow"` + `ToolTip`
- ? Testo leggibile e copiabile
- ? Cursore chiarisce che non è editabile
- ? Tooltip spiega il comportamento
### Aggiornamento URL
L'URL viene aggiornato in **2 eventi**:
1. **`NavigationStarting`**: Quando inizia la navigazione
```csharp
BrowserAddress.Text = e.Uri ?? string.Empty;
```
2. **`NavigationCompleted`**: Quando la navigazione finisce
```csharp
BrowserAddress.Text = EmbeddedWebView?.Source?.ToString() ?? BrowserAddress.Text;
```
**Perché entrambi?**
- `NavigationStarting`: Mostra subito dove stai andando
- `NavigationCompleted`: Aggiorna con URL finale (dopo redirect)
## Funzionalità Future
### Se si vuole Navigazione Manuale
1. Rimuovi `IsReadOnly="True"` da BrowserAddress
2. Ri-aggiungi pulsante "Vai":
```xaml
<Button Content="Vai" Click="BrowserGoButton_Click"/>
```
3. Implementa gestore:
```csharp
private void BrowserGoButton_Click(...)
{
var url = BrowserAddress.Text?.Trim();
if (!url.StartsWith("http")) url = "https://" + url;
EmbeddedWebView?.CoreWebView2?.Navigate(url);
}
```
### Se si vuole Autocompletamento
1. Sostituisci `TextBox` con `ComboBox` editabile
2. Popola con cronologia navigazione
3. Usa `IsEditable="True"` + suggerimenti
---
## ? Test di Verifica
- [x] URL visibile nel campo in alto
- [x] URL si aggiorna automaticamente
- [x] Campo non editabile (IsReadOnly)
- [x] Cursore freccia (non testo)
- [x] Tooltip informativo
- [x] Pulsante "Vai" rimosso
- [x] Pulsante "Aggiungi Asta" funziona
- [x] Navigazione con Indietro/Avanti funziona
- [x] URL copiabile con Ctrl+C
---
**Data Fix**: 2025
**Versione**: 4.0+
**Issue**: URL Browser non visibile e editabile
**Status**: ? RISOLTO
## Riepilogo
**Prima**:
- ? URL non sempre visibile
- ? Campo editabile (ma non funzionante)
- ? Pulsante "Vai" non implementato
**Dopo**:
- ? URL **sempre visibile** e aggiornato
- ? Campo **read-only** (chiaro e leggibile)
- ? Solo funzionalità **implementate** disponibili
- ? UX pulita e coerente
@@ -1,282 +0,0 @@
# ? Fix: Errore Falso Positivo "OpenClipboard non riuscita"
## ?? Problema
Quando si clicciva su **"Copia URL"** nelle impostazioni dell'asta, appariva un errore nel log:
```
[10:12:53] [ERRORE] Copia link: OpenClipboard non riuscita. (0x800401D0 (CLIPBRD_E_CANT_OPEN))
```
**Sintomi**:
- ? Errore mostrato nel log globale
- ? **MA** l'URL veniva **correttamente copiato** negli appunti
- ?? Comportamento confuso per l'utente
- ?? Nessun controllo se un'asta era selezionata
---
## ?? Causa del Problema
### Problema 1: Errore Clipboard
L'errore `0x800401D0` (`CLIPBRD_E_CANT_OPEN`) si verifica quando:
1. **Clipboard occupato**: Un'altra applicazione sta usando il clipboard nello stesso momento
2. **Race condition**: Windows sta ancora processando un'operazione precedente sul clipboard
3. **Timing issue**: Il sistema non riesce ad aprire il clipboard immediatamente
### Problema 2: Nessun Controllo Selezione
Il codice non verificava se un'asta fosse selezionata prima di tentare la copia, causando:
- Eccezioni `NullReferenceException` se `_selectedAuction` era `null`
- Nessun feedback chiaro all'utente
**Codice Problematico**:
```csharp
try
{
var url = _selectedAuction.AuctionInfo.OriginalUrl; // ? Possibile NullReferenceException
Clipboard.SetText(url);
Log("URL copiato negli appunti", LogLevel.Success);
}
catch (Exception ex)
{
Log($"[ERRORE] Copia link: {ex.Message}", LogLevel.Error);
}
```
---
## ? Soluzione Implementata
### Fix 1: Controllo Selezione Asta
Aggiunto controllo all'inizio del metodo per verificare che un'asta sia selezionata:
```csharp
if (_selectedAuction == null)
{
MessageBox.Show(
"Seleziona un'asta dalla griglia prima di copiare l'URL.",
"Nessuna Asta Selezionata",
MessageBoxButton.OK,
MessageBoxImage.Information);
Log("[INFO] Tentativo di copia URL senza asta selezionata", LogLevel.Info);
return;
}
```
### Fix 2: Retry Mechanism per Clipboard
Implementato un **meccanismo di retry con delay** per gestire correttamente il caso del clipboard temporaneamente occupato.
**Caratteristiche**:
1. **Retry automatico**: Fino a 3 tentativi
2. **Delay breve**: 50ms tra ogni tentativo
3. **Gestione intelligente degli errori**:
- Identifica specificamente l'errore `CLIPBRD_E_CANT_OPEN`
- Riprova automaticamente per clipboard occupato
- Logga warning invece di errore se il testo è stato probabilmente copiato
4. **Nessun impatto UX**: L'utente non nota il retry (totale max 150ms)
### Codice Completo Implementato
```csharp
private void CopyAuctionUrlButton_Click(object sender, RoutedEventArgs e)
{
// ? NUOVO: Verifica selezione asta
if (_selectedAuction == null)
{
MessageBox.Show(
"Seleziona un'asta dalla griglia prima di copiare l'URL.",
"Nessuna Asta Selezionata",
MessageBoxButton.OK,
MessageBoxImage.Information);
Log("[INFO] Tentativo di copia URL senza asta selezionata", LogLevel.Info);
return;
}
var url = _selectedAuction.AuctionInfo.OriginalUrl;
if (string.IsNullOrEmpty(url))
url = $"https://it.bidoo.com/auction.php?a=asta_{_selectedAuction.AuctionId}";
// ? Tenta di copiare con retry mechanism
const int maxAttempts = 3;
const int delayMs = 50;
for (int attempt = 1; attempt <= maxAttempts; attempt++)
{
try
{
Clipboard.SetText(url);
Log("URL copiato negli appunti", LogLevel.Success);
return; // Successo, esci
}
catch (System.Runtime.InteropServices.COMException ex) when (ex.ErrorCode == unchecked((int)0x800401D0)) // CLIPBRD_E_CANT_OPEN
{
if (attempt < maxAttempts)
{
// Clipboard occupato, riprova dopo un breve delay
System.Threading.Thread.Sleep(delayMs);
continue;
}
// Ultimo tentativo fallito
Log($"[WARN] Clipboard temporaneamente occupato. Il testo potrebbe essere stato copiato.", LogLevel.Warn);
return;
}
catch (Exception ex)
{
// Altri errori
Log($"[ERRORE] Impossibile copiare URL: {ex.Message}", LogLevel.Error);
return;
}
}
}
```
---
## ?? Comportamento
### Prima della Fix
**Scenario 1: Nessuna Asta Selezionata**
1. Nessuna asta selezionata
2. Utente clicca **"Copia URL"**
3. ? Crash o eccezione `NullReferenceException`
4. ? Log: `[ERRORE] Copia link: Object reference not set...`
**Scenario 2: Clipboard Occupato**
1. Utente clicca **"Copia URL"**
2. ? Log mostra: `[ERRORE] Copia link: OpenClipboard non riuscita`
3. ? URL viene copiato correttamente
4. ?? Utente confuso: "C'è un errore ma funziona?"
---
### Dopo la Fix
**Scenario 1: Nessuna Asta Selezionata** ?
1. Nessuna asta selezionata
2. Utente clicca **"Copia URL"**
3. ? MessageBox: "Seleziona un'asta dalla griglia prima di copiare l'URL."
4. ?? Log: `[INFO] Tentativo di copia URL senza asta selezionata`
5. ?? Utente informato chiaramente
**Scenario 2: Successo al Primo Tentativo** ? (99% dei casi)
1. Asta selezionata
2. Utente clicca **"Copia URL"**
3. ? Log mostra: `URL copiato negli appunti` (verde)
4. ? URL copiato correttamente
5. ?? Utente felice
**Scenario 3: Clipboard Occupato** ? (1% dei casi)
1. Asta selezionata
2. Utente clicca **"Copia URL"**
3. ?? Tentativo 1 fallisce (clipboard occupato)
4. ? Attende 50ms
5. ?? Tentativo 2 riesce
6. ? Log mostra: `URL copiato negli appunti` (verde)
7. ? URL copiato correttamente
8. ?? Utente non nota nulla (totale 50ms)
**Scenario 4: Clipboard Persistentemente Occupato** ?? (rarissimo)
1. Asta selezionata
2. Utente clicca **"Copia URL"**
3. ?? Tentativo 1, 2, 3 falliscono
4. ?? Log mostra: `[WARN] Clipboard temporaneamente occupato. Il testo potrebbe essere stato copiato.`
5. ?? Utente informato in modo appropriato
---
## ?? Test di Verifica
### Test 1: Nessuna Asta Selezionata ?
**Passi**:
1. Avvia l'applicazione
2. Non selezionare nessuna asta (o deseleziona se già selezionata)
3. Clicca **"Copia URL"** nelle impostazioni
**Risultato Atteso**:
- ? MessageBox: "Seleziona un'asta dalla griglia prima di copiare l'URL."
- ? Log: `[INFO] Tentativo di copia URL senza asta selezionata`
- ? Nessun errore o crash
- ? Nessuna copia negli appunti
---
### Test 2: Copia con Asta Selezionata ?
**Passi**:
1. Seleziona un'asta dalla griglia
2. Clicca **"Copia URL"**
3. Incolla in Notepad (`Ctrl+V`)
**Risultato Atteso**:
- ? Log: `URL copiato negli appunti` (verde)
- ? URL corretto negli appunti
- ? Nessun errore
---
### Test 3: Copia con Clipboard Occupato ?
**Passi**:
1. Apri un'applicazione che usa intensivamente il clipboard
2. Seleziona un'asta
3. Fai molte operazioni di copia rapidamente nell'altra app
4. Durante le operazioni, clicca **"Copia URL"** in AutoBidder
5. Incolla in Notepad
**Risultato Atteso**:
- ? Log: `URL copiato negli appunti` (verde) OPPURE
- ?? Log: `[WARN] Clipboard temporaneamente occupato...` (giallo)
- ? URL probabilmente copiato
- ? **NESSUN** errore rosso
---
### Test 4: Copie Multiple con/senza Selezione ?
**Passi**:
1. Clicca **"Copia URL"** senza asta selezionata
2. Verifica messaggio
3. Seleziona un'asta
4. Clicca **"Copia URL"** 5 volte rapidamente
5. Deseleziona l'asta (clicca altrove)
6. Clicca **"Copia URL"** di nuovo
**Risultato Atteso**:
- Step 1-2: ? MessageBox "Seleziona un'asta..."
- Step 4: ? 5 messaggi `URL copiato negli appunti`
- Step 6: ? MessageBox "Seleziona un'asta..."
- ? Comportamento coerente
---
## ?? Log Esempi
### Nessuna Asta Selezionata
```
[10:12:50] [INFO] Tentativo di copia URL senza asta selezionata
```
? + MessageBox informativo
---
### Copia Normale (Asta Selezionata)
```
[10:12:53] URL copiato negli appunti
[10:12:54] URL copiato negli appunti
[10:12:55] URL copiato negli appunti
```
? Tutto funziona perfettamente!
---
### Clipboard Temporaneamente Occupato
```
[10:12:53] URL copiato negli appunti
[10:12:54] [WARN] Clipboard temporaneamente occupato. Il testo potrebbe essere stato copiato.
[10:12:55] URL copiato negli appunti
```
@@ -1,398 +0,0 @@
# ?? Fix: Cookie Caricato ma Dati Utente Non Visualizzati
## ?? Problema Rilevato
**Sintomi**:
- ? Cookie salvato correttamente in `session.dat`
- ? Cookie visualizzato nella TextBox Impostazioni
- ? Dati utente NON caricati all'avvio (username, puntate, credito)
- ? Banner utente vuoto all'avvio dell'applicazione
- ? Dopo aver salvato manualmente il cookie ? dati utente appaiono correttamente
---
## ?? Causa del Problema
Il problema era nel metodo `LoadSavedSession()` in `Core\MainWindow.UserInfo.cs`.
### Codice Problematico
```csharp
// ? PROBLEMA: Regex manipolava il cookie in modo errato
private void LoadSavedSession()
{
var session = SessionManager.LoadSession();
if (session != null && session.IsValid)
{
if (!string.IsNullOrEmpty(session.CookieString))
{
// ? QUESTO ERA CORRETTO: inizializza con cookie completo
_auctionMonitor.InitializeSessionWithCookie(session.CookieString, session.Username);
}
// ? PROBLEMA: Mostrava solo una parte del cookie nella UI
try
{
if (!string.IsNullOrEmpty(session.CookieString))
{
// ? Regex estraeva solo __stattrb=VALUE (senza altri cookie)
var m = System.Text.RegularExpressions.Regex.Match(
session.CookieString,
"__stattrb=([^;]+)"
);
// ? Logica invertita: mostrava solo valore se NON c'erano ;
if (m.Success && !session.CookieString.Contains(";"))
{
SettingsCookieTextBox.Text = m.Groups[1].Value; // Solo valore
}
else
{
SettingsCookieTextBox.Text = session.CookieString; // Stringa completa
}
}
}
catch { }
}
}
```
### Perché Causava il Problema
1. **Stringa Cookie Salvata**: `"__stattrb=xxx; altri_cookie=yyy; ..."`
2. **Regex**: Cercava di estrarre solo il valore di `__stattrb`
3. **Logica Invertita**: Il controllo `!session.CookieString.Contains(";")` era **invertito**
- Se il cookie conteneva `;` (caso normale) ? mostrava la stringa completa ?
- Se il cookie NON conteneva `;` (caso raro) ? mostrava solo il valore estratto ?
4. **Risultato**: A volte veniva mostrato un cookie incompleto o manipolato
5. **Impatto**:
- Il cookie veniva inizializzato nel monitor ?
- Ma poteva essere corrotto o incompleto in UI ?
- Questo poteva causare problemi nel caricamento dati utente
---
## ? Soluzione Implementata
**File**: `Core\MainWindow.UserInfo.cs`
### Nuovo Codice Corretto
```csharp
private void LoadSavedSession()
{
try
{
var session = SessionManager.LoadSession();
if (session != null && session.IsValid)
{
// ? Ripristina sessione nel monitor con il cookie COMPLETO
if (!string.IsNullOrEmpty(session.CookieString))
{
_auctionMonitor.InitializeSessionWithCookie(session.CookieString, session.Username);
// ? Mostra il cookie COMPLETO nella TextBox delle impostazioni
try
{
SettingsCookieTextBox.Text = session.CookieString;
}
catch { }
}
else if (!string.IsNullOrEmpty(session.AuthToken))
{
// Fallback per sessioni vecchie che usavano solo AuthToken
var cookieString = $"__stattrb={session.AuthToken}";
_auctionMonitor.InitializeSessionWithCookie(cookieString, session.Username);
try
{
SettingsCookieTextBox.Text = session.AuthToken;
}
catch { }
}
StartButton.IsEnabled = true;
Log($"[OK] Sessione ripristinata per: {session.Username}");
// ? Verifica validità cookie (background) - USA HTML come metodo principale
Task.Run(async () =>
{
try
{
// Prova prima HTML scraping (più affidabile)
var htmlUser = await _auctionMonitor.GetUserDataFromHtmlAsync();
if (htmlUser != null && !string.IsNullOrEmpty(htmlUser.Username))
{
Dispatcher.Invoke(() =>
{
SetUserBanner(htmlUser.Username, htmlUser.RemainingBids);
Log($"[OK] Dati utente rilevati via HTML - Utente: {htmlUser.Username}, Puntate residue: {htmlUser.RemainingBids}");
});
return; // Successo con HTML
}
// Fallback: prova API
var success = await _auctionMonitor.UpdateUserInfoAsync();
var updatedSession = _auctionMonitor.GetSession();
Dispatcher.Invoke(() =>
{
if (success && updatedSession != null && !string.IsNullOrEmpty(updatedSession.Username))
{
SetUserBanner(updatedSession.Username, updatedSession.RemainingBids);
Log($"[OK] Cookie valido - Crediti disponibili: {updatedSession.RemainingBids}");
}
else
{
Log($"[WARN] Impossibile verificare sessione: verifica cookie nelle Impostazioni");
}
});
}
catch (Exception ex)
{
Dispatcher.Invoke(() =>
{
Log($"[WARN] Errore verifica sessione: {ex.Message}");
});
}
});
}
else
{
Log("[INFO] Nessuna sessione salvata trovata");
Log("[INFO] Usa 'Configura Sessione' per inserire il cookie");
}
}
catch (Exception ex)
{
Log($"[WARN] Errore caricamento sessione: {ex.Message}");
}
}
```
---
## ?? Flusso Corretto
### Avvio Applicazione
```
1. MainWindow() Constructor
?
2. LoadSavedSession()
?
3. SessionManager.LoadSession()
?? Carica session.dat (crittografato DPAPI)
?? Restituisce BidooSession con CookieString COMPLETO
?
4. InitializeSessionWithCookie(session.CookieString, session.Username)
?? Imposta cookie nel HttpClient ?
?? Cookie COMPLETO: "__stattrb=xxx; altri=yyy; ..."
?
5. SettingsCookieTextBox.Text = session.CookieString
?? Mostra cookie COMPLETO in UI ?
?
6. Task.Run() - Verifica validità in background
?? GetUserDataFromHtmlAsync() (PRINCIPALE)
? ?? Scarica HTML e estrae dati utente via regex
?? UpdateUserInfoAsync() (FALLBACK se HTML fallisce)
?? Chiama API per dati utente
?
7. SetUserBanner(username, remainingBids)
?? Aggiorna header (puntate, credito)
?? Aggiorna sidebar (username, email, ID)
?
? Dati utente visualizzati correttamente
```
---
## ?? Confronto Prima/Dopo
| Aspetto | Prima ? | Dopo ? |
|---------|----------|---------|
| **Cookie salvato** | Stringa completa | Stringa completa |
| **Cookie caricato in Monitor** | Completo ? | Completo ? |
| **Cookie mostrato in UI** | ? Manipolato con regex | ? Completo come salvato |
| **Dati utente caricati** | ? A volte falliva | ? Sempre caricati |
| **Banner utente** | ? Vuoto all'avvio | ? Popolato all'avvio |
| **Log di successo** | ? Spesso "WARN" | ? "[OK] Dati utente rilevati" |
---
## ?? Test di Verifica
### Test 1: Avvio con Sessione Salvata
**Steps**:
1. ? Assicurati di aver salvato un cookie valido
2. ? Chiudi completamente l'applicazione
3. ? Riapri l'applicazione
4. ? **Verifica immediata**:
- Header mostra numero puntate corrette
- Header mostra credito Bidoo Shop
- Sidebar mostra username
- Sidebar mostra email e ID utente
5. ? **Verifica Log**:
```
[OK] Sessione ripristinata per: username
[OK] Dati utente rilevati via HTML - Utente: username, Puntate residue: XX
```
6. ? Vai su Impostazioni
7. ? **Verifica**: Cookie completo visualizzato nella TextBox
**Risultato atteso**: ? Tutti i dati utente caricati correttamente all'avvio
---
### Test 2: Cookie con Multipli Valori
**Steps**:
1. ? Inserisci un cookie con formato: `"__stattrb=xxx; altro_cookie=yyy; terzo=zzz"`
2. ? Clicca **Salva**
3. ? Chiudi e riapri l'applicazione
4. ? **Verifica**: Dati utente caricati correttamente
5. ? Vai su Impostazioni
6. ? **Verifica**: Cookie completo visualizzato: `"__stattrb=xxx; altro_cookie=yyy; terzo=zzz"`
**Risultato atteso**: ? Cookie salvato e ripristinato senza manipolazioni
---
### Test 3: Cookie Solo __stattrb
**Steps**:
1. ? Inserisci un cookie con formato semplice: `"__stattrb=xxx"`
2. ? Clicca **Salva**
3. ? Chiudi e riapri l'applicazione
4. ? **Verifica**: Dati utente caricati correttamente
5. ? Vai su Impostazioni
6. ? **Verifica**: Cookie visualizzato: `"__stattrb=xxx"`
**Risultato atteso**: ? Cookie salvato e ripristinato correttamente
---
## ?? Lezioni Apprese
### 1. Non Manipolare i Dati Salvati
```csharp
// ? SBAGLIATO: Manipola i dati durante il caricamento
var savedData = Storage.Load();
var extractedValue = Regex.Match(savedData, pattern).Groups[1].Value;
UI.Text = extractedValue; // Valore manipolato
// ? CORRETTO: Usa i dati esattamente come salvati
var savedData = Storage.Load();
UI.Text = savedData; // Valore originale intatto
```
**Motivo**: Qualsiasi manipolazione (regex, substring, trim) può causare:
- Perdita di informazioni
- Corruzione dei dati
- Comportamenti imprevedibili
---
### 2. Principio "Save What You See, Load What You Save"
```csharp
// ? PATTERN CORRETTO
// Salvataggio
Storage.Save(UI.Text); // Salva esattamente quello che vedi
// Caricamento
UI.Text = Storage.Load(); // Carica esattamente quello che hai salvato
```
**Evita**:
- Trasformazioni durante il salvataggio
- Manipolazioni durante il caricamento
- Logiche condizionali complesse basate sul formato
---
### 3. Regex per Validazione, NON per Trasformazione
```csharp
// ? USO CORRETTO: Validazione
var cookie = UI.Text;
if (Regex.IsMatch(cookie, @"__stattrb=[a-zA-Z0-9]+"))
{
Storage.Save(cookie); // Salva valore originale
}
// ? USO SBAGLIATO: Trasformazione
var cookie = UI.Text;
var match = Regex.Match(cookie, @"__stattrb=([^;]+)");
Storage.Save(match.Groups[1].Value); // Salva valore estratto (SBAGLIATO)
```
---
### 4. Log per Debug
Aggiungi log dettagliati per capire cosa viene salvato/caricato:
```csharp
// ? Log di debug durante caricamento
var session = SessionManager.LoadSession();
Log($"[DEBUG] Cookie caricato: lunghezza={session.CookieString?.Length}, formato={session.CookieString?.Substring(0, Math.Min(50, session.CookieString.Length))}...");
// ? Log di debug durante salvataggio
SessionManager.SaveSession(session);
Log($"[DEBUG] Cookie salvato: lunghezza={session.CookieString?.Length}");
```
---
## ?? Modifiche Implementate
### File: `Core\MainWindow.UserInfo.cs`
**Modifiche**:
1. ? **Rimossa la regex** che manipolava il cookie
2. ? **Rimosso il controllo condizionale** `!session.CookieString.Contains(";")`
3. ? **Caricamento diretto**: `SettingsCookieTextBox.Text = session.CookieString;`
4. ? **Mantenuto fallback** per vecchie sessioni con solo `AuthToken`
**Righe modificate**: ~20 righe
**Righe rimosse**: ~10 righe (regex e logica condizionale)
**Righe aggiunte**: ~2 righe (commenti esplicativi)
---
## ? Conclusione
### Problema Risolto
- ? **Prima**: Cookie manipolato con regex ? dati utente a volte non caricati
- ? **Dopo**: Cookie caricato intatto ? dati utente sempre caricati correttamente
### Benefici
- ? **Affidabilità**: Dati utente sempre visualizzati all'avvio
- ? **Semplicità**: Codice più semplice senza regex complesse
- ? **Manutenibilità**: Meno logica condizionale = meno bug
- ? **Prevedibilità**: Comportamento consistente in tutti i casi
### Status
?? **FIX COMPLETATO CON SUCCESSO**
---
**Data Fix**: 2025
**Versione**: 5.4+
**Issue**: Cookie salvato ma dati utente non caricati all'avvio
**Causa**: Regex manipolava il cookie durante il caricamento
**Soluzione**: Rimossa manipolazione, caricamento diretto del cookie salvato
**Status**: ? RISOLTO
## ?? Riferimenti
- `Services\SessionManager.cs` - Sistema di persistenza sessione
- `Core\MainWindow.UserInfo.cs` - Gestione info utente e banner
- `Documentation\FIX_COOKIE_PERSISTENCE.md` - Fix precedente persistenza cookie
- `Documentation\REFACTORING_SETTINGS_PERSISTENCE.md` - Refactoring sistema impostazioni
@@ -1,404 +0,0 @@
# ?? Fix: Cookie Non Salvato nelle Impostazioni
## ?? Problema Rilevato
Il cookie di autenticazione **non persisteva** tra le sessioni dell'applicazione. Ogni volta che si chiudeva e riapriva l'applicazione, il cookie doveva essere reinserito manualmente, nonostante fosse stato salvato correttamente.
### Sintomi
- ? Cookie salvato correttamente (log: `[OK] Cookie valido per utente: Username`)
- ? Sessione funzionante durante l'esecuzione
- ? Cookie NON visualizzato nella TextBox quando si riapre l'applicazione
- ? Cookie NON visualizzato quando si apre il tab Impostazioni
- ? Cookie NON visualizzato dopo aver cliccato "Annulla"
### Altre Impostazioni Funzionanti
- ? Anticipo puntata
- ? Prezzo min/max
- ? Max clicks
- ? Stati iniziali aste
- ? Limiti log
- ? Impostazioni export
---
## ?? Causa del Problema
Il cookie viene salvato e caricato da **due sistemi separati**:
1. **`SessionManager`** (file: `session.dat` crittografato)
- Salva la sessione completa incluso il cookie
- File location: `%AppData%\AutoBidder\session.dat`
- Crittografia DPAPI di Windows
2. **`SettingsManager`** (file: `settings.json`)
- Salva le altre impostazioni (defaults, export, ecc.)
- File location: `%LocalAppData%\AutoBidder\settings.json`
- Formato JSON in chiaro
### Il Problema Specifico
```csharp
// ? PROBLEMA 1: Cookie NON caricato all'avvio
private void LoadDefaultSettings()
{
var settings = SettingsManager.Load();
// Carica tutte le impostazioni TRANNE il cookie
DefaultBidBeforeDeadlineMs.Text = settings.DefaultBidBeforeDeadlineMs.ToString();
// ...
// ? MANCAVA: Caricamento del cookie da SessionManager
}
// ? PROBLEMA 2: Cookie NON caricato quando si apre tab Impostazioni
private void TabImpostazioni_Checked(object sender, RoutedEventArgs e)
{
ShowPanel(Settings);
// ? MANCAVA: Caricamento del cookie
}
// ? PROBLEMA 3: "Annulla" svuotava il cookie invece di ripristinarlo
private void CancelCookieButton_Click(object sender, RoutedEventArgs e)
{
SettingsCookieTextBox.Text = string.Empty; // ? SBAGLIATO
}
```
---
## ? Soluzione Implementata
### 1?? Caricamento Cookie all'Avvio
**File**: `Core\EventHandlers\MainWindow.EventHandlers.Settings.cs`
```csharp
private void LoadDefaultSettings()
{
try
{
var settings = SettingsManager.Load();
// Carica tutte le altre impostazioni...
DefaultBidBeforeDeadlineMs.Text = settings.DefaultBidBeforeDeadlineMs.ToString();
// ...
// ? NUOVO: Carica il cookie salvato nella TextBox
var session = Services.SessionManager.LoadSession();
if (session != null && !string.IsNullOrEmpty(session.CookieString))
{
SettingsCookieTextBox.Text = session.CookieString;
}
}
catch (Exception ex)
{
Log($"[ERRORE] Caricamento impostazioni: {ex.Message}", LogLevel.Error);
}
}
```
**Quando viene chiamato**: All'avvio dell'applicazione (nel costruttore `MainWindow()`)
### 2?? Caricamento Cookie all'Apertura Tab Impostazioni
**File**: `Core\MainWindow.ControlEvents.cs`
```csharp
private void TabImpostazioni_Checked(object sender, RoutedEventArgs e)
{
ShowPanel(Settings);
// ? NUOVO: Carica il cookie salvato quando si apre il tab Impostazioni
try
{
var session = Services.SessionManager.LoadSession();
if (session != null && !string.IsNullOrEmpty(session.CookieString))
{
SettingsCookieTextBox.Text = session.CookieString;
}
}
catch { }
}
```
**Quando viene chiamato**: Ogni volta che l'utente clicca sul tab "Impostazioni"
### 3?? Ripristino Cookie sul pulsante "Annulla"
**File**: `Core\EventHandlers\MainWindow.EventHandlers.Settings.cs`
```csharp
// ? PRIMA (SBAGLIATO)
private void CancelCookieButton_Click(object sender, RoutedEventArgs e)
{
SettingsCookieTextBox.Text = string.Empty; // Svuota il cookie
}
// ? DOPO (CORRETTO)
private void CancelCookieButton_Click(object sender, RoutedEventArgs e)
{
// Ricarica il cookie salvato invece di svuotarlo
var session = Services.SessionManager.LoadSession();
if (session != null && !string.IsNullOrEmpty(session.CookieString))
{
SettingsCookieTextBox.Text = session.CookieString;
}
else
{
SettingsCookieTextBox.Text = string.Empty;
}
}
```
**Quando viene chiamato**: Quando l'utente clicca "Annulla" nella sezione cookie
---
## ?? Flusso Completo
### Avvio Applicazione
```
1. MainWindow()
?
2. LoadDefaultSettings()
?
3. SettingsManager.Load() ? Carica settings.json
4. SessionManager.LoadSession() ? Carica session.dat
?
5. SettingsCookieTextBox.Text = session.CookieString
?
? Cookie visualizzato all'avvio
```
### Apertura Tab Impostazioni
```
1. Utente clicca tab "Impostazioni"
?
2. TabImpostazioni_Checked()
?
3. SessionManager.LoadSession() ? Carica session.dat
?
4. SettingsCookieTextBox.Text = session.CookieString
?
? Cookie sempre visualizzato
```
### Salvataggio Cookie
```
1. Utente inserisce cookie
2. Clicca "Salva"
?
3. SaveCookieButton_Click()
?
4. _auctionMonitor.InitializeSessionWithCookie(cookie)
5. UpdateUserInfoAsync() ? Valida cookie
?
6. SessionManager.SaveSession(session) ? Salva su session.dat
?
? Cookie salvato e persistente
```
### Annulla Modifiche
```
1. Utente modifica cookie (ma non salva)
2. Clicca "Annulla"
?
3. CancelCookieButton_Click()
?
4. SessionManager.LoadSession() ? Ricarica session.dat
?
5. SettingsCookieTextBox.Text = session.CookieString
?
? Cookie ripristinato al valore salvato
```
---
## ?? Confronto Prima/Dopo
| Scenario | Prima ? | Dopo ? |
|----------|----------|---------|
| **Avvio app** | Cookie vuoto | Cookie caricato da `session.dat` |
| **Apertura tab Impostazioni** | Cookie vuoto | Cookie caricato da `session.dat` |
| **Salvataggio** | Cookie salvato | Cookie salvato (invariato) |
| **Annulla** | Cookie svuotato | Cookie ripristinato da `session.dat` |
| **Chiusura app** | Cookie perso | Cookie mantenuto in `session.dat` |
| **Riapertura app** | Devi reinserire | Cookie già presente |
---
## ?? Test di Verifica
### Test 1: Persistenza Cookie
1. ? Apri applicazione
2. ? Vai su Impostazioni
3. ? Inserisci cookie valido
4. ? Clicca **Salva**
5. ? **Verifica**: Log `[OK] Cookie valido per utente: Username`
6. ? **Chiudi** applicazione
7. ? **Riapri** applicazione
8. ? Vai su Impostazioni
9. ? **Verifica**: Cookie è presente nella TextBox
### Test 2: Apertura Tab
1. ? Hai già salvato un cookie
2. ? Apri applicazione
3. ? Vai su tab **Aste Attive** (non Impostazioni)
4. ? Vai su tab **Impostazioni**
5. ? **Verifica**: Cookie è visualizzato
### Test 3: Annulla Modifiche
1. ? Vai su Impostazioni (cookie presente)
2. ? Modifica il cookie (aggiungi caratteri a caso)
3. ? Clicca **Annulla**
4. ? **Verifica**: Cookie torna al valore salvato (non vuoto)
### Test 4: Workflow Completo
1. ? Prima apertura ? Cookie vuoto
2. ? Inserisci cookie ? Clicca Salva
3. ? Chiudi e riapri ? Cookie presente
4. ? Modifica cookie ? Clicca Annulla ? Cookie ripristinato
5. ? Chiudi e riapri ? Cookie ancora presente
6. ? Cambia tab ? Torna su Impostazioni ? Cookie ancora presente
---
## ??? File Modificati
### 1. `Core\EventHandlers\MainWindow.EventHandlers.Settings.cs`
**Modifiche**:
- ? `LoadDefaultSettings()`: Aggiunto caricamento cookie da `SessionManager`
- ? `CancelCookieButton_Click()`: Cambiato da svuotamento a ripristino
**Righe modificate**: ~15 righe
### 2. `Core\MainWindow.ControlEvents.cs`
**Modifiche**:
- ? `TabImpostazioni_Checked()`: Aggiunto caricamento cookie all'apertura tab
**Righe modificate**: ~10 righe
---
## ?? Lezioni Apprese
### 1. Sistemi di Persistenza Separati
Quando si hanno **due sistemi di storage separati** (come `SessionManager` e `SettingsManager`), bisogna:
- ? Documentare chiaramente **cosa** salva **dove**
- ? Assicurarsi che il caricamento acceda al sistema corretto
- ? Non confondere i due sistemi
### 2. UI Sync con Storage
L'UI deve essere **sincronizzata** con lo storage in tre momenti:
1. **Avvio applicazione** (constructor o initialization)
2. **Apertura pannello** (tab change, window load)
3. **Annulla modifiche** (ripristino da storage)
### 3. Pattern Corretto
```csharp
// ? PATTERN CORRETTO per caricare dati in UI
private void LoadUIFromStorage()
{
try
{
// 1. Carica da storage appropriato
var data = StorageSystem.Load();
// 2. Verifica che i dati esistano
if (data != null && !string.IsNullOrEmpty(data.Value))
{
// 3. Popola UI
UIControl.Text = data.Value;
}
else
{
// 4. Fallback se dati non esistono
UIControl.Text = string.Empty;
}
}
catch (Exception ex)
{
// 5. Log errori
Log($"[ERRORE] Caricamento: {ex.Message}", LogLevel.Error);
}
}
```
### 4. "Annulla" = "Ripristina", NON "Svuota"
```csharp
// ? SBAGLIATO: Annulla = Svuota
private void Cancel_Click()
{
TextBox.Text = string.Empty;
}
// ? CORRETTO: Annulla = Ripristina da storage
private void Cancel_Click()
{
var saved = Storage.Load();
TextBox.Text = saved?.Value ?? string.Empty;
}
```
---
## ?? Struttura Storage
```
%AppData%\AutoBidder\
??? session.dat ? SessionManager (crittografato DPAPI)
? ??? Cookie, Username, RemainingBids
?
%LocalAppData%\AutoBidder\
??? settings.json ? SettingsManager (JSON)
? ??? DefaultBidBeforeDeadlineMs
? ??? DefaultMinPrice
? ??? DefaultMaxPrice
? ??? ExportPath
? ??? ...tutte le altre impostazioni
?
??? auctions.json ? PersistenceManager (JSON)
??? Lista aste salvate
```
---
## ?? Note Importanti
### Sicurezza Cookie
- ? Il cookie è crittografato con **DPAPI** (Windows Data Protection API)
- ? Solo l'utente corrente può decrittare `session.dat`
- ? Il cookie NON è salvato in `settings.json` (che è in chiaro)
### Compatibilità
- ? Se `session.dat` non esiste, il cookie sarà vuoto (primo avvio)
- ? Se il file è corrotto, viene ignorato e l'utente deve reinserire il cookie
- ? Nessun crash se i file non esistono
### Performance
- ? `SessionManager.LoadSession()` è veloce (legge file piccolo)
- ? Viene chiamato solo quando necessario (avvio, apertura tab, annulla)
- ? Non impatta le performance generali
---
**Data Fix**: 2025
**Versione**: 5.2+
**Issue**: Cookie non persisteva tra sessioni
**Causa**: Cookie mai caricato nella TextBox UI
**Soluzione**: Caricamento esplicito da `SessionManager.LoadSession()`
**Status**: ? RISOLTO
## ?? Riferimenti
- Vedi anche: `Services\SessionManager.cs` per dettagli storage sessione
- Vedi anche: `Utilities\SettingsManager.cs` per altre impostazioni
- Vedi anche: `Documentation\FIX_SETTINGS_SAVE_AND_LOGGING.md` per logging
@@ -1,430 +0,0 @@
# ?? Fix: Cookie Funziona Solo Dopo Salvataggio Manuale
## ?? Problema Rilevato
**Sintomi**:
- ? Cookie salvato correttamente in `session.dat`
- ? Cookie visualizzato nella TextBox Impostazioni
- ? **All'avvio**: "Impossibile leggere HTML" ? dati utente NON caricati
- ? **Dopo "Salva" (senza modifiche)**: Cookie funziona e dati utente appaiono
**Comportamento Anomalo**:
```
1. Avvio applicazione
?
2. Cookie caricato da session.dat ?
?
3. Tentativo lettura HTML bids_history.php ?
?
4. ERRORE: "Impossibile leggere HTML"
?
5. Dati utente NON visualizzati ?
--- MA SE CLICCO "SALVA" NELLE IMPOSTAZIONI ---
6. Clic su "Salva" (senza modificare nulla)
?
7. UpdateUserInfoAsync() chiamato ?
?
8. Cookie FUNZIONA improvvisamente ?
?
9. Dati utente visualizzati correttamente ?
```
---
## ?? Causa del Problema
### Analisi del Flusso
#### All'Avvio (`LoadSavedSession()`)
```csharp
// ? PROBLEMA: Cookie non "attivato" lato server
private void LoadSavedSession()
{
var session = SessionManager.LoadSession();
// 1. Inizializza cookie nel client HTTP ?
_auctionMonitor.InitializeSessionWithCookie(session.CookieString, session.Username);
// 2. Verifica in background
Task.Run(async () =>
{
// ? PROBLEMA: Va direttamente a HTML scraping
var htmlUser = await _auctionMonitor.GetUserDataFromHtmlAsync();
// Usa: https://it.bidoo.com/bids_history.php
// ? FALLISCE: bids_history.php richiede sessione attiva server-side
// Fallback: prova API
var success = await _auctionMonitor.UpdateUserInfoAsync();
// Usa: https://it.bidoo.com/buy_bids.php
// ? QUESTO FUNZIONA, ma viene chiamato DOPO il fallimento
});
}
```
#### Quando Salvi (`SaveCookieButton_Click()`)
```csharp
// ? FUNZIONA: Cookie "attivato" correttamente
private async void SaveCookieButton_Click(object sender, RoutedEventArgs e)
{
var cookie = SettingsCookieTextBox.Text;
// 1. Inizializza cookie nel client HTTP ?
_auctionMonitor.InitializeSessionWithCookie(cookie, string.Empty);
// 2. ? CHIAVE: Chiama SUBITO UpdateUserInfoAsync
var success = await _auctionMonitor.UpdateUserInfoAsync();
// Usa: https://it.bidoo.com/buy_bids.php
// ? QUESTO "ATTIVA" IL COOKIE LATO SERVER
// Ora bids_history.php funzionerà anche
}
```
### Il Problema Tecnico
**`bids_history.php` richiede una sessione "calda" lato server**:
1. **Cookie nel browser**: Quando usi il browser, ogni caricamento pagina "riscalda" la sessione server
2. **Cookie nell'app**: All'avvio, il cookie è "freddo" - il server non ha ancora creato lo stato di sessione
3. **`buy_bids.php`**: Questa pagina **inizializza la sessione server-side** (crea stato, valida cookie, ecc.)
4. **`bids_history.php`**: Questa pagina **assume che la sessione sia già attiva**
**Quindi**:
- ? All'avvio: `bids_history.php` chiamato per primo ? sessione non inizializzata ? ERRORE
- ? Dopo "Salva": `buy_bids.php` chiamato per primo ? sessione inizializzata ? `bids_history.php` funziona
---
## ? Soluzione Implementata
**File**: `Core\MainWindow.UserInfo.cs`
### Cambiamento nel `LoadSavedSession()`
```csharp
// ? DOPO IL FIX
Task.Run(async () =>
{
try
{
// ? NUOVO: PRIMA chiama UpdateUserInfoAsync per "attivare" il cookie
// Questo è necessario perché buy_bids.php inizializza la sessione server-side
Log("[INFO] Attivazione cookie tramite buy_bids.php...", LogLevel.Info);
var activationSuccess = await _auctionMonitor.UpdateUserInfoAsync();
if (activationSuccess)
{
var activatedSession = _auctionMonitor.GetSession();
if (activatedSession != null && !string.IsNullOrEmpty(activatedSession.Username))
{
Dispatcher.Invoke(() =>
{
SetUserBanner(activatedSession.Username, activatedSession.RemainingBids);
Log($"[OK] Cookie attivato e validato - Utente: {activatedSession.Username}, Puntate: {activatedSession.RemainingBids}");
});
return; // ? Successo immediato
}
}
// Fallback: prova HTML scraping (ora il cookie è attivato)
Log("[WARN] UpdateUserInfoAsync non ha restituito dati, provo HTML scraping...", LogLevel.Warn);
var htmlUser = await _auctionMonitor.GetUserDataFromHtmlAsync();
if (htmlUser != null && !string.IsNullOrEmpty(htmlUser.Username))
{
Dispatcher.Invoke(() =>
{
SetUserBanner(htmlUser.Username, htmlUser.RemainingBids);
Log($"[OK] Dati utente rilevati via HTML - Utente: {htmlUser.Username}, Puntate residue: {htmlUser.RemainingBids}");
});
return;
}
// Se entrambi i metodi falliscono
Dispatcher.Invoke(() =>
{
Log($"[WARN] Impossibile verificare sessione: verifica cookie nelle Impostazioni");
});
}
catch (Exception ex)
{
Dispatcher.Invoke(() =>
{
Log($"[WARN] Errore verifica sessione: {ex.Message}");
});
}
});
```
---
## ?? Nuovo Flusso Corretto
### Avvio Applicazione
```
1. MainWindow() Constructor
?
2. LoadSavedSession()
?? Carica session.dat ?
?? InitializeSessionWithCookie(cookie) ?
?
3. Task.Run() - Verifica validità in background
?
4. ? NUOVO: UpdateUserInfoAsync() PRIMA
?? GET https://it.bidoo.com/buy_bids.php
?? ? Inizializza sessione server-side
?
5. Se successo:
?? Estrae username, puntate, email, ID, credito
?? SetUserBanner() ? ? Dati visualizzati
?
6. Se fallisce:
?? Fallback a GetUserDataFromHtmlAsync()
?? GET https://it.bidoo.com/bids_history.php
?? Ora funziona perché sessione è "calda" ?
?
? Dati utente sempre visualizzati correttamente
```
### Quando Salvi Cookie (comportamento invariato)
```
1. Clic "Salva"
?
2. InitializeSessionWithCookie(cookie) ?
?
3. UpdateUserInfoAsync()
?? GET https://it.bidoo.com/buy_bids.php
?? Inizializza sessione + estrae dati ?
?
4. SetUserBanner() ? ? Dati visualizzati
```
---
## ?? Confronto Prima/Dopo
| Scenario | Prima ? | Dopo ? |
|----------|----------|---------|
| **Avvio app** | HTML scraping fallisce | UpdateUserInfoAsync attiva cookie |
| **Ordine chiamate** | HTML ? API (fallback) | API ? HTML (fallback) |
| **Stato sessione** | "Fredda" ? errore | "Calda" ? successo |
| **Dati visualizzati** | ? Solo dopo "Salva" | ? Subito all'avvio |
| **Log avvio** | "Impossibile leggere HTML" | "[OK] Cookie attivato" |
| **Necessità "Salva"** | ?? Obbligatorio | ? Non necessario |
---
## ?? Test di Verifica
### Test 1: Avvio con Sessione Salvata
**Steps**:
1. ? Assicurati di aver salvato un cookie valido
2. ? Chiudi completamente l'applicazione
3. ? Riapri l'applicazione
4. ? **Verifica immediata** (entro 5 secondi):
- Header mostra numero puntate corrette
- Header mostra credito Bidoo Shop
- Sidebar mostra username, email, ID
5. ? **Verifica Log**:
```
[OK] Sessione ripristinata per: username
[INFO] Attivazione cookie tramite buy_bids.php...
[OK] Cookie attivato e validato - Utente: username, Puntate: XX
```
6. ? **NON** dovrebbe esserci:
- "Impossibile leggere HTML"
- "Impossibile verificare sessione"
**Risultato atteso**: ? Dati utente caricati SENZA bisogno di "Salva"
---
### Test 2: Cookie Scaduto
**Steps**:
1. ? Inserisci un cookie scaduto o non valido
2. ? Salva
3. ? Chiudi e riapri l'applicazione
4. ? **Verifica Log**:
```
[OK] Sessione ripristinata per: (vuoto o vecchio username)
[INFO] Attivazione cookie tramite buy_bids.php...
[WARN] UpdateUserInfoAsync non ha restituito dati, provo HTML scraping...
[WARN] Impossibile verificare sessione: verifica cookie nelle Impostazioni
```
5. ? Banner utente rimane vuoto o mostra dati vecchi
**Risultato atteso**: ? Messaggi di errore chiari, no crash
---
### Test 3: Primo Avvio (Nessuna Sessione)
**Steps**:
1. ? Elimina `%AppData%\AutoBidder\session.dat`
2. ? Avvia applicazione
3. ? **Verifica Log**:
```
[INFO] Nessuna sessione salvata trovata
[INFO] Usa 'Configura Sessione' per inserire il cookie
```
4. ? Banner utente vuoto
5. ? Vai su Impostazioni ? inserisci cookie ? Salva
6. ? **Verifica**: Dati utente appaiono immediatamente
**Risultato atteso**: ? Comportamento corretto per primo utilizzo
---
## ?? Lezioni Apprese
### 1. Ordine delle Chiamate API Importa
```csharp
// ? SBAGLIATO: Endpoint che assume sessione attiva chiamato per primo
var htmlData = await GetUserDataFromHtmlAsync(); // bids_history.php
var apiData = await UpdateUserInfoAsync(); // buy_bids.php (fallback)
// ? CORRETTO: Endpoint che inizializza sessione chiamato per primo
var apiData = await UpdateUserInfoAsync(); // buy_bids.php (principale)
var htmlData = await GetUserDataFromHtmlAsync(); // bids_history.php (fallback)
```
---
### 2. Sessioni Server-Side Hanno Stati
**Stati di sessione**:
1. **Fredda** (Cookie presente ma server non ha stato):
- Cookie valido nel client ?
- Server non ha inizializzato session data ?
- Alcuni endpoint falliscono ??
2. **Calda** (Cookie + stato server attivo):
- Cookie valido nel client ?
- Server ha session data attiva ?
- Tutti gli endpoint funzionano ??
**Come riscaldare**:
- Chiamare un endpoint che **crea/valida la sessione** (es. `buy_bids.php`)
- POI chiamare endpoint che **assumono sessione esistente** (es. `bids_history.php`)
---
### 3. Pattern: Warmup + Fallback
```csharp
// ? PATTERN CORRETTO
async Task<UserData> GetUserDataWithWarmup()
{
// 1. WARMUP: Attiva sessione con endpoint principale
var primaryData = await GetDataFromPrimaryEndpoint(); // buy_bids.php
if (primaryData != null) return primaryData;
// 2. FALLBACK: Ora la sessione è calda, possiamo usare altri endpoint
var fallbackData = await GetDataFromFallbackEndpoint(); // bids_history.php
if (fallbackData != null) return fallbackData;
// 3. FAILURE: Se entrambi falliscono
return null;
}
```
**Principio**:
- Endpoint **principale** = quello che inizializza + restituisce dati
- Endpoint **fallback** = quello che assume sessione già attiva
---
### 4. Debug di Sessioni HTTP
**Strumenti per diagnosticare**:
```csharp
// ? Log dettagliati per capire il flusso
Log("[INFO] Tentativo attivazione cookie...");
var success = await UpdateUserInfoAsync();
if (success)
{
Log("[OK] Cookie attivato e validato");
}
else
{
Log("[WARN] Attivazione fallita, provo fallback...");
var fallback = await GetUserDataFromHtmlAsync();
if (fallback != null)
{
Log("[OK] Fallback riuscito (sessione ora attiva)");
}
else
{
Log("[ERROR] Sia primario che fallback falliti");
}
}
```
**Indicatori**:
- "Impossibile leggere HTML" ? Sessione fredda
- "Cookie attivato" ? Sessione calda
- "Fallback riuscito" ? Primario ha riscaldato la sessione
---
## ?? Modifiche Implementate
### File: `Core\MainWindow.UserInfo.cs`
**Modifiche**:
1. ? **Invertito ordine** chiamate: `UpdateUserInfoAsync()` **prima** di `GetUserDataFromHtmlAsync()`
2. ? **Log esplicativo**: "Attivazione cookie tramite buy_bids.php..."
3. ? **Successo immediato**: Se `UpdateUserInfoAsync()` funziona, non serve fallback
4. ? **Fallback migliorato**: HTML scraping solo se API primaria fallisce (ma ora sessione è calda)
5. ? **Messaggio chiaro**: "[OK] Cookie attivato e validato" invece di messaggi criptici
**Righe modificate**: ~40 righe
**Righe aggiunte**: ~15 righe (log e commenti esplicativi)
**Logica invertita**: Sì (API first, HTML fallback invece di viceversa)
---
## ? Conclusione
### Problema Risolto
- ? **Prima**: Cookie "freddo" all'avvio ? HTML scraping fallisce ? dati non caricati
- ? **Dopo**: Cookie "attivato" con `buy_bids.php` ? sessione calda ? dati sempre caricati
### Benefici
- ? **Funzionamento immediato**: Dati utente all'avvio senza "Salva"
- ? **Più robusto**: Fallback HTML funziona perché sessione è già attiva
- ? **Log chiari**: Messaggi esplicativi per diagnosticare problemi
- ? **Esperienza utente**: Non serve più "Salva" manuale per attivare cookie
### Status
?? **FIX COMPLETATO CON SUCCESSO**
---
**Data Fix**: 2025
**Versione**: 5.5+
**Issue**: Cookie funziona solo dopo "Salva" manuale
**Causa**: Sessione server non inizializzata all'avvio (chiamata diretta a bids_history.php)
**Soluzione**: Chiama UpdateUserInfoAsync (buy_bids.php) PRIMA per "attivare" la sessione
**Status**: ? RISOLTO
## ?? Riferimenti
- `Core\MainWindow.UserInfo.cs` - Gestione sessione e banner utente
- `Services\BidooApiClient.cs` - Client HTTP con metodi `UpdateUserInfoAsync()` e `GetUserDataFromHtmlAsync()`
- `Documentation\FIX_COOKIE_LOADING_USER_DATA.md` - Fix precedente caricamento cookie
- `Documentation\FIX_COOKIE_PERSISTENCE.md` - Fix persistenza cookie
@@ -1,297 +0,0 @@
# ?? CORREZIONE FINALE - Indici Campi Risposta Bidoo
## ?? Formato Risposta Server CORRETTO
Il server Bidoo restituisce **9 campi** separati da `|`:
```
ok|<remainingBids>|<campo3>|<campo4>|<bidsUsedOnThisAuction>|<campo6>|<campo7>|<campo8>|<campo9>
```
### Esempio Risposta Reale:
```
ok|47|xxx|xxx|1|xxx|xxx|xxx|xxx
```
### Mappatura Campi:
| Campo | Indice | Contenuto | Uso |
|-------|--------|-----------|-----|
| 1 | 0 | `ok` | Conferma successo |
| **2** | **1** | `47` | **?? Puntate residue totali** |
| 3 | 2 | `xxx` | Dato non utilizzato |
| 4 | 3 | `xxx` | Dato non utilizzato |
| **5** | **4** | `1` | **?? Puntate usate su questa asta** |
| 6 | 5 | `xxx` | Dato non utilizzato |
| 7 | 6 | `xxx` | Dato non utilizzato |
| 8 | 7 | `xxx` | Dato non utilizzato |
| 9 | 8 | `xxx` | Dato non utilizzato |
---
## ? Correzione Implementata
### Prima (ERRATO)
```csharp
// ? SBAGLIATO - Leggeva indici 2 e 3
if (parts.Length > 2 && int.TryParse(parts[2], out var remaining))
{
result.RemainingBids = remaining;
}
if (parts.Length > 3 && int.TryParse(parts[3], out var usedOnAuction))
{
result.BidsUsedOnThisAuction = usedOnAuction;
}
```
### Dopo (CORRETTO)
```csharp
// ? CORRETTO - Legge indici 1 e 4
if (parts.Length > 1 && int.TryParse(parts[1], out var remaining))
{
result.RemainingBids = remaining; // Campo 2 (indice 1)
_session.RemainingBids = remaining;
Log($"[BID SUCCESS] ? Puntate residue totali: {remaining}", auctionId);
}
if (parts.Length > 4 && int.TryParse(parts[4], out var usedOnAuction))
{
result.BidsUsedOnThisAuction = usedOnAuction; // Campo 5 (indice 4)
Log($"[BID SUCCESS] ? Puntate usate su questa asta: {usedOnAuction}", auctionId);
}
```
---
## ?? Logging Dettagliato Aggiunto
Per facilitare il debugging, ora il log mostra:
1. **Risposta completa** del server
2. **Numero totale campi** parsati
3. **Ogni campo specifico** che viene letto
4. **Tutti i campi** con indici e valori
### Esempio Log Completo:
```
[BID PARSE] Risposta completa: ok|47|xxx|xxx|1|xxx|xxx|xxx|xxx
[BID PARSE] Numero totale campi: 9
[BID PARSE] Campo 2 (indice 1) - Remaining bids: '47'
[BID SUCCESS] ? Puntate residue totali: 47
[BID PARSE] Campo 5 (indice 4) - Bids used on auction: '1'
[BID SUCCESS] ? Puntate usate su questa asta: 1
[BID PARSE DEBUG] Tutti i campi della risposta:
Campo 1 (indice 0): 'ok'
Campo 2 (indice 1): '47'
Campo 3 (indice 2): 'xxx'
Campo 4 (indice 3): 'xxx'
Campo 5 (indice 4): '1'
Campo 6 (indice 5): 'xxx'
Campo 7 (indice 6): 'xxx'
Campo 8 (indice 7): 'xxx'
Campo 9 (indice 8): 'xxx'
[BANNER UPDATE] Puntate residue aggiornate: 47
```
---
## ?? Comportamento Corretto
### Test 1: Prima Puntata
**Azioni**:
1. Punta su un'asta (Puntate residue prima: 48)
2. Server risponde: `ok|47|xxx|xxx|1|xxx|xxx|xxx|xxx`
**Risultato Atteso**:
- ? Campo 2 (indice 1) letto: `47`
- ? Campo 5 (indice 4) letto: `1`
- ? Banner "Puntate" aggiornato: `48` ? `47`
- ? Colonna "Clicks" aggiornata: `0` ? `1`
### Test 2: Seconda Puntata
**Azioni**:
1. Punta di nuovo (Puntate residue prima: 47)
2. Server risponde: `ok|46|xxx|xxx|2|xxx|xxx|xxx|xxx`
**Risultato Atteso**:
- ? Campo 2 (indice 1) letto: `46`
- ? Campo 5 (indice 4) letto: `2`
- ? Banner "Puntate" aggiornato: `47` ? `46`
- ? Colonna "Clicks" aggiornata: `1` ? `2`
### Test 3: Puntate Multiple
**Sequenza**:
```
Puntata 1: ok|47|xxx|xxx|1|... ? Clicks: 1, Puntate: 47
Puntata 2: ok|46|xxx|xxx|2|... ? Clicks: 2, Puntate: 46
Puntata 3: ok|45|xxx|xxx|3|... ? Clicks: 3, Puntate: 45
Puntata 4: ok|44|xxx|xxx|4|... ? Clicks: 4, Puntate: 44
```
---
## ?? Come Verificare la Correzione
### Passo 1: Controlla i Log
Dopo una puntata, cerca nel log:
```
[BID PARSE] Numero totale campi: 9
```
? **Se vedi 9 campi** = formato risposta corretto
? **Se vedi altro numero** = formato risposta diverso dal previsto
### Passo 2: Verifica Parsing Campi
Cerca:
```
[BID PARSE] Campo 2 (indice 1) - Remaining bids: 'XX'
[BID SUCCESS] ? Puntate residue totali: XX
```
? **Se vedi questo** = campo 2 letto correttamente
```
[BID PARSE] Campo 5 (indice 4) - Bids used: 'X'
[BID SUCCESS] ? Puntate usate su questa asta: X
```
? **Se vedi questo** = campo 5 letto correttamente
### Passo 3: Verifica Aggiornamento UI
Dopo la puntata, controlla:
1. **Banner "Puntate"** in alto
- ? Deve decrementare immediatamente
- ? Valore deve corrispondere al campo 2 della risposta
2. **Colonna "Clicks"** nella griglia
- ? Deve incrementare immediatamente
- ? Valore deve corrispondere al campo 5 della risposta
---
## ?? Troubleshooting
### Problema: Banner Non Si Aggiorna
**Verifica nel log**:
```
[BID PARSE] Campo 2 (indice 1) - Remaining bids: 'XX'
[BID SUCCESS] ? Puntate residue totali: XX
```
- ? **Log presente** = Parsing OK, problema UI binding
- ? **Log mancante** = Parsing FALLITO
**Se parsing fallito, cerca**:
```
[BID PARSE WARN] ?? Impossibile parsare campo 2
```
**Causa**: Il campo 2 non contiene un numero
**Soluzione**: Guarda `[BID PARSE DEBUG] Tutti i campi` e verifica quale campo contiene le puntate residue
### Problema: Clicks Rimane a 0
**Verifica nel log**:
```
[BID PARSE] Campo 5 (indice 4) - Bids used: 'X'
[BID SUCCESS] ? Puntate usate su questa asta: X
```
- ? **Log presente** = Parsing OK, problema UI
- ? **Log mancante** = Parsing FALLITO
**Se parsing fallito, cerca**:
```
[BID PARSE ERROR] ? Risposta non ha campo 5
```
**Causa**: La risposta ha meno di 5 campi
**Soluzione**:
1. Controlla `[BID PARSE] Numero totale campi: X`
2. Se X < 5, il server non restituisce abbastanza campi
3. Guarda `[BID PARSE DEBUG] Tutti i campi` per vedere quale campo contiene il contatore
---
## ?? File Modificati
| File | Modifiche |
|------|-----------|
| `Services/BidooApiClient.cs` | ?? Corretto parsing: campo 2 (indice 1) e campo 5 (indice 4) |
| `Services/BidooApiClient.cs` | ? Aggiunto logging dettagliato per debugging |
| `Documentation/FIX_BID_COUNT_FROM_SERVER.md` | ?? Aggiornato con indici corretti |
| `Documentation/FIX_UI_UPDATE_AFTER_BID.md` | ?? Aggiornato con indici corretti |
---
## ? Checklist Verifica
Prima di chiudere l'issue, verifica:
- [ ] Log mostra `Numero totale campi: 9`
- [ ] Log mostra `Campo 2 (indice 1) - Remaining bids: 'XX'`
- [ ] Log mostra `Campo 5 (indice 4) - Bids used: 'X'`
- [ ] Log mostra `? Puntate residue totali: XX`
- [ ] Log mostra `? Puntate usate su questa asta: X`
- [ ] Banner "Puntate" si aggiorna immediatamente
- [ ] Colonna "Clicks" si aggiorna immediatamente
- [ ] Valori corrispondono alla risposta del server
- [ ] Nessun warning/errore di parsing
- [ ] Build compila senza errori
---
**Data Fix**: 2025-01-23
**Versione**: 4.1+
**Issue**: Indici campi risposta server errati
**Status**: ? RISOLTO
---
## ?? Riepilogo Completo
### Problema Originale:
- ? Clicks mostra sempre 0
- ? Banner puntate non si aggiorna
- ? Parsing leggeva campi sbagliati (indici 2 e 3 invece di 1 e 4)
### Soluzione Finale:
- ? **Campo 2 (indice 1)**: Puntate residue totali
- ? **Campo 5 (indice 4)**: Puntate usate su questa asta
- ? Logging dettagliato per debugging
- ? Aggiornamento immediato UI (banner + clicks)
- ? Thread UI corretto per `RefreshCounters()`
- ? `UpdateRemainingBidsDisplay()` chiamato dopo ogni puntata
### Formato Risposta Server:
```
ok|<campo2>|<campo3>|<campo4>|<campo5>|<campo6>|<campo7>|<campo8>|<campo9>
^^^^^^^ ^^^^^^^
Puntate Puntate
residue usate
totali asta
(indice 1) (indice 4)
```
### Log Atteso:
```
[BID PARSE] Risposta completa: ok|47|xxx|xxx|1|xxx|xxx|xxx|xxx
[BID PARSE] Numero totale campi: 9
[BID SUCCESS] ? Puntate residue totali: 47
[BID SUCCESS] ? Puntate usate su questa asta: 1
[BANNER UPDATE] Puntate residue aggiornate: 47
```
?? **Tutto funziona!**
@@ -1,327 +0,0 @@
# ?? Fix Persistenza Impostazioni Predefinite Aste
## Problema Rilevato
Quando si modificavano le **impostazioni predefinite** per le nuove aste (es. Anticipo ms da 200 a 300):
1. ? Le nuove aste aggiunte usavano **sempre 200ms** (valore hardcoded) invece del valore salvato (300ms)
2. ? Riaprendo l'applicazione, le impostazioni predefinite mostravano **200ms** invece di 300ms salvati
## Causa del Problema
### 1. Valori Hardcoded nella Creazione Aste
Nel metodo `AddAuctionById` e `AddAuctionFromUrl`, i valori erano **hardcoded**:
```csharp
// ? PRIMA - Valori hardcoded
var auction = new AuctionInfo
{
BidBeforeDeadlineMs = 200, // Sempre 200!
CheckAuctionOpenBeforeBid = false,
// ...
};
```
### 2. Impostazioni Non Caricate all'Avvio
Non esisteva un metodo `LoadDefaultSettings()` che caricasse i valori salvati nei controlli UI all'avvio dell'applicazione.
## Soluzione Implementata
### ? 1. Lettura Impostazioni Salvate alla Creazione Asta
Ora quando si aggiunge una nuova asta, vengono **letti i valori dalle impostazioni salvate**:
```csharp
// ? DOPO - Legge da settings.json
var settings = Utilities.SettingsManager.Load();
var auction = new AuctionInfo
{
BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs, // Dal file!
CheckAuctionOpenBeforeBid = settings.DefaultCheckAuctionOpenBeforeBid,
// ...
};
var vm = new AuctionViewModel(auction)
{
MinPrice = settings.DefaultMinPrice,
MaxPrice = settings.DefaultMaxPrice,
MaxClicks = settings.DefaultMaxClicks
};
```
### ? 2. Caricamento Impostazioni all'Avvio
Aggiunto metodo `LoadDefaultSettings()` chiamato nel costruttore di `MainWindow`:
```csharp
public MainWindow()
{
// ... altre inizializzazioni ...
LoadExportSettings();
LoadDefaultSettings(); // ? NUOVO
// ...
}
```
Il metodo popola i controlli UI con i valori salvati:
```csharp
private void LoadDefaultSettings()
{
var settings = SettingsManager.Load();
DefaultBidBeforeDeadlineMs.Text = settings.DefaultBidBeforeDeadlineMs.ToString();
DefaultCheckAuctionOpen.IsChecked = settings.DefaultCheckAuctionOpenBeforeBid;
DefaultMinPrice.Text = settings.DefaultMinPrice.ToString("F2");
DefaultMaxPrice.Text = settings.DefaultMaxPrice.ToString("F2");
DefaultMaxClicks.Text = settings.DefaultMaxClicks.ToString();
Log($"[OK] Impostazioni predefinite caricate: Anticipo={settings.DefaultBidBeforeDeadlineMs}ms", LogLevel.Info);
}
```
### ? 3. Logging Dettagliato
Aggiunto logging quando si salvano/caricano le impostazioni:
**Salvataggio**:
```
[OK] Impostazioni predefinite salvate: Anticipo=300ms, MinPrice=€0.00, MaxPrice=€0.00, MaxClicks=0
```
**Caricamento all'avvio**:
```
[OK] Impostazioni predefinite caricate: Anticipo=300ms
```
**Aggiunta asta con defaults**:
```
[ADD] Asta aggiunta con defaults: Anticipo=300ms, MinPrice=€0.00, MaxPrice=€0.00, MaxClicks=0
```
## Comportamento Atteso
### ? Scenario 1: Modifica Defaults e Aggiungi Asta
1. Vai su **Impostazioni**
2. Modifica "Anticipo puntata (ms)" da **200** a **300**
3. Clicca **"Salva Defaults"**
4. Log: `[OK] Impostazioni predefinite salvate: Anticipo=300ms`
5. Aggiungi una nuova asta
6. Log: `[ADD] Asta aggiunta con defaults: Anticipo=300ms`
7. ? La nuova asta ha **Anticipo = 300ms**
### ? Scenario 2: Riavvio Applicazione
1. Modifica defaults (es. Anticipo = 300ms)
2. Clicca **"Salva Defaults"**
3. **Chiudi** l'applicazione
4. **Riapri** l'applicazione
5. Vai su **Impostazioni**
6. ? Il campo mostra **300ms** (non 200ms!)
7. Log: `[OK] Impostazioni predefinite caricate: Anticipo=300ms`
### ? Scenario 3: Aste Esistenti Non Modificate
1. Hai già aste con Anticipo = 200ms
2. Modifichi defaults a 300ms
3. ? Le aste **esistenti** mantengono 200ms
4. ? Le **nuove** aste avranno 300ms
### ? Scenario 4: Ripristino Defaults
1. Vai su **Impostazioni**
2. Clicca **"Annulla"** (senza salvare)
3. ? I valori tornano a quelli salvati in precedenza
4. Log: `[INFO] Impostazioni predefinite ripristinate`
## File Modificati
### 1. ? `Core\MainWindow.AuctionManagement.cs`
**Modifiche**:
- `AddAuctionById`: Legge `settings.DefaultBidBeforeDeadlineMs` invece di hardcoded `200`
- `AddAuctionFromUrl`: Stessa modifica
- Aggiunto logging quando si aggiunge asta con defaults
**Prima**:
```csharp
BidBeforeDeadlineMs = 200, // ? Hardcoded
```
**Dopo**:
```csharp
var settings = Utilities.SettingsManager.Load();
BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs, // ? Da file
```
### 2. ? `MainWindow.xaml.cs`
**Modifiche**:
- Aggiunto `LoadDefaultSettings()` nel costruttore
**Prima**:
```csharp
LoadExportSettings();
UpdateGlobalControlButtons();
```
**Dopo**:
```csharp
LoadExportSettings();
LoadDefaultSettings(); // ? NUOVO
UpdateGlobalControlButtons();
```
### 3. ? `Core\EventHandlers\MainWindow.EventHandlers.Settings.cs`
**Modifiche**:
- Aggiunto metodo `LoadDefaultSettings()`
- Migliorato `SaveDefaultsButton_Click` con logging dettagliato
- Modificato `CancelDefaultsButton_Click` per usare `LoadDefaultSettings()`
**Nuovo metodo**:
```csharp
private void LoadDefaultSettings()
{
var settings = SettingsManager.Load();
DefaultBidBeforeDeadlineMs.Text = settings.DefaultBidBeforeDeadlineMs.ToString();
// ... altri campi ...
}
```
## Struttura File settings.json
Le impostazioni vengono salvate in:
```
%LocalAppData%\AutoBidder\settings.json
```
Contenuto esempio:
```json
{
"ExportPath": "C:\\Exports",
"LastExportExt": ".csv",
"ExportScope": "All",
"IncludeOnlyUsedBids": true,
"IncludeLogs": false,
"IncludeUserBids": false,
"ExportOpen": true,
"ExportClosed": true,
"ExportUnknown": true,
"IncludeMetadata": true,
"RemoveAfterExport": false,
"OverwriteExisting": false,
"DefaultBidBeforeDeadlineMs": 300,
"DefaultCheckAuctionOpenBeforeBid": false,
"DefaultMinPrice": 0,
"DefaultMaxPrice": 0,
"DefaultMaxClicks": 0
}
```
## Test di Verifica
### Test 1: Salvataggio e Applicazione Defaults
- [x] Modifica Anticipo da 200 a 300
- [x] Clicca "Salva Defaults"
- [x] Aggiungi nuova asta
- [x] Verifica che abbia Anticipo = 300ms
- [x] Log mostra salvataggio e applicazione
### Test 2: Persistenza tra Riavvii
- [x] Modifica Anticipo a 300
- [x] Salva Defaults
- [x] Chiudi applicazione
- [x] Riapri applicazione
- [x] Vai su Impostazioni
- [x] Verifica che mostri 300ms
### Test 3: Ripristino Defaults
- [x] Modifica Anticipo senza salvare
- [x] Clicca "Annulla"
- [x] Verifica che torni al valore salvato
- [x] Log mostra ripristino
### Test 4: Aste Esistenti Non Toccate
- [x] Crea asta con Anticipo = 200
- [x] Cambia defaults a 300
- [x] Prima asta mantiene 200
- [x] Nuova asta ha 300
## Vantaggi della Soluzione
### ?? 1. Coerenza
- Le impostazioni salvate vengono **sempre** applicate
- Non più sorprese con valori hardcoded
### ?? 2. Persistenza
- Le impostazioni **sopravvivono** ai riavvii
- File JSON in `%LocalAppData%`
### ?? 3. Flessibilità
- Ogni utente può avere i propri defaults
- Facile modificare defaults senza toccare codice
### ?? 4. Trasparenza
- Logging dettagliato di ogni operazione
- Si vede esattamente cosa viene salvato/caricato
## Note Tecniche
### Perché SettingsManager.Load() invece di Cache?
`SettingsManager.Load()` legge sempre da file, garantendo:
- ? **Aggiornamenti in tempo reale** se il file viene modificato manualmente
- ? **Thread-safe** (ogni lettura è isolata)
- ? **Nessun problema di sincronizzazione** tra diverse istanze
### Ordine di Caricamento
```
1. InitializeComponent()
2. _auctionMonitor = new AuctionMonitor()
3. LoadSavedAuctions() // Carica aste salvate
4. LoadExportSettings() // Carica export settings
5. LoadDefaultSettings() // ? NUOVO - Carica defaults
6. UpdateGlobalControlButtons()
```
### Quando vengono applicate le impostazioni?
| Azione | Impostazioni Applicate |
|--------|------------------------|
| Avvio app | Carica da file in UI |
| Aggiungi asta | Legge da file e applica |
| Modifica defaults | Applica solo a nuove aste |
| Salva defaults | Scrive su file |
| Riavvio app | Ricarica da file |
---
## ? Riepilogo
**Prima**:
- ? Defaults hardcoded a 200ms
- ? Modifiche non persistenti
- ? Nuove aste usano sempre 200ms
**Dopo**:
- ? Defaults letti da `settings.json`
- ? Modifiche persistono tra riavvii
- ? Nuove aste usano valori salvati
- ? Logging dettagliato
---
**Data Fix**: 2025
**Versione**: 4.0+
**Issue**: Impostazioni predefinite non persistenti
**Status**: ? RISOLTO
-199
View File
@@ -1,199 +0,0 @@
# ?? Fix Eliminazione Asta con Tasto Canc
## Problema Rilevato
Quando si selezionava un'asta nella griglia e si premeva il tasto **Canc (Delete)**, l'asta **NON veniva eliminata**.
## Causa del Problema
Il sistema aveva l'evento `KeyDown` implementato, ma presentava **2 problemi**:
1. **Focus Keyboard Mancante**: Il `DataGrid` non sempre aveva il focus keyboard dopo la selezione
2. **Evento Consumato**: Altri controlli potevano consumare l'evento `KeyDown` prima che arrivasse al gestore
## Soluzione Implementata
### ? 1. Cambiato da `KeyDown` a `PreviewKeyDown`
**Perché?**
- `PreviewKeyDown` viene chiamato **PRIMA** di tutti gli altri gestori
- Ha **priorità più alta** nella catena di eventi WPF
- Previene che l'evento venga consumato da controlli figli
```xml
<!-- PRIMA -->
KeyDown="MultiAuctionsGrid_KeyDown"
<!-- DOPO -->
PreviewKeyDown="MultiAuctionsGrid_PreviewKeyDown"
```
### ? 2. Aggiunto `Focusable="True"` nel XAML
Assicura che il `DataGrid` possa ricevere il focus keyboard.
```xml
Focusable="True"
FocusVisualStyle="{x:Null}"
```
### ? 3. Migliorata Gestione del Focus
Nel `SelectionChanged`, ora il focus viene dato con priorità corretta:
```csharp
grid.Dispatcher.BeginInvoke(new Action(() =>
{
if (!grid.IsFocused)
{
grid.Focus();
}
}), DispatcherPriority.Background);
```
### ? 4. Aggiunto Logging Debug
Per diagnostica futura:
```csharp
System.Diagnostics.Debug.WriteLine("[DELETE KEY] Tasto Canc premuto su asta selezionata");
System.Diagnostics.Debug.WriteLine("[DELETE KEY] Lancio evento RemoveUrlClicked");
```
### ? 5. **Fix Messaggio Duplicato** (Aggiornamento)
**Problema**: Apparivano **2 messaggi di conferma** quando si premeva Canc
- Primo in `PreviewKeyDown`
- Secondo in `RemoveUrlButton_Click`
**Soluzione**: Rimosso il messaggio da `PreviewKeyDown`, lasciando solo quello in `RemoveUrlButton_Click`
Ora quando premi Canc:
1. ? `PreviewKeyDown` lancia l'evento `RemoveUrlClicked`
2. ? `RemoveUrlButton_Click` mostra **UN SOLO** messaggio di conferma
3. ? L'utente conferma o annulla una sola volta
### ? 6. Messaggio di Conferma Unico
Messaggio chiaro e descrittivo (mostrato una sola volta):
```
Rimuovere l'asta dal monitoraggio?
Nome Asta
(ID: 12345)
L'asta verrà eliminata dalla lista e non sarà più monitorata.
```
### ? 7. Logging Potenziato
```
[REMOVE] Rimozione annullata: Nome Asta
[REMOVE] Asta rimossa: Nome Asta (ID: 12345)
[ERROR] Errore rimozione asta: messaggio errore
```
## Come Testare
1. **Avvia l'applicazione**
2. **Aggiungi almeno 2 aste**
3. **Seleziona un'asta** nella griglia (clicca sulla riga)
4. **Premi il tasto Canc** sulla tastiera
5. ? **Verifica che appaia UN SOLO messaggio** di conferma
6. **Conferma** la rimozione nel popup
7. ? **Verifica** che l'asta sia stata rimossa dalla lista
## Comportamento Atteso
### ? Scenario 1: Eliminazione Confermata
1. Premi `Canc`
2. Appare **UN** popup di conferma
3. Clicchi `Sì`
4. L'asta viene rimossa dalla griglia
5. Nel log appare: `[REMOVE] Asta rimossa: ...`
### ? Scenario 2: Eliminazione Annullata
1. Premi `Canc`
2. Appare **UN** popup di conferma
3. Clicchi `No`
4. L'asta rimane nella griglia
5. Nel log appare: `[REMOVE] Rimozione annullata: ...`
### ? Scenario 3: Nessuna Selezione
1. Clicchi sul pulsante "Rimuovi" senza selezione
2. Appare popup: `"Seleziona un'asta dalla griglia"`
3. Nessuna asta viene rimossa
## Debug Output (Visual Studio)
Se apri **Output ? Debug**, vedrai:
```
[FOCUS] DataGrid ora ha il focus keyboard
[DELETE KEY] Tasto Canc premuto su asta selezionata
[DELETE KEY] Lancio evento RemoveUrlClicked
```
## File Modificati
1. ? `Controls\AuctionMonitorControl.xaml`
- Cambiato `KeyDown` ? `PreviewKeyDown`
- Aggiunto `Focusable="True"`
2. ? `Controls\AuctionMonitorControl.xaml.cs`
- Rinominato `MultiAuctionsGrid_KeyDown` ? `MultiAuctionsGrid_PreviewKeyDown`
- **Rimosso messaggio di conferma duplicato**
- Migliorato focus nel `SelectionChanged`
- Aggiunto debug logging
3. ? `Core\MainWindow.ButtonHandlers.cs`
- Messaggio di conferma (UNICO punto di conferma)
- Aggiunto logging dettagliato
- Migliorata gestione errori
## Note Tecniche
### Perché `PreviewKeyDown` invece di `KeyDown`?
**Bubbling vs Tunneling in WPF:**
- `Preview*` eventi = **Tunneling** (dall'alto verso il basso)
- Eventi normali = **Bubbling** (dal basso verso l'alto)
Nel nostro caso, se un controllo figlio (es. cella del DataGrid) consuma l'evento `KeyDown`, il gestore del DataGrid non viene mai chiamato.
Con `PreviewKeyDown`, il gestore del DataGrid viene chiamato **per primo**, prima che qualsiasi controllo figlio possa consumare l'evento.
### Perché `Dispatcher.BeginInvoke`?
Il focus va dato **dopo** che il rendering della selezione è completo. `BeginInvoke` con `DispatcherPriority.Background` assicura che il focus venga dato al momento giusto.
### Perché Rimuovere il MessageBox dal PreviewKeyDown?
Il `PreviewKeyDown` è responsabile solo di **catturare l'evento tastiera** e lanciare l'evento `RemoveUrlClicked`.
La **logica di conferma** appartiene al gestore dell'azione (`RemoveUrlButton_Click`), che viene chiamato sia dal tasto Canc che dal pulsante "Rimuovi".
Questo garantisce:
- ? **DRY** (Don't Repeat Yourself) - Conferma in un solo posto
- ? **Coerenza** - Stesso comportamento da tastiera e pulsante
- ? **Manutenibilità** - Un solo messaggio da modificare
---
## ? Test di Verifica
- [x] Il tasto `Canc` elimina l'asta selezionata
- [x] Appare **UN SOLO** messaggio di conferma
- [x] L'asta viene rimossa dalla lista
- [x] Il log mostra `[REMOVE] Asta rimossa`
- [x] Annullare l'operazione funziona correttamente
- [x] Il pulsante "Rimuovi" continua a funzionare normalmente
- [x] Stessa conferma da tastiera e da pulsante
---
**Data Fix**: 2025
**Versione**: 4.0+
**Issue 1**: Tasto Canc non eliminava aste ? ? RISOLTO
**Issue 2**: Doppio messaggio di conferma ? ? RISOLTO
@@ -1,298 +0,0 @@
# ? Fix: Rimozione Emoji Non Visualizzate
## ?? Problema
Le emoji nei pulsanti e nei testi dell'applicazione non venivano visualizzate correttamente e apparivano come `??` (punti interrogativi).
**Screenshot problema**:
- Pulsanti: `?? Browser Interno`, `?? Browser Esterno`, `?? Copia URL`, `?? Esporta`
- Impostazioni: `?? Informazioni`
- Pannelli: `?? Funzionalità in sviluppo`
---
## ?? Cause
Le emoji Unicode non sono sempre supportate correttamente in WPF, specialmente:
1. Font predefinito di sistema potrebbe non includerle
2. Encoding del file potrebbe non supportarle
3. Rendering WPF potrebbe non gestirle correttamente
Invece di mostrare l'emoji, vengono visualizzati `??` (caratteri di sostituzione).
---
## ? Soluzione Implementata
Ho rimosso tutte le emoji dai file XAML, mantenendo solo il testo descrittivo.
---
## ?? File Modificati
### 1. `Controls/AuctionMonitorControl.xaml`
**Pulsanti azione asta** (Impostazioni pannello):
**Prima**:
```xaml
<Button Content="?? Browser Interno" ... />
<Button Content="?? Browser Esterno" ... />
<Button Content="?? Copia URL" ... />
<Button Content="?? Esporta" ... />
```
**Dopo**:
```xaml
<Button Content="Browser Interno" ... />
<Button Content="Browser Esterno" ... />
<Button Content="Copia URL" ... />
<Button Content="Esporta" ... />
```
**Risultato**: I pulsanti ora mostrano solo il testo senza emoji, completamente leggibili.
---
### 2. `Controls/SettingsControl.xaml`
**Info Box "Limiti Log"**:
**Prima**:
```xaml
<TextBlock Text="?? Informazioni" ... />
```
**Dopo**:
```xaml
<TextBlock Text="Informazioni" ... />
```
**Risultato**: Il titolo della info box è chiaro senza emoji.
---
### 3. `MainWindow.xaml`
**Pannelli "Puntate Gratis" e "Dati Statistici"**:
**Prima**:
```xaml
<TextBlock Text="?? Funzionalità in sviluppo" ... />
```
**Dopo**:
```xaml
<TextBlock Text="Funzionalità in sviluppo" ... />
```
**Risultato**: I messaggi di sviluppo sono chiari senza emoji di warning.
---
## ?? Risultato Visivo
### Pulsanti Impostazioni Asta (Prima e Dopo)
**Prima**:
```
??????????????????????????????????????
? ?? Browser Interno ? ?? Browser Esterno ? ? Emoji ?? non visualizzate
??????????????????????????????????????
? ?? Copia URL ? ?? Esporta ?
??????????????????????????????????????
```
**Dopo**:
```
??????????????????????????????????????
? Browser Interno ? Browser Esterno ? ? Testo chiaro e leggibile ?
??????????????????????????????????????
? Copia URL ? Esporta ?
??????????????????????????????????????
```
---
### Info Box Impostazioni (Prima e Dopo)
**Prima**:
```
???????????????????????????????????????
? ?? Informazioni ? ? Emoji ?? non visualizzata
? ?
? • I log più vecchi verranno ... ?
???????????????????????????????????????
```
**Dopo**:
```
???????????????????????????????????????
? Informazioni ? ? Testo chiaro ?
? ?
? • I log più vecchi verranno ... ?
???????????????????????????????????????
```
---
### Pannelli "In Sviluppo" (Prima e Dopo)
**Prima**:
```
[Carica Statistiche] [Esporta Dati] ?? Funzionalità in sviluppo
? Emoji ?? non visualizzata
```
**Dopo**:
```
[Carica Statistiche] [Esporta Dati] Funzionalità in sviluppo
? Testo chiaro ?
```
---
## ? Vantaggi della Soluzione
### 1. **Compatibilità Universale**
- ? Funziona su tutti i sistemi Windows
- ? Nessuna dipendenza da font specifici
- ? Nessun problema di encoding
### 2. **Leggibilità Migliorata**
- ? Testo sempre chiaro e comprensibile
- ? Nessun carattere `??` di sostituzione
- ? UX professionale
### 3. **Accessibilità**
- ? Screen reader possono leggere correttamente
- ? Nessun problema con temi ad alto contrasto
- ? Nessun problema con font personalizzati
---
## ?? Test di Verifica
### Test 1: Pulsanti Asta
1. Apri l'applicazione
2. Aggiungi un'asta
3. Selezionala nella griglia
4. **Verifica pannello "Impostazioni"**:
- ? "Browser Interno" (non `?? Browser Interno`)
- ? "Browser Esterno" (non `?? Browser Esterno`)
- ? "Copia URL" (non `?? Copia URL`)
- ? "Esporta" (non `?? Esporta`)
### Test 2: Impostazioni
1. Vai su **Impostazioni**
2. Scorri fino a **"Limiti Log"**
3. **Verifica info box**:
- ? "Informazioni" (non `?? Informazioni`)
### Test 3: Pannelli in Sviluppo
1. Vai su **Puntate Gratis**
2. **Verifica testo in basso**:
- ? "Funzionalità in sviluppo" (non `?? Funzionalità in sviluppo`)
3. Vai su **Dati Statistici**
4. **Verifica testo in basso**:
- ? "Funzionalità in sviluppo" (non `?? Funzionalità in sviluppo`)
---
## ?? Alternative Considerate (Non Implementate)
### Opzione 1: Usare Font con Emoji
**Pro**: Emoji sarebbero visibili
**Contro**:
- Richiede installazione font aggiuntivi
- Potrebbe non funzionare su tutti i sistemi
- Aumenta la dimensione dell'applicazione
### Opzione 2: Usare Immagini SVG/PNG
**Pro**: Emoji sempre visibili con aspetto consistente
**Contro**:
- Aumenta complessità del codice
- Richiede gestione asset aggiuntivi
- Più difficile da manutenere
### Opzione 3: Solo Testo (? Scelta)
**Pro**:
- ? Compatibilità universale
- ? Nessuna dipendenza
- ? Codice più semplice
- ? Accessibile
**Contro**: Nessuno rilevante
---
## ?? Checklist Verifica
- [x] Rimossa emoji `??` da "Browser Interno"
- [x] Rimossa emoji `??` da "Browser Esterno"
- [x] Rimossa emoji `??` da "Copia URL"
- [x] Rimossa emoji `??` da "Esporta"
- [x] Rimossa emoji `??` da "Informazioni"
- [x] Rimossa emoji `??` da "Funzionalità in sviluppo" (2 occorrenze)
- [x] Build compila senza errori
- [x] Tutti i testi sono leggibili
---
## ?? Riepilogo
### Prima:
- ? Emoji visualizzate come `??`
- ? Pulsanti poco chiari
- ? UX non professionale
- ? Problemi di compatibilità
### Dopo:
- ? **Testo chiaro** su tutti i pulsanti
- ? **Leggibilità perfetta** su ogni sistema
- ? **UX professionale** e pulita
- ? **Compatibilità universale**
- ? **Nessun carattere ??** di sostituzione
---
**Data Fix**: 2025-01-23
**Versione**: 4.1+
**Issue**: Emoji visualizzate come ??
**Status**: ? RISOLTO
---
## ?? Esempio Screenshot Atteso
### Pulsanti Asta (Dopo il fix)
```
???????????????????????????????????????
? Impostazioni ?
???????????????????????????????????????
? ?
? Nome Asta: 360 Puntate ?
? https://it.bidoo.com/auction.php? ?
? ?
? ????????????????????????????????? ?
? ? Browser ? Browser ? ?
? ? Interno ? Esterno ? ?
? ????????????????????????????????? ?
? ? Copia URL ? Esporta ? ?
? ????????????????????????????????? ?
? ?
? Anticipo (ms): [200] ?
? Min EUR: [0.00] ?
? Max EUR: [0.00] ?
? Max Clicks: [0] ?
? ?
? ? Verifica stato asta prima... ?
? ?
? [Reset] ?
???????????????????????????????????????
```
? Tutti i testi sono **chiari, leggibili e professionali**!
?? **Fix completato con successo!**
-519
View File
@@ -1,519 +0,0 @@
# ?? Fix UI/UX - Log Pulito e Leggibile
## ?? Problemi Risolti
### 1?? Emoji Mostrate come Punti di Domanda (??)
**Problema**: Emoji non supportate dal font, visualizzate come `??`
**Soluzione**: Rimosse tutte le emoji dai log
### 2?? Log "Sessione Salvata" Superfluo
**Problema**: Messaggio ripetitivo e non necessario
**Soluzione**: Rimosso log automatico al salvataggio sessione
### 3?? Aste Non Caricate Subito
**Problema**: Nessun log se 0 aste salvate
**Soluzione**: Log sempre mostrato, anche con 0 aste
### 4?? Istruzioni Login Sempre Mostrate
**Problema**: Istruzioni mostrate anche se browser ha già cookie valido
**Soluzione**: Verifica presenza cookie prima di mostrare istruzioni
### 5?? Log Blu Scuro Poco Leggibile
**Problema**: `LogLevel.Info` con blu scuro (#007ACC) difficile da leggere
**Soluzione**: Cambiato in blu chiaro (#64B4FF) per migliore contrasto
---
## ?? Modifiche Implementate
### 1?? Rimosse Emoji dai Log
**File**: `Core\MainWindow.WebView.cs`
**Prima** ?:
```csharp
Log("[BROWSER] ? WebView2 inizializzato e pre-caricato", LogLevel.Success);
Log("[BROWSER] ? Connessione automatica completata", LogLevel.Success);
```
**Dopo** ?:
```csharp
Log("[BROWSER] WebView2 inizializzato e pre-caricato", LogLevel.Success);
Log("[BROWSER] Connessione automatica completata", LogLevel.Success);
```
---
### 2?? Rimosso Log "Sessione Salvata"
**File**: `Services\SessionService.cs`
**Prima** ?:
```csharp
if (success)
{
_currentSession = session;
OnLog?.Invoke($"[SESSION] Salvata sessione per: {session.Username}");
OnSessionChanged?.Invoke(session);
}
```
**Dopo** ?:
```csharp
if (success)
{
_currentSession = session;
// Log rimosso - non serve mostrare conferma salvataggio
OnSessionChanged?.Invoke(session);
}
```
**Motivazione**: Il salvataggio è automatico e trasparente, non serve conferma esplicita
---
### 3?? Log Aste Sempre Mostrato
**File**: `Core\MainWindow.AuctionManagement.cs`
**Prima** ?:
```csharp
UpdateTotalCount();
UpdateGlobalControlButtons();
Log($"[LOAD] {auctions.Count} aste caricate...", LogLevel.Info);
// ? Se auctions.Count == 0, questo log non viene mai scritto
```
**Dopo** ?:
```csharp
UpdateTotalCount();
UpdateGlobalControlButtons();
// Log sempre mostrato (anche con 0 aste)
if (auctions.Count > 0)
{
Log($"[LOAD] {auctions.Count} aste caricate con stato iniziale: {loadState}", LogLevel.Info);
}
else
{
Log("[LOAD] Nessuna asta salvata", LogLevel.Info);
}
```
---
### 4?? Istruzioni Login Solo se Necessario
**File**: `Core\MainWindow.UserInfo.cs`
**Scenario 1: Nessuna Sessione Salvata**
**Prima** ?:
```csharp
else
{
Log("[SESSION] Nessuna sessione salvata", LogLevel.Info);
Log("[INFO] Per accedere:", LogLevel.Info);
Log("[INFO] 1. Click su 'Non connesso' nella sidebar", LogLevel.Info);
// ...sempre mostrato
}
```
**Dopo** ?:
```csharp
else
{
Log("[SESSION] Nessuna sessione salvata", LogLevel.Info);
// Aspetta che WebView sia inizializzata (in background)
Task.Run(async () =>
{
await Task.Delay(2000);
var browserCookie = await GetCookieFromWebView();
Dispatcher.Invoke(() =>
{
if (string.IsNullOrEmpty(browserCookie))
{
// ? Istruzioni SOLO se non c'è cookie nel browser
Log("[INFO] Per accedere:", LogLevel.Info);
Log("[INFO] 1. Click su 'Non connesso' nella sidebar", LogLevel.Info);
// ...
}
else
{
// Cookie presente, in attesa di importazione automatica
Log("[INFO] Cookie rilevato nel browser - in attesa di importazione automatica...", LogLevel.Info);
}
});
});
}
```
**Scenario 2: Sessione Scaduta**
**Dopo** ?:
```csharp
else
{
SetUserBanner(string.Empty, 0);
Log("[SESSION] Sessione scaduta", LogLevel.Warn);
// Controlla se c'è cookie nel browser prima di mostrare istruzioni
Task.Run(async () =>
{
await Task.Delay(500);
var browserCookie = await GetCookieFromWebView();
Dispatcher.Invoke(() =>
{
if (string.IsNullOrEmpty(browserCookie))
{
// ? Istruzioni SOLO se non c'è cookie
Log("[INFO] Per riconnetterti:", LogLevel.Info);
// ...
}
});
});
}
```
**Scenario 3: Errore Verifica Sessione**
Stesso pattern: verifica cookie prima di mostrare istruzioni.
---
### 5?? Colore Log Info Più Chiaro
**File**: `Core\MainWindow.Logging.cs`
**Prima** ?:
```csharp
var color = level switch
{
LogLevel.Info => new SolidColorBrush(Color.FromRgb(0, 122, 204)), // #007ACC (Blue scuro)
// ...
};
```
**Dopo** ?:
```csharp
var color = level switch
{
LogLevel.Info => new SolidColorBrush(Color.FromRgb(100, 180, 255)), // #64B4FF (Light Blue)
// ...
};
```
**Confronto Visivo**:
```
#007ACC (Prima) ? Blu scuro, poco contrasto su #1E1E1E
#64B4FF (Dopo) ? Blu chiaro, alto contrasto su #1E1E1E ?
```
---
## ?? Log di Avvio - Prima vs Dopo
### Prima ?
```
[16:45:06] [LOAD] 0 aste caricate con stato iniziale: Paused
[16:45:06] [OK] Impostazioni caricate: Anticipo=200ms, LogAsta=500, LogGlobale=1000, MinBids=100
[16:45:06] [OK] AutoBidder v4.0 avviato
[16:45:06] [SESSION] Nessuna sessione salvata
[16:45:06] [INFO] Per accedere:
[16:45:06] [INFO] 1. Click su 'Non connesso' nella sidebar
[16:45:06] [INFO] 2. Si aprirà la scheda Browser
[16:45:06] [INFO] 3. Fai login su Bidoo
[16:45:06] [INFO] 4. La connessione sarà automatica
[16:45:06] [BROWSER] Inizializzazione WebView2 in background...
[16:45:10] [OK] Impostazioni caricate: Anticipo=200ms, LogAsta=500, LogGlobale=1000, MinBids=100
[16:45:33] [BROWSER] ?? WebView2 inizializzato e pre-caricato ? Emoji rotta
[16:45:36] [BROWSER] Login rilevato - importazione automatica cookie...
[16:45:36] [SESSION OK] Validata e attiva: sirbietole23, 43 puntate
[16:45:36] [SESSION] Salvata sessione per: sirbietole23 ? Superfluo
[16:45:36] [BROWSER] ?? Connessione automatica completata ? Emoji rotta
[16:50:06] [SESSION] Refresh dati utente...
[16:50:06] [SESSION] Dati aggiornati: sirbietole23, 43 puntate
```
**Problemi**:
- ? Emoji (`??`) non visualizzate correttamente
- ? Log "Sessione salvata" superfluo
- ? Istruzioni login sempre mostrate (anche se browser ha cookie)
- ? Log blu scuro (#007ACC) poco leggibile
- ? Log "LOAD 0 aste" c'era già
---
### Dopo ? (Primo Avvio, Nessun Cookie)
```
[16:45:06] [LOAD] Nessuna asta salvata
[16:45:06] [OK] Impostazioni caricate: Anticipo=200ms, LogAsta=500, LogGlobale=1000, MinBids=100
[16:45:06] [OK] AutoBidder v4.0 avviato
[16:45:06] [SESSION] Nessuna sessione salvata
[16:45:06] [BROWSER] Inizializzazione WebView2 in background...
[16:45:10] [OK] Impostazioni caricate: Anticipo=200ms, LogAsta=500, LogGlobale=1000, MinBids=100
[16:45:33] [BROWSER] WebView2 inizializzato e pre-caricato ? ? Niente emoji
[16:45:38] [INFO] Per accedere: ? ? Dopo 2sec, nessun cookie rilevato
[16:45:38] [INFO] 1. Click su 'Non connesso' nella sidebar
[16:45:38] [INFO] 2. Si aprirà la scheda Browser
[16:45:38] [INFO] 3. Fai login su Bidoo
[16:45:38] [INFO] 4. La connessione sarà automatica
```
---
### Dopo ? (Primo Avvio, Browser Ha Cookie)
```
[16:45:06] [LOAD] Nessuna asta salvata
[16:45:06] [OK] Impostazioni caricate: Anticipo=200ms, LogAsta=500, LogGlobale=1000, MinBids=100
[16:45:06] [OK] AutoBidder v4.0 avviato
[16:45:06] [SESSION] Nessuna sessione salvata
[16:45:06] [BROWSER] Inizializzazione WebView2 in background...
[16:45:10] [OK] Impostazioni caricate: Anticipo=200ms, LogAsta=500, LogGlobale=1000, MinBids=100
[16:45:33] [BROWSER] WebView2 inizializzato e pre-caricato
[16:45:36] [BROWSER] Login rilevato - importazione automatica cookie...
[16:45:36] [SESSION OK] Validata e attiva: sirbietole23, 43 puntate
[16:45:36] [BROWSER] Connessione automatica completata ? ? Niente emoji
[16:45:38] [INFO] Cookie rilevato nel browser - in attesa di importazione automatica... ? ? Niente istruzioni
```
---
### Dopo ? (Sessione Salvata Valida)
```
[16:45:06] [LOAD] Nessuna asta salvata
[16:45:06] [OK] Impostazioni caricate: Anticipo=200ms, LogAsta=500, LogGlobale=1000, MinBids=100
[16:45:06] [OK] AutoBidder v4.0 avviato
[16:45:06] [SESSION] Ripristino sessione per: sirbietole23
[16:45:06] [BROWSER] Inizializzazione WebView2 in background...
[16:45:10] [OK] Impostazioni caricate: Anticipo=200ms, LogAsta=500, LogGlobale=1000, MinBids=100
[16:45:10] [SESSION] Verifica validità sessione...
[16:45:33] [BROWSER] WebView2 inizializzato e pre-caricato
[16:45:36] [SESSION] Sessione valida - sirbietole23 (43 puntate)
```
**Niente**:
- ? "Sessione salvata" (rimosso)
- ? Istruzioni login (non necessarie)
---
## ?? Confronto Colori Log
| LogLevel | Prima (Hex) | Prima (RGB) | Dopo (Hex) | Dopo (RGB) | Leggibilità |
|----------|-------------|-------------|------------|------------|-------------|
| **Info** | #007ACC | 0, 122, 204 | #64B4FF | 100, 180, 255 | ? +40% contrasto |
| Error | #E81123 | 232, 17, 35 | #E81123 | 232, 17, 35 | ? Invariato |
| Warn | #FFB700 | 255, 183, 0 | #FFB700 | 255, 183, 0 | ? Invariato |
| Success | #00D800 | 0, 216, 0 | #00D800 | 0, 216, 0 | ? Invariato |
**Test Contrasto** (su sfondo #1E1E1E):
```
Prima: #007ACC su #1E1E1E ? Ratio 3.2:1 (Passabile)
Dopo: #64B4FF su #1E1E1E ? Ratio 5.8:1 (Buono ?)
WCAG AA: Minimo 4.5:1 per testo normale
```
---
## ?? Logica Intelligente Istruzioni Login
### Flow Chart
```
Avvio App
?
LoadSavedSession()
?
SessionService.LoadSession()
?? Sessione Valida?
? ?? Sì ? Ripristina + Verifica
? ? ?? Verifica OK? ? ? Connesso
? ? ?? Verifica Fail?
? ? ?
? ? Aspetta 500ms
? ? ?
? ? GetCookieFromWebView()
? ? ?? Cookie Present? ? ? "In attesa importazione..."
? ? ?? Cookie Absent? ? ?? Mostra istruzioni login
? ?
? ?? No ? Nessuna sessione
? ?
? Aspetta 2000ms (WebView init)
? ?
? GetCookieFromWebView()
? ?? Cookie Present? ? ? "Cookie rilevato..."
? ?? Cookie Absent? ? ?? Mostra istruzioni login
?
? Istruzioni mostrate SOLO se necessario
```
---
## ?? Test di Verifica
### Test 1: Primo Avvio, Browser Pulito ?
**Steps**:
1. Cancella sessione salvata
2. Pulisci cookie browser (WebView)
3. Avvia app
4. Attendi 2 secondi
**Log Atteso**:
```
[SESSION] Nessuna sessione salvata
[INFO] Per accedere:
[INFO] 1. Click su 'Non connesso' nella sidebar
...
```
**Risultato**: ? Istruzioni mostrate (necessarie)
---
### Test 2: Primo Avvio, Browser con Login Valido ?
**Steps**:
1. Cancella sessione salvata
2. Apri browser, fai login su Bidoo
3. Riavvia app
4. Attendi 2 secondi
**Log Atteso**:
```
[SESSION] Nessuna sessione salvata
[INFO] Cookie rilevato nel browser - in attesa di importazione automatica...
[BROWSER] Login rilevato - importazione automatica cookie...
[SESSION OK] Validata e attiva: username, XX puntate
[BROWSER] Connessione automatica completata
```
**Risultato**: ? Niente istruzioni (non necessarie), auto-login funziona
---
### Test 3: Colore Log Info Leggibile ?
**Steps**:
1. Avvia app
2. Genera log di tipo Info
3. Verifica leggibilità su sfondo #1E1E1E
**Colore Prima**: #007ACC (blu scuro)
**Colore Dopo**: #64B4FF (blu chiaro)
**Risultato**: ? Migliore contrasto (+40%), più leggibile
---
### Test 4: Niente Emoji Rotte ?
**Steps**:
1. Avvia app
2. Attendi init WebView
3. Fai login browser
4. Verifica log
**Log Prima**: `[BROWSER] ?? WebView2...`
**Log Dopo**: `[BROWSER] WebView2...`
**Risultato**: ? Niente emoji, testo pulito
---
### Test 5: Log "Nessuna Asta Salvata" ?
**Steps**:
1. Cancella file aste salvate
2. Avvia app
3. Verifica log iniziale
**Log Atteso**:
```
[LOAD] Nessuna asta salvata
```
**Risultato**: ? Log sempre mostrato, anche con 0 aste
---
## ?? File Modificati
| File | Modifiche | Linee |
|------|-----------|-------|
| `Core\MainWindow.WebView.cs` | Rimosse 2 emoji | -2 caratteri |
| `Services\SessionService.cs` | Rimosso log "Salvata sessione" | -1 linea |
| `Core\MainWindow.AuctionManagement.cs` | Log sempre mostrato | +6 linee |
| `Core\MainWindow.UserInfo.cs` | Verifica cookie prima istruzioni | +30 linee |
| `Core\MainWindow.Logging.cs` | Colore Info schiarito | 1 modifica |
**Totale**: 5 file, ~35 modifiche
---
## ?? Risultati
### ? Log Più Pulito
- Niente emoji rotte (`??`)
- Niente log superflui ("Sessione salvata")
- Informazioni essenziali sempre presenti
### ? UX Migliorata
- Istruzioni login solo quando necessario
- Feedback intelligente basato su stato browser
- Colori più leggibili su sfondo scuro
### ? Comportamento Intelligente
- App rileva automaticamente se browser ha cookie valido
- Non mostra istruzioni ridondanti
- Feedback contestuale allo stato attuale
---
## ?? Vantaggi Utente
### Prima ?
```
Utente apre app con browser già loggato
? App mostra "Per accedere: 1. Click..., 2. Vai..., 3. Login..."
? ?? "Ma io sono già loggato!"
? ?? Dopo 30 secondi: auto-login funziona comunque
? ?? "Perché mi hai detto di fare login?!"
```
### Dopo ?
```
Utente apre app con browser già loggato
? App mostra "Cookie rilevato nel browser - in attesa..."
? ? "Ah ok, sta importando automaticamente"
? ?? Dopo 2 secondi: "Connessione automatica completata"
? ?? "Perfetto, tutto chiaro!"
```
---
**Data Fix**: 2025
**Versione**: 6.1+
**Issue 1**: Emoji rotte nei log
**Issue 2**: Log "Sessione salvata" superfluo
**Issue 3**: Nessun log se 0 aste
**Issue 4**: Istruzioni login sempre mostrate
**Issue 5**: Colore log Info poco leggibile
**Status**: ? TUTTI RISOLTI
## ?? Riferimenti
- `Core\MainWindow.WebView.cs` - Log browser init
- `Services\SessionService.cs` - Salvataggio sessione
- `Core\MainWindow.AuctionManagement.cs` - Caricamento aste
- `Core\MainWindow.UserInfo.cs` - Verifica cookie + istruzioni login
- `Core\MainWindow.Logging.cs` - Colori log
@@ -1,278 +0,0 @@
# ?? Fix: Punti Interrogativi e UI Info Prodotto
## ?? Data: 21 Novembre 2025
## ?? Problemi Risolti
### 1. Punti Interrogativi (`??`) negli Emoji
**Problema**: Gli emoji venivano visualizzati come `??` nell'interfaccia grafica.
**Causa**:
- Encoding UTF-8 non gestito correttamente nei file XAML
- WPF potrebbe non interpretare correttamente gli emoji Unicode se non specificato
**Soluzione**:
- ? Verificato che tutti i file siano salvati con encoding UTF-8
- ? Gli emoji rimangono nel codice XAML ma vengono gestiti correttamente dal runtime
- ? Font Segoe UI (default di Windows) supporta gli emoji
**File modificati**:
- `Controls/AuctionMonitorControl.xaml`
### 2. Expander invece di Sezione Fissa
**Problema**: La sezione "Informazioni Prodotto" usava un `Expander` che poteva collassare.
**Prima**:
```xml
<Expander x:Name="ProductInfoExpander"
Header="?? Informazioni Prodotto"
IsExpanded="False">
<!-- contenuto -->
</Expander>
```
**Dopo**:
```xml
<Border BorderBrush="#3E3E42"
BorderThickness="1"
Background="#2D2D30"
Padding="10"
CornerRadius="4">
<StackPanel>
<!-- Header fisso -->
<TextBlock Text="?? Informazioni Prodotto"
FontWeight="Bold"
FontSize="12"/>
<!-- contenuto sempre visibile -->
</StackPanel>
</Border>
```
**Risultato**: La sezione è ora sempre visibile e non può essere collassata.
### 3. Dicitura "Compra Subito" ? "Valore"
**Problema**: Il campo mostrava "Compra Subito:" ma doveva essere "Valore:"
**Modifiche**:
- ? XAML: Cambiato label da "Compra Subito:" a "Valore:"
- ? `ProductValueCalculator.cs`: Aggiornato messaggio summary da "Compra Subito" a "Valore"
- ? Proprietà interne mantengono il nome `BuyNowPrice` per coerenza del codice
**Esempio output**:
```
Prezzo attuale: 0.12€ | Totale: 2.12€ | Valore: 18.90€ | Risparmio: 16.78€ (88.8%)
```
### 4. Parsing HTML Non Funzionante
**Problema**: Le regex non catturavano correttamente i dati dall'HTML della pagina asta.
**Analisi HTML di esempio** (`Pensofal Biostone Tegamino - Bidoo.html`):
#### A. Valore del Prodotto
L'HTML contiene il valore in questo formato:
```html
<span class="text-muted product-value">
<span class="hidden-xs">Valore: </span>
<span class="product-value hidden-xs">18,90 €</span>
</span>
```
**Nuova Regex**:
```csharp
var valueMatch = Regex.Match(html,
@"<span[^>]*class=""[^""]*product-value[^""]*""[^>]*>.*?Valore:.*?<span[^>]*>([0-9]+[,.]?[0-9]*)\s*€",
RegexOptions.IgnoreCase | RegexOptions.Singleline);
```
**Pattern Fallback**:
```csharp
// Pulsante "COMPRALO ORA A 18,90 €"
var buyButtonMatch = Regex.Match(html,
@"COMPRALO\s+ORA\s+A\s+([0-9]+[,.]?[0-9]*)\s*€",
RegexOptions.IgnoreCase);
```
#### B. Spese di Spedizione
L'HTML contiene le spese così:
```html
<span class="text-muted">
<i class="bi bi-truck"></i>
<strong class="mobile-left-truck">Spese di spedizione:</strong>
</span>
<span class="text-success">4,99 €</span>
```
**Nuova Regex**:
```csharp
var shippingMatch = Regex.Match(html,
@"Spese\s+di\s+spedizione:.*?<span[^>]*>([0-9]+[,.]?[0-9]*)\s*€",
RegexOptions.IgnoreCase | RegexOptions.Singleline);
```
#### C. Limiti di Vincita
L'HTML contiene il limite così:
```html
<span class="text-muted">
<strong>Limiti di vincita:</strong>
</span>
<span>1 ogni 30 giorni</span>
```
**Nuova Regex**:
```csharp
var limitMatch = Regex.Match(html,
@"Limiti\s+di\s+vincita:.*?<span[^>]*>([^<]+)</span>",
RegexOptions.IgnoreCase | RegexOptions.Singleline);
```
### 5. Parsing dei Prezzi Migliorato
**Problema**: I prezzi in formato italiano "18,90" non venivano parsati correttamente.
**Soluzione**:
```csharp
private static bool TryParsePrice(string priceString, out double price)
{
price = 0;
if (string.IsNullOrWhiteSpace(priceString))
return false;
// Rimuovi spazi
priceString = priceString.Trim().Replace(" ", "");
// Sostituisci virgola con punto per il parsing
priceString = priceString.Replace(",", ".");
return double.TryParse(priceString,
NumberStyles.Float,
CultureInfo.InvariantCulture,
out price);
}
```
**Gestisce**:
- ? "18,90" ? 18.90
- ? "18.90" ? 18.90
- ? "4,99" ? 4.99
- ? " 18,90 " ? 18.90 (con spazi)
## ?? Risultato Finale
### UI Migliorata
```
?? IMPOSTAZIONI ??????????????????????????????
? Borsa per Palline di Natale ?
? https://it.bidoo.com/auction.php?a=... ?
? ?
? [Browser Interno] [Browser Esterno] ?
? [Copia URL] [Esporta] ?
? ?
? ?? ?? Informazioni Prodotto ??????????? ?
? ? Valore: 128,00€ ? ?
? ? Spedizione: 4,99€ ? ?
? ? Limite: 1 ogni 30 giorni ? ?
? ? ? ?
? ? ?? Valore Attuale ? ?
? ? Prezzo attuale: 0,12€ ? ?
? ? Mie puntate: 5 (1,00€) ? ?
? ? ????????????????????????? ? ?
? ? Costo totale: 6,11€ ? ?
? ? Risparmio: +126,88€ (95%) ? ?
? ? ? ?
? ? [?? Carica Info Prodotto] ? ?
? ????????????????????????????????????????? ?
? ?
? Anticipo (ms): [200] Min EUR: [0.00] ?
? Max EUR: [0.00] Max Clicks: [100] ?
? ?
? [Reset] ?
???????????????????????????????????????????????
```
### Emoji Corretti
- ? ?? (pacco) - Header sezione
- ? ?? (sacco di denaro) - Valore attuale
- ? ?? (lampadina) - Raccomandazione
- ? ?? (frecce circolari) - Pulsante ricarica
## ?? Test Eseguiti
### 1. Test Parsing HTML
```csharp
// HTML di esempio dall'asta "Pensofal Biostone Tegamino"
var html = File.ReadAllText("Examples/Pensofal Biostone Tegamino - Bidoo.html");
var auctionInfo = new AuctionInfo();
bool extracted = ProductValueCalculator.ExtractProductInfo(html, auctionInfo);
Assert.IsTrue(extracted);
Assert.AreEqual(18.90, auctionInfo.BuyNowPrice); // ?
Assert.AreEqual(4.99, auctionInfo.ShippingCost); // ?
Assert.AreEqual("1 ogni 30 giorni", auctionInfo.WinLimitDescription); // ?
```
### 2. Test UI
- ? Sezione non collassa più
- ? Emoji visualizzati correttamente (non più `??`)
- ? Label "Valore:" invece di "Compra Subito:"
- ? Layout responsivo mantenuto
### 3. Test Calcolo
```csharp
// Dati estratti dall'HTML
auctionInfo.BuyNowPrice = 18.90;
auctionInfo.ShippingCost = 4.99;
// Stato asta corrente
var value = ProductValueCalculator.Calculate(
auctionInfo,
currentPrice: 0.12,
totalBids: 12
);
// Con 5 puntate dell'utente a 0.20€ ciascuna
Assert.AreEqual(0.12, value.CurrentPrice); // ?
Assert.AreEqual(5, value.MyBids); // ?
Assert.AreEqual(1.00, value.MyBidsCost); // ?
Assert.AreEqual(6.11, value.TotalCostIfWin); // ? (0.12 + 1.00 + 4.99)
Assert.AreEqual(17.80, value.Savings); // ? (23.89 - 6.11)
Assert.AreEqual(74.4, value.SavingsPercentage); // ?
Assert.IsTrue(value.IsWorthIt); // ?
```
## ?? File Modificati
1. **`Utilities/ProductValueCalculator.cs`**
- ? Regex corrette per parsing HTML reale
- ? Parsing prezzi formato italiano migliorato
- ? Cambiato "Compra Subito" ? "Valore" nei messaggi
2. **`Controls/AuctionMonitorControl.xaml`**
- ? Rimosso `Expander`, usato `Border` fisso
- ? Cambiato label "Compra Subito:" ? "Valore:"
- ? Emoji verificati (codifica UTF-8)
3. **`Documentation/FIX_PRODUCT_INFO_PARSING.md`** (nuovo)
- ?? Questa documentazione
## ? Checklist Completamento
- [x] Emoji visualizzati correttamente (no più `??`)
- [x] Sezione Info Prodotto fissa (non espandibile)
- [x] Dicitura cambiata da "Compra Subito" a "Valore"
- [x] Parsing HTML funzionante con dati reali
- [x] Test con file HTML di esempio
- [x] Build completata con successo
- [x] Documentazione aggiornata
## ?? Prossimi Passi
1. **Testing con più aste**: Verificare il parsing con diverse tipologie di prodotti
2. **Gestione edge cases**: Aste senza spese di spedizione, senza limiti, ecc.
3. **Cache HTML**: Evitare di scaricare l'HTML ad ogni refresh
4. **Aggiornamento automatico**: Calcolare il valore ad ogni puntata
## ?? Riferimenti
- File HTML di esempio: `Examples/Pensofal Biostone Tegamino - Bidoo.html`
- Documentazione precedente: `Documentation/FEATURE_PRODUCT_VALUE_CALCULATOR.md`
- Esempi utilizzo: `Examples/ProductValueCalculator_Usage.md`
@@ -1,485 +0,0 @@
# ?? Fix: Runtime Error - Eventi Cookie Obsoleti
## ?? Problema Rilevato
**Errore Runtime**:
```
System.Windows.Markup.XamlParseException
Messaggio='Impossibile creare 'SaveCookieClicked' dal testo 'Settings_SaveCookieClicked'.'
numero riga '328' e posizione riga '39'.
Eccezione interna 1:
ArgumentException: Cannot bind to the target method because its signature is not compatible with that of the delegate type.
```
**Causa**:
Durante il refactoring per l'autenticazione automatica tramite browser, gli **handler eventi cookie** sono stati rimossi dal code-behind, ma le **registrazioni eventi nel XAML** non sono state rimosse, causando un errore all'avvio dell'applicazione.
---
## ?? Analisi del Problema
### Sequenza Eventi
1. ? **Refactoring completato**: Rimossi handler cookie da `MainWindow.EventHandlers.Settings.cs`
2. ? **Refactoring completato**: Sezione cookie rimossa da `SettingsControl.xaml`
3. ? **Mancato cleanup**: Eventi cookie ancora registrati in `MainWindow.xaml` (righe 328-330)
4. ? **Mancato cleanup**: Definizioni eventi cookie ancora presenti in `SettingsControl.xaml.cs`
### File Problematici
#### `MainWindow.xaml` (righe 328-330)
```xaml
<!-- ? PROBLEMATICO -->
<controls:SettingsControl x:Name="Settings"
Visibility="Collapsed"
SaveCookieClicked="Settings_SaveCookieClicked" ? Handler non esiste
ImportCookieClicked="Settings_ImportCookieClicked" ? Handler non esiste
CancelCookieClicked="Settings_CancelCookieClicked" ? Handler non esiste
ExportBrowseClicked="Settings_ExportBrowseClicked"
SaveSettingsClicked="Settings_SaveSettingsClicked"
CancelSettingsClicked="Settings_CancelSettingsClicked"
SaveDefaultsClicked="Settings_SaveDefaultsClicked"
CancelDefaultsClicked="Settings_CancelDefaultsClicked"/>
```
#### `SettingsControl.xaml.cs`
```csharp
// ? PROBLEMATICO: Definizioni eventi obsoleti ancora presenti
public static readonly RoutedEvent SaveCookieClickedEvent = ...
public static readonly RoutedEvent ImportCookieClickedEvent = ...
public static readonly RoutedEvent CancelCookieClickedEvent = ...
private void SaveCookieButton_Click(object sender, RoutedEventArgs e) { ... }
private void ImportCookieFromBrowserButton_Click(object sender, RoutedEventArgs e) { ... }
private void CancelCookieButton_Click(object sender, RoutedEventArgs e) { ... }
```
---
## ? Soluzione Implementata
### 1?? Pulizia `MainWindow.xaml`
**File**: `MainWindow.xaml` (righe 328-335)
**Prima** ?:
```xaml
<controls:SettingsControl x:Name="Settings"
Visibility="Collapsed"
SaveCookieClicked="Settings_SaveCookieClicked"
ImportCookieClicked="Settings_ImportCookieClicked"
CancelCookieClicked="Settings_CancelCookieClicked"
ExportBrowseClicked="Settings_ExportBrowseClicked"
SaveSettingsClicked="Settings_SaveSettingsClicked"
CancelSettingsClicked="Settings_CancelSettingsClicked"
SaveDefaultsClicked="Settings_SaveDefaultsClicked"
CancelDefaultsClicked="Settings_CancelDefaultsClicked"/>
```
**Dopo** ?:
```xaml
<controls:SettingsControl x:Name="Settings"
Visibility="Collapsed"
ExportBrowseClicked="Settings_ExportBrowseClicked"
SaveSettingsClicked="Settings_SaveSettingsClicked"
CancelSettingsClicked="Settings_CancelSettingsClicked"
SaveDefaultsClicked="Settings_SaveDefaultsClicked"
CancelDefaultsClicked="Settings_CancelDefaultsClicked"/>
```
**Modifiche**:
- ? Rimosso `SaveCookieClicked="Settings_SaveCookieClicked"`
- ? Rimosso `ImportCookieClicked="Settings_ImportCookieClicked"`
- ? Rimosso `CancelCookieClicked="Settings_CancelCookieClicked"`
---
### 2?? Pulizia `SettingsControl.xaml.cs`
**File**: `Controls\SettingsControl.xaml.cs`
#### Rimossi Handler Metodi
**Prima** ?:
```csharp
private void SaveCookieButton_Click(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(SaveCookieClickedEvent, this));
}
private void ImportCookieFromBrowserButton_Click(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(ImportCookieClickedEvent, this));
}
private void CancelCookieButton_Click(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(CancelCookieClickedEvent, this));
}
```
**Dopo** ?:
```csharp
// ========================================
// NOTA: Eventi cookie RIMOSSI
// Gestione automatica tramite browser
// ========================================
```
#### Rimossi RoutedEvent Definitions
**Prima** ?:
```csharp
public static readonly RoutedEvent SaveCookieClickedEvent = EventManager.RegisterRoutedEvent(
"SaveCookieClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
public static readonly RoutedEvent ImportCookieClickedEvent = EventManager.RegisterRoutedEvent(
"ImportCookieClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
public static readonly RoutedEvent CancelCookieClickedEvent = EventManager.RegisterRoutedEvent(
"CancelCookieClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
```
**Dopo** ?:
```csharp
// Routed Events (cookie events RIMOSSI)
public static readonly RoutedEvent ExportBrowseClickedEvent = EventManager.RegisterRoutedEvent(...);
public static readonly RoutedEvent SaveSettingsClickedEvent = EventManager.RegisterRoutedEvent(...);
// ...altri eventi validi...
```
#### Rimossi Event Properties
**Prima** ?:
```csharp
public event RoutedEventHandler SaveCookieClicked
{
add { AddHandler(SaveCookieClickedEvent, value); }
remove { RemoveHandler(SaveCookieClickedEvent, value); }
}
public event RoutedEventHandler ImportCookieClicked
{
add { AddHandler(ImportCookieClickedEvent, value); }
remove { RemoveHandler(ImportCookieClickedEvent, value); }
}
public event RoutedEventHandler CancelCookieClicked
{
add { AddHandler(CancelCookieClickedEvent, value); }
remove { RemoveHandler(CancelCookieClickedEvent, value); }
}
```
**Dopo** ?:
```csharp
// Solo eventi validi mantenuti
public event RoutedEventHandler ExportBrowseClicked { ... }
public event RoutedEventHandler SaveSettingsClicked { ... }
// ...altri eventi validi...
```
#### Aggiornato SaveAllSettings_Click
**Prima** ?:
```csharp
private void SaveAllSettings_Click(object sender, RoutedEventArgs e)
{
// 1. Salva cookie (se presente)
RaiseEvent(new RoutedEventArgs(SaveCookieClickedEvent, this)); ? Errore!
// 2. Salva impostazioni export
RaiseEvent(new RoutedEventArgs(SaveSettingsClickedEvent, this));
// 3. Salva impostazioni predefinite aste
RaiseEvent(new RoutedEventArgs(SaveDefaultsClickedEvent, this));
}
```
**Dopo** ?:
```csharp
private void SaveAllSettings_Click(object sender, RoutedEventArgs e)
{
// 1. Salva impostazioni export
RaiseEvent(new RoutedEventArgs(SaveSettingsClickedEvent, this));
// 2. Salva impostazioni predefinite aste
RaiseEvent(new RoutedEventArgs(SaveDefaultsClickedEvent, this));
// UNICO MessageBox di conferma
MessageBox.Show(
"Tutte le impostazioni sono state salvate con successo.\n\nLe nuove impostazioni verranno applicate alle aste future.",
"Impostazioni Salvate",
MessageBoxButton.OK,
MessageBoxImage.Information
);
}
```
#### Aggiornato CancelAllSettings_Click
**Prima** ?:
```csharp
private void CancelAllSettings_Click(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(CancelCookieClickedEvent, this)); ? Errore!
RaiseEvent(new RoutedEventArgs(CancelSettingsClickedEvent, this));
RaiseEvent(new RoutedEventArgs(CancelDefaultsClickedEvent, this));
}
```
**Dopo** ?:
```csharp
private void CancelAllSettings_Click(object sender, RoutedEventArgs e)
{
// Annulla tutte le modifiche
RaiseEvent(new RoutedEventArgs(CancelSettingsClickedEvent, this));
RaiseEvent(new RoutedEventArgs(CancelDefaultsClickedEvent, this));
}
```
---
## ?? Confronto Prima/Dopo
### Eventi Registrati in MainWindow.xaml
| Evento | Prima | Dopo |
|--------|-------|------|
| `SaveCookieClicked` | ? Registrato | ? Rimosso |
| `ImportCookieClicked` | ? Registrato | ? Rimosso |
| `CancelCookieClicked` | ? Registrato | ? Rimosso |
| `ExportBrowseClicked` | ? Registrato | ? Mantenuto |
| `SaveSettingsClicked` | ? Registrato | ? Mantenuto |
| `CancelSettingsClicked` | ? Registrato | ? Mantenuto |
| `SaveDefaultsClicked` | ? Registrato | ? Mantenuto |
| `CancelDefaultsClicked` | ? Registrato | ? Mantenuto |
### Eventi Definiti in SettingsControl.xaml.cs
| Componente | Prima | Dopo |
|------------|-------|------|
| **Handler Metodi** | 8 metodi | 5 metodi |
| **RoutedEvent Definitions** | 8 eventi | 5 eventi |
| **Event Properties** | 8 properties | 5 properties |
| **Totale righe** | ~180 righe | ~130 righe |
**Riduzione**: -50 righe (~28% più compatto)
---
## ?? Test di Verifica
### Test 1: Avvio Applicazione ?
**Steps**:
1. Compila progetto
2. Avvia applicazione
3. Verifica nessun errore runtime
**Risultato Atteso**: ? Applicazione si avvia senza errori
**Prima**:
```
? System.Windows.Markup.XamlParseException
? 'Impossibile creare SaveCookieClicked...'
? Crash all'avvio
```
**Dopo**:
```
? Compilazione riuscita
? Avvio senza errori
? UI caricata correttamente
```
---
### Test 2: Tab Impostazioni ?
**Steps**:
1. Avvia applicazione
2. Click tab "Impostazioni"
3. Verifica UI caricata
**Risultato Atteso**: ? Impostazioni visibili senza sezione cookie
**Prima**:
```
? Crash durante caricamento XAML
```
**Dopo**:
```
? Impostazioni Export visibili
? Impostazioni Predefinite visibili
? Protezione Account visibile
? Limiti Log visibili
```
---
### Test 3: Salvataggio Impostazioni ?
**Steps**:
1. Modifica impostazioni export
2. Modifica impostazioni predefinite
3. Click "Salva"
4. Verifica conferma
**Risultato Atteso**: ? Salvataggio funziona senza errori
**Log Attesi**:
```
[OK] Tutte le impostazioni salvate con successo
```
---
## ?? Lezioni Apprese
### 1. Cleanup Completo Durante Refactoring
Quando si rimuove una funzionalità, verificare **tutti** i punti di integrazione:
**Checklist Cleanup**:
- [ ] Code-behind handlers (`MainWindow.EventHandlers.Settings.cs`)
- [ ] XAML event registrations (`MainWindow.xaml`)
- [ ] UserControl event definitions (`SettingsControl.xaml.cs`)
- [ ] UserControl XAML buttons/controls (`SettingsControl.xaml`)
- [ ] Event properties exposure (`MainWindow.xaml.cs`)
- [ ] Documentazione
### 2. Pattern Pulizia Eventi WPF
```csharp
// ? SBAGLIATO: Rimuovere solo code-behind
// File: MainWindow.EventHandlers.Settings.cs
// private void Settings_SaveCookieClicked() { } // ? Rimosso
// ? MA DIMENTICATO:
// File: MainWindow.xaml
// SaveCookieClicked="Settings_SaveCookieClicked" ? DEVE essere rimosso!
// ? CORRETTO: Rimuovere entrambi
// 1. Handler in code-behind
// 2. Registrazione in XAML
```
### 3. Testing Runtime Essenziale
```csharp
// ? Build riuscita ? Funzionamento garantito
//
// Il compilatore verifica:
// - Sintassi corretta
// - Tipi corretti
// - Membri accessibili
//
// MA NON verifica:
// - Event binding XAML ? Code-behind
// - Resource keys esistenti
// - Template bindings
//
// ? SEMPRE testare runtime dopo refactoring UI
```
### 4. Refactoring Incrementale
**Approccio Corretto**:
```
1. Rimuovi UI (XAML controls)
?
2. Rimuovi event handlers (code-behind)
?
3. Rimuovi event registrations (XAML)
?
4. Rimuovi event definitions (UserControl)
?
5. ? BUILD + RUN + TEST
```
**Approccio Sbagliato** ?:
```
1. Rimuovi tutto in un colpo
?
2. Build (successo falso)
?
3. Run ? CRASH
```
---
## ? Stato Finale
### Build Status
```
? Compilazione riuscita
? 0 Errori
? 0 Warning
```
### Runtime Status
```
? Avvio applicazione: OK
? Caricamento XAML: OK
? Eventi funzionanti: OK
? UI responsive: OK
```
### File Modificati
| File | Modifiche |
|------|-----------|
| `MainWindow.xaml` | Rimossi 3 event bindings |
| `Controls\SettingsControl.xaml.cs` | Rimossi 3 eventi + handlers (-50 righe) |
### Funzionalità Impattate
| Funzionalità | Status |
|--------------|--------|
| **Gestione Cookie** | ? Automatica tramite browser |
| **Impostazioni Export** | ? Funzionante |
| **Impostazioni Predefinite** | ? Funzionante |
| **Protezione Account** | ? Funzionante |
| **Limiti Log** | ? Funzionante |
---
## ?? Conclusione
### Problema Risolto
- ? **Prima**: Runtime crash all'avvio per eventi cookie obsoleti
- ? **Dopo**: Applicazione si avvia correttamente, autenticazione automatica funzionante
### Cleanup Completato
- ? Rimossi eventi cookie da MainWindow.xaml
- ? Rimossi eventi cookie da SettingsControl.xaml.cs
- ? Aggiornato SaveAllSettings_Click per non usare eventi cookie
- ? Aggiornato CancelAllSettings_Click per non usare eventi cookie
### Testing Verificato
- ? Build riuscita
- ? Runtime senza errori
- ? UI funzionante
- ? Salvataggio impostazioni OK
**Status**: ? **FIX COMPLETATO E TESTATO**
---
**Data Fix**: 2025
**Versione**: 5.8+
**Issue**: Runtime error - eventi cookie obsoleti in XAML
**Causa**: Cleanup incompleto durante refactoring autenticazione automatica
**Soluzione**: Rimozione completa eventi cookie da XAML e code-behind
**Status**: ? RISOLTO
## ?? Riferimenti
- `MainWindow.xaml` - Event bindings
- `Controls\SettingsControl.xaml.cs` - Event definitions e handlers
- `Core\MainWindow.ConnectionHandlers.cs` - Nuovo sistema autenticazione
- `Core\MainWindow.WebView.cs` - Auto-import cookie
- `Documentation\FEATURE_WEBVIEW_PRELOAD_AND_COOKIE_EXTRACTION.md` - Feature autenticazione automatica
@@ -1,368 +0,0 @@
# ?? Fix: Salvataggio Impostazioni e Logging
## ?? Problema Rilevato
### Problema 1: Impostazioni Stato Aste Non Salvate
Le impostazioni per lo stato iniziale delle aste (al caricamento e per nuove aste) **non venivano salvate** correttamente.
**Causa**: Il codice cercava i RadioButton con `this.FindName()` nella MainWindow, ma i controlli sono definiti dentro il `SettingsControl`. Il metodo `FindName()` non trovava i controlli e restituiva `null`, quindi le impostazioni non venivano mai salvate.
### Problema 2: Log Eccessivo
Il log globale veniva riempito con messaggi di successo ogni volta che si salvavano le impostazioni, anche quando non c'erano problemi.
**Comportamento precedente**:
```
[OK] Impostazioni export salvate
[OK] Impostazioni salvate: Anticipo=200ms, MinPrice=€0.00, MaxPrice=€0.00, MaxClicks=0, LogAsta=500, LogGlobale=1000, LoadState=Active, NewState=Stopped
```
### Problema 3: SaveSettingsButton_Click Non Completo
Il metodo `SaveSettingsButton_Click()` salvava solo le impostazioni di export, **perdendo** tutte le altre impostazioni già salvate (stati aste, defaults, limiti log).
---
## ? Soluzioni Implementate
### 1?? Accesso Corretto ai Controlli
**Prima (ERRATO)**:
```csharp
// Cerca nella MainWindow - NON FUNZIONA
var loadAuctionsActive = this.FindName("LoadAuctionsActive") as RadioButton;
```
**Dopo (CORRETTO)**:
```csharp
// Cerca nel SettingsControl - FUNZIONA
var loadAuctionsActive = Settings.FindName("LoadAuctionsActive") as RadioButton;
```
#### Dettagli Tecnici
- I controlli sono definiti in `Controls\SettingsControl.xaml`
- Il campo `Settings` nella MainWindow è di tipo `SettingsControl`
- `Settings.FindName()` cerca i controlli nel Visual Tree del UserControl
- `this.FindName()` cerca solo nella MainWindow (dove i controlli non esistono)
### 2?? Logging Ridotto e Mirato
#### Rimossi Log Generici di Successo
- ? **Rimosso**: `[OK] Impostazioni export salvate`
- ? **Rimosso**: `[OK] Impostazioni salvate: ...`
- ? **Rimosso**: `[INFO] Impostazioni ripristinate`
#### Mantenuti Solo Log Importanti
- ? **Cookie valido**: `[OK] Cookie valido per utente: Username`
- ? **Cookie non valido**: `[ERRORE] Cookie non valido o scaduto`
- ? **Cookie importato**: `[OK] Cookie importato dal browser`
- ? **Errori generici**: `[ERRORE] Salvataggio impostazioni: ...`
- ? **Errori validazione**: `[ERRORE] Valore anticipo puntata non valido`
#### Motivazione
- Gli utenti non devono vedere log di routine per operazioni riuscite
- Il MessageBox `"Tutte le impostazioni sono state salvate"` è sufficiente
- Il log deve essere usato solo per problemi o eventi importanti (cookie, errori)
### 3?? Salvataggio Completo delle Impostazioni
**Prima (PARZIALE)**:
```csharp
private void SaveSettingsButton_Click(object sender, RoutedEventArgs e)
{
var s = new AppSettings() // ? Crea nuovo oggetto vuoto - perde altre impostazioni
{
ExportPath = ExportPathTextBox.Text,
LastExportExt = lastExt,
// ... solo export
};
SettingsManager.Save(s); // Sovrascrive tutto con oggetto parziale
}
```
**Dopo (COMPLETO)**:
```csharp
private void SaveSettingsButton_Click(object sender, RoutedEventArgs e)
{
// ? Carica le impostazioni esistenti
var settings = SettingsManager.Load() ?? new AppSettings();
// ? Aggiorna SOLO le impostazioni di export
settings.ExportPath = ExportPathTextBox.Text;
settings.LastExportExt = lastExt;
// ... altre proprietà export
SettingsManager.Save(settings); // Mantiene tutte le altre impostazioni
}
```
#### Stesso Problema Risolto in SaveDefaultsButton_Click
```csharp
private void SaveDefaultsButton_Click(object sender, RoutedEventArgs e)
{
// ? Carica le impostazioni esistenti
var settings = SettingsManager.Load() ?? new AppSettings();
// ? Aggiorna SOLO le impostazioni defaults
settings.DefaultBidBeforeDeadlineMs = bidMs;
// ... altre proprietà defaults
SettingsManager.Save(settings); // Mantiene tutte le altre impostazioni
}
```
---
## ?? Flusso di Salvataggio Corretto
### Pulsante "Salva" (SaveAllSettings_Click)
```
1. Utente clicca "Salva"
?
2. SettingsControl.SaveAllSettings_Click()
?
3. RaiseEvent(SaveCookieClickedEvent)
? MainWindow.SaveCookieButton_Click()
- Valida cookie
- Se valido: Log "[OK] Cookie valido per utente: Username"
- Se invalido: Log "[ERRORE] Cookie non valido o scaduto"
- Salva sessione
?
4. RaiseEvent(SaveSettingsClickedEvent)
? MainWindow.SaveSettingsButton_Click()
- Carica impostazioni esistenti ?
- Aggiorna solo impostazioni export
- Salva (mantiene tutto il resto)
- NESSUN LOG (operazione di routine)
?
5. RaiseEvent(SaveDefaultsClickedEvent)
? MainWindow.SaveDefaultsButton_Click()
- Carica impostazioni esistenti ?
- Aggiorna defaults aste
- Legge stati aste tramite Settings.FindName() ?
- Salva (mantiene tutto il resto)
- NESSUN LOG (operazione di routine)
?
6. MessageBox: "Tutte le impostazioni sono state salvate con successo"
```
---
## ?? Modifiche al Codice
### File: `Core\EventHandlers\MainWindow.EventHandlers.Settings.cs`
#### 1. LoadDefaultSettings()
```csharp
// ? CORRETTO: Accesso tramite Settings.FindName
var loadAuctionsStopped = Settings.FindName("LoadAuctionsStopped") as RadioButton;
var loadAuctionsPaused = Settings.FindName("LoadAuctionsPaused") as RadioButton;
var loadAuctionsActive = Settings.FindName("LoadAuctionsActive") as RadioButton;
// ? PRIMA: this.FindName (SBAGLIATO - cercava nella MainWindow)
```
#### 2. SaveCookieButton_Click()
```csharp
if (success && session != null)
{
Services.SessionManager.SaveSession(session);
SetUserBanner(session.Username ?? string.Empty, session.RemainingBids);
StartButton.IsEnabled = true;
Log($"[OK] Cookie valido per utente: {session.Username}", LogLevel.Success);
// ? LOG SOLO per cookie valido (informazione importante)
}
else
{
Log($"[ERRORE] Cookie non valido o scaduto", LogLevel.Error);
// ? LOG SOLO per cookie invalido (problema)
}
```
#### 3. ImportCookieFromBrowserButton_Click()
```csharp
if (stattrb != null)
{
SettingsCookieTextBox.Text = stattrb.Value;
Log("[OK] Cookie importato dal browser", LogLevel.Success);
// ? LOG per import riuscito (azione utile)
}
else
{
Log("[ERRORE] Cookie __stattrb non trovato nel browser", LogLevel.Error);
// ? LOG per import fallito (problema)
}
```
#### 4. SaveSettingsButton_Click()
```csharp
// ? Carica le impostazioni esistenti per non perdere gli altri valori
var settings = SettingsManager.Load() ?? new AppSettings();
// Aggiorna solo le impostazioni di export
settings.ExportPath = ExportPathTextBox.Text;
// ...
SettingsManager.Save(settings);
// ? RIMOSSO log di successo (operazione di routine)
```
#### 5. SaveDefaultsButton_Click()
```csharp
// ? Carica le impostazioni esistenti per non perdere gli altri valori
var settings = SettingsManager.Load() ?? new AppSettings();
// Validazione con log di errore
if (int.TryParse(DefaultBidBeforeDeadlineMs.Text, out var bidMs) && bidMs >= 0 && bidMs <= 5000)
{
settings.DefaultBidBeforeDeadlineMs = bidMs;
}
else
{
Log("[ERRORE] Valore anticipo puntata non valido (deve essere 0-5000ms)", LogLevel.Error);
return; // ? Log e return in caso di errore
}
// ? CORRETTO: Accesso tramite Settings.FindName
var loadAuctionsActive = Settings.FindName("LoadAuctionsActive") as RadioButton;
var loadAuctionsPaused = Settings.FindName("LoadAuctionsPaused") as RadioButton;
settings.DefaultStartAuctionsOnLoad = loadAuctionsActive?.IsChecked == true ? "Active" :
loadAuctionsPaused?.IsChecked == true ? "Paused" :
"Stopped";
// Stesso per NewAuctionState
var newAuctionActive = Settings.FindName("NewAuctionActive") as RadioButton;
var newAuctionPaused = Settings.FindName("NewAuctionPaused") as RadioButton;
settings.DefaultNewAuctionState = newAuctionActive?.IsChecked == true ? "Active" :
newAuctionPaused?.IsChecked == true ? "Paused" :
"Stopped";
SettingsManager.Save(settings);
// ? RIMOSSO log di successo (operazione di routine)
```
---
## ?? Test di Verifica
### Test 1: Salvataggio Stato Aste
1. ? Vai su Impostazioni
2. ? Imposta "Nuove aste" su **"In Pausa"**
3. ? Clicca **Salva**
4. ? Riavvia applicazione
5. ? Vai su Impostazioni
6. ? **Verifica**: "In Pausa" è ancora selezionato
7. ? Aggiungi una nuova asta
8. ? **Verifica**: L'asta è in pausa (IsActive=true, IsPaused=true)
### Test 2: Log Ridotto
1. ? Vai su Impostazioni
2. ? Modifica qualche valore
3. ? Clicca **Salva**
4. ? **Verifica**: Nel log globale NON appare `[OK] Impostazioni salvate...`
5. ? **Verifica**: Appare solo il MessageBox di conferma
### Test 3: Cookie Log
1. ? Vai su Impostazioni
2. ? Inserisci un cookie valido
3. ? Clicca **Salva**
4. ? **Verifica**: Nel log appare `[OK] Cookie valido per utente: Username`
### Test 4: Salvataggio Completo
1. ? Imposta stato aste: "In Pausa"
2. ? Imposta anticipo: 300ms
3. ? Imposta max log asta: 1000
4. ? Clicca **Salva**
5. ? Riavvia applicazione
6. ? **Verifica**: Tutte le impostazioni sono state mantenute
### Test 5: Errori di Validazione
1. ? Vai su Impostazioni
2. ? Imposta anticipo: **9999** (fuori range)
3. ? Clicca **Salva**
4. ? **Verifica**: Nel log appare `[ERRORE] Valore anticipo puntata non valido`
---
## ?? Confronto Prima/Dopo
### Salvataggio Stato Aste
| Aspetto | Prima ? | Dopo ? |
|---------|----------|---------|
| Metodo accesso | `this.FindName()` | `Settings.FindName()` |
| Controlli trovati | `null` (non trovati) | Oggetto valido |
| Stato salvato | **NO** (sempre default) | **SÌ** (correttamente) |
| Funzionamento | **Non funziona** | **Funziona** |
### Logging
| Evento | Prima ? | Dopo ? |
|--------|----------|---------|
| Salva export | Log generico | Nessun log |
| Salva defaults | Log lungo | Nessun log |
| Cookie valido | Log generico | `[OK] Cookie valido...` |
| Cookie invalido | Log warning | `[ERRORE] Cookie non valido` |
| Errore validazione | Log warning | `[ERRORE] Valore non valido` |
| Import cookie | Log generico | `[OK] Cookie importato` |
### Persistenza Impostazioni
| Metodo | Prima ? | Dopo ? |
|--------|----------|---------|
| SaveSettingsButton_Click | Crea nuovo oggetto | Carica esistente |
| SaveDefaultsButton_Click | Crea nuovo oggetto | Carica esistente |
| Impostazioni perse | **SÌ** (sovrascrive) | **NO** (mantiene) |
---
## ?? Lezioni Apprese
### 1. FindName() e Visual Tree
- `FindName()` cerca solo nel Visual Tree dell'elemento su cui viene chiamato
- I UserControl hanno il loro Visual Tree separato
- Per accedere ai controlli di un UserControl, usa `userControl.FindName()`
### 2. Pattern Corretto per Salvataggio Impostazioni
```csharp
// ? SEMPRE caricare prima di modificare
var settings = SettingsManager.Load() ?? new AppSettings();
// Modifica solo le proprietà necessarie
settings.Property1 = newValue;
settings.Property2 = otherValue;
// Salva (mantiene tutte le altre proprietà)
SettingsManager.Save(settings);
```
### 3. Logging Efficace
- **Non loggare** operazioni di routine riuscite
- **Logga solo** eventi importanti, problemi o errori
- **Usa MessageBox** per conferme all'utente
- **Usa il log** per debugging e problemi
---
## ?? File Modificati
1. ? `Core\EventHandlers\MainWindow.EventHandlers.Settings.cs`
- Corretto accesso ai RadioButton (`Settings.FindName`)
- Rimossi log generici di successo
- Aggiunto caricamento impostazioni esistenti prima di salvare
- Mantenuti log solo per cookie ed errori
---
**Data Fix**: 2025
**Versione**: 5.1+
**Issue 1**: Stati aste non salvati (RadioButton non trovati)
**Issue 2**: Log eccessivo per operazioni routine
**Issue 3**: Salvataggio parziale perdeva altre impostazioni
**Status**: ? RISOLTO
## ?? Riferimenti
- Vedi anche: `Documentation\FEATURE_INITIAL_AUCTION_STATE.md` per funzionalità stati aste
- Vedi anche: `Documentation\FEATURE_CONFIGURABLE_LOG_LIMITS.md` per limiti log
@@ -1,452 +0,0 @@
# ? Fix UI - Sidebar Sempre Visibile + WebView Init Background
## ?? Problemi Risolti
### 1?? Nome Utente Duplicato
**Problema**: Username mostrato sia nel banner che nella sidebar
**Soluzione**: Rimosso dal banner, mantenuto solo in sidebar
### 2?? Sidebar Non Visibile quando Disconnesso
**Problema**: Sidebar nascosta se utente non connesso
**Soluzione**: Sidebar sempre visibile, mostra "Non connesso" in rosso chiaro
### 3?? WebView Non Inizializzata in Background
**Problema**: WebView init solo al primo click su tab Browser
**Soluzione**: Init forzata all'avvio con `EnsureCoreWebView2Async()`
---
## ?? Modifiche Implementate
### ?? UI Banner (Header)
**Prima** ?:
```
[sirbietole23] Puntate: 50 (20) Credito: EUR 15.00
```
**Dopo** ?:
```
Puntate: 50 (20) Credito: EUR 15.00
```
**Rimosso**: Indicatore connessione duplicato
---
### ?? UI Sidebar (Sinistra)
**Prima** ?:
```
???????????????????????
? [nascosta] ? ? Nascosta se non connesso
???????????????????????
```
**Dopo - Non Connesso** ?:
```
???????????????????????
? Non connesso ? ? Rosso chiaro (#FF5252)
? ? ? ID/Email nascosti
???????????????????????
```
**Dopo - Connesso** ?:
```
???????????????????????
? sirbietole23 ? ? Verde (#00D800), Grassetto
? ID: 6707664 ? ? Grigio scuro
? email@email.com ? ? Grigio medio
???????????????????????
```
---
### ?? Modifiche Codice
#### 1. `Controls\AuctionMonitorControl.xaml`
Rimosso pulsante ConnectionStatus dal banner:
```xaml
<!-- PRIMA ? -->
<Button x:Name="ConnectionStatusButton" ...>
<TextBlock x:Name="ConnectionStatusText" Text="Non connesso" .../>
</Button>
<TextBlock Text="Puntate: " .../>
<!-- DOPO ? -->
<TextBlock Text="Puntate: " .../>
```
---
#### 2. `MainWindow.xaml`
Sidebar sempre visibile, mostra "Non connesso" quando disconnesso:
```xaml
<!-- PRIMA ? -->
<Border x:Name="SidebarUserInfoPanel"
Visibility="Collapsed"> ? Nascosta
...
</Border>
<!-- DOPO ? -->
<Border x:Name="SidebarUserInfoPanel"> ? Sempre visibile
<StackPanel>
<!-- Username (rosso se disconnesso, verde se connesso) -->
<TextBlock x:Name="SidebarUsernameText"
Text="Non connesso"
Foreground="#FF5252"
MouseLeftButtonDown="SidebarUsername_Click"
Cursor="Hand"/>
<!-- Dettagli (visibili solo quando connesso) -->
<StackPanel x:Name="SidebarUserDetailsPanel"
Visibility="Collapsed">
<TextBlock x:Name="SidebarUserIdText"/>
<TextBlock x:Name="SidebarUserEmailText"/>
</StackPanel>
</StackPanel>
</Border>
```
---
#### 3. `Core\MainWindow.UserInfo.cs`
Aggiornato `SetUserBanner()` per gestire sidebar:
```csharp
private void SetUserBanner(string username, int? remainingBids)
{
if (!string.IsNullOrEmpty(username))
{
// === CONNESSO ===
// Banner: Puntate + Credito
RemainingBidsText.Text = remainingBids?.ToString() ?? "0";
AuctionMonitor.ShopCreditText.Text = $"EUR {session.ShopCredit:F2}";
// Sidebar: Username Verde
SidebarUsernameText.Text = username;
SidebarUsernameText.Foreground = Verde;
SidebarUsernameText.FontWeight = Bold;
// Sidebar: Mostra dettagli (ID + Email)
SidebarUserDetailsPanel.Visibility = Visible;
SidebarUserIdText.Text = $"ID: {session.UserId}";
SidebarUserEmailText.Text = session.Email;
}
else
{
// === NON CONNESSO ===
// Banner: Reset
RemainingBidsText.Text = "0";
AuctionMonitor.ShopCreditText.Text = "EUR 0.00";
// Sidebar: "Non connesso" Rosso
SidebarUsernameText.Text = "Non connesso";
SidebarUsernameText.Foreground = RossoChiaro;
SidebarUsernameText.FontWeight = Bold;
// Sidebar: Nascondi dettagli
SidebarUserDetailsPanel.Visibility = Collapsed;
}
}
```
**Rimosso**: Metodo `UpdateConnectionStatus()` obsoleto
---
#### 4. `Core\MainWindow.ConnectionHandlers.cs`
Aggiunto handler click per username sidebar:
```csharp
/// <summary>
/// Handler per il click sul nome utente nella sidebar
/// </summary>
private void SidebarUsername_Click(object sender, MouseButtonEventArgs e)
{
// Riusa la logica del pulsante connessione
ConnectionStatusButton_Click(sender, new RoutedEventArgs());
}
```
**Comportamento**:
- Click su "Non connesso" ? Apre tab Browser per login
- Click su Username ? Mostra opzioni disconnetti
---
#### 5. `Core\MainWindow.WebView.cs`
WebView2 inizializzata subito all'avvio:
```csharp
/// <summary>
/// Inizializza WebView2 in background all'avvio
/// </summary>
private async void InitializeWebView2()
{
Log("[BROWSER] Inizializzazione WebView2 in background...", LogLevel.Info);
// ? FIX: Aspetta che CoreWebView2 sia inizializzato SINCRONAMENTE
await EmbeddedWebView.EnsureCoreWebView2Async(null);
if (EmbeddedWebView.CoreWebView2 != null)
{
_isWebViewInitialized = true;
// Pre-carica Bidoo in background
EmbeddedWebView.CoreWebView2.Navigate("https://it.bidoo.com");
Log("[BROWSER] ? WebView2 inizializzato e pre-caricato", LogLevel.Success);
// Registra evento per auto-login
EmbeddedWebView.CoreWebView2.NavigationCompleted += OnWebViewNavigationCompleted;
}
}
```
**Chiamato da**: `MainWindow()` constructor
**Effetto**:
- Browser pre-caricato in background
- Pronto immediatamente quando utente apre tab
- Cookie extraction funziona subito al login
---
## ?? Comportamento Finale
### Scenario 1: Primo Avvio (Non Connesso)
**Sidebar**:
```
???????????????????????
? Non connesso ? ? Rosso chiaro, clickable
???????????????????????
```
**Banner**:
```
Puntate: 0 Credito: EUR 0.00
```
**Log**:
```
[SESSION] Nessuna sessione salvata
[INFO] Per accedere:
[INFO] 1. Click su 'Non connesso' nella sidebar
[INFO] 2. Si aprirà la scheda Browser
[INFO] 3. Fai login su Bidoo
[INFO] 4. La connessione sarà automatica
[BROWSER] Inizializzazione WebView2 in background...
[BROWSER] ? WebView2 inizializzato e pre-caricato
```
---
### Scenario 2: Dopo Login Automatico
**Sidebar**:
```
???????????????????????
? sirbietole23 ? ? Verde, grassetto, clickable
? ID: 6707664 ?
? email@email.com ?
???????????????????????
```
**Banner**:
```
Puntate: 50 (20) Credito: EUR 15.00
```
**Log**:
```
[BROWSER] Login rilevato - importazione automatica cookie...
[BROWSER] ? Connessione automatica completata
[SESSION] ? Sessione valida - sirbietole23 (50 puntate)
```
---
### Scenario 3: Click su Username quando Connesso
**MessageBox**:
```
????????????????????????????????????
? Gestione Connessione ?
????????????????????????????????????
? Connesso come: sirbietole23 ?
? Puntate residue: 50 ?
? Credito Shop: EUR 15.00 ?
? ?
? Vuoi disconnettere e accedere ?
? con un altro account? ?
? ?
? [ Sì ] [ No ] ?
????????????????????????????????????
```
**Se "Sì"**:
- SessionService.ClearSession()
- Sidebar mostra "Non connesso" rosso
- Banner reset a 0
---
### Scenario 4: Click su "Non connesso"
**MessageBox**:
```
????????????????????????????????????
? Accedi a Bidoo ?
????????????????????????????????????
? Per accedere: ?
? ?
? 1. Fai login su Bidoo nella ?
? scheda Browser ?
? 2. La connessione sarà automatica?
? ?
? Apertura scheda Browser... ?
? ?
? [ OK ] ?
????????????????????????????????????
```
**Effetto**:
- Tab Browser selezionato automaticamente
- Browser già caricato (pre-init background)
- Pronto per login immediato
---
## ?? File Modificati
| File | Modifiche |
|------|-----------|
| `Controls\AuctionMonitorControl.xaml` | Rimosso pulsante ConnectionStatus |
| `MainWindow.xaml` | Sidebar sempre visibile + handler click |
| `MainWindow.xaml.cs` | Rimossi properties ConnectionStatus obsoleti |
| `Core\MainWindow.UserInfo.cs` | `SetUserBanner()` gestisce sidebar, rimosso `UpdateConnectionStatus()` |
| `Core\MainWindow.ConnectionHandlers.cs` | Aggiunto `SidebarUsername_Click()`, rimosso `UpdateConnectionStatus()` |
| `Core\MainWindow.WebView.cs` | Init sincrona WebView2 in background |
**Totale**: 6 file modificati
---
## ? Test di Verifica
### Test 1: Sidebar Sempre Visibile ?
**Steps**:
1. Avvia app (prima volta, senza cookie)
2. Verifica sidebar mostra "Non connesso" in rosso
3. Fai login tramite browser
4. Verifica sidebar mostra username in verde
5. Disconnetti
6. Verifica sidebar torna a "Non connesso" rosso
**Risultato**: ? Sidebar sempre visibile, cambia solo testo/colore
---
### Test 2: WebView Init Background ?
**Steps**:
1. Avvia app
2. Controlla log per "[BROWSER] Inizializzazione WebView2..."
3. Aspetta 2-3 secondi
4. Controlla log per "[BROWSER] ? WebView2 inizializzato"
5. Click su tab Browser
6. Verifica Bidoo già caricato (non loader bianco)
**Risultato**: ? Browser pre-caricato in background
---
### Test 3: Click Sidebar ?
**Steps**:
1. Avvia app senza cookie
2. Click su "Non connesso" in sidebar
3. Verifica tab Browser si apre
4. Fai login su Bidoo
5. Verifica auto-login funziona
6. Click su username in sidebar
7. Verifica MessageBox con opzioni
**Risultato**: ? Click sidebar funziona come previsto
---
## ?? Confronto Prima/Dopo
### Indicatore Connessione
| Aspetto | Prima | Dopo |
|---------|-------|------|
| **Posizione** | Banner + Sidebar | Solo Sidebar ? |
| **Visibilità Non Connesso** | Nascosto | Sempre visibile ? |
| **Colore Non Connesso** | - | Rosso chiaro (#FF5252) ? |
| **Colore Connesso** | Verde | Verde (#00D800) ? |
| **Clickable** | Solo banner | Sidebar username ? |
| **Dettagli (ID/Email)** | Sempre visibili | Nascosti se disconnesso ? |
### WebView Init
| Aspetto | Prima | Dopo |
|---------|-------|------|
| **Quando Init** | Click tab Browser | Avvio app ? |
| **Tempo init** | 2-3 sec dopo click | Background asincrono ? |
| **Pronta quando aperta** | No (loader bianco) | Sì (già caricata) ? |
| **Auto-login** | Non funzionava subito | Funziona subito ? |
| **Log visible** | No | Sì con progress ? |
### User Experience
| Scenario | Prima | Dopo |
|----------|-------|------|
| **Capire se connesso** | Ambiguo | Chiaro (sidebar) ? |
| **Accedere** | Non intuitivo | Click su "Non connesso" ? |
| **Disconnettere** | Nascosto in impostazioni | Click su username ? |
| **Browser pronto** | Attesa 2-3 sec | Immediato ? |
---
## ?? Risultati
### ? UI Pulita
- Username mostrato una sola volta (sidebar)
- Banner compatto con solo dati essenziali
- Sidebar sempre visibile = stato sempre chiaro
### ? UX Migliorata
- Stato connessione immediatamente visibile
- Click su sidebar per azioni rapide
- Browser pre-caricato = esperienza fluida
### ? Codice Pulito
- Rimosso codice duplicato (UpdateConnectionStatus)
- Logica connessione centralizzata in SetUserBanner
- WebView init ben separata
---
**Data Fix**: 2025
**Versione**: 5.9+
**Issue 1**: Username duplicato in banner e sidebar
**Issue 2**: Sidebar nascosta quando disconnesso
**Issue 3**: WebView init solo al click tab
**Status**: ? TUTTI RISOLTI
## ?? Riferimenti
- `Controls\AuctionMonitorControl.xaml` - Banner header
- `MainWindow.xaml` - Sidebar layout
- `Core\MainWindow.UserInfo.cs` - SetUserBanner()
- `Core\MainWindow.ConnectionHandlers.cs` - Click handlers
- `Core\MainWindow.WebView.cs` - WebView init
@@ -1,234 +0,0 @@
# ?? Fix Avvio Singola Asta dalla Griglia
## Problema Rilevato
Quando si cliccava il pulsante **"Avvia"** su una singola asta nella griglia, l'asta **non veniva monitorata** a meno che prima non si fosse cliccato **"Avvia Tutti"**.
## Causa del Problema
Il sistema di monitoraggio aveva una **dipendenza rigida** sul flag `_isAutomationActive`:
1. ? Clic su "Avvia Tutti" ? Avvia `AuctionMonitor.Start()` + imposta `IsActive = true` su tutte le aste
2. ? Clic su "Avvia" (singola asta) ? Imposta solo `IsActive = true` MA **non avvia** `AuctionMonitor.Start()`
3. ? Risultato: L'asta era marcata come attiva, ma il loop di monitoraggio **non era in esecuzione**
### Codice Problematico (Prima)
```csharp
private void ExecuteGridStart(AuctionViewModel? vm)
{
if (vm == null) return;
vm.IsActive = true;
vm.IsPaused = false;
Log($"[START] Asta avviata: {vm.Name}");
UpdateGlobalControlButtons();
}
```
**Mancava**: Avvio del `AuctionMonitor` se non già attivo.
## Soluzione Implementata
### ? 1. Auto-Start del Monitoraggio
Ora, quando si avvia una singola asta, **il monitoraggio viene avviato automaticamente** se non è già attivo:
```csharp
private void ExecuteGridStart(AuctionViewModel? vm)
{
if (vm == null) return;
// Attiva l'asta
vm.IsActive = true;
vm.IsPaused = false;
// Se il monitoraggio globale non è attivo, avvialo automaticamente
if (!_isAutomationActive)
{
_auctionMonitor.Start();
_isAutomationActive = true;
Log($"[AUTO-START] Monitoraggio avviato automaticamente per asta: {vm.Name}", LogLevel.Info);
}
else
{
Log($"[START] Asta avviata: {vm.Name}", LogLevel.Info);
}
UpdateGlobalControlButtons();
}
```
### ? 2. Auto-Stop del Monitoraggio
Quando si ferma l'ultima asta attiva, **il monitoraggio viene fermato automaticamente**:
```csharp
private void ExecuteGridStop(AuctionViewModel? vm)
{
if (vm == null) return;
vm.IsActive = false;
// Se tutte le aste sono fermate, ferma anche il monitoraggio globale
bool hasActiveAuctions = _auctionViewModels.Any(a => a.IsActive);
if (!hasActiveAuctions && _isAutomationActive)
{
_auctionMonitor.Stop();
_isAutomationActive = false;
Log($"[AUTO-STOP] Monitoraggio fermato: nessuna asta attiva", LogLevel.Info);
}
else
{
Log($"[STOP] Asta fermata: {vm.Name}", LogLevel.Info);
}
UpdateGlobalControlButtons();
}
```
### ? 3. Migliorato Logging
Aggiunto logging dettagliato per capire quando il monitoraggio viene avviato/fermato automaticamente:
- `[AUTO-START] Monitoraggio avviato automaticamente per asta: Nome`
- `[AUTO-STOP] Monitoraggio fermato: nessuna asta attiva`
- `[START] Asta avviata: Nome` (se monitoraggio già attivo)
- `[STOP] Asta fermata: Nome` (se ci sono altre aste attive)
### ? 4. Coerenza con Pulsanti Globali
I pulsanti globali ora sono coerenti con il nuovo comportamento:
- **"Avvia Tutti"**: Avvia monitoraggio + attiva tutte le aste
- **"Ferma Tutti"**: Ferma monitoraggio + disattiva tutte le aste
- **"Pausa Tutti"**: Mette in pausa tutte le aste attive (monitoraggio rimane attivo)
## Comportamento Atteso
### ? Scenario 1: Avvio Singola Asta (Monitoraggio Fermo)
1. Nessuna asta attiva
2. Clic su "Avvia" su Asta A
3. ? Monitoraggio si avvia automaticamente
4. ? Asta A inizia ad essere monitorata
5. ? Log: `[AUTO-START] Monitoraggio avviato automaticamente per asta: Asta A`
### ? Scenario 2: Avvio Singola Asta (Monitoraggio Già Attivo)
1. Asta A già attiva
2. Clic su "Avvia" su Asta B
3. ? Monitoraggio già attivo (non viene riavviato)
4. ? Asta B inizia ad essere monitorata
5. ? Log: `[START] Asta avviata: Asta B`
### ? Scenario 3: Stop Ultima Asta
1. Solo Asta A è attiva
2. Clic su "Ferma" su Asta A
3. ? Asta A viene fermata
4. ? Monitoraggio si ferma automaticamente (nessuna asta attiva)
5. ? Log: `[AUTO-STOP] Monitoraggio fermato: nessuna asta attiva`
### ? Scenario 4: Stop Asta (Altre Attive)
1. Asta A e Asta B attive
2. Clic su "Ferma" su Asta A
3. ? Asta A viene fermata
4. ? Monitoraggio rimane attivo (Asta B ancora attiva)
5. ? Log: `[STOP] Asta fermata: Asta A`
### ? Scenario 5: Avvia Tutti
1. Asta A e Asta B ferme
2. Clic su "Avvia Tutti"
3. ? Monitoraggio si avvia
4. ? Tutte le aste vengono attivate
5. ? Log: `[START] Monitoraggio avviato!` + `[START ALL] Tutte le aste avviate/riprese`
### ? Scenario 6: Ferma Tutti
1. Alcune aste attive
2. Clic su "Ferma Tutti"
3. ? Tutte le aste vengono fermate
4. ? Monitoraggio si ferma
5. ? Log: `[STOP ALL] Monitoraggio fermato e tutte le aste arrestate`
## Vantaggi della Soluzione
### ?? 1. Maggiore Flessibilità
- Puoi avviare solo le aste che ti interessano
- Non serve più avviare tutte le aste per monitorarne una
### ?? 2. Risparmio Risorse
- Il monitoraggio si ferma automaticamente quando non serve
- Polling solo sulle aste effettivamente attive
### ?? 3. UX Migliorata
- Comportamento più intuitivo
- Non serve capire la differenza tra "Avvia Tutti" e "Avvia" singolo
### ?? 4. Logging Chiaro
- Si vede esattamente quando il monitoraggio parte/si ferma
- Distingue tra start manuale e automatico
## File Modificati
1. ? `Core\MainWindow.Commands.cs`
- Aggiunto auto-start in `ExecuteGridStart`
- Aggiunto auto-stop in `ExecuteGridStop`
- Aggiunta importazione `System.Linq` e `AutoBidder.Utilities`
- Migliorato logging con `LogLevel`
2. ? `Core\MainWindow.ButtonHandlers.cs`
- Migliorato logging in `StartButton_Click`
- Migliorato logging in `StopButton_Click`
- Migliorato logging in `PauseAllButton_Click`
## Note Tecniche
### Perché Auto-Start è Sicuro?
1. **Idempotente**: `AuctionMonitor.Start()` controlla se è già attivo
2. **Thread-safe**: Il lock interno previene race conditions
3. **Logging**: Si vede esattamente cosa succede
```csharp
public void Start()
{
if (_monitoringTask != null && !_monitoringTask.IsCompleted)
{
OnLog?.Invoke("[WARN] Monitoraggio gia' attivo");
return; // Non fa nulla se già attivo
}
// ...
}
```
### Perché Auto-Stop è Sicuro?
1. **Controlla tutte le aste**: Verifica se ci sono altre aste attive prima di fermare
2. **Non forza**: Se ci sono altre aste attive, non ferma il monitoraggio
3. **Graceful**: Usa `Stop()` che fa cleanup corretto
## Test di Verifica
- [x] Avviare singola asta da griglia (monitoraggio fermo)
- [x] Avviare seconda asta (monitoraggio già attivo)
- [x] Fermare asta (altre attive) ? Monitoraggio continua
- [x] Fermare ultima asta ? Monitoraggio si ferma
- [x] "Avvia Tutti" continua a funzionare
- [x] "Ferma Tutti" continua a funzionare
- [x] "Pausa Tutti" continua a funzionare
- [x] Pulsanti di griglia abilitati/disabilitati correttamente
- [x] Log mostra AUTO-START/AUTO-STOP
---
**Data Fix**: 2025
**Versione**: 4.0+
**Issue**: Pulsante "Avvia" singolo non funzionava senza "Avvia Tutti"
**Status**: ? RISOLTO
## Riepilogo
Prima: **Dovevi cliccare "Avvia Tutti" per monitorare anche una sola asta**
Dopo: **Clicchi "Avvia" su un'asta e parte automaticamente il monitoraggio** ??
@@ -1,341 +0,0 @@
# ?? Fix Puntata su Asta Già Vinta
## Problema Rilevato
Il sistema tentava di **puntare anche quando l'utente era già il vincitore corrente** dell'asta, causando:
1. ? **Errori inutili** - La puntata falliva con messaggio "Asta chiusa" o simile
2. ? **Spreco risorse** - Chiamate API non necessarie
3. ? **Logging confuso** - Messaggi di errore quando tutto andava bene
4. ? **Puntate perse** - Tentativo di puntata quando non aveva senso
## Causa del Problema
Il metodo `ShouldBid()` non controllava se l'utente era già il vincitore corrente prima di decidere di puntare.
La logica era:
```csharp
// ? PRIMA - Non controllava IsMyBid
private bool ShouldBid(AuctionInfo auction, AuctionState state)
{
// Controlli prezzo, reset count, max clicks, cooldown...
// MA mancava: controllo se sono già vincitore!
return true;
}
```
Scenario problematico:
1. ? Utente punta alle 10:00:00 e vince
2. ? Timer riparte da 20 secondi
3. ? Timer scende a 0.3 secondi (dentro finestra anticipo)
4. ? Sistema cerca di puntare di nuovo
5. ? Server risponde: "Asta chiusa" o errore simile
6. ? Log mostra errore anche se l'utente ha già vinto!
## Soluzione Implementata
### ? 1. Controllo `IsMyBid` in `ShouldBid()`
Aggiunto controllo come **prima condizione**:
```csharp
private bool ShouldBid(AuctionInfo auction, AuctionState state)
{
// ? NUOVO: Non puntare se sono già il vincitore corrente
if (state.IsMyBid)
{
// Sono già io l'ultimo ad aver puntato, non serve puntare di nuovo
return false;
}
// ... altri controlli ...
return true;
}
```
### ? 2. Logging Chiaro in `ExecuteBidStrategy()`
Aggiunto messaggio informativo quando si evita la puntata:
```csharp
private async Task ExecuteBidStrategy(...)
{
if (timerMs <= auction.BidBeforeDeadlineMs)
{
auction.AddLog($"[STRATEGIA] Finestra di puntata raggiunta: {timerMs:F0}ms <= {auction.BidBeforeDeadlineMs}ms");
// ? NUOVO: Log quando skippo perché sono già vincitore
if (state.IsMyBid)
{
auction.AddLog($"[STRATEGIA] SKIP: Sono già il vincitore corrente (ultimo bidder: {state.LastBidder})");
return;
}
// ... continua con puntata ...
}
}
```
### ? 3. Come Funziona `IsMyBid`
Il flag `state.IsMyBid` viene calcolato in `BidooApiClient.ParsePollingResponse()`:
```csharp
state.IsMyBid = !string.IsNullOrEmpty(_session.Username) &&
state.LastBidder.Equals(_session.Username, StringComparison.OrdinalIgnoreCase);
```
Confronta il `LastBidder` dall'API con lo `Username` della sessione (case-insensitive).
## Comportamento Atteso
### ? Scenario 1: Utente NON Vincitore (Deve Puntare)
```
Timer: 0.3s (dentro finestra 0.5s)
Ultimo bidder: "altroUtente123"
IsMyBid: false
[STRATEGIA] Finestra di puntata raggiunta: 300ms <= 500ms
[STRATEGIA] Eseguo puntata...
[BID OK] Latenza: 45ms -> EUR 1.50
```
**Risultato**: ? Punta correttamente
### ? Scenario 2: Utente GIÀ Vincitore (SKIP Puntata)
```
Timer: 0.3s (dentro finestra 0.5s)
Ultimo bidder: "miousername"
IsMyBid: true
[STRATEGIA] Finestra di puntata raggiunta: 300ms <= 500ms
[STRATEGIA] SKIP: Sono già il vincitore corrente (ultimo bidder: miousername)
```
**Risultato**: ? NON punta (evita errore)
### ? Scenario 3: Altro Utente Supera
```
t=10s: Io puntp -> IsMyBid = true
t=8s: [STRATEGIA] SKIP: Sono già vincitore
t=6s: [STRATEGIA] SKIP: Sono già vincitore
t=4s: altroUtente punta -> IsMyBid = false
t=0.3s: [STRATEGIA] Finestra raggiunta
t=0.3s: [BID OK] Riprendo il controllo!
```
**Risultato**: ? Punta solo quando necessario
## Vantaggi della Soluzione
### ?? 1. Nessun Errore Inutile
- ? **Prima**: "Asta chiusa" quando eri già vincitore
- ? **Dopo**: Nessun errore, log chiaro
### ?? 2. Risparmio Risorse
- ? **Prima**: Chiamata API inutile quando già vincitore
- ? **Dopo**: Skip immediato, nessuna chiamata
### ?? 3. Logging Trasparente
```
? [STRATEGIA] SKIP: Sono già il vincitore corrente
```
Invece di:
```
? [BID FAIL] Asta chiusa
```
### ?? 4. Strategia Ottimizzata
- Punta **solo** quando serve riprendersi l'asta
- Non spreca puntate quando sei già vincitore
## Test Scenario
### Test 1: Vincitore Corrente (Non Deve Puntare)
**Setup**:
- Imposta Anticipo = 500ms
- Aggiungi asta X
- Punta manualmente
- Sei il vincitore (LastBidder = "tuousername")
**Verifica**:
1. ? Timer scende da 20s a 0.4s
2. ? Log: `[STRATEGIA] Finestra di puntata raggiunta: 400ms <= 500ms`
3. ? Log: `[STRATEGIA] SKIP: Sono già il vincitore corrente`
4. ? **Nessuna puntata** effettuata
5. ? **Nessun errore** mostrato
### Test 2: Altro Utente Supera (Deve Puntare)
**Setup**:
- Sei il vincitore
- Altro utente punta e diventa vincitore
- Timer scende a 0.3s
**Verifica**:
1. ? Log: `[STRATEGIA] Finestra di puntata raggiunta: 300ms <= 500ms`
2. ? **Nessun SKIP** (non sei più vincitore)
3. ? Log: `[BID OK] Latenza: XXms`
4. ? Puntata **effettuata correttamente**
### Test 3: Alternanza Vincitori
**Setup**:
- Tu: punta
- Altro: punta
- Tu: riprende controllo
- Altro: riprende controllo
**Verifica**:
- ? SKIP solo quando sei vincitore
- ? Punta solo quando NON sei vincitore
- ? Log chiaro per ogni decisione
## File Modificati
### 1. ? `Services\AuctionMonitor.cs`
**Modifiche**:
- `ShouldBid()`: Aggiunto controllo `state.IsMyBid` come prima condizione
- `ExecuteBidStrategy()`: Aggiunto logging quando si skippa per vincitore corrente
**Prima**:
```csharp
private bool ShouldBid(AuctionInfo auction, AuctionState state)
{
// ? Mancava controllo IsMyBid
// Controlli prezzo...
// Controlli reset...
// Controlli clicks...
return true;
}
```
**Dopo**:
```csharp
private bool ShouldBid(AuctionInfo auction, AuctionState state)
{
// ? NUOVO: Prima controlla se sei già vincitore
if (state.IsMyBid)
{
return false;
}
// ... altri controlli ...
return true;
}
```
## Ordine di Controllo in `ShouldBid()`
```
1. ? IsMyBid? ? false (skip, sei già vincitore)
2. ? Price OK? ? false (skip, prezzo fuori range)
3. ? Reset Count OK? ? false (skip, troppi/pochi reset)
4. ? Max Clicks OK? ? false (skip, raggiunto limite click)
5. ? Cooldown OK? ? false (skip, troppo presto dall'ultimo click)
6. ? Tutti OK? ? true (PUNTA!)
```
**Importante**: `IsMyBid` è il **primo** controllo perché è la condizione più comune e più veloce da verificare.
## Note Tecniche
### Perché Prima Condizione?
1. **Performance**: Controllo più veloce (confronto string)
2. **Frequenza**: Caso più comune quando monitori un'asta che già vinci
3. **Logica**: Non ha senso controllare prezzo/reset se sei già vincitore
### Quando `IsMyBid` è `true`?
```csharp
// In BidooApiClient.cs
state.IsMyBid = !string.IsNullOrEmpty(_session.Username) &&
state.LastBidder.Equals(_session.Username, StringComparison.OrdinalIgnoreCase);
```
Condizioni:
- ? Sessione ha username valido
- ? LastBidder dall'API = Username sessione (case-insensitive)
### Possibili Edge Case
#### Caso 1: Username Non Impostato
```
_session.Username = null o ""
? IsMyBid = false sempre
? Sistema continua a puntare
```
**Soluzione**: Richiedi sempre configurazione sessione all'avvio
#### Caso 2: Username Diverso (Typo)
```
Username sessione: "MioUsername"
LastBidder API: "miousername"
? IsMyBid = false (StringComparison.OrdinalIgnoreCase gestisce)
```
**Soluzione**: Confronto case-insensitive già implementato
## Log Esempi
### Log Normale (Non Vincitore)
```
[STRATEGIA] Finestra di puntata raggiunta: 450ms <= 500ms
[BID OK] Latenza: 42ms -> EUR 1.25
```
### Log con SKIP (Già Vincitore)
```
[STRATEGIA] Finestra di puntata raggiunta: 380ms <= 500ms
[STRATEGIA] SKIP: Sono già il vincitore corrente (ultimo bidder: miousername)
```
### Log Alternanza
```
[STRATEGIA] Finestra di puntata raggiunta: 450ms <= 500ms
[STRATEGIA] SKIP: Sono già il vincitore corrente (ultimo bidder: miousername)
[RESET] Puntata: EUR 1.30 da altroUtente
[STRATEGIA] Finestra di puntata raggiunta: 420ms <= 500ms
[BID OK] Latenza: 38ms -> EUR 1.31
```
---
## ? Test di Verifica
- [x] Non punta quando è già vincitore
- [x] Log mostra SKIP con motivo chiaro
- [x] Punta quando altro utente supera
- [x] Nessun errore "Asta chiusa" quando vincitore
- [x] Risparmia chiamate API inutili
- [x] Logging chiaro in tutti gli scenari
---
**Data Fix**: 2025
**Versione**: 4.0+
**Issue**: Puntata inutile quando già vincitore
**Status**: ? RISOLTO
## Riepilogo
**Prima**:
- ? Puntava anche quando già vincitore
- ? Errori "Asta chiusa" senza motivo
- ? Spreco risorse e puntate
**Dopo**:
- ? SKIP automatico se già vincitore
- ? Log chiaro: `[STRATEGIA] SKIP: Sono già il vincitore corrente`
- ? Punta solo quando serve riprendersi l'asta
- ? Nessun errore inutile
@@ -1,380 +0,0 @@
# ?? Fix Critici - Tab Impostazioni + WebView Init
## ?? Problemi Rilevati
### 1?? Tab Impostazioni Non Si Visualizza
**Sintomo**: Click sulla tab "Impostazioni" ? tab selezionata ma contenuto non mostrato
**Causa**:
```csharp
// ? PROBLEMA
private void TabImpostazioni_Checked(object sender, RoutedEventArgs e)
{
LoadDefaultSettings(); // Carica impostazioni
// MANCA: ShowPanel(Settings); ? Non chiamato!
}
```
### 2?? WebView Non Inizializzata Correttamente
**Sintomo**: Cookie extraction non funziona, browser non pre-caricato
**Causa**:
- `InitializeWebView2()` chiamato troppo presto (nel constructor)
- UI non ancora completamente renderizzata
- `EnsureCoreWebView2Async()` fallisce silenziosamente
---
## ? Soluzioni Implementate
### 1?? Fix Tab Impostazioni
**File**: `Core\MainWindow.ControlEvents.cs`
**Prima** ?:
```csharp
private void TabImpostazioni_Checked(object sender, RoutedEventArgs e)
{
try
{
// Carica impostazioni quando si apre la tab
LoadDefaultSettings();
// NOTA: Caricamento cookie RIMOSSO - ora automatico tramite browser
}
catch { }
}
```
**Dopo** ?:
```csharp
private void TabImpostazioni_Checked(object sender, RoutedEventArgs e)
{
try
{
// ? FIX: Mostra il pannello Impostazioni
ShowPanel(Settings);
// Carica impostazioni quando si apre la tab
LoadDefaultSettings();
// NOTA: Caricamento cookie RIMOSSO - ora automatico tramite browser
}
catch { }
}
```
**Effetto**:
- ? Click su tab "Impostazioni" ? pannello Settings visualizzato
- ? Impostazioni caricate correttamente
- ? Coerente con altre tab (tutte chiamano ShowPanel)
---
### 2?? Fix WebView Init Background
**File**: `Core\MainWindow.WebView.cs`
**Prima** ?:
```csharp
private async void InitializeWebView2()
{
if (EmbeddedWebView == null)
{
Log("[WARN] WebView2 non disponibile", LogLevel.Warn);
return;
}
Log("[BROWSER] Inizializzazione WebView2 in background...", LogLevel.Info);
// ? PROBLEMA: UI non ancora completamente caricata
await EmbeddedWebView.EnsureCoreWebView2Async(null);
// ...
}
```
**Dopo** ?:
```csharp
private async void InitializeWebView2()
{
try
{
if (EmbeddedWebView == null)
{
Log("[WARN] WebView2 non disponibile", LogLevel.Warn);
return;
}
Log("[BROWSER] Inizializzazione WebView2 in background...", LogLevel.Info);
// ? FIX: Aspetta 500ms che UI sia completamente caricata
await System.Threading.Tasks.Task.Delay(500);
// ? Ora l'init funziona correttamente
await EmbeddedWebView.EnsureCoreWebView2Async(null);
if (EmbeddedWebView.CoreWebView2 != null)
{
_isWebViewInitialized = true;
// Pre-carica Bidoo
EmbeddedWebView.CoreWebView2.Navigate("https://it.bidoo.com");
Log("[BROWSER] ? WebView2 inizializzato e pre-caricato", LogLevel.Success);
// Registra evento auto-login
EmbeddedWebView.CoreWebView2.NavigationCompleted += OnWebViewNavigationCompleted;
}
}
catch (Exception ex)
{
Log($"[WARN] Inizializzazione WebView2 fallita: {ex.Message}", LogLevel.Warn);
Log("[INFO] WebView2 sarà inizializzata al primo utilizzo del browser", LogLevel.Info);
}
}
```
**Miglioramenti**:
- ? `Task.Delay(500)` - Aspetta che UI sia renderizzata
- ? `try-catch` completo - Gestisce errori gracefully
- ? Log fallback - Informa utente se init fallisce
- ? Fallback automatico - WebView init al primo uso se background fallisce
---
## ?? Confronto Prima/Dopo
### Tab Impostazioni
| Aspetto | Prima ? | Dopo ? |
|---------|----------|---------|
| **Click tab** | Tab selezionata | Tab selezionata |
| **Pannello mostrato** | Niente (rimane tab precedente) | Settings visualizzato |
| **Impostazioni caricate** | Sì (ma invisibili) | Sì (e visibili) |
| **Coerenza con altre tab** | No | Sì |
### WebView Init
| Aspetto | Prima ? | Dopo ? |
|---------|----------|---------|
| **Timing init** | Troppo presto | Dopo 500ms (UI pronta) |
| **Successo init** | Spesso fallisce | Quasi sempre successo |
| **Gestione errori** | Silenzioso | Log + fallback |
| **Cookie extraction** | Non funziona | Funziona |
| **Pre-load Bidoo** | Non eseguito | Eseguito |
---
## ?? Test di Verifica
### Test 1: Tab Impostazioni ?
**Steps**:
1. Avvia app
2. App si apre su tab "Aste Attive" (default)
3. Click su tab "Impostazioni"
4. Verifica pannello Settings mostrato
5. Verifica campi impostazioni visibili
6. Modifica un'impostazione
7. Salva
8. Cambia tab
9. Torna su "Impostazioni"
10. Verifica impostazione salvata
**Risultato Atteso**: ? Settings sempre visibile quando tab selezionata
---
### Test 2: WebView Init Background ?
**Steps**:
1. Avvia app (primo avvio)
2. Aspetta 5 secondi (non aprire tab Browser)
3. Controlla log per:
```
[BROWSER] Inizializzazione WebView2 in background...
[BROWSER] ? WebView2 inizializzato e pre-caricato
```
4. Click su tab "Browser"
5. Verifica Bidoo già caricato (non loader bianco)
6. Fai login su Bidoo
7. Controlla log per:
```
[BROWSER] Login rilevato - importazione automatica cookie...
[BROWSER] ? Connessione automatica completata
```
**Risultato Atteso**: ? WebView pre-caricata, auto-login funzionante
---
### Test 3: Fallback WebView (Se Init Fallisce) ?
**Scenario**: WebView2 Runtime non installato o problema temporaneo
**Steps**:
1. Simula errore init (disconnetti rete)
2. Avvia app
3. Controlla log per:
```
[WARN] Inizializzazione WebView2 fallita: [errore]
[INFO] WebView2 sarà inizializzata al primo utilizzo del browser
```
4. Click su tab "Browser"
5. Verifica WebView inizializzata al primo uso
**Risultato Atteso**: ? App non crasha, fallback funziona
---
## ?? Flusso Completo Corretto
### Avvio Applicazione
```
1. MainWindow() Constructor
?
2. InitializeComponent() ? XAML caricato
?
3. InitializeCommands()
4. LoadSavedAuctions()
5. LoadExportSettings()
6. LoadDefaultSettings()
7. UpdateGlobalControlButtons()
?
8. InitializeUserInfoTimers()
9. LoadSavedSession()
?
10. InitializeWebView2() ? Async, non blocca
? (in background)
- Task.Delay(500ms) ? Aspetta UI
- EnsureCoreWebView2Async()
- Navigate("bidoo.com")
- Log success ?
?
11. App pronta ?
```
### Click Tab Impostazioni
```
1. User click tab "Impostazioni"
?
2. TabImpostazioni_Checked()
?
3. ShowPanel(Settings) ?
?
- AuctionMonitor.Visibility = Collapsed
- Browser.Visibility = Collapsed
- PuntateGratisPanel.Visibility = Collapsed
- StatisticsPanel.Visibility = Collapsed
- Settings.Visibility = Visible ?
?
4. LoadDefaultSettings()
?
5. Settings visualizzato ?
```
---
## ?? File Modificati
| File | Modifiche | Linee |
|------|-----------|-------|
| `Core\MainWindow.ControlEvents.cs` | Aggiunto `ShowPanel(Settings)` | +1 |
| `Core\MainWindow.WebView.cs` | Delay 500ms + try-catch completo | +5 |
**Totale**: 2 file, 6 righe modificate
---
## ?? Note Importanti
### Timing WebView Init
**Perché 500ms?**
- 100ms ? Troppo poco, UI non pronta
- 500ms ? Giusto compromesso
- 1000ms ? Troppo, utente aspetta troppo
**Alternative considerate**:
1. ? `Loaded` event ? Troppo presto
2. ? `ContentRendered` event ? Non affidabile con WPF moderno
3. ? `Task.Delay(500)` ? Semplice e funziona
### Gestione Errori WebView
**Scenari coperti**:
1. ? WebView2 Runtime non installato
2. ? Problema temporaneo di rete
3. ? Permessi insufficienti
4. ? Altro controllo attivo su WebView
**Fallback**:
- WebView inizializzata al primo utilizzo del browser
- App continua a funzionare normalmente
- Solo funzionalità browser ritardata
---
## ?? Risultati Finali
### ? Tab Impostazioni
- Click su tab ? Pannello visualizzato immediatamente
- Impostazioni caricate e mostrate
- Modifiche salvate correttamente
- Coerente con tutte le altre tab
### ? WebView Background Init
- Inizializzata automaticamente dopo 500ms
- Bidoo pre-caricato in background
- Pronta all'uso quando utente apre tab Browser
- Auto-login funzionante
- Fallback graceful se init fallisce
### ? User Experience
- App si avvia velocemente
- Tutte le tab funzionano correttamente
- Browser immediatamente disponibile
- Nessun crash o errore visibile
---
**Data Fix**: 2025
**Versione**: 6.0+
**Issue 1**: Tab Impostazioni non visualizzata
**Issue 2**: WebView init falliva silenziosamente
**Status**: ? ENTRAMBI RISOLTI
## ?? Riferimenti
- `Core\MainWindow.ControlEvents.cs` - Tab navigation handlers
- `Core\MainWindow.WebView.cs` - WebView initialization
- `MainWindow.xaml.cs` - Constructor e inizializzazione
---
## ?? Debug Tips
### Se Tab Impostazioni Non Si Vede
1. Controlla log per errori durante `LoadDefaultSettings()`
2. Verifica `Settings.Visibility` in debugger
3. Controlla che `ShowPanel()` sia chiamato
### Se WebView Non Si Inizializza
1. Controlla log per:
- `[BROWSER] Inizializzazione WebView2...`
- `[BROWSER] ? WebView2 inizializzato` oppure
- `[WARN] Inizializzazione WebView2 fallita`
2. Verifica WebView2 Runtime installato
3. Prova ad aprire manualmente tab Browser
**Comando check WebView2 Runtime**:
```powershell
Get-ItemProperty -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" -Name pv
```
Se non presente, scarica da: https://developer.microsoft.com/en-us/microsoft-edge/webview2/
@@ -1,425 +0,0 @@
# ?? Fix Aggiornamento UI Contatori Puntate
## ?? Problema Rilevato
Dopo una puntata riuscita:
- ? La colonna "Clicks" nella griglia mostra **0** invece del numero corretto
- ? Il banner "Puntate residue" in alto non si aggiorna immediatamente
- ? L'aggiornamento avviene solo dopo 5-10 minuti (timer automatico)
### Screenshot del Problema
- **Clicks**: mostra `0` anche dopo puntata
- **Puntate**: mostra `48` (non aggiornato dopo puntata)
---
## ?? Analisi del Problema
### Problema 1: `RefreshCounters()` non sul Thread UI
`RefreshCounters()` veniva chiamato dal thread worker invece che dal thread UI, quindi la UI non si aggiornava.
```csharp
// ? PRIMA - thread worker
vm.RefreshCounters();
```
### Problema 2: Banner Aggiornato Solo dai Timer
Il banner delle puntate residue veniva aggiornato solo dai timer (ogni 5-10 minuti), non immediatamente dopo la puntata.
### Problema 3: Parsing Risposta Server Poco Chiaro
Il parsing della risposta non aveva logging dettagliato, quindi era impossibile capire se i dati arrivavano correttamente.
---
## ? Soluzioni Implementate
### 1?? Aggiunto Logging Dettagliato per Debugging
**File**: `Services/BidooApiClient.cs`
Ora quando punti, il log mostra **esattamente** cosa restituisce il server:
```csharp
if (responseText.StartsWith("ok", StringComparison.OrdinalIgnoreCase))
{
result.Success = true;
var parts = responseText.Split('|');
// Log della risposta completa per debugging
Log($"[BID PARSE] Risposta completa: {responseText}", auctionId);
Log($"[BID PARSE] Numero totale campi: {parts.Length}", auctionId);
// ? FORMATO RISPOSTA BIDOO: 9 campi
// Campo 1 (indice 0): "ok"
// Campo 2 (indice 1): Puntate residue totali
// Campo 5 (indice 4): Puntate usate su questa asta
// Campo 2 (indice 1): Puntate residue totali
if (parts.Length > 1)
{
Log($"[BID PARSE] Campo 2 (indice 1) - Remaining bids: '{parts[1]}'", auctionId);
if (int.TryParse(parts[1], out var remaining))
{
result.RemainingBids = remaining;
_session.RemainingBids = remaining;
Log($"[BID SUCCESS] ? Puntate residue totali: {remaining}", auctionId);
}
else
{
Log($"[BID PARSE WARN] ?? Impossibile parsare campo 2", auctionId);
}
}
// Campo 5 (indice 4): Puntate usate su questa asta
if (parts.Length > 4)
{
Log($"[BID PARSE] Campo 5 (indice 4) - Bids used: '{parts[4]}'", auctionId);
if (int.TryParse(parts[4], out var usedOnAuction))
{
result.BidsUsedOnThisAuction = usedOnAuction;
Log($"[BID SUCCESS] ? Puntate usate su questa asta: {usedOnAuction}", auctionId);
}
else
{
Log($"[BID PARSE WARN] ?? Impossibile parsare campo 5", auctionId);
}
}
// Log tutti i campi per debugging completo
Log($"[BID PARSE DEBUG] Tutti i campi della risposta:", auctionId);
for (int i = 0; i < parts.Length; i++)
{
Log($" Campo {i+1} (indice {i}): '{parts[i]}'", auctionId);
}
}
```
### 2?? Aggiunto Metodo per Aggiornare Banner Immediatamente
**File**: `Core/MainWindow.UserInfo.cs`
Nuovo metodo `UpdateRemainingBidsDisplay()` per aggiornare il banner senza aspettare i timer:
```csharp
/// <summary>
/// Aggiorna immediatamente il banner delle puntate residue (chiamato dopo ogni puntata)
/// </summary>
public void UpdateRemainingBidsDisplay()
{
try
{
var session = _auctionMonitor.GetSession();
if (session != null && session.RemainingBids > 0)
{
RemainingBidsText.Text = session.RemainingBids.ToString();
Log($"[BANNER UPDATE] Puntate residue aggiornate: {session.RemainingBids}", LogLevel.Info);
}
}
catch (Exception ex)
{
Log($"[ERROR] Errore aggiornamento banner: {ex.Message}", LogLevel.Error);
}
}
```
### 3?? Aggiornamento Banner dopo Puntata Manuale
**File**: `Core/MainWindow.Commands.cs`
Ora `ExecuteGridBidAsync` chiama `UpdateRemainingBidsDisplay()` e `RefreshCounters()` sul thread UI:
```csharp
private async Task ExecuteGridBidAsync(AuctionViewModel? vm)
{
if (vm == null) return;
try
{
Log($"[BID] Puntata manuale richiesta su: {vm.Name}", LogLevel.Info);
var result = await _auctionMonitor.PlaceManualBidAsync(vm.AuctionInfo);
// Aggiorna dati puntate da risposta server per puntata manuale
if (result.Success)
{
if (result.RemainingBids.HasValue)
{
vm.AuctionInfo.RemainingBids = result.RemainingBids.Value;
// ? NUOVO: Aggiorna immediatamente il banner in alto - SUL THREAD UI
Dispatcher.Invoke(() => UpdateRemainingBidsDisplay());
}
if (result.BidsUsedOnThisAuction.HasValue)
{
vm.AuctionInfo.BidsUsedOnThisAuction = result.BidsUsedOnThisAuction.Value;
}
// ? NUOVO: Notifica aggiornamento contatori - SUL THREAD UI
Dispatcher.Invoke(() => vm.RefreshCounters());
Log($"[OK] Puntata manuale su {vm.Name}: {result.LatencyMs}ms", LogLevel.Success);
}
else
{
Log($"[FAIL] Puntata manuale su {vm.Name}: {result.Error}", LogLevel.Error);
}
}
catch (System.Exception ex)
{
Log($"[ERRORE] Puntata manuale: {ex.Message}", LogLevel.Error);
}
}
```
### 4?? Aggiornamento Banner dopo Puntata Automatica
**File**: `MainWindow.xaml.cs`
Modificato `AuctionMonitor_OnBidExecuted` per aggiornare anche il banner:
```csharp
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)
{
// ? NUOVO: 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);
}
}
});
}
```
---
## ?? Comportamento Corretto
### ? Scenario: Puntata Manuale
**Azioni**:
1. Clicchi "Punta" nella griglia
2. Server risponde: `ok|47|xxx|xxx|1|xxx|xxx|xxx|xxx` (9 campi)
**Risultato Atteso**:
- ?? **Colonna "Clicks"**: aggiornata immediatamente da `0` ? `1`
- ?? **Banner "Puntate"**: aggiornato immediatamente da `48` ? `47`
- ?? **Log dettagliato**:
```
[BID PARSE] Risposta completa: ok|47|xxx|xxx|1|xxx|xxx|xxx|xxx
[BID PARSE] Numero totale campi: 9
[BID PARSE] Campo 2 (indice 1) - Remaining bids: '47'
[BID SUCCESS] ? Puntate residue totali: 47
[BID PARSE] Campo 5 (indice 4) - Bids used: '1'
[BID SUCCESS] ? Puntate usate su questa asta: 1
[BID PARSE DEBUG] Tutti i campi della risposta:
Campo 1 (indice 0): 'ok'
Campo 2 (indice 1): '47'
Campo 3 (indice 2): 'xxx'
Campo 4 (indice 3): 'xxx'
Campo 5 (indice 4): '1'
Campo 6 (indice 5): 'xxx'
Campo 7 (indice 6): 'xxx'
Campo 8 (indice 7): 'xxx'
Campo 9 (indice 8): 'xxx'
[BANNER UPDATE] Puntate residue aggiornate: 47
[OK] Puntata manuale su Balenciaga Collana: 45ms
```
### ? Scenario: Puntata Automatica
**Azioni**:
1. Strategia punta automaticamente
2. Server risponde: `ok|46|xxx|xxx|2|xxx|xxx|xxx|xxx` (9 campi)
**Risultato Atteso**:
- ?? **Colonna "Clicks"**: aggiornata automaticamente `1` ? `2`
- ?? **Banner "Puntate"**: aggiornato automaticamente `47` ? `46`
- ?? **Log dettagliato** (come sopra)
---
## ?? Log di Debugging
### Cosa Cercare nei Log
Dopo una puntata, cerca nel log questi messaggi:
```
[BID PARSE] Risposta completa: ok|XX|xxx|xxx|X|xxx|xxx|xxx|xxx
[BID PARSE] Numero totale campi: 9
[BID PARSE] Campo 2 (indice 1) - Remaining bids: 'XX'
[BID SUCCESS] ? Puntate residue totali: XX
[BID PARSE] Campo 5 (indice 4) - Bids used: 'X'
[BID SUCCESS] ? Puntate usate su questa asta: X
[BID PARSE DEBUG] Tutti i campi della risposta:
Campo 1 (indice 0): 'ok'
Campo 2 (indice 1): 'XX'
Campo 3 (indice 2): 'xxx'
Campo 4 (indice 3): 'xxx'
Campo 5 (indice 4): 'X'
...
[BANNER UPDATE] Puntate residue aggiornate: XX
```
### Se Vedi Questi Messaggi = Problema Risolto ?
Se vedi:
- `[BID PARSE] Numero totale campi: 9` ?
- `[BID PARSE] Campo 2 (indice 1) - Remaining bids: 'XX'` ?
- `[BID SUCCESS] ? Puntate residue totali: XX` ?
- `[BID PARSE] Campo 5 (indice 4) - Bids used: 'X'` ?
- `[BID SUCCESS] ? Puntate usate su questa asta: X` ?
- `[BANNER UPDATE] Puntate residue aggiornate: XX` ?
Significa che:
- ? Il server restituisce i dati correttamente
- ? Il parsing legge i campi giusti (campo 2 e campo 5)
- ? Il banner viene aggiornato
- ? La colonna "Clicks" si aggiorna
### Se Vedi Questi Warning = Problema con Risposta Server ??
Se vedi:
- `[BID PARSE] Numero totale campi: X` (dove X ? 9) ??
- `[BID PARSE ERROR] ? Risposta non ha campo 2` ??
- `[BID PARSE ERROR] ? Risposta non ha campo 5` ??
- `[BID PARSE WARN] ?? Impossibile parsare campo X` ??
Significa che:
- ?? Il server **non restituisce** 9 campi come previsto
- ?? I campi sono in posizioni diverse
- ?? Il formato risposta è cambiato
---
## ?? Come Testare
### Test 1: Puntata Manuale con Log Abilitati
1. Apri l'applicazione
2. Aggiungi un'asta
3. **Guarda il banner in alto** - nota le puntate residue (es. 48)
4. Clicca "Punta" nella griglia
5. **Controlla il log** - devi vedere i messaggi `[BID PARSE]`
6. **Verifica**:
- ? Colonna "Clicks" aggiornata immediatamente
- ? Banner "Puntate" decrementato (es. 48 ? 47)
- ? Log mostra parsing dettagliato
### Test 2: Puntata Automatica
1. Configura strategia (Anticipo = 200ms)
2. Avvia l'asta
3. Aspetta che punti automaticamente
4. **Verifica** (come sopra)
### Test 3: Puntate Multiple
1. Punta 3 volte manualmente
2. **Verifica** che ad ogni puntata:
- Clicks: `0` ? `1` ? `2` ? `3`
- Puntate: `48` ? `47` ? `46` ? `45`
---
## ?? Troubleshooting
### Problema: Clicks Rimane a 0
**Possibili cause**:
1. Il server non restituisce il campo "bids used" nella risposta
2. Il campo è in una posizione diversa
**Soluzione**:
Guarda il log `[BID PARSE]` e verifica:
- Quanti campi ha la risposta?
- Quale campo contiene il contatore?
- Potrebbe servire modificare gli indici del parsing
### Problema: Banner Non Si Aggiorna
**Possibili cause**:
1. Il server non restituisce "remaining bids"
2. `UpdateRemainingBidsDisplay()` non viene chiamato
**Soluzione**:
Cerca nel log:
- `[BANNER UPDATE] Puntate residue aggiornate` ?
- Se non c'è, il metodo non viene chiamato
- Se c'è ma il banner non cambia, problema UI binding
### Problema: Log Non Mostra `[BID PARSE]`
**Possibile causa**:
La puntata fallisce prima del parsing
**Soluzione**:
Cerca errori prima di `[BID PARSE]`:
- `[BID ERROR]` - puntata fallita
- `[BID EXCEPTION]` - errore durante chiamata
---
## ?? File Modificati
| File | Modifiche |
|------|-----------|
| `Services/BidooApiClient.cs` | ?? Aggiunto logging dettagliato parsing risposta |
| `Core/MainWindow.UserInfo.cs` | ? Aggiunto metodo `UpdateRemainingBidsDisplay()` |
| `Core/MainWindow.Commands.cs` | ?? Chiamata `UpdateRemainingBidsDisplay()` e `RefreshCounters()` su UI thread |
| `MainWindow.xaml.cs` | ?? Aggiornamento banner in `AuctionMonitor_OnBidExecuted` |
---
## ? Checklist Test
### Prima di Chiudere Issue
- [ ] Puntata manuale aggiorna colonna "Clicks" immediatamente
- [ ] Puntata manuale aggiorna banner "Puntate" immediatamente
- [ ] Puntata automatica aggiorna colonna "Clicks"
- [ ] Puntata automatica aggiorna banner "Puntate"
- [ ] Log mostra `[BID PARSE]` con tutti i campi
- [ ] Log mostra `[BID SUCCESS] Puntate residue totali: XX`
- [ ] Log mostra `[BID SUCCESS] Puntate usate su questa asta: X`
- [ ] Log mostra `[BANNER UPDATE] Puntate residue aggiornate: XX`
- [ ] Nessun errore/warning nel parsing
- [ ] Build compila senza errori
---
**Data Fix**: 2025
**Versione**: 4.1+
**Issue**: UI non aggiorna contatori dopo puntata
**Status**: ? RISOLTO
---
## ?? Riepilogo
### Prima:
- ? Colonna "Clicks" mostra sempre 0
- ? Banner aggiornato solo dopo 5-10 minuti
- ? Nessun logging dettagliato
- ? `RefreshCounters()` su thread sbagliato
### Dopo:
- ? Colonna "Clicks" aggiornata **immediatamente**
- ? Banner aggiornato **immediatamente**
- ? Log **dettagliato** per debugging
- ? `RefreshCounters()` sul **thread UI** corretto
- ? `UpdateRemainingBidsDisplay()` chiamato dopo ogni puntata
@@ -1,296 +0,0 @@
# ?? Fix: WebView2 Already Initialized Error
## ?? Problema
### Log Errore
```
[18:47:29] [ERROR] Inizializzazione WebView2 fallita:
WebView2 was already initialized with a different CoreWebView2Environment.
Check to see if the Source property was already set or
EnsureCoreWebView2Async was previously called with different values.
[18:47:29] [DEBUG] Exception type: ArgumentException
```
### Root Cause
**XAML** stava inizializzando automaticamente WebView2:
```xaml
<!-- ? PROBLEMA: Source inizializza WebView con environment default -->
<wv2:WebView2 x:Name="EmbeddedWebView"
Source="https://it.bidoo.com" ? Inizializzazione automatica!
.../>
```
**Sequenza Eventi** (PRIMA ?):
```
1. XAML carica ? WebView2 vede Source="https://..."
2. WebView2 auto-init con CoreWebView2Environment.Default
3. InitializeWebView2() chiama EnsureCoreWebView2Async(customEnv)
4. ? ArgumentException: Already initialized with different environment!
```
---
## ? Soluzione
**Rimuovere `Source` da XAML** e gestire init completamente via codice.
### File: `Controls\BrowserControl.xaml`
#### BEFORE ?
```xaml
<wv2:WebView2 x:Name="EmbeddedWebView"
Source="https://it.bidoo.com" ? ? Causa init automatica
PreviewMouseRightButtonUp="..."/>
```
#### AFTER ?
```xaml
<wv2:WebView2 x:Name="EmbeddedWebView"
PreviewMouseRightButtonUp="..."/> ? ? Nessuna init automatica
```
---
## ?? Flusso Corretto
### Dopo il Fix ?
```
1. XAML carica ? WebView2 NON inizializzata (nessun Source)
2. MainWindow() constructor ? InitializeWebView2()
3. CreateAsync(userDataFolder) ? Crea environment personalizzato
4. EnsureCoreWebView2Async(env) ? Init con environment custom ?
5. Navigate("https://it.bidoo.com") ? Carica pagina via codice
```
---
## ?? Benefici
| Aspetto | Prima ? | Dopo ? |
|---------|----------|---------|
| **Init Source** | XAML (automatico) | Codice (controllato) |
| **Environment** | Default (auto) | Custom (esplicito) |
| **UserDataFolder** | Auto-detect (problematico) | Esplicito (sicuro) |
| **Timing** | Immediato (prima del codice) | Controllato (quando vogliamo) |
| **Errore** | ArgumentException | Nessuno |
---
## ?? Test Richiesto
### Step 1: Pulisci Cache
```powershell
# Rimuovi vecchia cache WebView
Remove-Item "$env:LOCALAPPDATA\AutoBidder\WebView2" -Recurse -Force -ErrorAction SilentlyContinue
```
### Step 2: Riavvia App
1. Chiudi completamente l'app
2. Ricompila (già fatto)
3. Avvia app
4. Aspetta 30 secondi
5. Osserva log
### Step 3: Verifica Log
**Log Atteso** ?:
```
[18:47:28] [BROWSER] Inizializzazione WebView2 in background...
[18:47:29] [DEBUG] Chiamata EnsureCoreWebView2Async...
[18:47:29] [DEBUG] UserDataFolder: C:\Users\...\AutoBidder\WebView2
[18:47:29] [DEBUG] CoreWebView2Environment creato
[18:47:29] [DEBUG] EnsureCoreWebView2Async completata ? ? NESSUN ERRORE!
[18:47:29] [DEBUG] CoreWebView2 disponibile, navigating...
[18:47:29] [BROWSER] WebView2 inizializzato e pre-caricato
[18:47:29] [DEBUG] Notifica WebView pronta (TrySetResult)
[18:47:29] [DEBUG] Inizio CheckAndImportCookieIfAvailable
[18:47:30] [DEBUG] CheckAndImportCookieIfAvailable - inizio
[18:47:31] [DEBUG] Delay 1000ms completato, chiamo GetCookieFromWebView
[18:47:32] [DEBUG] GetCookieFromWebView ritornato, cookie presente: True
[18:47:32] [BROWSER] Cookie rilevato - importazione automatica...
[18:47:33] [SESSION OK] Validata e attiva: sirbietole23, XX puntate
```
**NON Deve Comparire** ?:
```
[ERROR] Inizializzazione WebView2 fallita: WebView2 was already initialized...
```
---
## ?? Checklist
- [x] Rimosso `Source="https://it.bidoo.com"` da XAML
- [x] WebView2 init gestita completamente via codice
- [x] Environment custom con UserDataFolder esplicito
- [x] Navigate chiamato via codice dopo init
- [ ] Test con cache pulita (da fare)
- [ ] Verifica auto-login funzionante (da fare)
---
## ?? Perché Succedeva
### XAML Source Property
In WPF, quando imposti `Source` su un controllo WebView2 in XAML:
```xaml
<wv2:WebView2 Source="https://..." />
```
**Dietro le quinte**:
```csharp
// WPF chiama automaticamente (internamente)
await webView.EnsureCoreWebView2Async(null); // null = environment default
webView.CoreWebView2.Navigate(Source);
```
**Problema**: Quando poi noi chiamiamo:
```csharp
var env = await CoreWebView2Environment.CreateAsync(...); // Environment custom
await webView.EnsureCoreWebView2Async(env); // ? Already initialized!
```
**Soluzione**: Rimuovi `Source` da XAML, gestisci tutto via codice:
```csharp
// Prima init con environment custom
var env = await CoreWebView2Environment.CreateAsync(...);
await webView.EnsureCoreWebView2Async(env); // ? Prima chiamata
// Poi navigate
webView.CoreWebView2.Navigate("https://...");
```
---
## ?? Pattern Corretto
### ? Anti-Pattern (Causa Errore)
```xaml
<!-- XAML -->
<wv2:WebView2 Source="https://site.com"/> ? Init automatica
<!-- C# -->
var env = CreateAsync(...); // Troppo tardi!
await webView.EnsureCoreWebView2Async(env); // ? Exception
```
### ? Pattern Corretto
```xaml
<!-- XAML -->
<wv2:WebView2 x:Name="WebView"/> ? Nessuna init
<!-- C# -->
var env = await CreateAsync(...);
await WebView.EnsureCoreWebView2Async(env); // ? Prima chiamata
WebView.CoreWebView2.Navigate("https://site.com"); // ? Navigate via codice
```
---
## ?? Risultato Atteso
### Ora il Flow è:
```
Avvio App
?
XAML carica (WebView2 NON inizializzata)
?
MainWindow() constructor
?
InitializeWebView2() (async background)
?
await CoreWebView2Environment.CreateAsync(customUserDataFolder)
? [2-3 secondi]
?
await EnsureCoreWebView2Async(env) ? ? Prima e unica chiamata!
?
CoreWebView2.Navigate("https://it.bidoo.com")
?
CheckAndImportCookieIfAvailable()
?
GetCookieFromWebView() ? Cookie trovato
?
ValidateAndActivateSessionAsync()
?
[SESSION OK] Connesso!
```
---
## ?? Prossimi Passi
1. ? **Pulisci cache**: `Remove-Item "$env:LOCALAPPDATA\AutoBidder\WebView2" -Recurse -Force`
2. ? **Riavvia app** (già compilata)
3. ? **Aspetta 30 secondi** senza cliccare
4. ? **Copia log completo** e inviami
**Cerco specificamente**:
- ? `[DEBUG] EnsureCoreWebView2Async completata` senza errori
- ? `[DEBUG] GetCookieFromWebView ritornato, cookie presente: True`
- ? `[SESSION OK] Validata e attiva`
**NON deve esserci**:
- ? `[ERROR] ... already initialized ...`
- ? `[WARN] Timeout attesa inizializzazione`
---
**Data Fix**: 2025
**Versione**: 7.2+
**Issue**: ArgumentException - WebView already initialized
**Root Cause**: XAML Source property inizializza WebView prima del codice
**Soluzione**: Rimosso Source da XAML, init completamente gestita via codice
**Status**: ? Fix applicato, test richiesto
## ?? Riferimenti
- `Controls\BrowserControl.xaml` - Rimosso Source property
- `Core\MainWindow.WebView.cs` - Init con environment custom
- [WebView2 Source Property](https://learn.microsoft.com/en-us/dotnet/api/microsoft.web.webview2.wpf.webview2.source)
- [EnsureCoreWebView2Async](https://learn.microsoft.com/en-us/dotnet/api/microsoft.web.webview2.wpf.webview2.ensurecorewebview2async)
---
## ?? Note Importanti
### Se ancora non funziona dopo questo fix:
1. **Verifica nessun altro `Source=` in XAML**:
```powershell
Select-String -Path "*.xaml" -Pattern 'Source="' -Recurse
```
2. **Verifica nessuna altra init in codice**:
```powershell
Select-String -Path "*.cs" -Pattern 'EnsureCoreWebView2Async' -Recurse
```
3. **Pulisci bin/obj**:
```powershell
Remove-Item bin, obj -Recurse -Force
```
4. **Rebuild completo**:
```
Build ? Clean Solution
Build ? Rebuild Solution
```

Some files were not shown because too many files have changed in this diff Show More