From f70430e1bfab75c517257fb0206a7d5fd251105c Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Thu, 3 Sep 2026 15:10:18 +0200 Subject: [PATCH] Retro-documenta i moduli v1.0 mancanti in docs/modules/ Presenze, Serie di presenze, Scout Live, Pagelle, MVP, Badge, Palloni, Obiettivi di squadra, Infortuni, Notifiche: chiudono il debito di documentazione tracciato in TODO.md (DD-002). Le sei route API pubbliche vengono descritte dentro il modulo a cui appartengono. --- docs/TODO.md | 11 --- docs/modules/badge.md | 75 ++++++++++++++++++++ docs/modules/infortuni.md | 52 ++++++++++++++ docs/modules/mvp.md | 52 ++++++++++++++ docs/modules/notifiche.md | 93 ++++++++++++++++++++++++ docs/modules/obiettivi-squadra.md | 60 ++++++++++++++++ docs/modules/pagelle.md | 63 +++++++++++++++++ docs/modules/palloni.md | 71 +++++++++++++++++++ docs/modules/presenze.md | 93 ++++++++++++++++++++++++ docs/modules/scout-live.md | 114 ++++++++++++++++++++++++++++++ docs/modules/serie-presenze.md | 60 ++++++++++++++++ 11 files changed, 733 insertions(+), 11 deletions(-) create mode 100644 docs/modules/badge.md create mode 100644 docs/modules/infortuni.md create mode 100644 docs/modules/mvp.md create mode 100644 docs/modules/notifiche.md create mode 100644 docs/modules/obiettivi-squadra.md create mode 100644 docs/modules/pagelle.md create mode 100644 docs/modules/palloni.md create mode 100644 docs/modules/presenze.md create mode 100644 docs/modules/scout-live.md create mode 100644 docs/modules/serie-presenze.md diff --git a/docs/TODO.md b/docs/TODO.md index 6a694b0..07c17b7 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -22,17 +22,6 @@ Il profilo giocatore lato giocatore e i certificati medici sono fatti: `ProfiloA in `src/routes/profilo.tsx` carica documento, certificato e foto con le date di scadenza, e la dashboard amministratore legge quei dati. -## Debito di documentazione - -Moduli v1.0 in produzione senza scheda in [modules/](modules/) — DD-002 ne prevede la -retro-documentazione: Presenze, Scout Live, Badge, Pagelle, MVP, Palloni, Obiettivi di -squadra, Notifiche, Serie di presenze, Infortuni (`src/lib/infortuni.ts`, usato ma non -documentato in nessun punto). - -Non documentate nemmeno le route API pubbliche in `src/routes/api/public/` (`csi`, -`promemoria-palloni`, `push-config`, `push-messaggio`, `push-subscribe`, -`sollecita-presenze`). - ## Manutenzione ricorrente - Collegamento CSI: aggiornare `project_id` e `team_id` a inizio stagione 2026/27 diff --git a/docs/modules/badge.md b/docs/modules/badge.md new file mode 100644 index 0000000..ea6542a --- /dev/null +++ b/docs/modules/badge.md @@ -0,0 +1,75 @@ +# Modulo — Badge + +**Stato:** implementato (v1.0), coerente con DD-007 e DD-008 +**File principali:** `src/lib/badges.ts`, `src/lib/badge-social.ts`, +`src/components/crapp/CollezioneBadge.tsx`, `src/components/crapp/BadgeDrawer.tsx`, +`src/components/crapp/CelebrazioneBadge.tsx`, `src/components/crapp/VotoSocial.tsx` + +--- + +## Obiettivo + +Gamification: sbloccare badge (gradi bronzo/argento/oro, più badge "segreti") in base a +statistiche personali reali del giocatore, per motivare la partecipazione senza penalizzare i +ruoli con meno statistiche "spettacolari" (DD-008). + +--- + +## Dati + +Nessuna tabella dedicata ai badge sbloccati: **calcolati interamente a runtime** +dall'oggetto `Giocatore` (DD-007). L'unica tabella coinvolta è `badge_social_voti`, per i +badge assegnati per voto dai compagni. + +--- + +## Implementazione + +- `badgeDefs`/`badgeSegreti` (`badges.ts`) definiscono ogni badge con una funzione + `valore(g)` e soglie bronzo/argento/oro. Le fonti dato sono solo statistiche indipendenti + dal ruolo in campo: MVP, media pagelle, turni palloni, presenze, serie, infortuni, + ritardi, cacche — **mai** punti/ace/muri dello Scout Live, coerentemente con DD-008. +- I badge segreti restano nascosti (icona lucchetto) finché non sbloccati. +- **Badge social** (`badge-social.ts`, tabella `badge_social_voti`): 5 categorie fisse per + partita ("Compagno affidabile", "Miglior spirito di squadra", "Fair play", "Meme della + partita", "Cuore del gruppo"), votabili una volta a testa per categoria/partita + (modificabile), con **auto-voto escluso sia in UI sia in logica** (`VotoSocial.tsx`). A + differenza delle [Pagelle](pagelle.md), qui non c'è alcun tentativo di anonimato: + `votante_id`/`votato_id` sono entrambi visibili. +- `CollezioneBadge.tsx` mostra sbloccati, in progresso, badge social vinti e un contatore di + badge segreti ancora da scoprire; `BadgeDrawer.tsx` il dettaglio di un singolo badge; + `CelebrazioneBadge.tsx` l'overlay celebrativo alla prima visualizzazione di un badge nuovo. +- Il rilevamento "nuovo" (`notifiche-smart.ts`) confronta id deterministici con quelli già + visti, salvati in `localStorage` — quindi **locale al dispositivo**, non sincronizzato tra + dispositivi dello stesso giocatore. + +--- + +## Regole rispettate + +- **DD-007**: nessuna tabella `badge_sbloccati`, tutto calcolato a runtime dai dati + esistenti. +- **DD-008**: nessun `BadgeDef` usa dati di reparto (punti/ace/muri); solo statistiche + raggiungibili da qualunque ruolo. + +--- + +## Limiti noti + +- **Dipendenza dal modulo [Serie](serie-presenze.md)**, che oggi è inerte con dati reali: i + badge "Sempre in palestra", "Risposta lampo" e il segreto "Mai un forfait" non possono + sbloccarsi finché le serie non vengono calcolate davvero. +- Nessuno storico dei badge sbloccati: se cambiano le soglie o i dati sorgente, un badge già + "ottenuto" può sparire o apparire retroattivamente. +- RLS permissiva su `badge_social_voti` (stesso schema di `mvp_voti`): nessun controllo + server-side che `votante_id` coincida con l'utente autenticato. +- Notifiche "nuovo badge" solo locali al dispositivo (localStorage), si ripetono cambiando + browser o dispositivo. + +--- + +## Evoluzioni possibili + +- Sincronizzare lo stato "visto" su Supabase invece che solo in localStorage. +- Una volta risolta la dipendenza dal modulo Serie, verificare che i badge collegati si + sblocchino correttamente. diff --git a/docs/modules/infortuni.md b/docs/modules/infortuni.md new file mode 100644 index 0000000..6a494ed --- /dev/null +++ b/docs/modules/infortuni.md @@ -0,0 +1,52 @@ +# Modulo — Infortuni + +**Stato:** implementato in forma minima (solo conteggio) +**File principali:** `src/lib/infortuni.ts` + +--- + +## Obiettivo + +Tracciare quanti eventi (allenamenti o partite) un giocatore ha saltato per infortunio, +riusando lo stato di presenza `infortunato` già registrato per le convocazioni — nessun +modulo di gestione infortuni a sé stante. + +--- + +## Dati + +Nessuna tabella dedicata: il dato vive interamente dentro `risposte_presenze`, come uno dei +valori possibili dell'enum `Stato` (`presente`, `assente`, `forse`, `ritardo`, `infortunato`). + +--- + +## Implementazione + +`contaStato()`/`contaInfortuni()` (`infortuni.ts`) contano, per ciascun giocatore, quante +volte compare lo stato `infortunato` nella mappa presenze già in cache (nessuna query +aggiuntiva). Lo stesso meccanismo, con `contaRitardi()`, conta i ritardi. Il risultato +alimenta il campo `infortuni` del `Giocatore` in `useRosa()`. + +Visibile in UI solo indirettamente, tramite il [badge](badge.md) segreto "Cliente VIP +dell'Infermeria" (sbloccato con almeno 3 infortuni): non esiste uno StatTile dedicato nel +profilo che mostri il numero di infortuni come statistica di superficie. + +--- + +## Limiti noti + +- Nessuna durata o periodo tracciato: è solo un conteggio di eventi con quello stato, non un + inizio/fine infortunio. +- Il conteggio dipende dal fatto che qualcuno imposti correttamente lo stato "infortunato" + invece di "assente": nessuna validazione o promemoria lo garantisce. +- Poco visibile per valori bassi (1-2), perché emerge solo tramite un badge a soglia 3. +- `conInfortuni()`, una funzione di merge alternativa nello stesso file, non risulta usata da + nessuna parte del codice attuale — probabile residuo non collegato. + +--- + +## Evoluzioni possibili + +- Uno StatTile dedicato nel profilo, oltre al badge segreto. +- Se servisse un vero tracciamento (durata, tipo di infortunio), servirebbe una tabella + dedicata: oggi il modulo copre solo il conteggio. diff --git a/docs/modules/mvp.md b/docs/modules/mvp.md new file mode 100644 index 0000000..feba855 --- /dev/null +++ b/docs/modules/mvp.md @@ -0,0 +1,52 @@ +# Modulo — Votazione MVP + +**Stato:** implementato (v1.0) +**File principali:** `src/lib/mvp-voti.ts`, `src/components/crapp/VotazioneMvp.tsx` + +--- + +## Obiettivo + +Eleggere il MVP di una partita tramite voto tra compagni, un voto a testa, con vincitore +calcolato a runtime. + +--- + +## Dati + +Tabella `mvp_voti`, vincolo `UNIQUE (match_id, votante_id)` — un solo voto per giocatore per +partita, sovrascrivibile. + +--- + +## Implementazione + +- Il pannello compare in `partita.$id.tsx` solo se esiste un risultato (Scout Live salvato) + per la partita, altrimenti mostra "la partita non è ancora stata disputata". +- `useVotaMvp()` fa upsert `onConflict: match_id, votante_id`: il voto è modificabile senza + limiti, senza storico. +- `conteggioPartita()`/`vincitoriMvp()` richiedono un margine netto: in caso di parità, + nessun vincitore viene assegnato per quella partita finché non arrivano altri voti. +- `mvpVintiPerGiocatore()` conta una vittoria per ogni partita "vinta" con margine netto; il + risultato alimenta il campo `mvp` del `Giocatore` in `useRosa()`, mostrato come StatTile + nel profilo e in home. + +--- + +## Limiti noti + +- **Nessun controllo che impedisca di votare se stessi** — a differenza dei + [Badge social](badge.md), che escludono esplicitamente l'auto-voto. È una lacuna, non un + limite di design dichiarato altrove. +- Nessuna scadenza o chiusura della votazione: resta aperta indefinitamente. +- RLS permissiva: `votante_id`/`votato_id` sono testo libero inviato dal client (l'id + giocatore proviene da `localStorage`, non da un claim di sessione verificato server-side); + nessun trigger lega il voto all'utente autenticato. +- In caso di parità, nessun MVP viene assegnato per quella partita. + +--- + +## Evoluzioni possibili + +- Impedire l'auto-voto come già avviene nei Badge social. +- Introdurre una scadenza (es. la votazione si chiude N giorni dopo la partita). diff --git a/docs/modules/notifiche.md b/docs/modules/notifiche.md new file mode 100644 index 0000000..5f1f936 --- /dev/null +++ b/docs/modules/notifiche.md @@ -0,0 +1,93 @@ +# Modulo — Notifiche + +**Stato:** implementato parzialmente — solo il canale "turno palloni" è realmente collegato +(vedi Limiti noti) +**File principali:** `src/lib/notifiche-smart.ts`, `src/lib/push-client.ts`, +`src/lib/webpush.server.ts`, `src/routes/api/public/push-config.ts`, +`src/routes/api/public/push-messaggio.ts`, `src/routes/api/public/push-subscribe.ts`, +`public/push-sw.js` + +--- + +## Obiettivo + +Tenere aggiornati i giocatori senza che debbano aprire l'app, con due meccanismi +indipendenti: + +- **Push VAPID** — arrivano anche ad app chiusa (turno palloni, sollecito presenze). +- **Notifiche smart** — notifiche locali mostrate solo ad app aperta, generate da badge, + serie e obiettivi appena raggiunti; non è un canale push separato. + +--- + +## Dati + +`push_subscriptions` (un dispositivo per riga, chiave `endpoint`), `promemoria_push` (coda +"consuma e cancella" del testo da mostrare — nonostante il nome, **non** è uno storico +persistente: la riga viene eliminata non appena letta dal service worker). + +--- + +## Iscrizione alle notifiche push + +1. Il giocatore attiva "Notifiche turno palloni" in `/profilo` → richiesta permesso browser. +2. `GET /api/public/push-config` restituisce solo la chiave pubblica VAPID. +3. Registrazione del service worker `public/push-sw.js` e `pushManager.subscribe()`. +4. `POST /api/public/push-subscribe` registra endpoint e chiavi in `push_subscriptions` + (upsert). + +--- + +## Ruolo delle tre route pubbliche + +- **`push-config`** — espone la sola chiave pubblica VAPID. +- **`push-subscribe`** — registra o rimuove l'iscrizione di un dispositivo. +- **`push-messaggio`** — non invia nulla: il service worker la interroga **al momento della + ricezione** di una push (che arriva sempre "vuota", senza testo, per compatibilità) per + sapere quale messaggio mostrare. Priorità: un messaggio in coda su `promemoria_push` + (scritto da `sollecita-presenze`, vedi [Presenze](presenze.md)), altrimenti il messaggio + calcolato al volo sul turno palloni (vedi [Palloni](palloni.md)). + +L'invio effettivo (`src/lib/webpush.server.ts`, funzione `inviaPush`) firma un JWT VAPID +(ECDSA P-256) e fa una POST senza corpo all'endpoint push del browser; è riusato identico da +`sollecita-presenze.ts` e `promemoria-palloni.ts`. + +--- + +## Notifiche smart + +`calcolaNotifiche()` (`notifiche-smart.ts`) genera un evento solo quando "c'è qualcosa di +reale": badge appena sbloccato, "sei a un passo" da un traguardo, serie che raggiunge un +traguardo esatto, obiettivo di squadra tra il 90 e il 100%, badge social vinto. Ogni notifica +ha un id deterministico; quelli già mostrati sono salvati in `localStorage` per non +ripetersi — deduplica puramente locale al dispositivo, non sincronizzata. + +--- + +## Limiti noti + +- **Le 4 voci "Notifiche convocazioni", "Promemoria allenamenti", "Cambi orario", "Bacheca + squadra" in `/profilo` sono placeholder statici**: checkbox non controllati + (`defaultChecked`, nessun `onChange`), non collegati a nessuno stato, nessuna colonna DB + per queste preferenze. L'unica preferenza realmente funzionante è "Notifiche turno + palloni". +- `promemoria_push` è descritta altrove come "storico" ma nel codice è una coda che si + autocancella alla lettura: non conserva nulla. +- Nessuna verifica di autenticazione su `push-messaggio` (chiunque conosca un endpoint push + valido può leggerne il messaggio) né su `promemoria-palloni`. +- Compatibilità iOS/Safari non gestita esplicitamente nel codice (nessun branch dedicato): + serve l'installazione da schermata Home per funzionare, ma l'app non lo segnala + esplicitamente. +- Le notifiche smart dipendono da un service worker già registrato: se il giocatore non ha + mai attivato le push, `notificaSistema()` non ha un `reg` a cui appoggiarsi e la notifica + locale non viene mai mostrata, anche con permesso concesso. +- Payload push sempre vuoto: ogni notifica richiede una fetch aggiuntiva (`push-messaggio`) + per ottenere il testo, quindi serve rete disponibile anche solo per mostrare il messaggio. + +--- + +## Evoluzioni possibili + +- Collegare (o rimuovere) le 4 preferenze placeholder in `/profilo`. +- Aggiungere autenticazione alle route pubbliche coinvolte. +- Gestire esplicitamente il caso iOS (messaggio se l'app non è installata da Home). diff --git a/docs/modules/obiettivi-squadra.md b/docs/modules/obiettivi-squadra.md new file mode 100644 index 0000000..0d36f2b --- /dev/null +++ b/docs/modules/obiettivi-squadra.md @@ -0,0 +1,60 @@ +# Modulo — Obiettivi di squadra + +**Stato:** implementato (v1.0), con costanti stagionali da aggiornare a mano +**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` che legge dati +già aggregati altrove (`risposte_presenze`, `pagelle_voti`, i risultati ufficiali CSI, le +serie). + +--- + +## Obiettivi definiti + +| Obiettivo | Calcolo | Target | Fonte | +| ----------------------------------- | ------------------------------------------------- | ------ | -------------------------------------------- | +| 90% presenze ad agosto | risposte presente/ritardo sugli eventi del mese | 90% | `risposte_presenze` | +| Tutti rispondono alle convocazioni | risposte totali / eventi possibili | 90% | `risposte_presenze` | +| 250 presenze complessive | somma presenze di tutta la rosa | 250 | aggregato da `useRosa()` | +| Media pagelle da 7.5 | media di squadra | 7.5 | `pagelle_voti` | +| 200 pagelle compilate | conteggio voti | 200 | `pagelle_voti` | +| Continuità di squadra | giocatori con ≥3 allenamenti consecutivi | 12 | `serieAllenamenti` | +| 1 / 5 / 10 vittorie in campionato | partite vinte da dati CSI ufficiali | 1/5/10 | modulo [Collegamento CSI](collegamento-csi.md) | +| 1 evento di squadra al mese | eventi di tipo "evento" nel mese | 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`). + +--- + +## Limiti noti + +- **"Continuità di squadra" dipende da `serieAllenamenti`, che oggi è sempre 0** (vedi + [Serie di presenze](serie-presenze.md)): resta strutturalmente a 0/12 con i dati reali. +- **Il mese di riferimento è una costante fissa nel codice** (agosto 2026): gli obiettivi + legati al mese corrente vanno aggiornati manualmente a ogni cambio di mese o stagione, oggi + sono "congelati" su un mese già passato. +- Le vittorie di campionato dipendono dal parsing HTML del portale CSI: se quel parsing si + rompe, questi tre obiettivi restano a 0% anche a fronte di vittorie reali. +- I target (250 presenze, 200 pagelle, ecc.) sono costanti fisse, da rivedere manualmente a + ogni stagione. + +--- + +## Evoluzioni possibili + +- Calcolare il mese di riferimento dinamicamente invece di una costante hardcoded. +- Risolvere la dipendenza dal modulo Serie. diff --git a/docs/modules/pagelle.md b/docs/modules/pagelle.md new file mode 100644 index 0000000..2db540c --- /dev/null +++ b/docs/modules/pagelle.md @@ -0,0 +1,63 @@ +# Modulo — Pagelle + +**Stato:** implementato (v1.0) +**File principali:** `src/lib/pagelle.ts`, `src/components/crapp/Pagelle.tsx` + +--- + +## Obiettivo + +Voto tra compagni (1-10) a fine partita per ciascun convocato, usato per calcolare una media +personale mostrata nel profilo e una media di squadra. + +--- + +## Dati + +Tabella `pagelle_voti`, con vincoli imposti a livello database: `CHECK voto BETWEEN 1 AND 10`, +`CHECK votante_id <> votato_id` (anti auto-voto imposto anche dal database, non solo dalla +UI), `UNIQUE (match_id, votante_id, votato_id)`. + +--- + +## Implementazione + +- Il pannello `Pagelle` compare in `partita.$id.tsx` solo se esiste un risultato per la + partita (scout salvato o dato CSI). +- Ogni convocato può votare tutti gli altri convocati, mai se stesso — escluso sia in UI sia + dal vincolo DB. +- `useVotaPagella()` fa un upsert su `(match_id, votante_id, votato_id)`: si può votare più + volte, l'ultimo voto sovrascrive il precedente. +- `mediePagelle()` calcola la media aritmetica (arrotondata a un decimale) per giocatore su + tutti i voti della stagione; `pagellePartita()` la calcola per singola partita; + `mediaSquadra()` su tutti i voti di tutti — mostrata come StatTile in `squadra.tsx`. +- `useRosa()` inietta la media stagionale nel campo `mediaVoto` di ogni giocatore. + +--- + +## Regole rispettate + +- Anti auto-voto imposto anche a livello database (constraint, non solo filtro UI). +- L'admin può marcare un evento come `pagelleChiuse` (`eventi.ts`), che nasconde i bottoni di + voto in UI. + +--- + +## Limiti noti + +- **`pagelleChiuse` è solo un flag UI**: nessuna policy RLS lo controlla, quindi un voto + "fuori tempo" resta tecnicamente possibile bypassando l'interfaccia. +- **L'anonimato è solo applicativo, non tecnico**: la riga salvata contiene sia `votante_id` + sia `votato_id`, leggibili da chiunque sia autenticato (policy SELECT aperta). La UI non + mostra mai il votante, ma il dato non è né aggregato né mascherato lato server. +- Nessun controllo a livello database che il votante sia realmente un convocato della + partita: solo filtro applicativo. +- La media non richiede un numero minimo di voti: con un solo voto ricevuto, la media + coincide con quel voto. + +--- + +## Evoluzioni possibili + +- Una RPC o vista che nasconda `votante_id` per un anonimato garantito anche lato dati. +- Far rispettare `pagelleChiuse` anche via RLS. diff --git a/docs/modules/palloni.md b/docs/modules/palloni.md new file mode 100644 index 0000000..4b3673e --- /dev/null +++ b/docs/modules/palloni.md @@ -0,0 +1,71 @@ +# Modulo — Palloni + +**Stato:** implementato (v1.0) +**File principali:** `src/lib/palloni.ts`, `src/lib/palloni-core.ts`, +`src/components/crapp/TurnoPalloni.tsx`, `src/components/crapp/PromemoriaPalloni.tsx`, +`src/routes/api/public/promemoria-palloni.ts` + +--- + +## Obiettivo + +Gestire un turno a rotazione condiviso per chi porta e riporta i palloni ad allenamenti e +partite, con proposta automatica, possibilità di modifica manuale e promemoria push il +giorno stesso. + +--- + +## Dati + +Tabella `turni_palloni` (`evento_id`, `giocatore_id`, `aggiornato_da`, `aggiornato_il`) — +contiene solo i turni **confermati manualmente**; le proposte automatiche non salvate non vi +compaiono. + +--- + +## Implementazione + +- `completaTurni()` (`palloni-core.ts`) propone, per ogni evento senza turno già salvato, il + candidato con meno turni fatti, poi quello che non lo fa da più tempo, poi per ordine + alfabetico — un algoritmo greedy, non un ordine fisso né solo per data. +- `useAssegnaTurno()` (`palloni.ts`) conferma una proposta o riassegna manualmente, con + upsert su `evento_id`. +- Il conteggio "quante volte hai portato i palloni" mostrato nel profilo e nei badge è + ricalcolato a runtime da `conteggioTurni()` su turni salvati **più proposte non ancora + confermate** — non è uno storico in tabella dedicata. +- `TurnoPalloni.tsx` mostra/assegna il turno sulla card di un evento; `PromemoriaPalloni.tsx` + è il banner in Home per il giocatore di turno. + +--- + +## Route API pubblica `/api/public/promemoria-palloni` + +Pensata per essere chiamata quotidianamente da uno scheduler esterno (pg_cron o simile, +secondo `docs/PORTABILITA.md`), non da nessun componente client. Calcola i destinatari del +giorno — chi deve **prendere** i palloni oggi e chi deve **riportarli** (l'incaricato +dell'evento precedente) — e invia loro una push "vuota" (`src/lib/webpush.server.ts`); il +testo effettivo viene calcolato al volo dal service worker interrogando +`/api/public/push-messaggio` (vedi [Notifiche](notifiche.md)). + +--- + +## Limiti noti + +- **Nessuna verifica di autenticazione/secret** sulla route `promemoria-palloni`: chiunque + può invocarla via POST diretto, nonostante il piano originale prevedesse una protezione + con secret. +- **Nessun cron nel repository**: lo scheduling effettivo (se esiste) è configurato fuori dal + codice versionato — da verificare lato Supabase/hosting. +- Il conteggio dei turni include anche le proposte non confermate: badge e statistiche + possono contare turni mai effettivamente convalidati da nessuno. +- La rotazione non considera le assenze dichiarate: può proporre il turno a chi ha risposto + "assente" o "infortunato" per quell'evento. + +--- + +## Evoluzioni possibili + +- Aggiungere un secret/header di autorizzazione alla route pubblica. +- Versionare il cron (es. una migration con `cron.schedule`) invece di configurarlo solo + lato dashboard. +- Escludere dalla rotazione chi ha già dichiarato assenza per l'evento. diff --git a/docs/modules/presenze.md b/docs/modules/presenze.md new file mode 100644 index 0000000..9c99c7c --- /dev/null +++ b/docs/modules/presenze.md @@ -0,0 +1,93 @@ +# Modulo — Presenze + +**Stato:** implementato (v1.0) +**File principali:** `src/lib/presenze.ts`, `src/lib/presenze-mese.ts`, `src/components/crapp/RosaPresenze.tsx`, +`src/routes/api/public/sollecita-presenze.ts` + +--- + +## Obiettivo + +Permettere a ogni giocatore di confermare o rifiutare la propria partecipazione a un evento +(allenamento o partita) e mostrare a tutta la squadra chi ha risposto e come, sostituendo i +solleciti a voce o su chat esterne. + +--- + +## Dati + +Tabella `risposte_presenze` (PK composita `evento_id, giocatore_id`), letta e scritta da +`src/lib/presenze.ts`. È il modello "in uso" citato in `docs/DATABASE.md`; le tabelle +`eventi`/`presenze` previste da DD-014 non sono referenziate da nessun punto del codice +attuale. + +Stati possibili (`Stato` in `src/lib/crapp-data.ts`): `presente`, `assente`, `forse`, +`ritardo`, `infortunato`. Solo `presente` e `ritardo` contano come presenza effettiva nelle +statistiche. L'assenza di una riga per `(evento, giocatore)` equivale a "non ha ancora +risposto". + +--- + +## Implementazione + +``` +Giocatore tocca uno stato in RosaPresenze + ↓ +useSalvaPresenza() → src/lib/presenze.ts (upsert o delete su risposte_presenze, + ↓ onConflict evento_id+giocatore_id) +risposte_presenze (Supabase) + ↓ letta da +useRispostePresenze() → src/lib/presenze.ts (1 query per sessione, staleTime 5 min, + ↓ legge tutta la tabella) +RosaPresenze → src/components/crapp/RosaPresenze.tsx + ↑ montato da (riepilogo, bottoni di risposta, gruppi per stato) +allenamento.$id.tsx / partita.$id.tsx + +--- statistiche --- +contaPresenzeGiocatore() / totaliEventiGiocatore() → src/lib/presenze.ts +usePresenzeUltimoMese() → src/lib/presenze-mese.ts + (percentuale ultimi 30gg, da cache già in memoria) + +--- sollecito (solo admin) --- +Bottone "Sollecita" (RosaPresenze.tsx) → POST /api/public/sollecita-presenze + ↓ +src/routes/api/public/sollecita-presenze.ts + ├─ legge l'evento (eventi_app) e le risposte già date + ├─ calcola i destinatari: giocatori attivi senza risposta o con "forse" + ├─ per ciascuno registra il messaggio in promemoria_push e invia una push + │ (src/lib/webpush.server.ts) + └─ elimina le iscrizioni push scadute (404/410) +``` + +Un evento conta ai fini delle statistiche di presenza solo se è di tipo `partita` o +`allenamento` e il giocatore è tra i convocati (o non ci sono convocati specificati, cioè +vale per tutta la rosa) — `eventiContanoPresenze()` in `presenze.ts`. + +--- + +## Regole rispettate + +- Aggiornamento ottimistico della cache locale dopo ogni salvataggio: nessuna rilettura dal + server, la UI risponde subito. +- Il sollecito è **manuale**: nessun cron nel repository lo richiama automaticamente, parte + solo dal bottone admin. + +--- + +## Limiti noti + +- Nessuna finestra temporale per rispondere: si può cambiare risposta anche a evento passato. +- Il controllo "solo il giocatore risponde per sé" è solo lato UI: le policy RLS di + `risposte_presenze` permettono a qualunque utente autenticato di scrivere qualunque riga + (`USING(true) WITH CHECK(true)`). +- `useRispostePresenze()` legge sempre l'intera tabella, non filtrata per evento: adeguato per + una singola squadra, da rivedere se il volume cresce molto. +- La route `/api/public/sollecita-presenze` non verifica lato server che il chiamante sia + admin: la protezione è solo nell'interfaccia (bottone visibile solo se `useIsAdmin()`). + +--- + +## Evoluzioni possibili + +- Restringere anche lato RLS/route chi può scrivere una risposta o chiamare il sollecito. +- Filtrare la lettura delle presenze per evento invece di caricare tutta la tabella. diff --git a/docs/modules/scout-live.md b/docs/modules/scout-live.md new file mode 100644 index 0000000..b1049c6 --- /dev/null +++ b/docs/modules/scout-live.md @@ -0,0 +1,114 @@ +# Modulo — Scout Live + +**Stato:** implementato (v1.0, fix M7 per la persistenza condivisa) +**File principali:** `src/lib/scout-live.ts`, `src/lib/scout-stato.ts`, `src/lib/scout-store.ts`, +`src/lib/scout-export.ts`, `src/lib/cacche.ts`, `src/components/crapp/ScoutEntry.tsx`, +`src/components/crapp/SondaggioCacche.tsx`, `src/routes/scout.tsx`, `src/routes/partita.$id.tsx` + +--- + +## Obiettivo + +Permettere a un solo referente per volta di registrare in tempo reale, durante la partita, +punti, ace, muri ed errori di ciascun giocatore in campo, con salvataggio condiviso su +Supabase (non più solo `localStorage`, fix M7) così che tutta la squadra veda lo stato +aggiornato da qualunque dispositivo. + +--- + +## Dati + +- `scout_sessioni` — chi ha il controllo dello Scout Live per una partita (una riga per + `evento_id`, quindi un solo detentore). +- `scout_live` — stato in corso (azioni non ancora concluse) di una sessione. +- `scout_partite` — archivio delle partite scoutate concluse (risultato, parziali, azioni), + mai più modificato una volta salvato (solo eliminabile per intero). +- `cacche_partita` — sondaggio goliardico pre-partita, un voto per giocatore/evento + (`UNIQUE evento_id, giocatore_id`). + +--- + +## Chi può usarlo + +Solo gli admin lato UI: `ScoutEntry.tsx` e `scout.tsx` bloccano i non-admin con il messaggio +"Scout riservato". **Il controllo non è imposto a livello database**: le policy RLS di +`scout_sessioni`/`scout_live`/`scout_partite` sono aperte a qualunque utente autenticato, non +solo agli admin — la migration M4 toglie l'accesso solo al ruolo `anon`. + +--- + +## Meccanismo di lock condiviso + +- `useApriSessioneScout()` (`scout-live.ts`) prende il controllo con un upsert su + `scout_sessioni` (chiave `evento_id`), rifiutando se un altro giocatore ha già una sessione + non scaduta. +- Una sessione scade dopo 5 minuti di inattività; `useHeartbeatScout()` la rinnova ogni 60 + secondi finché lo scout resta aperto. +- Il rilascio (`useChiudiSessioneScout()`) avviene al bottone "Rilascia", a fine partita, e + sull'evento `pagehide` della finestra (per liberare il lock se il browser viene chiuso senza + uscire esplicitamente). +- Nessun realtime: la sessione si rilegge solo all'apertura/focus pagina o al bottone + "Aggiorna" (`staleTime` 30s). + +--- + +## Cosa registra + +Tipi di azione (`AzioneTipo`, `scout-store.ts`): `attacco`, `ace`, `muro`, `errore`, +`punto_avv`, `errore_avv` — attacco/ace/muro ed errore avversario valgono come punto nostro, +errore nostro e punto avversario come punto avversario. Le azioni con giocatore +(attacco/ace/muro/errore) richiedono di selezionarlo prima dalla griglia dei convocati +(filtrati sulle risposte "presente"/"ritardo", con fallback a tutta la rosa se nessuno ha +risposto). Salvataggio automatico su `scout_live` con debounce di 800ms a ogni cambiamento. + +--- + +## Fine partita + +`finePartita()` (`scout.tsx`) compone i parziali finali, inserisce la partita in +`scout_partite` (INSERT, non upsert), poi cancella la riga da `scout_live` (stato consumato) +e rilascia la sessione. + +--- + +## Export CSV + +`scout-export.ts` genera un CSV (separatore `;`, BOM UTF-8) con parziali, riepilogo per +giocatore e log cronologico delle azioni. Scaricabile dagli admin dalla pagina partita, +sezione "Report tecnico". + +--- + +## Sondaggio cacche + +`SondaggioCacche.tsx` chiede "quante cacche hai fatto prima di questa partita" (0-5+), sempre +modificabile, senza gating temporale reale (visibile sia prima sia dopo la partita nonostante +il nome). `statisticheCacche()` (`cacche.ts`) calcola media, record e `giornateTop` +(giornate con ≥3), soglia usata per un [badge](badge.md) segreto — coerente con DD-007 (badge +calcolati a runtime). + +--- + +## Regole rispettate + +- **DD-008 (gamification equa)**: i dati tecnici (punti/ace/muri) restano confinati allo Scout + Live come statistica di squadra e non entrano nel tipo `Giocatore` usato per badge o + classifiche individuali. + +--- + +## Limiti noti + +- Controllo "solo admin" non imposto dal database (vedi sopra). +- Possibile, per quanto improbabile, doppio "successo" applicativo nel prendere il lock: + lettura e upsert non sono atomici. +- `scout_partite` si inserisce ma non si corregge dall'interfaccia: solo eliminazione totale. +- Abbinamento partita↔scout fatto anche per uguaglianza di data come fallback: ambiguo se due + partite cadono lo stesso giorno. + +--- + +## Evoluzioni possibili + +- Realtime (Supabase Realtime) per aggiornare la sessione condivisa senza refresh manuale. +- Restringere le policy RLS al solo ruolo admin. diff --git a/docs/modules/serie-presenze.md b/docs/modules/serie-presenze.md new file mode 100644 index 0000000..1f11cd3 --- /dev/null +++ b/docs/modules/serie-presenze.md @@ -0,0 +1,60 @@ +# Modulo — Serie di presenze + +**Stato:** implementato solo lato definizione/UI — **non calcola valori reali** (vedi Limiti noti) +**File principali:** `src/lib/serie.ts`, `src/components/crapp/SerieCard.tsx` + +--- + +## Obiettivo + +Motivare la costanza dei giocatori mostrando "serie" (streak) di comportamenti positivi +consecutivi — presenza agli allenamenti, presenza alle partite, risposta entro 24 ore alla +convocazione — con traguardi progressivi, sullo stile delle app fitness. È anche uno dei +requisiti di sblocco di alcuni [badge](badge.md) e di un [obiettivo di squadra](obiettivi-squadra.md). + +--- + +## Dati + +Non esiste una tabella dedicata: le serie sono campi (`serieAllenamenti`, `seriePartite`, +`serieConferme`) del tipo `Giocatore` assemblato da `useRosa()` (`src/lib/rosa.ts`). + +--- + +## Implementazione + +- `src/lib/serie.ts` — definizione dei 3 tipi di serie (`serieDefs`, con label, descrizione e + 4 traguardi crescenti ciascuna), funzione pura `aggiornaSerie(valore, onorato)` (regola: +1 + se onorato, altrimenti azzeramento **solo di quella serie**), `statoSerie()` (progresso e + messaggio verso il prossimo traguardo), `serieGiocatore()`/`serieMigliore()` (aggregatori + per la UI). +- `src/components/crapp/SerieCard.tsx` — `SerieGriglia` (vista completa nel profilo) e + `SerieHome` (riepilogo compatto in home, solo la serie più alta). +- `src/routes/profilo.tsx` — monta `SerieGriglia` nella sezione "Serie di presenze". + +--- + +## Limiti noti + +**La funzione `aggiornaSerie()` non è invocata da nessun punto del codice.** I tre campi che +alimentano la UI (`serieAllenamenti`, `seriePartite`, `serieConferme`, oltre a `streak`) sono +impostati a `0` fisso in `useRosa()` (`src/lib/rosa.ts`) e nel seed storico di +`crapp-data.ts`. Di conseguenza, con i dati reali della rosa: + +- le card in `SerieGriglia` mostrano sempre progresso 0; +- i badge che dipendono dalle serie ("Sempre in palestra", "Risposta lampo", il segreto "Mai + un forfait") non possono mai sbloccarsi; +- l'obiettivo di squadra "Continuità di squadra" (≥3 allenamenti consecutivi per almeno 12 + giocatori) resta permanentemente a 0/12. + +Il modulo è quindi completo lato definizione e UI, ma **funzionalmente inerte**: manca il +collegamento che calcoli le serie da `risposte_presenze` e le derivi per ogni giocatore. + +--- + +## Evoluzioni possibili + +- Calcolare le tre serie a partire da `risposte_presenze` (ordinando gli eventi per data e + applicando `aggiornaSerie()` in sequenza), lato client in `useRosa()` o come valore + derivato lato server. +- Una volta corretto, verificare che i badge e l'obiettivo collegati si sblocchino davvero.