parseClassifica() legge project-sheets.php?project_id=848 con lo stesso
parsing del girone di campionato (stessa struttura di tabella), quindi
nessun parser dedicato. La Coppa resta limitata alla fase a gironi: la
finale secca vive in un project_id figlio non tracciato dal codice.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Il sottotitolo "N partite giocate" per il criterio MVP usava totaliEventiGiocatore(),
che conta anche gli allenamenti: un numero fuorviante perché l'MVP si vota solo alle
partite del calendario interno (eventi_app), non a quelle del sito CSI. Aggiunge
contaPartiteGiocate() (presenze.ts) e Giocatore.partiteGiocate.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
I giocatori a pari valore condividono ora la stessa posizione (dense rank) invece di
essere numerati in sequenza, e il sottotitolo di ogni riga segue il criterio scelto
invece di mostrare sempre le presenze consecutive. Per Palloni mostra le volte
consecutive in cui il giocatore li ha portati (nuovo Giocatore.seriePalloni).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Il test copriva solo 3 delle 9 istruzioni DELETE dentro
bonifica_dati_evento_orfani() (risposte_presenze, mvp_voti,
pagelle_voti), lasciando cacche_partita, turni_palloni,
scout_sessioni, scout_live, scout_partite e badge_social_voti senza
verifica automatica. Ora inserisce una riga orfana in tutte e sei le
tabelle evento_id e una riga orfana più due storiche (Scout, CSI) in
tutte e tre le tabelle match_id, verificando la stessa distinzione
delicata su ciascuna, non solo su un sottoinsieme.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
M15 ha ripulito una tantum le righe orfane con un blocco di DELETE
verificato solo a mano, senza lasciare nessuna rete di sicurezza
automatica per il futuro. Questa migration rende lo stesso corpo la
funzione bonifica_dati_evento_orfani(), riservata al service role, così
resta richiamabile se il trigger di M14 smettesse mai di funzionare.
Aggiunge test/integration/bonifica-evento.test.ts: inserisce una riga
orfana e una storica su id Scout/CSI, richiama la funzione via RPC e
verifica che tocchi solo la prima. Documenta l'aggiornamento in
DESIGN_DECISIONS.md (DD-029), DATABASE.md, test/README.md e CHANGELOG.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
M14 (DD-029) pulisce a cascata i dati collegati solo per le cancellazioni
future; questa migration ripulisce lo storico. Per risposte_presenze,
cacche_partita, turni_palloni, scout_sessioni, scout_live e scout_partite
elimina ogni riga il cui evento_id non esiste più in eventi_app.
Per mvp_voti, pagelle_voti e badge_social_voti serve più cautela: prima
del passaggio all'id evento CrAPP, match_id conteneva id Scout
(prefisso "s") o CSI (numerico o data-based), voti storici legittimi
mai collegati a un evento CrAPP. Il filtro si applica solo ai match_id
nel formato di nuovoIdEvento() ("e" + timestamp base36), così i vecchi
voti Scout/CSI restano intatti. Verificato manualmente sul database
locale prima dell'applicazione.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Un audit del modulo Obiettivi ha verificato che gli obiettivi non hanno
bisogno di pulizia: sono ricalcolati a runtime sull'elenco eventi
corrente. Il problema reale era un livello sotto: cancellare un evento
lasciava orfane le righe collegate in 8 tabelle (risposte_presenze,
cacche_partita, mvp_voti, pagelle_voti, badge_social_voti,
turni_palloni, scout_sessioni, scout_live, scout_partite), nessuna con
una foreign key verso eventi_app.
Aggiunge un trigger AFTER DELETE su eventi_app (migration
m14_pulizia_dati_evento_cancellato) invece di una FK con CASCADE, non
applicabile per dati storici disallineati (match_id che a volte punta
a id Scout/CSI). Verificato con un nuovo test di integrazione contro
il database locale; documentato in DESIGN_DECISIONS.md, DATABASE.md,
obiettivi-squadra.md, test/README.md e CHANGELOG.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rimuove l'hint statico "+2 questo mese" dalla StatTile Presenze. La StatTile
Media voto usa ora la stessa soglia minima di voti del badge Pagellone
(VOTI_MINIMI_PAGELLA), tramite la nuova funzione pura testabile
mediaVotoColpoDOcchio(). L'MVP di partita richiede un quorum minimo di 2
voti totali (VOTI_MINIMI_MVP) oltre al margine netto già richiesto: un
solo voto non assegna più la vittoria, con effetto anche sui conteggi
già mostrati. Aggiorna test unitari e di integrazione, e documenta la
decisione in DESIGN_DECISIONS.md, pagelle.md, mvp.md e CHANGELOG.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Aggiunge alle doc di presenze/pagelle il collegamento con le StatTile di
home «Colpo d'occhio» e registra nel changelog la rimozione dell'hint
statico. Uniforma i moduli allo stato "implementato" senza tag di
versione e riscrive in DESIGN_DECISIONS.md i riferimenti a v1.0/v1.1/v2.0
con descrizioni neutre, coerenti con il fatto che il progetto non usa
più quella numerazione.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Audit dedicato su tutti i 16 badge: s-tiebreak non applicava la soglia minima di
voti pagella di Pagellone sullo stesso campo mediaVoto (corretto), s-cacche
prometteva "partite di campionato" senza che il codice lo verificasse mai
(corretta la descrizione, comportamento invariato). Aggiunti test unit e
integration end-to-end mancanti su badge segreti e social, con dati scritti a
database anche per i cinque segreti che prima ne erano privi.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Analisi della logica di serieConferme() senza bug trovati; aggiunte le soglie
esplicite 3/8/15 e i rami mancanti (convocati ristretti, tipo misto, bordo
esatto delle 24h, evento futuro), più un integration test end-to-end che
verifica la mappatura creato_il/risposto_il con dati reali.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Aggiunto un punto ai Limiti noti di collegamento-csi.md: il portale può essere
del tutto irraggiungibile (non solo cambiare formato, già coperto dal punto 5),
con l'episodio dell'8 settembre 2026 come esempio verificato. Spiega perché
api.test.ts salta quei 5 test invece di farli fallire, e che tornano a girare da
soli quando il CSI risponde di nuovo. Aggiunta la stessa nota, più breve, in
test/README.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Il portale CSI (livescore.csibologna.it) è momentaneamente in manutenzione lato
loro (risponde con un errore SQL in chiaro al posto del JSON su
getEventsByTeamId.php). I 5 test che dipendono dal CSI reale ora sondano una
volta sola /api/public/csi: se non è raggiungibile vengono saltati con salta()
invece di fallire, senza toccare la loro logica né cancellarli — tornano a girare
non appena il portale è di nuovo su.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
rosa.ts calcolava Giocatore.palloni su completaTurni() (turni salvati + proposte
automatiche di rotazione non ancora confermate da nessuno): un giocatore poteva
avanzare nel badge senza aver mai confermato un turno, solo perché l'algoritmo lo
proponeva per un evento passato. Ora passa a conteggioTurni() solo turniSalvati
(i turni davvero confermati in turni_palloni); completaTurni() resta in uso solo
per la UI di rotazione (TurnoPalloni.tsx, PromemoriaPalloni.tsx). Corregge anche
la StatTile "quante volte hai portato i palloni" nel profilo, che leggeva lo
stesso valore.
Aggiornato palloni-badge.test.ts per verificare il nuovo comportamento (un evento
non confermato non conta più per nessuno) invece del vecchio. docs/modules/
badge.md e palloni.md aggiornati di conseguenza, spostando la voce da "Limiti
noti" a "risolto".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Nessun bug trovato: serieConsecutiva() gestisce correttamente buco (azzera) vs
infortunio (congela senza azzerare), lo stesso filtro sui convocati della funzione
pura protegge anche questo badge senza bisogno dell'estensione RLS di M13.
Confermato che il limite noto "risposto_il non ricostruibile prima di m9" riguarda
solo serie-conferme e il segreto s-mai-forfait, non questo badge: serieConsecutiva()
guarda solo lo stato della risposta, non il suo istante.
Aggiunti test unit sulle soglie (badges.test.ts) e un nuovo end-to-end
(serie-allenamenti-badge.test.ts) che scrive allenamenti e risposte reali sul
database locale e verifica l'intera pipeline fetchPresenze()/daRiga() ->
serieConsecutiva() -> statoBadge() attraverso le tre soglie, incluso il caso "un
buco azzera" e, separatamente, "un infortunio congela invece di azzerare".
docs/modules/badge.md aggiornato con la pipeline, la copertura test e la
descrizione corretta (l'infortunio non spezza la serie).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Nessun bug trovato: contaPresenzeGiocatore() filtra correttamente per convocati,
tipo evento e stato (presente/ritardo contano, assente/forse no), gli eventi
futuri restano esclusi. Confermato che, a differenza di MVP/pagelle/badge social,
questo badge non ha bisogno dell'estensione RLS di M13: il filtro sui convocati è
già nella funzione pura, non solo in UI.
Aggiunti test unit sulle soglie (badges.test.ts) e un nuovo end-to-end
(presenze-badge.test.ts) che scrive eventi e risposte reali sul database locale e
verifica l'intera pipeline fetchPresenze()/daRiga() -> contaPresenzeGiocatore() ->
statoBadge() attraverso le tre soglie, incluso il caso "non convocato, la riga
esiste ma non conta". docs/modules/badge.md aggiornato con la pipeline, la
copertura test e la correzione della dicitura "in stagione" (il conteggio è
sull'intero storico, coerente con gli altri badge).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Nessun bug nella logica di calcolo: soglie 3/6/10 corrette, gradoRaggiunto()/
statoBadge() si comportano come per gli altri badge da contatore. Trovato però un
comportamento reale ma non ovvio (già accennato in palloni.md, mai dimostrato con
dati veri): il conteggio del badge include anche le proposte automatiche di
completaTurni() non ancora confermate da nessuno, non solo i turni salvati in
turni_palloni.
Aggiunti test unit sulle soglie (badges.test.ts) e un nuovo end-to-end
(palloni-badge.test.ts) che scrive eventi/turni parzialmente confermati sul
database locale e verifica l'intera pipeline fetchTurni() -> completaTurni() ->
conteggioTurni() -> statoBadge(), incluso il caso "evento futuro non conta".
docs/modules/badge.md aggiornato con la pipeline, la copertura test e il limite
noto.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
npx eslint --fix su test/unit/obiettivi.test.ts, test/integration/obiettivi.test.ts,
BarraSottosezioni.tsx, EventoCard.tsx e calendario.tsx — solo formattazione,
nessuna modifica di comportamento. Verificato con la suite unit (32/32 verde).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Il badge Pagellone si sblocca ora solo con almeno 5 pagelle ricevute
(VOTI_MINIMI_PAGELLA): un voto solo poteva sbloccarlo o farlo sparire senza
significatività statistica. Aggiunto Giocatore.votiPagella per farlo funzionare.
La migration m13 (DD-027) estende le policy RLS di M11 su pagelle_voti, mvp_voti e
badge_social_voti: votante e votato devono essere convocati all'evento (prima solo
un filtro applicativo), e per le pagelle anche pagelle_chiuse=false. Le policy
admin restano permissive di proposito. Applicata anche al progetto cloud.
Copertura test completa: unit sulla soglia minima, integration sulle nuove policy
RLS (permessi.test.ts) e un end-to-end reale (pagella-badge.test.ts) sul modello
di mvp-badge.test.ts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Il bullet MVP ripeteva per intero il meccanismo di voto già documentato in mvp.md
(e con un'imprecisione: l'RLS di m11 non è una barriera contro l'autovoto, impedisce
solo di votare a nome di un altro). Ora rimanda a mvp.md e tiene solo ciò che serve
a capire il badge. Tolto anche il bullet iniziale sui segreti, che ripeteva parola
per parola l'intro della sezione "Elenco badge".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Aggiunge il caso positivo RLS mancante per mvp_voti in permessi.test.ts e un nuovo
test end-to-end (mvp-badge.test.ts) che verifica l'intera pipeline voti reali ->
mvpVintiPerGiocatore() -> statoBadge(). docs/modules/badge.md ora elenca tutti e 16
i badge con come funzionano, la copertura test badge per badge e i due problemi
minori trovati in audit (badgeSbloccati() morta, categoria senza vincolo DB).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rende la doc autosufficiente: tabella completa dei 10 obiettivi con id, calcolo
e target; sezione dedicata alla dipendenza dal JSON CSI per le vittorie (o3/o4/o5)
con il limite del parsing silenzioso e i passi per fixarlo se il portale cambia
formato; tabella di copertura test per ogni obiettivo con i file esatti.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
o5 non aveva il Math.min(vittorie, 10) dei fratelli o3/o4: il progresso
mostrato non cambiava (già cappato al 100%), ma il valore grezzo sì, mostrato
senza cap in squadra.tsx/index.tsx (es. "15/10 vittorie" invece di "10/10").
Nessuno dei tre obiettivi aveva test. Le vittorie non toccano Supabase -
arrivano dal portale CSI via /api/public/csi - quindi l'integration test
estende test/integration/api.test.ts (che già chiama quella route dal vivo)
invece di test/integration/obiettivi.test.ts, verificando o3/o4/o5 sui dati
CSI reali del giorno.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Le vittorie in campionato dipendono dal JSON delle partite (non dalla
classifica HTML, come erroneamente documentato prima). Se quel formato cambia,
il parsing fallisce in silenzio: nessun errore, nessuna notifica, le vittorie
restano ferme a 0% finché qualcuno non se ne accorge per caso.
Aggiunta partiteFormatoSospetto() in csi-core.ts: confronta gli eventi grezzi
con il risultato del parsing per distinguere un vero "formato cambiato" da un
legittimo "nessuna gara ancora in programma". La route logga l'errore server e
il flag arriva fino a /classifica, dove sostituisce la riga "Dati CSI
aggiornati alle..." con un badge discreto color warning - visibile a chi apre
la pagina, senza notifiche invasive.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Il target 12 non è un valore arbitrario da scalare con la rosa come gli altri
target fissi: è il minimo di giocatori per schierare due sestetti (6vs6) in
allenamento, confermato intenzionale. Documentato in
docs/modules/obiettivi-squadra.md.
Non aveva alcun test. Aggiunti unit test (rosa vuota, confine >= 3, target
fisso) e un integration test end-to-end che scrive tre allenamenti e presenze
reali sul database locale, calcola serieConsecutiva() (la stessa funzione pura
usata da useRosa() in produzione) sui dati riletti, e verifica il conteggio.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
C'era una sola asserzione (2 pagelle -> 2), nessun caso vuoto esplicito e
nessun integration test. Aggiunti il caso vuoto, il conteggio aggregato su
più match_id, e l'assert su o13 nell'integration test già scritto per la
media pagelle (stessi voti reali scritti su pagelle_voti, zero setup extra).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
o1 e o2 avevano solo test con un evento singolo per volta: mancavano rosa vuota
(divisione per zero), verifica che le partite contino come gli allenamenti con
eventi sociali/compleanni esclusi, e l'aggregazione su più eventi dello stesso
mese. Aggiunti unit e integration test per entrambi.
"Media pagelle da 7.5" aveva solo il caso vuoto e un caso che faceva già media
esatta: aggiunti test sull'arrotondamento a una cifra decimale e
sull'aggregazione di voti da più partite, più un integration test che scrive
voti veri su pagelle_voti rispettando i vincoli reali della tabella.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
C'era solo un confronto circolare (la stessa formula ricalcolata sugli stessi
dati reali) più il caso rosa vuota. Aggiunto un unit test con roster piccolo e
valori noti (5 + 10 + 15 = 30), e un integration test end-to-end che scrive
eventi/risposte veri sul database locale, calcola contaPresenzeGiocatore() (la
stessa funzione pura usata da useRosa() in produzione) sui dati riletti, e
verifica che obiettiviSquadra() sommi correttamente il risultato.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
La scadenza era una data fissa (2026-09-30), stesso problema già risolto per
"presenze del mese" e "evento di squadra al mese": ora segue l'ultimo giorno
del mese corrente. Aggiunti unit test e integration test end-to-end contro il
database locale (isolando gli eventi di test, perché questo obiettivo aggrega
su tutti gli eventi indipendentemente dal mese).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
"Presenze del mese" e "evento di squadra al mese" erano ancorati a una costante
MESE = "2026-08": passato agosto restavano congelati sul mese scorso invece di
azzerarsi a ogni cambio mese. Ora il mese di riferimento (e per il primo anche
titolo e scadenza) segue la data corrente, con un parametro `oggi` iniettabile
per test deterministici. Aggiunti unit test e un integration test end-to-end
contro il database locale.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Nessuna rotta aveva preload configurato: il chunk JS di una pagina iniziava a
scaricarsi solo a tap completato, mai prima. Con 4 tab piccole (8-12KB l'una) e
sempre visibili, non c'e' motivo di aspettare il tap per iniziare.
preload="intent" sui 4 Link: parte al touchstart, prima che il tap sia completo.
Precarica solo il codice, non i dati (nessuna rotta usa loader), quindi nessun
costo aggiuntivo di letture verso Supabase.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
EventoCard monta un hook per ogni card mostrata: usava useGiocatoreCorrente
(= useIo = useRosa, le 6 statistiche pesanti) solo per leggere il proprio id.
Il Calendario ne rende diverse insieme (prossimi eventi, compleanni, drawer del
giorno), quindi ogni apertura moltiplicava il ricalcolo dell'intera rosa.
Audit di tutti gli altri usi di useGiocatoreCorrente: 12 su 13 leggevano solo
id/nome/verita', mai una statistica. Corretti allo stesso modo (-> useGiocatoreBase):
benvenuto.tsx, VotazioneMvp, SondaggioCacche, VotoSocial, eventi.tsx,
PromemoriaPalloni (Home), TurnoPalloni, ScoutEntry, Pagelle.
partita.$id.tsx: `io` era dichiarato e mai piu' usato, rimosso.
Stesso pattern trovato anche su useRosa (non solo useGiocatoreCorrente) in
scout.tsx e RosaPresenze.tsx (montata su partita e allenamento): entrambi
passati al nuovo useAnagraficaRosa, esteso con ruolo e numero.
Unica eccezione: CelebrazioneBadge.tsx usa davvero le statistiche complete
(badge, MVP, serie) ed e' montato globalmente in __root.tsx - non downgradabile,
resta il costo di base piu' alto rimasto in giro.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
LinkProfilo (header di ogni pagina) e il Calendario (compleanni del mese) usavano
useIo/useRosa, che calcola MVP, pagelle, cacche, palloni e infortuni per l'intera
squadra tramite 6 hook e un useMemo pesante - anche se a entrambi serviva solo
id/nome/nascita/iniziali. Il Calendario in particolare pagava questo costo ad ogni
apertura solo per i compleanni del mese.
- crapp-data.ts: esporta inizialiDa, già usata internamente per il fallback avatar.
- ui-bits.tsx: LinkProfilo usa useGiocatoreBase (sola anagrafica) invece di useIo.
- rosa.ts: nuovo hook useAnagraficaRosa (id, nome, nascita) senza gli altri 5 hook
statistici ne' il useMemo su tutta la rosa.
- eventi.ts: compleanniEventi accetta solo i tre campi che usa, non l'intero
Giocatore.
- calendario.tsx: usa useAnagraficaRosa al posto di useRosa.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- squadra.tsx e classifica.tsx: il contenuto di ogni tab di BarraSottosezioni è ora
memoizzato con useMemo, cosi' uno stato locale o un refetch in background non
ricalcolano piu' anche le sezioni nascoste.
- BottomNav, BarraSottosezioni, calendario: useMotoRidotto (RAM bassa o
prefers-reduced-motion) disattiva sui device deboli la catena di filtri SVG piu'
pesante di .vetro, il backdrop-blur della barra tab e il drag orizzontale;
su iOS e device performanti resta tutto invariato.
- styles.css: rimossa l'utility .materiale, orfana da quando BottomNav e' passata
a .vetro.
- ui-bits.tsx: decoding="async" su TeamLogo.
- todo.md: traccia le cause individuate e lo stato di applicazione di ciascuna.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
sondaggioAperto() restava aperto per sempre una volta passate le 8:00 del
giorno della partita, anche nei giorni successivi. Ora chiude esattamente
all'ora di inizio (partita alle 21:00 → aperto fino alle 20:59, poi chiuso
anche nei giorni seguenti), richiedendo anche l'ora dell'evento oltre alla
data.
Aggiunta sondaggioTerminato() per distinguere nel messaggio "non ancora
aperto" da "già chiuso": altrimenti dopo la partita l'utente avrebbe
continuato a leggere "apre alle 8:00".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
contaInfortuni()/contaRitardi() leggevano tutta la mappa presenze senza
guardare la data dell'evento: un giocatore può segnarsi infortunato o in
ritardo anche su un evento non ancora avvenuto (l'UI lo permette finché
l'evento non è passato), e quel conteggio arrivava subito ai badge segreti
"Infermeria" e "Ritardi". Ora filtrano su data < oggi, come già fa
eventiContanoPresenze() per le statistiche di presenza: con l'avanzare
della data reale l'evento prima futuro entra da solo nel conteggio.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
La select nativa e il bottone-drawer cambiavano lo stesso criterio in due modi
contemporaneamente, effetto ridondante e poco curato. Ora un'unica riga
tappabile mostra il criterio attivo e apre il drawer di scelta.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
La sola sottolineatura era troppo minimale sulla tab attiva. Ora è uno sfondo
pieno arrotondato come le pillole del Profilo, ma le tab restano giustificate
su tutta la larghezza dello schermo senza scroll orizzontale.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
L'elenco si basava solo sull'ordine cronologico dei prossimi eventi,
ignorando la risposta di presenza già data dall'utente.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>