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>
175 lines
12 KiB
Markdown
175 lines
12 KiB
Markdown
# 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](serie-presenze.md)), 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](collegamento-csi.md).
|
|
|
|
---
|
|
|
|
## 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).
|