Files
CRAPP/docs/modules/obiettivi-squadra.md
T
davideandClaude Sonnet 5 c7cd527045 Pulisce a cascata i dati collegati quando un evento viene cancellato (DD-029)
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>
2026-09-09 10:16:29 +02:00

12 KiB

Modulo — Obiettivi di squadra

Stato: implementato — mesi/scadenze dinamici, target stagionali fissi da rivedere a mano, copertura test completa (unit + integration) su tutti e 10 gli obiettivi. File principali: src/lib/obiettivi.ts, src/lib/rosa.ts (useObiettivi())


Obiettivo

Mostrare traguardi collettivi (non individuali) che avanzano con il contributo di tutta la rosa — presenze, risposte alle convocazioni, pagelle, risultati di campionato — per motivare comportamenti di squadra oltre alla singola prestazione.


Dati

Nessuna tabella dedicata: ogni obiettivo è una funzione pura in obiettivi.ts (obiettiviSquadra()) che legge dati già aggregati altrove (risposte_presenze, pagelle_voti, i risultati ufficiali CSI, le serie di presenza). obiettiviOrdinati() li ordina mettendo i completati in coda e gli altri per progresso decrescente.

Non c'è nessuno stato da tenere sincronizzato quando un evento viene cancellato: gli obiettivi sono ricalcolati da zero a ogni render partendo dall'elenco eventi corrente, quindi un evento sparito da eventi_app smette semplicemente di contare, senza bisogno di nessuna pulizia esplicita. Il problema che sembrava riguardare gli obiettivi era in realtà nelle tabelle collegate a un evento (presenze, pagelle, MVP, ecc.), che restavano orfane a database dopo la cancellazione: risolto a livello database con un trigger (migration m14_pulizia_dati_evento_cancellato, DD-029), non nel modulo Obiettivi.

obiettiviSquadra(rosa, ctx, oggi) accetta un terzo parametro opzionale oggi: Date (default new Date()) per iniettare una data deterministica nei test — usato dai due obiettivi con mese corrente dinamico (vedi sotto).


Obiettivi definiti

id Obiettivo Calcolo Target Fonte
o1 90% presenze del mese risposte presente/ritardo su partite+allenamenti del mese corrente (dinamico) 90% risposte_presenze
o2 Tutti rispondono alle convocazioni risposte totali / eventi possibili (esclusi i compleanni) 90% risposte_presenze
o7 250 presenze complessive somma presenze di tutta la rosa, stagione intera 250 aggregato da useRosa()
o12 Media pagelle da 7.5 media di tutti i voti, arrotondata a una cifra decimale 7.5 pagelle_voti
o13 200 pagelle compilate conteggio voti 200 pagelle_voti
o11 Continuità di squadra giocatori con ≥3 allenamenti consecutivi 12 (min. per un 6vs6) serieAllenamenti
o3 Prima vittoria del campionato min(vittorie, 1) 1 JSON partite CSI (vedi sotto)
o4 5 vittorie in campionato min(vittorie, 5) 5 JSON partite CSI
o5 10 vittorie in campionato min(vittorie, 10) 10 JSON partite CSI
o6 1 evento di squadra al mese eventi di tipo "evento" nel mese corrente (dinamico) 1 eventi_app

Mostrati in squadra.tsx (elenco completo con barra di progresso) e in index.tsx (home: il primo obiettivo non completato). Un obiettivo che supera il 90% genera anche una notifica smart (notifiche-smart.ts). I target fissi (250 presenze, 200 pagelle, 7.5 di media, 1/5/10 vittorie) sono scelte editoriali da rivedere a mano a ogni stagione — nessuna configurazione o UI per farlo, si cambia il numero in obiettivi.ts. Fa eccezione "Continuità di squadra" (vedi sotto): il suo target ha un significato specifico, non va scalato come gli altri.


Obiettivi mensili — mese dinamico

o1 ("90% presenze del mese") e o6 ("1 evento di squadra al mese") si azzerano automaticamente a ogni cambio mese: il mese di riferimento è calcolato dalla data corrente (fuso Europe/Rome, meseCorrente(oggi)), non più una costante fissa. Per o1, titolo ("90% di presenze ad agosto" / "a settembre" / ...) e scadenza (ultimo giorno del mese) seguono di conseguenza.

o2 ("Tutti rispondono alle convocazioni") non si azzera — aggrega su tutti gli eventi in programma, non solo quelli del mese corrente — ma la sua scadenza mostrata in interfaccia è anch'essa l'ultimo giorno del mese corrente (fineMese(oggi)), non più una data fissa.


Continuità di squadra — il target 12 è il minimo per un 6vs6

Il target di 12 giocatori con almeno 3 allenamenti consecutivi (o11) non è arbitrario: è il numero minimo di giocatori per schierare due sestetti (6 contro 6) in allenamento. A differenza degli altri target fissi, non va scalato in proporzione alla rosa se questa cambia dimensione — resta 12 finché l'obiettivo è "riuscire ad allenarsi in modo completo".

Dipende da serieAllenamenti (vedi Serie di presenze), calcolato sui dati reali: un evento passato senza risposta vale come assenza e azzera la serie, quindi l'obiettivo misura anche quanto la squadra risponde alle convocazioni, non solo la presenza fisica.


Vittorie in campionato (o3/o4/o5) — dipendenza dal portale CSI

Le vittorie (ctx.vittorie) arrivano dal JSON delle partite del portale CSI Bologna (getEventsByTeamId.php, non la pagina HTML della classifica), tramite partiteGiocate(csi.partite).filter(p => p.setNostri > p.setLoro) calcolato in src/lib/rosa.ts (useObiettivi()). o3/o4/o5 sono lo stesso numero di vittorie letto a tre soglie diverse (1/5/10), ciascuna cappata con Math.min — nessuna delle tre supera mai il proprio target, nemmeno con più vittorie di quante ne servano.

Il limite: il parsing del JSON può rompersi in silenzio

result e partials nella risposta di getEventsByTeamId.php sono stringhe libere tipo "3-1", lette con un'espressione regolare (punteggio()/parziali() in csi-core.ts). Se il portale CSI cambiasse formato (es. "3:1", o un punteggio come oggetto invece che stringa), la regex non troverebbe corrispondenza e la partita risulterebbe "non ancora giocata" — senza errori. Se la risposta cambiasse forma radicalmente (non più un array), partiteDaEventi() torna []. In entrambi i casi o3/o4/o5 restano bloccati a 0% anche a fronte di vittorie reali, e il fallback della route (/api/public/csi) non se ne accorgerebbe da solo: lancia un errore solo se sia la classifica sia le partite sono vuote insieme, quindi se si rompe solo il JSON delle partite mentre la classifica HTML continua a funzionare, la route risponde comunque 200 con partite: [].

Come è mitigato oggi

  • partiteFormatoSospetto() (csi-core.ts) confronta gli eventi grezzi ricevuti con il risultato di partiteDaEventi(): se ci sono eventi ma nessuno è stato riconosciuto come nostra partita, il formato è quasi certamente cambiato (distingue così un vero "formato rotto" da un legittimo "nessuna gara ancora in programma", dove gli eventi grezzi sono vuoti anche loro).
  • La route (src/routes/api/public/csi.ts) logga un console.error quando succede.
  • Il flag viaggia anche nella risposta JSON (DatiCsi.formatoSospetto) fino a /classifica (src/routes/classifica.tsx), dove sostituisce la riga "Dati CSI aggiornati alle..." con un badge discreto color warning ("Il portale CSI potrebbe aver cambiato formato: dati da verificare.") — visibile a chi apre la pagina campionato, non solo nei log del server.

Come fixarlo, se succede

  1. Vedere il nuovo formato: guardare la risposta reale dell'endpoint, o lanciare CSI_LIVE=1 bun test/unit/csi-core.test.ts (interroga il portale vero).
  2. Aggiornare il parsing in src/lib/csi-core.ts: quasi sempre basta toccare punteggio()/parziali() (le regex sul formato del punteggio) o i nomi dei campi letti in partiteDaEventi(). Il resto dell'app consuma solo i tipi già puliti che questo file produce (DatiCsi, PartitaCsi[]), quindi il fix resta isolato.
  3. Serve toccare anche src/routes/api/public/csi.ts solo se cambiano gli URL/endpoint stessi o serve autenticazione — non per un semplice cambio di formato dei dati.
  4. Aggiornare i test: test/unit/csi-core.test.ts con fixture nel nuovo formato, altrimenti restano verdi contro un formato che non esiste più.

Dettagli completi (endpoint, identificativi di stagione, altri limiti del collegamento CSI) in Collegamento CSI.


Copertura test

Tutti e 10 gli obiettivi hanno unit test e integration test end-to-end (dati scritti/letti da un backend reale, non solo funzione pura con contesto costruito a mano).

Obiettivi Unit test Integration test
o1, o2, o6 test/unit/obiettivi.test.ts test/integration/obiettivi.test.ts (Supabase locale)
o7 test/unit/obiettivi.test.ts test/integration/obiettivi.test.ts (Supabase locale)
o11 test/unit/obiettivi.test.ts test/integration/obiettivi.test.ts (Supabase locale)
o12, o13 test/unit/obiettivi.test.ts test/integration/obiettivi.test.ts (Supabase locale)
o3, o4, o5 test/unit/obiettivi.test.ts test/integration/api.test.ts (CSI reale in produzione)
  • o1/o2/o6 (Supabase locale): scrive eventi e risposte veri su eventi_app/ risposte_presenze, li rilegge con leggiEventi() (la stessa funzione server dell'app) e una query REST equivalente a fetchPresenze(). Copre: contesto vuoto, aggregazione su più eventi, filtro sui tipi (partite/allenamenti contano, eventi sociali/compleanni no), il mese dinamico (evento dentro/fuori mese), scadenza dinamica.
  • o7 (Supabase locale): scrive eventi/presenze reali, calcola contaPresenzeGiocatore() (la stessa funzione pura usata da useRosa() in produzione) sui dati riletti, verifica la somma.
  • o11 (Supabase locale): scrive tre allenamenti e presenze reali, calcola serieConsecutiva() sui dati riletti, verifica che solo chi resta in serie venga contato.
  • o12/o13 (Supabase locale): scrive voti veri su pagelle_voti rispettando i vincoli reali della tabella (pagelle_no_autovoto, pagelle_voto_range), li rilegge, verifica media arrotondata e conteggio.
  • o3/o4/o5 (CSI reale, non Supabase — le vittorie non toccano il database): estende test/integration/api.test.ts, che già chiama /api/public/csi dal vivo. Legge le vittorie vere del giorno con la stessa logica di useObiettivi(), le passa a obiettiviSquadra() e verifica cap e target su dati reali.

Per rilanciare tutto: npm run test (unit, nessuna rete) e npm run test:integration (richiede npx supabase start per o1/o2/o6/o7/o11/o12/o13, e rete verso CSI Bologna per o3/o4/o5 — quest'ultimo gira comunque anche senza stack Supabase locale).