From 8f25143aa515efca4e85d1b8f02911c258be09a5 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Thu, 1 Oct 2026 08:53:38 +0200 Subject: [PATCH] Riorganizza la documentazione per l'uso con assistenti AI - Spezza il registro delle decisioni in docs/decisions/DD-XXX.md, con indice - Ripulisce DATABASE.md dalla storia delle migration e aggiunge la sezione Lettura - Riduce PROJECT_STATE.md a una pagina; la procedura di allestimento passa in OPERATIONS.md - Aggiunge OPERATIONS.md, GLOSSARIO.md, trappole note e checklist in ARCHITECTURE.md - Sintetizza la copertura test di badge.md e documenta docs/archive - Allinea AGENTS.md, README.md e la regola Cursor (tolto l'obbligo di develop, in contrasto con DD-019) Co-Authored-By: Claude Sonnet 5.5 --- .cursor/rules/crapp.mdc | 1 - AGENTS.md | 20 +- PROJECT_STATE.md | 238 +-- README.md | 9 +- docs/ARCHITECTURE.md | 37 + docs/CHANGELOG.md | 2 +- docs/DATABASE.md | 143 +- docs/DESIGN_DECISIONS.md | 1860 ----------------------- docs/GLOSSARIO.md | 29 + docs/OPERATIONS.md | 106 ++ docs/README.md | 37 +- docs/WORKER_EMAIL.md | 4 +- docs/archive/README.md | 8 + docs/decisions/DD-001.md | 24 + docs/decisions/DD-002.md | 24 + docs/decisions/DD-003.md | 26 + docs/decisions/DD-004.md | 22 + docs/decisions/DD-005.md | 23 + docs/decisions/DD-006.md | 22 + docs/decisions/DD-007.md | 23 + docs/decisions/DD-008.md | 22 + docs/decisions/DD-009.md | 24 + docs/decisions/DD-010.md | 23 + docs/decisions/DD-011.md | 24 + docs/decisions/DD-012.md | 22 + docs/decisions/DD-013.md | 22 + docs/decisions/DD-014.md | 16 + docs/decisions/DD-015.md | 30 + docs/decisions/DD-016.md | 45 + docs/decisions/DD-017.md | 37 + docs/decisions/DD-018.md | 25 + docs/decisions/DD-019.md | 33 + docs/decisions/DD-020.md | 31 + docs/decisions/DD-021.md | 40 + docs/decisions/DD-022.md | 32 + docs/decisions/DD-023.md | 59 + docs/decisions/DD-024.md | 60 + docs/decisions/DD-025.md | 54 + docs/decisions/DD-026.md | 44 + docs/decisions/DD-027.md | 52 + docs/decisions/DD-028.md | 50 + docs/decisions/DD-029.md | 77 + docs/decisions/DD-030.md | 71 + docs/decisions/DD-031.md | 90 ++ docs/decisions/DD-032.md | 40 + docs/decisions/DD-033.md | 35 + docs/decisions/DD-034.md | 51 + docs/decisions/DD-035.md | 57 + docs/decisions/DD-036.md | 76 + docs/decisions/DD-037.md | 55 + docs/decisions/DD-038.md | 68 + docs/decisions/DD-039.md | 40 + docs/decisions/DD-040.md | 95 ++ docs/decisions/README.md | 100 ++ docs/modules/allenatore.md | 2 +- docs/modules/badge.md | 274 +--- docs/modules/calendario.md | 8 +- docs/modules/collegamento-csi.md | 2 +- docs/modules/notifiche-email.md | 2 +- docs/modules/squadra.md | 20 +- test/integration/pulizia-evento.test.ts | 2 +- 61 files changed, 2155 insertions(+), 2413 deletions(-) delete mode 100644 docs/DESIGN_DECISIONS.md create mode 100644 docs/GLOSSARIO.md create mode 100644 docs/OPERATIONS.md create mode 100644 docs/archive/README.md create mode 100644 docs/decisions/DD-001.md create mode 100644 docs/decisions/DD-002.md create mode 100644 docs/decisions/DD-003.md create mode 100644 docs/decisions/DD-004.md create mode 100644 docs/decisions/DD-005.md create mode 100644 docs/decisions/DD-006.md create mode 100644 docs/decisions/DD-007.md create mode 100644 docs/decisions/DD-008.md create mode 100644 docs/decisions/DD-009.md create mode 100644 docs/decisions/DD-010.md create mode 100644 docs/decisions/DD-011.md create mode 100644 docs/decisions/DD-012.md create mode 100644 docs/decisions/DD-013.md create mode 100644 docs/decisions/DD-014.md create mode 100644 docs/decisions/DD-015.md create mode 100644 docs/decisions/DD-016.md create mode 100644 docs/decisions/DD-017.md create mode 100644 docs/decisions/DD-018.md create mode 100644 docs/decisions/DD-019.md create mode 100644 docs/decisions/DD-020.md create mode 100644 docs/decisions/DD-021.md create mode 100644 docs/decisions/DD-022.md create mode 100644 docs/decisions/DD-023.md create mode 100644 docs/decisions/DD-024.md create mode 100644 docs/decisions/DD-025.md create mode 100644 docs/decisions/DD-026.md create mode 100644 docs/decisions/DD-027.md create mode 100644 docs/decisions/DD-028.md create mode 100644 docs/decisions/DD-029.md create mode 100644 docs/decisions/DD-030.md create mode 100644 docs/decisions/DD-031.md create mode 100644 docs/decisions/DD-032.md create mode 100644 docs/decisions/DD-033.md create mode 100644 docs/decisions/DD-034.md create mode 100644 docs/decisions/DD-035.md create mode 100644 docs/decisions/DD-036.md create mode 100644 docs/decisions/DD-037.md create mode 100644 docs/decisions/DD-038.md create mode 100644 docs/decisions/DD-039.md create mode 100644 docs/decisions/DD-040.md create mode 100644 docs/decisions/README.md diff --git a/.cursor/rules/crapp.mdc b/.cursor/rules/crapp.mdc index 32b9f25..df3093b 100644 --- a/.cursor/rules/crapp.mdc +++ b/.cursor/rules/crapp.mdc @@ -7,7 +7,6 @@ Prima di qualsiasi modifica leggi @AGENTS.md e seguine le regole: sono vincolant per intero. - Non implementare funzionalità non documentate in `docs/`. -- Lavora su `develop`, mai direttamente su `main`. - Codice, commenti e documentazione in italiano. Non aggiungere regole in questo file: una regola nuova va in `AGENTS.md`, che leggono anche diff --git a/AGENTS.md b/AGENTS.md index c439835..b9200f4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,17 +11,22 @@ usabile dallo smartphone anche da chi non è pratico. ## Prima di modificare il codice 1. Leggi l'indice [docs/README.md](docs/README.md) e segui l'ordine di lettura che indica; poi - il documento del modulo interessato in [docs/modules/](docs/modules/). + il documento del modulo interessato in [docs/modules/](docs/modules/). Le decisioni + ([docs/decisions/](docs/decisions/README.md)) si leggono una per una, solo quelle che il modulo + richiama o che l'indice indica per il tema: non servono tutte. Per i termini del dominio c'è il + [glossario](docs/GLOSSARIO.md); le «Trappole note» in + [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) vanno lette prima di toccare codice o schema. 2. Verifica lo stato attuale del repository: commit recenti, modifiche non committate, lavoro introdotto da altri collaboratori o da altri assistenti. 3. Non presumere che il progetto sia come l'hai lasciato nell'ultima sessione: la fonte di verità è il repository, non la cronologia della conversazione. Non implementare funzionalità non documentate: prima si documenta -([DD-002](docs/DESIGN_DECISIONS.md#dd-002--sviluppo-document-first)), poi si scrive il codice. +([DD-002](docs/decisions/DD-002.md)), poi si scrive il codice. ## Comandi +Variabili d'ambiente, ambienti e rilascio sono in [docs/OPERATIONS.md](docs/OPERATIONS.md). Le dipendenze si installano con **bun** (`bun.lock`). `bunfig.toml` impone `minimumReleaseAge = 24h` come guardia supply-chain: aggiungere un pacchetto a `minimumReleaseAgeExcludes` richiede conferma esplicita dell'utente. @@ -82,9 +87,14 @@ Non committare, non fare push e non aprire PR senza che l'utente lo abbia chiest Ogni modifica significativa deve lasciare una traccia leggibile senza la cronologia delle conversazioni: commit con messaggio descrittivo, più il documento giusto tra [docs/CHANGELOG.md](docs/CHANGELOG.md) (cosa è stato rilasciato e quando), -[PROJECT_STATE.md](PROJECT_STATE.md) (stato generale del progetto), -[docs/DESIGN_DECISIONS.md](docs/DESIGN_DECISIONS.md) (decisioni architetturali, voci `DD-XXX`), -[docs/ROADMAP.md](docs/ROADMAP.md), [docs/DATABASE.md](docs/DATABASE.md) (se cambia lo schema). +[PROJECT_STATE.md](PROJECT_STATE.md) (a che punto siamo ora, in una pagina: non il registro delle +migration né l'elenco delle funzioni), +[docs/decisions/](docs/decisions/README.md) (decisioni architetturali, un file `DD-XXX.md` ciascuna), +[docs/ROADMAP.md](docs/ROADMAP.md), [docs/DATABASE.md](docs/DATABASE.md) (se cambia lo schema), +[docs/OPERATIONS.md](docs/OPERATIONS.md) (se cambiano variabili d'ambiente o procedure di rilascio). +Una decisione nuova è un file `docs/decisions/DD-XXX.md` copiato da `docs/_template-dd.md`, più la +riga nell'indice. Per aggiungere una funzione o un modulo segui la checklist in +[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). Quali contenuti vanno in quale file, e le convenzioni di scrittura, stanno nelle regole di manutenzione di [docs/README.md](docs/README.md): ogni informazione ha una sola casa, non diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index 1ec9398..5320a6a 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -1,219 +1,39 @@ # Project State -Ultimo aggiornamento: 29/09/2026 +Ultimo aggiornamento: 01/10/2026 + +Questo file dice **a che punto siamo ora**. Le funzionalità stanno in +[docs/ROADMAP.md](docs/ROADMAP.md), i rilasci in [docs/CHANGELOG.md](docs/CHANGELOG.md), lo +schema in [docs/DATABASE.md](docs/DATABASE.md), le procedure in +[docs/OPERATIONS.md](docs/OPERATIONS.md). Non ripetere qui quello che hanno già loro. ## Stato generale -Fase corrente: +- **Versione in produzione:** 1.2.1 su `main` — notifiche su tre canali (push, in-app, email), + promemoria che non si ripetono, turno palloni e solleciti presenze automatici. +- **Backend:** Supabase proprietario (`kfkcldwncxqaixetsjes`). Lovable Cloud e il vecchio + Project Ref `hetycilxgkdmccelwerq` non si usano più. +- **Migration:** 37 file in `supabase/migrations/`, fino a `m26_notifiche_automatiche`, tutte + applicate in produzione (verificato con `npx supabase migration list` il 30/09/2026). +- **Worker delle notifiche** (`mailer/`): in funzione su un Raspberry Pi con Docker dal 29/09/2026 + ([docs/WORKER_EMAIL.md](docs/WORKER_EMAIL.md)). Il codice con l'interruttore «Email» in Profilo + è su `develop`, non ancora su `main`. +- **Accesso:** il login Google è l'unica via d'ingresso; i permessi di amministrazione arrivano + solo da `user_roles`. Il collegamento degli account è un processo continuo: ogni giocatore si + collega al primo accesso (DD-018). +- **Lavoro in corso:** niente di assegnato. Le voci aperte stanno in + [docs/ROADMAP.md](docs/ROADMAP.md), sotto «Prossimo». -Backend migrato al nuovo Supabase proprietario. Autenticazione Google, dashboard -amministratore e Profilo Giocatore (lato giocatore e lato admin) sono in produzione su `main`. -Foto profilo (M6) e Scout Live (M7) non dipendono più da `localStorage`: entrambi ora -sincronizzano tra dispositivi tramite Supabase. Le serie di presenze sono calcolate sui dati -reali (M9). Prima versione pre-release rilasciata (0.9.0); la prima versione stabile, 1.0.0, ha -portato il ruolo allenatore. La versione corrente è la 1.2.1: notifiche su tre canali (push, in-app -ed email), promemoria che non si ripetono, turno palloni e solleciti presenze automatici (vedi -`docs/CHANGELOG.md`). -Cancellare un evento pulisce ora a cascata tutte le tabelle collegate (M14) e le righe orfane -da cancellazioni precedenti a M14 sono state bonificate una tantum (M15/M16). +## Cose da sapere ---- - -## Infrastruttura - -- Si lavora direttamente su `main` (DD-019): `develop` esiste ma è fermo indietro, quindi la - sua preview Vercel non rappresenta lo stato attuale -- Cursor e Claude Code come ambienti di sviluppo -- Vercel configurato; Environment Variables aggiornate al nuovo Supabase (Preview e Production) -- Supabase proprietario attivo — Project Ref: `kfkcldwncxqaixetsjes` -- 37 migration in `supabase/migrations/`, fino a `m26_notifiche_automatiche` (30/09/2026), tutte - applicate in produzione, verificato con `npx supabase migration list` il 30/09/2026 -- Worker delle notifiche email (`mailer/`) in funzione su un Raspberry Pi con Docker dal - 29/09/2026: invia via Gmail le notifiche di `notifiche_utente`. Installazione e gestione in - [docs/WORKER_EMAIL.md](docs/WORKER_EMAIL.md); il codice con l'interruttore «Email» in Profilo è - su `develop`, non ancora su `main` -- Sviluppo locale verificato con il nuovo Supabase - ---- - -## Backend - -- Backend operativo: Supabase proprietario (`kfkcldwncxqaixetsjes`) -- Lovable Cloud: non più backend operativo di CrAPP -- Vecchio Project Ref `hetycilxgkdmccelwerq`: deprecato, non utilizzare - ---- - -## Database - -- Schema v1.0 e migration da M1 a M16 applicate al nuovo Supabase - (`m16_funzione_bonifica_dati_evento_orfani` in produzione dal 09/09/2026, verificata con - `npx supabase migration list`) -- Migration `m17_notifiche_utente` (15/09/2026, DD-030): nuova tabella `notifiche_utente` - per il centro notifiche in-app — **in produzione** dal 15/09/2026 (è additiva: non tocca - schema o comportamento esistenti, vedi DD-030). Il codice che la usa (centro notifiche, - route aggiornate) non è ancora deployato: la tabella si riempie già in background -- Migration `m18_nascita_pubblica_giocatori_squadra` (15/09/2026, DD-031): nuova colonna - `giocatori_squadra.nascita`, sincronizzata da `profili_giocatore.data_nascita` — corregge - l'"Invalid Date" in Squadra per chi inserisce la propria nascita dal Profilo invece del - seed storico. **In produzione** dal 15/09/2026, ma il backfill copriva solo il seed - storico: chi aveva già un profilo compilato prima di M18 è rimasto scoperto -- Migration `m19_backfill_nascita_da_profili_esistenti` (15/09/2026, DD-031): completa il - backfill di M18 per tutti i profili già esistenti — **in produzione** (risulta applicata - in `npx supabase migration list` del 24/09/2026) -- Migration `m25_promemoria_solo_giocatori` (29/09/2026, DD-039): `giocatori_destinatari_evento()` - include solo i giocatori attivi, quindi l'allenatore non riceve più i promemoria degli eventi - (li riceveva sempre dalla `m21`). Cambia solo quella funzione; verificata in locale con - `npx supabase db reset` e i test di integrazione, **non ancora in produzione** -- Migration `m24_push_promemoria_e_sondaggio` (29/09/2026, DD-038): tabella `notifiche_push_coda` - con trigger e due funzioni per il worker (la push dei promemoria a 24 e 3 ore), e nuovo tipo - `sondaggio_cacche` in `notifiche_utente` (il sondaggio pre-partita compare in-app e per email). - Additiva; verificata in locale con `npx supabase db reset`, i test di integrazione e una prova - end-to-end del worker con un servizio push finto, **non ancora in produzione**. Richiede il - worker ricostruito e le chiavi VAPID nel suo `.env` (vedi `docs/WORKER_EMAIL.md`) -- Migration `m23_promemoria_gia_generati` (29/09/2026, DD-037): tabella - `promemoria_eventi_generati` e nuova `genera_promemoria_eventi()`, così un promemoria a 24 o 3 - ore eliminato dal centro notifiche non viene più rigenerato dal cron (e non riaccoda mail). - Corregge un difetto presente dalla `m17`, emerso con l'arrivo delle email. Additiva; verificata - in locale con `npx supabase db reset` e `test/integration/promemoria-eventi.test.ts`, **non - ancora in produzione** -- Migration `m22_notifiche_email` (29/09/2026, DD-036): tabelle `notifiche_email_coda` e - `preferenze_utente`, trigger che accoda ogni notifica di `notifiche_utente` e tre funzioni - RPC riservate alla service role, per il worker email. Additiva: non tocca lo schema - esistente. Verificata in locale con `npx supabase db reset`, test unit e di integrazione e - una prova end-to-end con il worker in Docker contro Mailpit, poi **in produzione** dal - 29/09/2026 (`supabase db push`) con il worker in funzione -- Migration `m20_ruolo_allenatore_enum` e `m21_ruolo_allenatore` (24/09/2026, DD-034): ruolo - allenatore (colonna `giocatori_squadra.tipo`, valore `allenatore` di `app_role`, policy e - trigger) — verificate in locale con `npx supabase db reset` e `npm run test:all`, **in - produzione** dal 24/09/2026 (`supabase db push`). Compatibili con la 0.9.2: senza allenatori - registrati non cambiano nulla. Il codice che le usa non è ancora deployato -- Slot `g18` «Beta Tester» impostato come allenatore in produzione il 24/09/2026 (colonna - `tipo`, ruolo assegnato dal trigger di M21). Numero 99 e ruolo «Palleggiatore» - lasciati apposta finché gira la 0.9.2, che lo mostra ancora come giocatore: da svuotare - da `/admin` dopo il deploy del codice nuovo -- `public.giocatori_squadra`: rosa iniziale di 17 giocatori (migration `m5_email_giocatori_squadra`) - più quelli aggiunti da `/admin` a stagione in corso; da settembre 2026 tutti i giocatori - attivi hanno l'email registrata (colonna `email`, DD-018), impostabile da `/admin` senza - bisogno di una migration -- `public.giocatori_squadra` è ora la source of truth della rosa letta dall'app (DD-015, - 03/09/2026): «Aggiungi giocatore» e «Disattiva giocatore» della dashboard admin si - riflettono su Squadra, Presenze, Pagelle, Badge e Scout. `src/lib/crapp-data.ts` resta - solo come seed storico, fallback offline e sorgente della data di nascita (colonna non - ancora presente su `giocatori_squadra`) -- Migration `m6_avatar_giocatori`: bucket pubblico `avatar-giocatori` per le foto profilo, - al posto di `localStorage` (una per giocatore, letto da `src/lib/avatar-store.ts`) -- Migration `m10_azzera_turni_palloni_allenamenti`: toglie i turni palloni salvati sugli - allenamenti, che non ricevono più una proposta automatica (vedi - [docs/modules/palloni.md](docs/modules/palloni.md)) -- Migration `m11_scritture_per_ruolo`: le policy di scrittura rispecchiano i permessi - dell'interfaccia (DD-023). Fino a M10 un qualsiasi utente autenticato poteva svuotare il - calendario o riscrivere il voto di un altro parlando direttamente con PostgREST; la tabella - dei permessi sta in [docs/DATABASE.md](docs/DATABASE.md) ed è verificata da - `test/integration/permessi.test.ts` -- Migration `m7_scout_partite`: nuova tabella `scout_partite` per l'archivio delle partite - scoutate concluse, e collegamento della tabella `scout_sessioni` (già presente nello - schema ma mai usata) al blocco condiviso dello Scout Live — prima entrambi vivevano solo - in `localStorage`, quindi visibili a un solo dispositivo -- Migration `m13_convocati_e_pagelle_chiuse`: le RLS di `mvp_voti`/`pagelle_voti`/ - `badge_social_voti` richiedono che votante e votato siano convocati all'evento (e per le - pagelle anche `pagelle_chiuse = false`) -- Migration `m14_pulizia_dati_evento_cancellato` (DD-029): cancellare un evento pulisce a - cascata, tramite trigger, tutte le tabelle collegate (`risposte_presenze`, - `cacche_partita`, `mvp_voti`, `pagelle_voti`, `badge_social_voti`, `turni_palloni`, - `scout_sessioni`, `scout_live`, `scout_partite`) -- Migration `m15_bonifica_dati_evento_orfani`: bonifica una tantum delle righe orfane da - cancellazioni di eventi precedenti a M14, senza toccare i vecchi voti MVP/pagelle/badge - social legati a id Scout o CSI -- Migration `m16_funzione_bonifica_dati_evento_orfani`: la stessa logica di bonifica di M15 - resta richiamabile come funzione `bonifica_dati_evento_orfani()` (riservata al service - role), se mai servisse di nuovo - ---- - -## Moduli completati - -- Squadra -- Presenze -- Badge (incluso badge social) -- Scout Live (blocco e archivio partite sincronizzati tra dispositivi, migration `m7_scout_partite`) -- Pagelle -- MVP -- Obiettivi di squadra -- Turno palloni (specifica in `docs/modules/palloni.md`) -- Infortuni, in forma minima (specifica in `docs/modules/infortuni.md`) -- Notifiche -- Profilo Giocatore (specifica in `docs/modules/profilo-giocatore.md`) -- Serie di presenze (specifica in `docs/modules/serie-presenze.md`) -- Collegamento CSI: classifica campionato e Coppa, storico e dettaglio partita — formazioni, - scontri diretti (specifica in `docs/modules/collegamento-csi.md`) - ---- - -## Autenticazione e dashboard amministratore - -In produzione su `main`. **Il login è l'unica via d'accesso** (31/08/2026): la selezione -libera del giocatore non esiste più, senza sessione Google si resta su `/benvenuto`, e i -permessi di amministrazione arrivano solo da `user_roles`. - -**Attenzione all'ordine:** finché il provider Google è spento in Supabase, «Accedi con -Google» risponde - -``` -{"code":400,"error_code":"validation_failed","msg":"Unsupported provider: provider is not enabled"} -``` - -e **nessuno entra nell'app**. Vale ancora per chi allestisce un ambiente nuovo (per esempio -lo stack Supabase locale): il passo 1 qui sotto va fatto per primo. - -Passaggi in ordine, nessuno dei quali è reversibile a metà. **Stato al 04/09/2026: fatti i -passaggi 1, 2, 3 e 5 (M4 applicata); il passaggio 4 è un processo continuo (7 dei 16 giocatori -attivi hanno già fatto il primo accesso).** - -1. **Provider Google in Supabase** — Google Cloud Console: consent screen _External_ (scope - `email` e `profile`, non sensibili: nessuna verifica richiesta, e la modalità _Testing_ - regge fino a 100 utenti, più che sufficiente per la squadra), credenziale - _Web application_ con redirect URI - `https://kfkcldwncxqaixetsjes.supabase.co/auth/v1/callback`. Client ID e - secret in _Authentication → Providers → Google_. In _URL Configuration_: Site URL di - produzione, più `localhost:8080` e il wildcard delle preview Vercel tra i Redirect URLs. - Per provare sullo stack locale invece che sul cloud servono anche `enabled = true` in - `[auth.external.google]` di `supabase/config.toml`, le due variabili - `SUPABASE_AUTH_GOOGLE_*` in `.env` e una credenziale con redirect URI - `http://127.0.0.1:54321/auth/v1/callback`. -2. **Migration M2** (`supabase db push`). È un `CREATE` puro: si può applicare in - produzione senza toccare il comportamento attuale. -3. **Primo admin**, dopo il primo login (l'ID esiste solo da quel momento): - `INSERT INTO public.user_roles (user_id, role) SELECT id, 'admin' FROM auth.users WHERE email = '';` -4. **Collegamento degli account**: ciascuno accede con Google e viene collegato in - automatico al proprio giocatore per email (DD-018) — nessuna scelta manuale. Finché - l'email di un giocatore non è impostata, il suo accesso mostra un errore; da `/admin` si - imposta l'email di un giocatore (nuovo o esistente) senza bisogno di una migration. Da - settembre 2026 tutti i giocatori attivi hanno l'email registrata, ma il collegamento vero - e proprio (`auth_user_id`) avviene solo al primo login di ciascuno, quindi resta un - processo continuo che si ripete a ogni nuovo giocatore aggiunto a stagione in corso. Uno - slot già collegato può essere liberato solo da un admin. -5. **Solo a squadra collegata**: migration `m4_solo_autenticati`, che toglie al ruolo `anon` - l'accesso alle tabelle v1.0. Da lì in poi i dati sono raggiungibili solo con una sessione; - le route in `src/routes/api/public/` usano la service role e continuano a funzionare. - **Applicata in produzione il 03/09/2026** — non è più necessario aspettare che l'intera - rosa abbia già fatto login: il login era già l'unica via d'accesso lato app, quindi i - giocatori non ancora collegati non erano comunque impattati; M4 chiudeva solo un residuo - di accesso diretto al database bypassando l'app. - -Attenzione: dev e produzione condividono lo stesso progetto Supabase. Un account di prova che -collega uno slot lo occupa anche in produzione, e va liberato da un admin. - -## Prossimo sviluppo - -Niente di assegnato: l'avviso certificati in scadenza (DD-035, 1.1.0), le notifiche su tre -canali (DD-036–DD-039, 1.2.0) e quelle automatiche di palloni e solleciti (DD-040, 1.2.1) sono implementati (vedi `docs/CHANGELOG.md`). Le voci ancora aperte stanno in -[docs/ROADMAP.md](docs/ROADMAP.md), sotto «Prossimo». - ---- +- Si lavora direttamente su `main` (DD-019); Cursor e Claude Code sono gli ambienti di sviluppo. +- Dev e produzione condividono lo stesso progetto Supabase: un account di prova che collega uno + slot lo occupa anche in produzione e va liberato da un admin. +- Lo slot `g18` «Beta Tester» è impostato come allenatore in produzione (24/09/2026). Numero 99 + e ruolo «Palleggiatore» sono rimasti apposta per la 0.9.2: da svuotare da `/admin` se ancora + presenti. ## Note -Il progetto segue una metodologia document-first. - -Ogni nuova funzionalità viene progettata nella cartella `docs/modules/` prima di essere implementata. +Il progetto segue una metodologia document-first (DD-002): ogni funzionalità si progetta in +`docs/modules/` prima di essere implementata. diff --git a/README.md b/README.md index 789bd95..9099a52 100644 --- a/README.md +++ b/README.md @@ -49,12 +49,9 @@ committare lo decide chi sviluppa (DD-019). ## Variabili d'ambiente -Il progetto richiede le seguenti variabili: - -- `SUPABASE_URL` -- `SUPABASE_PUBLISHABLE_KEY` -- `VITE_SUPABASE_URL` -- `VITE_SUPABASE_PUBLISHABLE_KEY` +Per far partire l'app servono `SUPABASE_URL`, `SUPABASE_PUBLISHABLE_KEY`, `VITE_SUPABASE_URL` e +`VITE_SUPABASE_PUBLISHABLE_KEY`. L'elenco completo (service role, VAPID, SMTP del worker) sta in +[docs/OPERATIONS.md](docs/OPERATIONS.md). ## Documentazione diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 431574e..1a74348 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -102,6 +102,43 @@ Il movimento usa molle interrompibili di `motion` con i preset in `src/lib/molla lanciato, così swipe come quello del calendario atterrano dove il gesto stava andando. `src/lib/motion.ts` conserva solo il rilevamento del movimento ridotto e i coriandoli. +## Trappole note + +Cose che si scoprono solo rompendole; ognuna ha la sua decisione o il suo documento. + +- **Non invalidare la cache**: dopo una mutazione si usa `setQueryData`, non `invalidateQueries` + ([EFFICIENZA_CLOUD.md](EFFICIENZA_CLOUD.md)). +- **Niente database dai componenti**: solo tramite `src/lib/` (DD-013). +- **Tabelle presenti ma non usate**: `giocatori`, `eventi`, `presenze` (DD-014) e `promemoria_push`. + Il modello in uso è `giocatori_squadra`, `eventi_app`, `risposte_presenze`. +- **Route in `src/routes/api/public/`**: usano la service role e saltano la RLS, quindi chiedono + le credenziali (DD-024). Una nuova route che avvisa la squadra deve farlo. +- **`convocati` vuoto significa tutta la rosa**, non nessuno (DD-027). +- **L'allenatore non è un giocatore**: è fuori da `inRosa()`, non vota e non riceve i promemoria + (DD-034, DD-039). +- **Cancellare un evento** pulisce a cascata le tabelle collegate; una tabella nuova legata + agli eventi va aggiunta al trigger (DD-029). +- **Dev e produzione condividono lo stesso Supabase**: le prove distruttive si fanno sullo stack + locale ([OPERATIONS.md](OPERATIONS.md)). Il seed gira solo in locale. +- **Migration**: mai riscrivere una già applicata; un `CHECK` sul tipo di `notifiche_utente` va + esteso con una migration nuova quando nasce un tipo nuovo. +- **Dipendenze**: `bunfig.toml` impone `minimumReleaseAge = 24h`; escluderne un pacchetto richiede + conferma dell'utente. + +## Aggiungere un modulo o una funzione + +Nell'ordine, perché il progetto è document-first (DD-002): + +1. scheda in `docs/modules/.md` (stato, file principali, obiettivo, dati, permessi) e riga in + [ROADMAP.md](ROADMAP.md); se la scelta è non ovvia, una `DD-XXX` in [decisions/](decisions/README.md); +2. se serve lo schema: tabella documentata in [DATABASE.md](DATABASE.md), **nuova** migration, + permessi di scrittura nella sua tabella e test in `test/integration/permessi.test.ts`; +3. logica pura in `src/lib/-core.ts` con test in `test/unit/`; hook e accesso ai dati in + `src/lib/.ts`; componenti in `src/components/crapp/`, riusando quelli esistenti; +4. se tocca le notifiche: il catalogo in [modules/notifiche.md](modules/notifiche.md); +5. voce in [CHANGELOG.md](CHANGELOG.md) sotto `[Non rilasciato]` e checklist «Fine lavoro» di + [AGENTS.md](../AGENTS.md). + ## Comandi ```bash diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 91db9c1..e74a0e4 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -40,7 +40,7 @@ produzione dal 30/09/2026; questa versione porta il codice dell'app che la accom finestre, destinatari, doppioni, revoca, code push ed email, permessi, pianificazione, e confronto con i testi e l'ordine del codice TypeScript. Nuovi test unitari di testi, destinatari, date e a capo delle notifiche; aggiornati i test di promemoria, code push ed email, worker e route dei pulsanti. -- **Documentazione** — DD-040 in `DESIGN_DECISIONS.md`; sezione «Testi delle notifiche» nel catalogo di +- **Documentazione** — DD-040 in `decisions/`; sezione «Testi delle notifiche» nel catalogo di `modules/notifiche.md`; tabella delle funzioni SQL e dei job in `DATABASE.md`. ### Modificato diff --git a/docs/DATABASE.md b/docs/DATABASE.md index bda377f..416ba6b 100644 --- a/docs/DATABASE.md +++ b/docs/DATABASE.md @@ -9,8 +9,8 @@ non in questo file. Chi può scrivere cosa, dopo la migration `m11_scritture_per_ruolo` (DD-023) e il ruolo allenatore di `m21_ruolo_allenatore` (DD-034). La **lettura** -resta aperta a tutti gli autenticati su ogni tabella di questo elenco; `anon` non arriva a -nessuna di esse da M4 (DD-011). +resta aperta a tutti gli autenticati su ogni tabella di questo elenco, con le eccezioni della +sezione «Lettura» qui sotto; `anon` non arriva a nessuna di esse da M4 (DD-011). | Tabella | Chi può scrivere | | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | @@ -30,61 +30,120 @@ nessuna di esse da M4 (DD-011). L'identità del giocatore è lo slot di `giocatori_squadra` con `auth_user_id = auth.uid()`. La tabella è verificata da `test/integration/permessi.test.ts` contro il database locale. +## Lettura + +Regola generale: ogni autenticato legge. Le eccezioni sono tutte dati personali o code interne: + +| Tabella / bucket | Chi legge | +| --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | +| `profili_giocatore` | il titolare e gli admin (contiene documento d'identità e certificato medico, DD-016) | +| `bucket profili-giocatore` | privato: il titolare la propria cartella, l'admin tutto via signed URL a scadenza breve | +| `notifiche_utente` | il destinatario, solo le proprie righe (RLS, DD-030) | +| `preferenze_utente` | l'utente, solo la propria riga | +| `notifiche_email_coda`, `notifiche_push_coda`, `promemoria_eventi_generati` | nessuno da client: RLS attiva senza policy, solo service role | +| `giocatori_squadra.nascita` | tutta la rosa attiva: è una colonna pubblica sincronizzata, non una lettura di `profili_giocatore` (DD-031) | + ## Anagrafica e utenti -| Tabella | Scopo | Note | -| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `giocatori_squadra` | Anagrafica operativa della squadra, con ID testuali (`g1`…`gN`), dati gestiti dagli admin (nome, cognome, numero, ruolo), collegamento all'account (`auth_user_id`) ed email registrata (`email`). | Introdotta dalla migration `m1_giocatori_squadra`, source of truth della rosa (DD-015): `useRosa()` e gli altri punti che elencano i giocatori la leggono tramite `useGiocatoriSquadra()` (client) o `leggiGiocatoriSquadra()` (server), filtrando `attivo`. `src/lib/crapp-data.ts` resta solo come seed storico e fallback (`rosaFallback()`) quando il database non risponde. Vedi DD-015 e DD-016. La colonna `email` (migration `m5_email_giocatori_squadra`, impostabile anche da `/admin`) è la chiave del collegamento automatico account↔giocatore al primo accesso (DD-018): NULL finché non nota, oggi impostata per tutta la rosa attiva. Le colonne `numero_tessera`/`data_tessera` (migration `m8_tesseramento_csi`) tracciano chi è già tesserato al CSI; come `numero`/`ruolo` le scrive solo un admin, il trigger di M1/M5 le include tra i campi bloccati per chi reclama il proprio slot. La colonna `nascita` (migration `m18_nascita_pubblica_giocatori_squadra`, DD-031) è pubblica a tutta la rosa attiva (stessa policy di lettura delle altre colonne): non si scrive mai da qui, un trigger su `profili_giocatore` (`sincronizza_nascita_pubblica`) la tiene allineata a `data_nascita` — impostata, aggiornata o azzerata dallo stesso form Profilo che scrive quella colonna. Il trigger copre solo le scritture successive a M18: `m19_backfill_nascita_da_profili_esistenti` ha allineato una tantum anche chi aveva già compilato il profilo prima di quella migration. La colonna `tipo` (migration `m21_ruolo_allenatore`, DD-034) distingue `giocatore` da `allenatore`: l'allenatore ha uno slot per collegarsi e per i dati personali, ma è fuori dalla rosa di gioco (`inRosa()` nel codice), non ha numero di maglia (`numero` è NULL, ammesso solo per lui dal vincolo `giocatori_squadra_numero_giocatori`) e ha `ruolo` vuoto. `tipo` lo scrive solo un admin. | -| `giocatori` | Anagrafica giocatori con UUID. | Presente ma **non usata** dal codice attuale: la convergenza è rinviata (DD-012, DD-014). | -| `profili_giocatore` | Dati personali, metadati del documento d'identità, certificato medico e path dei file, in relazione 1:1 con `giocatori_squadra`. | Creata dalla migration `m2_profili_giocatore` (DD-016). Letta da `src/lib/profili.ts`; le policy mostrano al giocatore solo il proprio profilo e all'admin tutti. I file non stanno qui: la tabella conserva i path nel bucket. | -| `user_roles` | Ruoli applicativi (es. amministratore, giocatore). | Fonte dei permessi di amministrazione, letta da `src/lib/ruoli.ts` (DD-011). Il primo admin va inserito a mano; vedi [PROJECT_STATE.md](../PROJECT_STATE.md). Il valore `allenatore` di `app_role` (migration `m20_ruolo_allenatore_enum`) non si inserisce a mano: lo aggiunge e lo toglie il trigger `sincronizza_ruolo_allenatore` quando uno slot con `tipo = 'allenatore'` viene collegato, scollegato, disattivato o cambia tipo (DD-034). | +| Tabella | Scopo | Note | +| ------------------- | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `giocatori_squadra` | Anagrafica operativa della squadra: source of truth della rosa (DD-015). | ID testuali (`g1`…`gN`). Dettaglio colonne qui sotto. Letta con `useGiocatoriSquadra()` (client) o `leggiGiocatoriSquadra()` (server), filtrando `attivo`; `src/lib/crapp-data.ts` resta solo come seed e fallback (`rosaFallback()`). | +| `giocatori` | Anagrafica giocatori con UUID. | Presente ma **non usata** dal codice: la convergenza è rinviata (DD-012, DD-014). | +| `profili_giocatore` | Dati personali, metadati del documento, certificato medico e path dei file; 1:1 con `giocatori_squadra`. | Letta da `src/lib/profili.ts` (DD-016). I file non stanno qui: la tabella conserva i path nel bucket. | +| `user_roles` | Ruoli applicativi (`admin`, `allenatore`, giocatore). | Fonte dei permessi di amministrazione, letta da `src/lib/ruoli.ts` (DD-011). Il primo admin va inserito a mano (vedi [OPERATIONS.md](OPERATIONS.md)). `allenatore` non si inserisce a mano: lo gestisce il trigger `sincronizza_ruolo_allenatore` (DD-034). | -`giocatori_squadra` / `giocatori` sono usate da: Squadra, Profili, Presenze, Scout, Badge, Pagelle. +Colonne di `giocatori_squadra` che meritano una nota: + +| Colonna | Significato | +| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `email` | Chiave del collegamento automatico account↔giocatore al primo accesso (DD-018). Impostabile da `/admin`. | +| `auth_user_id` | Collega lo slot all'account; NULL finché nessuno lo reclama. | +| `numero_tessera`, `data_tessera` | Chi è già tesserato al CSI. Come `numero` e `ruolo` le scrive solo un admin: il trigger le include tra i campi bloccati per chi reclama il proprio slot. | +| `nascita` | Pubblica a tutta la rosa (DD-031). Non si scrive mai da qui: il trigger `sincronizza_nascita_pubblica` su `profili_giocatore` la tiene allineata a `data_nascita` (impostata, aggiornata o azzerata). | +| `tipo` | `giocatore` o `allenatore` (DD-034). L'allenatore ha uno slot per collegarsi e per i dati personali ma è fuori dalla rosa di gioco (`inRosa()`), con `numero` NULL (vincolo `giocatori_squadra_numero_giocatori`) e `ruolo` vuoto. Lo scrive solo un admin. | + +`giocatori_squadra` è usata da: Squadra, Profili, Presenze, Scout, Badge, Pagelle. ## Storage -| Bucket | Scopo | Note | -| ------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `profili-giocatore` | Documento d'identità, certificato medico e foto tessera, in cartelle per giocatore (`/.`). | **Privato** e destinato a restare tale: contiene documenti e dati sanitari, che non devono mai avere URL pubblici (DD-016 regola 4). Il giocatore gestisce solo la propria cartella, l'admin può scaricare tutto tramite signed URL a scadenza breve. Creato dalla migration `m2_profili_giocatore`. | -| `avatar-giocatori` | Foto profilo mostrate nel cerchio avatar (Squadra, Profilo), un file per giocatore (`/avatar.jpg`). | **Pubblico**: foto informali, non documenti sensibili. Qualsiasi autenticato può caricare/sostituire/eliminare un file (nessun controllo per-proprietario, la maggior parte dei giocatori non ha ancora `auth_user_id` collegato, DD-018). Letto da `src/lib/avatar-store.ts`. Creato dalla migration `m6_avatar_giocatori`. | +| Bucket | Scopo | Note | +| ------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `profili-giocatore` | Documento d'identità, certificato medico e foto tessera, in cartelle per giocatore (`/.`). | **Privato e destinato a restare tale**: documenti e dati sanitari non devono mai avere URL pubblici (DD-016 regola 4). Il giocatore gestisce solo la propria cartella; l'admin scarica tutto con signed URL a scadenza breve. | +| `avatar-giocatori` | Foto profilo (`/avatar.jpg`). | **Pubblico**: foto informali. Qualsiasi autenticato può caricare, sostituire o eliminare un file (nessun controllo per proprietario: la maggior parte dei giocatori non ha ancora `auth_user_id`, DD-018). Letto da `src/lib/avatar-store.ts`. | ## Eventi e presenze -| Tabella | Scopo | Note | -| ------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `eventi_app` | Eventi gestionali utilizzati dall'app. | Modello in uso dal codice attuale. Cancellare un evento pulisce a cascata, tramite trigger, tutte le tabelle collegate elencate in questa pagina (`risposte_presenze`, `cacche_partita`, `mvp_voti`, `pagelle_voti`, `badge_social_voti`, `turni_palloni`, `scout_sessioni`, `scout_live`, `scout_partite`) — migration `m14_pulizia_dati_evento_cancellato`, DD-029. Le righe orfane da cancellazioni precedenti sono state bonificate una tantum da `m15_bonifica_dati_evento_orfani`, senza toccare i vecchi voti MVP/pagelle/badge social legati a id Scout o CSI; la stessa logica resta richiamabile come funzione `bonifica_dati_evento_orfani()` (`m16_funzione_bonifica_dati_evento_orfani`, riservata al service role) se mai servisse di nuovo. | -| `risposte_presenze` | Risposte dei giocatori agli eventi. | Modello in uso dal codice attuale. `risposto_il` è l'istante della **prima** risposta (migration `m9_risposte_presenze_risposto_il`): confrontato con `eventi_app.creato_il` dà la serie "Conferme 24h". Un trigger lo rende immutabile, così un ripensamento non fa risultare rapida una risposta lenta — `aggiornato_il` resta l'ultima modifica. | -| `eventi` | Calendario generale: allenamenti, partite, eventi della squadra. | Modello "nuovo" con autenticazione e vincoli, non ancora adottato (DD-014). | -| `presenze` | Presenze agli eventi. | Come sopra (DD-014). | +| Tabella | Scopo | Note | +| ------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `eventi_app` | Eventi gestionali usati dall'app. | Modello in uso. Cancellare un evento pulisce a cascata, via trigger, tutte le tabelle collegate: `risposte_presenze`, `cacche_partita`, `mvp_voti`, `pagelle_voti`, `badge_social_voti`, `turni_palloni`, `scout_sessioni`, `scout_live`, `scout_partite` (DD-029). `bonifica_dati_evento_orfani()` (solo service role) ripulisce eventuali righe orfane. | +| `risposte_presenze` | Risposte dei giocatori agli eventi. | `risposto_il` è l'istante della **prima** risposta e un trigger lo rende immutabile: confrontato con `eventi_app.creato_il` dà la serie «Conferme 24h». `aggiornato_il` è l'ultima modifica. | +| `cacche_partita` | Sondaggio prepartita. | Usato per statistiche e badge segreti. | +| `eventi` | Calendario generale: allenamenti, partite, eventi della squadra. | Modello «nuovo» con autenticazione e vincoli, non adottato (DD-014). | +| `presenze` | Presenze agli eventi. | Come sopra (DD-014). | ## Scout -| Tabella | Scopo | Note | -| ---------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `scout_sessioni` | Chi ha il controllo dello Scout Live per una partita (blocco condiviso), una riga per evento. | Letta/scritta da `src/lib/scout-live.ts`. Prima viveva solo in `localStorage`: "Scout occupato da X" non funzionava mai tra dispositivi diversi (fix M7). | -| `scout_live` | Stato in corso (azioni non ancora concluse) di una sessione di Scout Live. | Serve esclusivamente per statistiche di squadra, mai per classifiche individuali (DD-008). Letta/scritta da `src/lib/scout-stato.ts`. | -| `scout_partite` | Archivio delle partite scoutate concluse (risultato, parziali, azioni). | Letta/scritta da `src/lib/scout-store.ts`. Prima il risultato finale finiva solo in `localStorage`: invisibile a chiunque non fosse il dispositivo di chi aveva chiuso la partita (fix M7). | +| Tabella | Scopo | Note | +| ---------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | +| `scout_sessioni` | Chi ha il controllo dello Scout Live per una partita (blocco condiviso), una riga per evento. | Letta/scritta da `src/lib/scout-live.ts`. | +| `scout_live` | Stato in corso (azioni non ancora concluse) di una sessione. | Solo per statistiche di squadra, mai classifiche individuali (DD-008). `src/lib/scout-stato.ts`. | +| `scout_partite` | Archivio delle partite scoutate concluse (risultato, parziali, azioni). | Letta/scritta da `src/lib/scout-store.ts`. | ## Votazioni -| Tabella | Scopo | Note | -| ------------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `mvp_voti` | Voti MVP assegnati a fine partita. | Un voto per votante e partita; auto-voto rifiutato (`mvp_no_autovoto`, migration `m12_niente_autovoto`); votante e votato devono essere convocati all'evento (RLS, `m13_convocati_e_pagelle_chiuse`). | -| `pagelle_voti` | Voti anonimi assegnati ai giocatori. | Usati per il voto medio. Voto 1-10 e auto-voto rifiutato dai vincoli originari della tabella; votante/votato convocati e `pagelle_chiuse = false` richiesti dalla RLS di `m13_convocati_e_pagelle_chiuse`. | -| `badge_social_voti` | Voti social per i badge. | Un voto per categoria, votante e partita; auto-voto rifiutato (`badge_social_no_autovoto`, `m12_niente_autovoto`); votante e votato devono essere convocati all'evento (RLS, `m13_convocati_e_pagelle_chiuse`). | +Per tutte: un voto per votante e partita (per categoria nei badge social), auto-voto rifiutato, +votante e votato convocati all'evento (RLS, `evento_permette_voto()`, DD-027). + +| Tabella | Scopo | Note | +| ------------------- | ---------------------------------- | --------------------------------------------------------------------- | +| `mvp_voti` | Voti MVP assegnati a fine partita. | Vincolo `mvp_no_autovoto`. | +| `pagelle_voti` | Voti anonimi ai giocatori (1-10). | Usati per il voto medio. Si scrive solo con `pagelle_chiuse = false`. | +| `badge_social_voti` | Voti social per i badge. | Vincolo `badge_social_no_autovoto`. | ## Turni e notifiche -| Tabella | Scopo | Note | -| ---------------------------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `turni_palloni` | Gestione dei turni palloni. | Solo turni **confermati**. Gli allenamenti non ricevono proposta automatica (vedi [palloni.md](modules/palloni.md)); M10 azzera i turni salvati su allenamenti da oggi in poi. | -| `push_subscriptions` | Dispositivi registrati per le notifiche Push. | | -| `promemoria_push` | Non più usata. | Serviva da coda del testo quando la push partiva vuota; dal payload cifrato non la scrive né la legge nessuno. Tabella ancora presente, da eliminare con una migrazione. | -| `notifiche_utente` | Centro notifiche in-app, con stato letto/non letto, sull'avatar del profilo. | Introdotta dalla migration `m17_notifiche_utente`. Cinque sorgenti (le prime quattro dalla `m17`, il sondaggio dalla `m24`), tutte in parallelo alla push esistente tranne i promemoria, la cui push la manda il worker (`notifiche_push_coda`): messaggio libero dell'admin (`notifica-personalizzata.ts`), un promemoria automatico prima di un evento — 24h e 3h prima, due `cron.schedule` (estensione `pg_cron`, abilitata il 31/07/2026 ma usata per la prima volta qui) — il turno palloni (`promemoria-palloni.ts`) e il sollecito presenze (`sollecita-presenze.ts`) e il sondaggio pre-partita (`apri-sondaggio.ts`, tipo `sondaggio_cacche`, `m24`), questi ultimi tre avviati a mano da un admin (il sollecito anche da un allenatore, DD-034). Volutamente non c'è una notifica sulla sola creazione di un evento: conta l'avvicinarsi della data, non il momento in cui è stato messo in calendario. I destinatari di un evento seguono la stessa convenzione di `convocatiEvento()`/`evento_permette_voto()` (M13): `convocati` vuoto = tutta la rosa attiva. I promemoria evento arrivano solo ai giocatori attivi: ai convocati, o a tutti se `convocati` è vuoto (`giocatori_destinatari_evento()`, riscritta da M21 per includere sempre gli allenatori e da M25 per escluderli, DD-039); il sollecito presenze (avviato da admin o allenatore) e il turno palloni non arrivano mai a loro. Letta/segnata come letta dal client con `useNotificheMie()`/`useSegnaLette()` (`src/lib/notifiche-utente.ts`), RLS-only, nessuna route API dedicata per queste due operazioni. Migration `m26_notifiche_automatiche` (DD-040): il vincolo `CHECK` sul tipo ammette anche `turno_palloni_12h`, `_6h`, `_3h`, `turno_palloni_revocato` e `sollecita_presenze_24h`, `_12h`, `_6h`, scritti dai job; `turno_palloni` e `sollecita_presenze` restano i tipi dei pulsanti. Il trigger delle email salta tutti i tipi `turno_palloni*`; quello delle push accoda i tipi automatici ma non i due dei pulsanti. | -| `notifiche_email_coda` | Coda delle email per le notifiche in-app: una riga per notifica, con stato d'invio. | Migration `m22_notifiche_email` (DD-036). Chiave `notifica_id` → `notifiche_utente(id)` con `ON DELETE CASCADE`. Stati: `in_coda`, `in_invio`, `inviata`, `fallita`, `saltata`; colonne `tentativi`, `prossimo_tentativo` (backoff), `errore` (solo codici SMTP), `inviata_il`, `aggiornata_il`. La riempie il trigger `notifiche_utente_accoda_email` (`AFTER INSERT OR UPDATE OF creato_il`): ogni notifica nuova e ogni rinvio via `upsert` finiscono in coda; le notifiche presenti prima della migration no. RLS attiva **senza policy** e permessi tolti a `anon`/`authenticated`: la usa solo il worker `mailer/` con la service role, tramite tre funzioni `SECURITY DEFINER` riservate a `service_role` — `prendi_notifiche_email(p_max)` (lotto con `FOR UPDATE SKIP LOCKED`, salta gli slot senza account o email o con le email spente, recupera le righe ferme `in_invio` da più di 10 minuti), `esito_notifica_email(...)` e `email_inviate_ultime_24h()`. Tabella separata da `notifiche_utente` perché la policy `UPDATE` del giocatore su quella riga gli permetterebbe di riportare a `in_coda` una mail già partita. Specifica in [notifiche-email.md](modules/notifiche-email.md). Con DD-040 (migration `m26`) il trigger salta i tipi del turno palloni, che non mandano email. | -| `preferenze_utente` | Preferenze del singolo account (non dello slot giocatore). | Migration `m22_notifiche_email`. Chiave `auth_user_id` → `auth.users(id)` con cascade; oggi una sola colonna, `email_notifiche` (default `true`), più `aggiornata_il`. **Nessuna riga = email attive**: la riga nasce solo quando il giocatore tocca l'interruttore «Email» in Profilo → Opzioni (upsert da `useEmailNotifiche()`, `src/lib/preferenze-utente.ts`). RLS: ognuno legge, inserisce e aggiorna solo la propria riga. | -| `promemoria_eventi_generati` | Registro dei promemoria evento (24 e 3 ore) già generati, per giocatore, evento e tipo. | Migration `m23_promemoria_gia_generati` (DD-037). Chiave `(giocatore_id, evento_id, tipo)` con `ON DELETE CASCADE` verso `giocatori_squadra` ed `eventi_app` (sparisce con l'evento, come le altre tabelle collegate, DD-029); `generato_il`. Scritta solo da `genera_promemoria_eventi()`, che crea la notifica in `notifiche_utente` soltanto per le righe appena registrate: eliminare la notifica dal centro notifiche non fa rigenerare il promemoria né accoda una nuova mail. RLS attiva **senza policy** e permessi tolti a `anon`/`authenticated`. La migration registra come già generati i promemoria presenti al momento dell'applicazione. Con DD-040 (migration `m26`) il `CHECK` sul tipo ammette anche i tipi dei palloni (12h, 6h, 3h, revocato) e dei solleciti (24h, 12h, 6h); è anche la memoria che permette la revoca. | -| `notifiche_push_coda` | Coda delle push per i promemoria a 24 e 3 ore: una riga per notifica, con stato d'invio. | Migration `m24_push_promemoria_e_sondaggio` (DD-038). Stessa struttura di `notifiche_email_coda`: chiave `notifica_id` → `notifiche_utente(id)` con `ON DELETE CASCADE`, stati `in_coda`, `in_invio`, `inviata`, `fallita`, `saltata`, `tentativi`, `prossimo_tentativo`, `errore` (solo codici HTTP), `inviata_il`, `aggiornata_il`. La riempie il trigger `notifiche_utente_accoda_push_promemoria` (`AFTER INSERT`, solo tipi `evento_promemoria_24h` e `evento_promemoria_3h`: le altre notifiche mandano la push dall'app; nessun backfill). Le usa solo il worker `mailer/` con la service role, tramite `prendi_push_promemoria(p_max)` (lotto con `FOR UPDATE SKIP LOCKED` con testo e dispositivi iscritti del giocatore; salta i giocatori senza dispositivi e i promemoria creati da oltre 3 ore, recupera le righe ferme `in_invio` da più di 10 minuti) ed `esito_push_promemoria(...)`, entrambe `SECURITY DEFINER` riservate a `service_role`. RLS attiva **senza policy** e permessi tolti a `anon`/`authenticated`. Con DD-040 (migration `m26`) accoda anche turno palloni e solleciti presenze. | +| Tabella | Scopo | Note | +| ---------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `turni_palloni` | Turni palloni **confermati**. | Gli allenamenti non ricevono proposta automatica (vedi [palloni.md](modules/palloni.md)). | +| `push_subscriptions` | Dispositivi registrati per le notifiche push. | | +| `promemoria_push` | **Non più usata.** | Serviva da coda del testo quando la push partiva vuota (DD-026). Da eliminare con una migration. | +| `notifiche_utente` | Centro notifiche in-app, con stato letto/non letto. | Vedi sotto. Letta e segnata come letta dal client con `useNotificheMie()`/`useSegnaLette()` (`src/lib/notifiche-utente.ts`), solo via RLS. | +| `notifiche_email_coda` | Coda delle email: una riga per notifica, con stato d'invio. | Vedi sotto. Specifica in [notifiche-email.md](modules/notifiche-email.md). | +| `notifiche_push_coda` | Coda delle push dei promemoria: una riga per notifica, con stato. | Vedi sotto. | +| `preferenze_utente` | Preferenze del singolo account (non dello slot giocatore). | Chiave `auth_user_id`. Oggi una colonna, `email_notifiche` (default `true`). **Nessuna riga = email attive**: la riga nasce solo quando il giocatore tocca l'interruttore in Profilo → Opzioni (`useEmailNotifiche()`). | +| `promemoria_eventi_generati` | Registro dei promemoria già generati, per giocatore, evento e tipo. | Chiave `(giocatore_id, evento_id, tipo)`, `ON DELETE CASCADE` verso giocatore ed evento (DD-029, DD-037). Eliminare la notifica non fa rigenerare il promemoria. È anche la memoria che permette la revoca (DD-040). | + +### `notifiche_utente` + +- Sorgenti: messaggio libero dell'admin, promemoria evento automatici, turno palloni, sollecito + presenze e sondaggio pre-partita (`sondaggio_cacche`). Dettaglio e testi in + [modules/notifiche.md](modules/notifiche.md). +- Non c'è una notifica alla sola creazione di un evento: conta l'avvicinarsi della data. +- Destinatari di un evento (`giocatori_destinatari_evento()`): giocatori attivi, i convocati o tutta + la rosa se `convocati` è vuoto; mai gli allenatori (DD-039). +- Il `CHECK` sul tipo ammette anche `turno_palloni_12h`, `_6h`, `_3h`, `turno_palloni_revocato` e + `sollecita_presenze_24h`, `_12h`, `_6h`, scritti dai job (DD-040). `turno_palloni` e + `sollecita_presenze` restano i tipi dei pulsanti manuali. +- Il trigger delle email salta tutti i tipi `turno_palloni*`; quello delle push accoda i tipi + automatici ma non quelli dei pulsanti. + +### Code di invio (`notifiche_email_coda`, `notifiche_push_coda`) + +Stessa struttura: chiave `notifica_id` → `notifiche_utente(id)` con `ON DELETE CASCADE`; stati +`in_coda`, `in_invio`, `inviata`, `fallita`, `saltata`; colonne `tentativi`, `prossimo_tentativo` +(backoff), `errore` (solo codici SMTP per l'email, HTTP per la push), `inviata_il`, `aggiornata_il`. + +- Sono riempite da trigger su `notifiche_utente` (`accoda_notifica_email`, `accoda_push_promemoria`) + per le notifiche create dopo le rispettive migration, senza backfill. +- Le usa solo il worker `mailer/` con la service role, tramite funzioni `SECURITY DEFINER` + riservate a `service_role`: `prendi_notifiche_email(p_max)`, `esito_notifica_email(...)`, + `email_inviate_ultime_24h()`, `prendi_push_promemoria(p_max)`, `esito_push_promemoria(...)`. + I lotti usano `FOR UPDATE SKIP LOCKED` e recuperano le righe ferme in `in_invio` da più di 10 minuti. +- RLS attiva **senza policy** e permessi tolti a `anon`/`authenticated`. +- Sono tabelle separate da `notifiche_utente` perché la policy `UPDATE` del giocatore su quella riga + gli permetterebbe di riportare a `in_coda` una mail già partita (DD-036). +- Il worker salta gli slot senza account o email, o con le email spente; per le push salta chi non + ha dispositivi e i promemoria creati da oltre 3 ore. ### Funzioni SQL e job delle notifiche @@ -102,12 +161,6 @@ riservate alla service role (anon e utenti autenticati non possono chiamarle). T | `giocatori_destinatari_evento(evento)` (M17, M21, M25) | Giocatori attivi destinatari: i convocati, o tutta la rosa se `convocati` è vuoto; mai gli allenatori. | — | | `accoda_notifica_email()`, `accoda_push_promemoria()` (M22, M24, M26) | Trigger su `notifiche_utente`: la mail salta i tipi `turno_palloni*`; la push accoda i tipi automatici (non quelli dei pulsanti). | — | -## Funzioni speciali - -| Tabella | Scopo | Note | -| ---------------- | --------------------- | -------------------------------------- | -| `cacche_partita` | Sondaggio prepartita. | Usato per statistiche e badge segreti. | - ## Badge Non esiste una tabella dedicata: i badge vengono **calcolati a runtime** dall'applicazione a diff --git a/docs/DESIGN_DECISIONS.md b/docs/DESIGN_DECISIONS.md deleted file mode 100644 index 2900f0f..0000000 --- a/docs/DESIGN_DECISIONS.md +++ /dev/null @@ -1,1860 +0,0 @@ -# Registro delle decisioni di progetto - -Questo documento raccoglie le **decisioni importanti** prese nel corso della vita di CrAPP: scelte che hanno influito sulla direzione del prodotto, sull’organizzazione del lavoro o su come l’app si evolve nel tempo. - -Non descrive _come_ è fatto il codice. Per quello esistono `ARCHITECTURE.md` e `DATABASE.md`. - -Serve a rispondere a domande del tipo: - -- _Perché abbiamo scelto così?_ -- _Cosa avevamo escluso e perché?_ -- _Quando conviene riaprire una decisione?_ - ---- - -## Indice - -**Accettate** - -| ID | Titolo | -| ------------------------------------------------------------------------------------------------ | ------------------------------------- | -| [DD-001](#dd-001--crapp-deve-restare-indipendente-da-lovable) | Indipendenza da Lovable | -| [DD-002](#dd-002--sviluppo-document-first) | Sviluppo document-first | -| [DD-004](#dd-004--ogni-versione-aggiunge-non-riscrive) | Ogni versione aggiunge, non riscrive | -| [DD-005](#dd-005--mobile-first-pochi-click-pochi-schermi) | Mobile-first | -| [DD-006](#dd-006--intelligenza-artificiale-solo-se-porta-beneficio-reale) | AI solo se utile | -| [DD-007](#dd-007--badge-calcolati-dallapp-non-salvati-nel-database) | Badge calcolati, non in DB | -| [DD-008](#dd-008--gamification-equa-tra-ruoli) | Gamification equa tra ruoli | -| [DD-009](#dd-009--tesseramento-csi-manuale-integrazione-api-in-una-fase-successiva) | CSI manuale, poi integrazione API | -| [DD-010](#dd-010--profilo-giocatore-niente-storico-certificati) | Niente storico certificati | -| [DD-011](#dd-011--autenticazione-reale-prima-del-profilo-amministrativo-completo) | Auth reale prima del profilo | -| [DD-012](#dd-012--rimandare-la-migrazione-degli-id-giocatore) | Rimandare la migrazione ID | -| [DD-013](#dd-013--portabilità-lapp-non-deve-dipendere-da-servizi-esclusivi) | Portabilità dello stack | -| [DD-015](#dd-015--rosa-anagrafica-da-codice-hardcoded-a-database) | Rosa da hardcoded a DB | -| [DD-016](#dd-016--schema-dati-profilo-giocatore-f0) | Schema dati Profilo Giocatore | -| [DD-017](#dd-017--lamministratore-può-compilare-i-dati-al-posto-del-giocatore) | L'admin scrive al posto del giocatore | -| [DD-018](#dd-018--collegamento-automatico-giocatoreaccount-per-email) | Collegamento automatico per email | -| [DD-019](#dd-019--il-branch-dei-commit-lo-decide-lutente) | Il branch lo decide l'utente | -| [DD-020](#dd-020--una-funzione-modificata-senza-test-non-è-finita) | Test obbligatori e verdi | -| [DD-021](#dd-021--molle-interrompibili-al-posto-delle-animazioni-a-durata-fissa) | Molle interrompibili con motion | -| [DD-022](#dd-022--lapp-è-solo-chiara) | App solo chiara | -| [DD-023](#dd-023--ogni-scrittura-è-limitata-a-chi-la-fa) | Scritture limitate per ruolo | -| [DD-024](#dd-024--le-route-che-avvisano-la-squadra-chiedono-le-credenziali) | Route di notifica autenticate | -| [DD-025](#dd-025--il-promemoria-palloni-lo-manda-ladmin-per-un-evento) | Promemoria palloni manuale | -| [DD-026](#dd-026--il-testo-della-notifica-viaggia-dentro-la-push) | Payload push cifrato | -| [DD-027](#dd-027--chi-vota-deve-essere-convocato-non-solo-autenticato-come-sé-stesso) | Voto limitato ai convocati | -| [DD-028](#dd-028--soglia-minima-di-campione-per-media-voto-e-mvp-in-home) | Soglia minima Media voto e MVP | -| [DD-029](#dd-029--cancellare-un-evento-pulisce-a-cascata-i-dati-collegati) | Pulizia a cascata evento cancellato | -| [DD-030](#dd-030--centro-notifiche-in-app-storico-separato-dalla-push-letto-solo-con-rls) | Centro notifiche in-app | -| [DD-031](#dd-031--data-di-nascita-pubblica-una-colonna-sincronizzata-non-una-seconda-rls-aperta) | Nascita pubblica sincronizzata | -| [DD-032](#dd-032--la-stagione-csi-si-ricava-dalle-date-delle-gare) | Stagione CSI dalle date delle gare | -| [DD-033](#dd-033--gestione-eventi-solo-calendario-giorno-fissato-dal-tocco) | Gestione eventi solo da calendario | -| [DD-034](#dd-034--lallenatore-è-uno-slot-della-squadra-con-tipo-diverso-non-un-giocatore) | Ruolo allenatore | -| [DD-035](#dd-035--avviso-certificati-calcolato-in-home-visibile-solo-al-titolare-e-agli-admin) | Avviso certificati in Home | -| [DD-036](#dd-036--notifiche-email-via-gmail-da-un-worker-docker-sullhost-di-casa) | Notifiche email via Gmail | -| [DD-037](#dd-037--i-promemoria-evento-ricordano-di-essere-già-stati-generati) | Promemoria evento senza ritorni | -| [DD-038](#dd-038--ogni-notifica-ha-tre-canali-la-push-dei-promemoria-parte-dal-worker) | Tre canali per ogni notifica | -| [DD-039](#dd-039--lallenatore-non-riceve-i-promemoria-degli-eventi) | Niente promemoria all'allenatore | -| [DD-040](#dd-040--promemoria-con-oggi-o-domani-turno-palloni-e-solleciti-automatici) | Palloni e solleciti automatici | - -**In valutazione** - -| ID | Titolo | -| ---------------------------------------------------------------- | --------------------- | -| [DD-014](#dd-014--convergenza-schema-database-eventi-e-presenze) | Convergenza schema DB | - -**Sostituite** - -| ID | Titolo | -| ---------------------------------------------------------------- | --------------------- | -| [DD-003](#dd-003--due-branch-main-stabile-develop-per-il-lavoro) | Branch main / develop | - ---- - -## Come usare questo registro - -Ogni decisione segue lo stesso schema: - -| Campo | Significato | -| ------------------------ | ------------------------------------------------------ | -| **Data** | Quando la decisione è stata presa o confermata | -| **Stato** | Accettata · In valutazione · Sostituita · Obsoleta | -| **Contesto** | Quale problema o opportunità avevamo di fronte | -| **Decisione** | Cosa abbiamo scelto di fare | -| **Alternative scartate** | Cosa non abbiamo fatto e perché | -| **Conseguenze** | Cosa comporta nel quotidiano (utenti, admin, sviluppo) | -| **Riesame** | Quando ha senso riconsiderarla | - -**Quando aggiungere una voce** - -- una scelta influisce su più moduli o su più release; -- escludiamo un’alternativa non ovvia; -- accettiamo un compromesso consapevole (debito, limitazione, ritardo); -- cambiamo una decisione precedente. - -**Quando non serve** - -- dettagli implementativi locali; -- scelte estetiche minori; -- bugfix o correzioni puntuali. - -**Come registrare una nuova decisione** - -Copiare [`_template-dd.md`](_template-dd.md) in fondo al documento, assegnare il primo ID -libero e aggiungerlo all'indice. - ---- - -## Decisioni accettate - ---- - -### DD-001 — CrAPP deve restare indipendente da Lovable - -**Data:** luglio 2026 -**Stato:** Accettata - -**Contesto** -Il progetto nasce come prototipo su Lovable Cloud. Per crescere serve controllo su codice, deploy, database e costi. - -**Decisione** -Spostare lo sviluppo su repository GitHub indipendente, con deploy su Vercel e database Supabase gestito dal team. - -**Alternative scartate** - -- Restare su Lovable come unica piattaforma → troppa dipendenza da un servizio esterno. -- Riscrivere tutto da zero → costo e rischio inutili; il prototipo funzionava già. - -**Conseguenze** - -- Maggiore libertà e responsabilità per il team. -- Restano tracce del passaggio (dipendenze, meta tag): vanno eliminate gradualmente, non in blocco. -- L’app deve poter girare anche fuori dall’ecosistema Lovable (vedi `PORTABILITA.md`). - -**Riesame** -Quando il progetto non userà più alcun componente Lovable. - ---- - -### DD-002 — Sviluppo document-first - -**Data:** agosto 2026 -**Stato:** Accettata - -**Contesto** -Con più persone (e assistenti AI) che lavorano sul codice, serviva un modo per evitare funzionalità “inventate” al volo e incoerenze tra moduli. - -**Decisione** -Ogni nuova funzionalità significativa viene prima **progettata e documentata** in `docs/modules/`, poi implementata. Il flusso ufficiale è: idea → progettazione → documentazione → database → codice → test → release. - -**Alternative scartate** - -- Documentare solo a posteriori → troppo spesso incompleto o assente. -- Affidarsi solo al codice come documentazione → illeggibile per chi non programma. - -**Conseguenze** - -- Rallenta leggermente l’avvio di nuove feature, ma riduce rework e discussioni infinite. -- I moduli preesistenti vanno retro-documentati quando possibile. -- Nessuna feature non documentata entra in produzione. - -**Riesame** -Se il team diventa molto piccolo e la documentazione smette di essere consultata. - ---- - -### DD-003 — Due branch: main stabile, develop per il lavoro - -**Data:** agosto 2026 -**Stato:** Sostituita da [DD-019](#dd-019--il-branch-dei-commit-lo-decide-lutente) (settembre 2026) - -**Contesto** -Serve separare ciò che i giocatori usano ogni giorno da ciò che è ancora in prova. - -**Decisione** - -- `main` → produzione, sempre funzionante, deploy automatico. -- `develop` → sviluppo e preview, merge su `main` solo dopo test. - -**Alternative scartate** - -- Sviluppare direttamente su `main` → rischio di rotture in produzione. -- Branch per ogni feature → eccessivo per la dimensione attuale del team. - -**Conseguenze** - -- Gli utenti in produzione non vedono lavori incompleti. -- Ogni release su `main` deve includere verifica delle funzionalità esistenti. - -**Riesame** -Sostituita: nella pratica il lavoro è finito direttamente su `main` e `develop` è rimasto -indietro. Vedi DD-019. - ---- - -### DD-004 — Ogni versione aggiunge, non riscrive - -**Data:** agosto 2026 -**Stato:** Accettata - -**Contesto** -CrAPP è già usata dalla squadra per presenze, calendario, scout, badge e notifiche. Rischiare regressioni su moduli funzionanti vanifica la fiducia degli utenti. - -**Decisione** -Le nuove versioni **introducono** funzionalità. Non si riscrive un modulo già operativo salvo richiesta esplicita e pianificata. - -**Alternative scartate** - -- Refactoring ampio “per pulire” insieme a ogni release → alto rischio, poco valore immediato per gli utenti. - -**Conseguenze** - -- Coesistono temporaneamente soluzioni vecchie e nuove (es. dati hardcoded accanto a tabelle database). -- Il debito tecnico va gestito con migration dedicate, non di nascosto. - -**Riesame** -Quando un modulo diventa ingestibile o blocca una release importante. - ---- - -### DD-005 — Mobile-first, pochi click, pochi schermi - -**Data:** origine progetto -**Stato:** Accettata - -**Contesto** -I giocatori usano l’app soprattutto da smartphone, spesso in spogliatoio o in palestra, con poco tempo e poca pazienza. - -**Decisione** -Interfaccia semplice, veloce, ottimizzata per telefono. Navigazione ridotta (barra inferiore). Ogni schermata deve avere uno scopo chiaro. - -**Alternative scartate** - -- Layout da desktop con menu complessi → scomodo in mobilità. -- App nativa iOS/Android → costi e tempi di pubblicazione non giustificati per una squadra amatoriale. - -**Conseguenze** - -- Funzionalità amministrative complesse vanno semplificate o suddivise con cura. -- La PWA è la forma giusta per questo pubblico. - -**Riesame** -Se emergono esigenze desktop forti (es. gestione documenti massiva solo da PC). - ---- - -### DD-006 — Intelligenza artificiale solo se porta beneficio reale - -**Data:** origine progetto -**Stato:** Accettata - -**Contesto** -L’AI è attraente ma può complicare l’app, aumentare i costi e creare aspettative irrealistiche. - -**Decisione** -Usare l’AI solo quando riduce lavoro agli admin o migliora concretamente l’esperienza dei giocatori. Non introdurla “perché si può”. - -**Alternative scartate** - -- AI ovunque (chatbot, suggerimenti automatici, analisi predittive) → fuori focus per una squadra amatoriale. - -**Conseguenze** - -- “AI Allenamenti” è tra le voci ancora da fare in roadmap. -- Ogni proposta AI va valutata con la domanda: _chi risparmia tempo e quanto?_ - -**Riesame** -Quando l’AI diventa economica e affidabile per casi d’uso chiari (es. generazione allenamenti). - ---- - -### DD-007 — Badge calcolati dall’app, non salvati nel database - -**Data:** origine progetto -**Stato:** Accettata - -**Contesto** -I badge dipendono da statistiche già disponibili (presenze, MVP, cacche, ecc.). Salvare ogni badge sbloccato nel database aggiungerebbe complessità senza beneficio immediato. - -**Decisione** -I badge vengono **calcolati al volo** dall’applicazione in base ai dati esistenti. Non esiste una tabella badge dedicata. - -**Alternative scartate** - -- Tabella `badge_sbloccati` con storico → utile in futuro per notifiche retroattive o audit, ma non necessaria ora. - -**Conseguenze** - -- Meno migration e meno sincronizzazione. -- Lo “sblocco” celebrativo usa cache locale per non ripetere animazioni. -- Un eventuale storico badge richiederà una nuova decisione. - -**Riesame** -Se servono badge manuali assegnati dagli admin o storico immutabile. - ---- - -### DD-008 — Gamification equa tra ruoli - -**Data:** origine progetto -**Stato:** Accettata - -**Contesto** -In pallavolo i ruoli hanno statistiche diverse (un libero non segna punti d’attacco). Confrontare tutti sugli stessi numeri sarebbe ingiusto e scoraggiante. - -**Decisione** -Le statistiche **personali** in profilo e squadra devono essere **eque per tutti i ruoli**. Dati tecnici di reparto (punti, ace, muri) restano nello Scout Live come informazione di squadra, non come leva competitiva individuale. - -**Alternative scartate** - -- Classifiche individuali basate su punti → penalizza libero, palleggiatore, centrale. - -**Conseguenze** - -- Badge e obiettivi usano presenze, MVP, pagelle, serie, cacche — metriche accessibili a tutti. -- Lo scout resta strumento tecnico, non gioco. - -**Riesame** -Se la squadra chiede esplicitamente classifiche tecniche per ruolo. - ---- - -### DD-009 — Tesseramento CSI manuale, integrazione API in una fase successiva - -**Data:** agosto 2026 -**Stato:** Accettata - -**Contesto** -Il modulo Profilo Giocatore deve aiutare gli admin a raccogliere documenti e dati per il tesseramento CSI. Un collegamento automatico al sistema CSI è complesso e non urgente. - -**Decisione** - -- **Prima fase:** profilo completo, dashboard admin, download documenti, export CSV con i campi richiesti dal CSI. -- **Fase successiva:** eventuale collegamento automatico a CSI (calendario, risultati, classifica ufficiale). - -**Alternative scartate** - -- Integrazione CSI già nella prima fase → scope troppo ampio, dipendenza da API esterne non controllate. - -**Conseguenze** - -- Gli admin guadagnano subito tempo (niente più Excel e chat per i documenti). -- L’export CSV deve essere affidabile e completo: è il deliverable chiave di questa prima fase. - -**Riesame** -Quando il CSI mette a disposizione API stabili o quando il volume di tesseramenti giustifica l’automazione. - ---- - -### DD-010 — Profilo giocatore: niente storico certificati - -**Data:** agosto 2026 -**Stato:** Accettata - -**Contesto** -Il certificato medico va aggiornato ogni stagione. Tenere lo storico di tutte le versioni complica upload, storage e privacy. - -**Decisione** -Il giocatore può **sovrascrivere** certificato e data di scadenza. Lo storico delle versioni precedenti non viene conservato. - -**Alternative scartate** - -- Archivio certificati → utile per audit, rinviato a versioni future. - -**Conseguenze** - -- Implementazione più semplice e veloce. -- Gli admin vedono solo il certificato attuale. -- Va comunicato chiaramente ai giocatori che sostituire il file elimina quello precedente. - -**Riesame** -Se il CSI o il regolamento interno richiedono conservazione storica. - ---- - -### DD-011 — Autenticazione reale prima del profilo amministrativo completo - -**Data:** agosto 2026 -**Stato:** Accettata - -**Contesto** -Oggi l’app identifica l’utente con la selezione del giocatore da una lista, senza login. Documenti, certificati e dati personali richiedono sapere _chi_ sta operando e impedire accessi non autorizzati. - -**Decisione** -Prima di completare il modulo Profilo Giocatore, introdurre **login con Google o email** tramite Supabase Auth — non tramite Lovable Auth. Dopo il login, il giocatore associa il proprio profilo squadra. - -**Alternative scartate** - -- Continuare solo con selezione da lista → inaccettabile per dati sensibili. -- Lovable Auth → crea dipendenza da piattaforma che stiamo abbandonando. - -**Conseguenze** - -- Tutti dovranno fare login almeno una volta. -- Gli admin useranno ruoli veri (`user_roles`), non una lista di nomi hardcoded. -- È prerequisito per dashboard admin e export CSI. - -**Riesame** -Dopo il rollout auth, se emergono problemi di adozione (giocatori poco digitali). - ---- - -### DD-012 — Rimandare la migrazione degli ID giocatore - -**Data:** agosto 2026 -**Stato:** Accettata - -**Contesto** -L’app usa identificativi semplici (`g1`, `g2`, …) collegati a presenze, voti, palloni e altre funzioni già in uso. Nel database esiste anche una tabella `giocatori` con UUID, non collegata al codice attuale. - -**Decisione** -Per ora **non** unificare gli ID. I nuovi dati del profilo si agganciano agli identificativi già in uso. La migrazione verso UUID resta un lavoro separato, pianificato e testato. - -**Alternative scartate** - -- Migrare subito tutto a UUID → rischio alto di rompere presenze, voti, scout e notifiche. - -**Conseguenze** - -- Coesistono due modelli anagrafici fino a migration dedicata. -- `DATABASE.md` va tenuto aggiornato su cosa è “attivo” e cosa è “futuro”. - -**Riesame** -Quando il modulo Profilo Giocatore è stabile e c’è tempo per una migration con checklist regressioni completa. - ---- - -### DD-013 — Portabilità: l’app non deve dipendere da servizi esclusivi - -**Data:** luglio 2026 -**Stato:** Accettata - -**Contesto** -La squadra potrebbe voler cambiare hosting, database o fornitore auth in futuro. - -**Decisione** -CrAPP deve poter girare su **Node.js + PostgreSQL standard**. Niente funzionalità bloccate su servizi proprietari. I dati si accedono solo tramite moduli in `src/lib/`, non direttamente dai componenti. - -**Alternative scartate** - -- Accettare lock-in per velocità → contrario alla lunga vita del progetto. - -**Conseguenze** - -- Supabase va bene perché è PostgreSQL e self-hostable. -- Le API push e i job restano endpoint HTTP richiamabili da qualsiasi scheduler. - -**Riesame** -Se si adotta un servizio che viola questa regola. - ---- - -### DD-016 — Schema dati Profilo Giocatore (F0) - -**Data:** agosto 2026 -**Stato:** Accettata - -**Contesto** -La progettazione F0 del modulo Profilo Giocatore ha definito come persistere dati personali, documenti e certificati, in coesistenza con l’anagrafica attuale (`g1`…`g17` nel codice) e con la tabella `giocatori` UUID già presente ma non usata. Serviva una scelta chiara su dove salvare i dati, come collegare l’autenticazione e come proteggere documenti sensibili — senza toccare le tabelle preesistenti già operative. - -**Decisione** -Si introducono **due nuove tabelle additive**: - -- **`giocatori_squadra`** — anagrafica squadra con ID testuali (`g1`…`g17`), dati gestiti dagli admin (nome, cognome, numero, ruolo) e collegamento account (`auth_user_id`). -- **`profili_giocatore`** — dati personali, metadati documento identità, certificato medico e path dei file, in relazione 1:1 con `giocatori_squadra`. - -Regole vincolanti: - -1. **`giocatori_squadra` diventa progressivamente la source of truth** per l’anagrafica squadra. Durante la transizione, `crapp-data.ts` resta come **fallback** se il database non è disponibile o i dati non sono ancora migrati. -2. L’associazione **`auth_user_id` ↔ giocatore** è un’operazione **controllata e atomica** (es. al primo accesso da `/benvenuto`, con `UPDATE … WHERE auth_user_id IS NULL`). Il giocatore **non può modificare liberamente** `auth_user_id`; solo un admin può resettarlo in casi eccezionali. -3. I file (documento identità, certificato, foto tessera) vivono nel bucket Storage **`profili-giocatore`**, configurato come **privato**. -4. Documenti personali e sanitari **non devono mai essere esposti tramite URL pubblici**. Accesso solo tramite client autenticato con policy RLS, o signed URL a scadenza breve per download admin. -5. Le **tabelle preesistenti non vengono modificate** (`eventi_app`, `risposte_presenze`, voti, palloni, scout, push, ecc.). Il profilo si aggancia agli ID `g1`…`g17` già in uso, senza migrare verso UUID per ora (coerente con DD-012). -6. La tabella `giocatori` (UUID) resta **invariata e non usata** dal modulo profilo. - -**Alternative scartate** - -- Estendere la tabella `giocatori` UUID → conflitto con ID operativi del codice e rischio di regressioni. -- Salvare file come base64 nel database → ingestibile, difficile da gestire e da scaricare. -- Bucket pubblico con URL permanenti → inaccettabile per dati sanitari e documenti d’identità. -- Permettere al giocatore di cambiare `auth_user_id` liberamente → rischio di impersonazione e race condition. -- Modificare le tabelle preesistenti per aggiungere FK verso il profilo → viola DD-004 e DD-012. - -**Conseguenze** - -- Coesistono temporaneamente tre rappresentazioni dell’anagrafica: `crapp-data.ts` (fallback), `giocatori_squadra` (target), `giocatori` UUID (dormiente). -- `src/lib/rosa.ts` legge dal database e ricade su `crapp-data.ts` in caso di errore o assenza dati (DD-015). -- Il completamento profilo (30/30/30/10) si calcola in app, non si persiste nel database. -- Lo storico certificati non viene conservato (coerente con DD-010). -- Le migration M1–M2 (tabelle, RLS, bucket) restano **additive**: solo `CREATE`, nessun `ALTER`/`DROP` su schema esistente. -- Raffina e attua quanto proposto in DD-015 per la rosa anagrafica, senza sostituire formalmente quella voce. - -**Riesame** - -- Quando `giocatori_squadra` è stabile in produzione e il fallback `crapp-data.ts` non serve più. -- Quando si pianifica la convergenza verso UUID (DD-012, dopo che il profilo sarà stabile). -- Se il CSI o il regolamento richiedono conservazione storica documenti o consensi privacy dedicati. - ---- - -### DD-017 — L'amministratore può compilare i dati al posto del giocatore - -**Data:** agosto 2026 -**Stato:** Accettata - -**Contesto** -Il modulo Profilo Giocatore era costruito su un confine netto: ognuno scrive solo la propria riga, l'amministratore legge e scarica. Nella pratica quel confine blocca il lavoro che il modulo doveva togliere: se metà squadra non compila i propri dati, l'export per il tesseramento CSI resta incompleto e l'admin torna a chiedere le informazioni in chat — esattamente ciò che CrAPP deve eliminare. Inoltre le docs assegnavano già agli admin la gestione dei dati squadra (nome, cognome, numero, ruolo) e il reset del collegamento all'account (DD-016 regola 2), senza che esistesse una schermata per farlo. - -**Decisione** -Dalla dashboard amministratore, un admin può: - -1. modificare i **dati squadra** di qualsiasi giocatore (nome, cognome, numero, ruolo); -2. compilare e correggere i **dati personali e del documento** di qualsiasi giocatore; -3. **scollegare** un account da un profilo, liberando lo slot. - -Restano fuori, e non cambiano: - -- i **file** (documento, certificato, foto): l'admin li scarica ma non li carica né li sostituisce. Un documento d'identità lo produce il suo titolare, e la catena di responsabilità deve restare leggibile; -- il **giocatore**, che continua a non poter toccare i propri dati squadra. - -**Alternative scartate** - -- Lasciare tutto al giocatore → l'export CSI resta incompleto e il lavoro amministrativo torna in chat, contro la missione del progetto. -- Dare all'admin anche l'upload dei file → confonde chi ha fornito un documento, su dati sanitari e d'identità dove serve il contrario. -- Un ruolo intermedio (segreteria) per i soli dati personali → un ruolo in più per una squadra sola, con gli stessi tre amministratori di adesso. - -**Conseguenze** - -- Il modello dei permessi non è più "ognuno i suoi": è "ognuno i suoi, più l'admin su tutti, tranne i file". Le policy RLS di M1 e M2 lo consentivano già, quindi non servono migration. -- Un admin può correggere un errore di battitura in un numero di documento senza inseguire il giocatore. -- Un admin vede e scrive dati personali altrui: è un potere reale, dato a tre persone su diciassette. Va assegnato con la stessa cura di prima (una riga in `user_roles`, nessuna auto-promozione). -- Il completamento del profilo smette di essere un indicatore di _chi ha risposto_ e diventa un indicatore di _quali dati mancano_, chiunque li abbia inseriti. - -**Riesame** - -- Se la squadra cresce al punto da rendere sensato un ruolo di sola segreteria. -- Se serve tracciare _chi_ ha modificato un dato: oggi non c'è audit, e con la scrittura condivisa la domanda prima o poi arriva. - ---- - -### DD-018 — Collegamento automatico giocatore↔account per email - -**Data:** settembre 2026 -**Stato:** Accettata - -**Contesto** -DD-016 regola 2 prevedeva che, al primo accesso, il giocatore scegliesse manualmente il proprio slot libero da un elenco (`/benvenuto`). In pratica ogni giocatore ha un'email nota (o presto nota), quindi far scegliere un nome da una lista è un passaggio superfluo e un rischio: un giocatore può selezionare per errore lo slot di un compagno, e nulla nel flusso lo impedisce a livello di prodotto. - -**Decisione** -Al primo accesso, `giocatori_squadra` viene interrogata per email (case-insensitive, tramite la nuova colonna `email`) invece di mostrare un elenco di slot liberi. Se l'email dell'account Google corrisponde a una riga libera, il collegamento avviene automaticamente. Se non corrisponde a nessuna riga (email non ancora nota, o nessun profilo per quella persona), l'utente vede solo un messaggio d'errore che invita a contattare un amministratore, con un pulsante per uscire e riprovare con un altro account — nessuna selezione manuale di ripiego. Le email sono popolate via migration (`m5_email_giocatori_squadra`) per la rosa iniziale; un'interfaccia in `/admin` per impostarle su nuovi giocatori è arrivata poco dopo (vedi "Alternative scartate"). Il trigger `enforce_giocatori_squadra_update` (DD-016) viene esteso per richiedere anche la corrispondenza email, non solo lo slot libero: il vincolo resta nel database, non solo nella UI. - -**Alternative scartate** - -- Mantenere la selezione manuale come ripiego quando l'email non trova corrispondenza → scartata: vanificherebbe la garanzia "ognuno collega solo il proprio profilo" e reintrodurrebbe il rischio di scelta errata che questa decisione vuole eliminare. -- Un'interfaccia admin per scrivere l'email dei giocatori → rimandata al momento della decisione, poi implementata nel form "Aggiungi giocatore" di `/admin` (`src/routes/admin.tsx`): serviva per collegare i giocatori aggiunti a metà stagione senza passare da una nuova migration ogni volta. - -**Conseguenze** - -- Le righe senza email nota restano bloccate — nessuno può collegarle, nemmeno per errore — finché un admin non la imposta da `/admin` (o, per la rosa iniziale, una migration). Da settembre 2026 tutta la rosa attiva ha l'email registrata. -- `slotLiberi` (funzione ed elenco "slot liberi" in `/benvenuto`) è stato rimosso: non aveva più chiamanti in produzione dopo il cambio. -- Un utente che accede con l'account Google sbagliato resta bloccato su `/benvenuto` finché non esce e riprova con l'account giusto. - -**Riesame** - -- Se in futuro serve un'assistenza admin diretta dal flusso di login invece che da `/admin`. - ---- - -## Decisioni in valutazione - ---- - -### DD-014 — Convergenza schema database (eventi e presenze) - -**Data:** — -**Stato:** In valutazione - -**Contesto** -Esistono due modelli paralleli: tabelle “legacy” usate dall’app (`eventi_app`, `risposte_presenze`) e tabelle “nuove” con autenticazione e vincoli (`eventi`, `presenze`, `giocatori` UUID). - -**Decisione proposta** -Unificare gradualmente sul modello autenticato, dopo auth e profilo stabili. - -**Perché non ora** -Rischio regressioni su calendario e presenze, moduli più usati della squadra. - -**Riesame previsto** -Dopo che il modulo Profilo Giocatore sarà stabile, con migration e test dedicati. - ---- - -### DD-015 — Rosa anagrafica: da codice hardcoded a database - -**Data:** 3 settembre 2026 -**Stato:** Accettata - -**Contesto** -La lista giocatori viveva nel codice sorgente (`src/lib/crapp-data.ts`). Il database aveva già -`giocatori_squadra` (migration M1) popolata ma non letta da nessuna schermata tranne -`/benvenuto` e `/admin`: «Aggiungi giocatore» e «Disattiva giocatore» della dashboard non -avevano effetto su Squadra, Presenze, Pagelle, Badge e Scout, mantenendo gli stessi ID finché -non si farà DD-012. - -**Decisione** -`useRosa()` (e con lei `useIo`, `useObiettivi`) legge ora `giocatori_squadra` tramite -`useGiocatoriSquadra()`, filtrando solo i giocatori `attivo`. Tutti i punti che prima -importavano la lista statica (`convocatiEvento`, `compleanniEventi`, `completaTurni`, -`csvScoutMatch`, i widget di voto/scout/palloni, le due route API che mandano push) sono stati -agganciati allo stesso hook o, lato server, a `leggiGiocatoriSquadra()` -(`src/lib/giocatori-squadra.server.ts`, stesso pattern di `eventi.server.ts`). - -**Conseguenze** - -- Un giocatore aggiunto o disattivato dalla dashboard admin ora si riflette ovunque, non solo - in `/benvenuto` e `/admin`. -- `giocatori_squadra` non ha ancora una colonna per la data di nascita: per i 17 giocatori - storici resta quella di `crapp-data.ts` (`nascitaPerId`, lookup per id); un giocatore - aggiunto dopo la migrazione non ha nascita nota finché la colonna non esiste. Follow-up da - aprire quando serve davvero. -- `src/lib/crapp-data.ts` resta come seed storico e fallback (`rosaFallback()`), non più come - fonte viva. - ---- - -### DD-019 — Il branch dei commit lo decide l'utente - -**Data:** 4 settembre 2026 -**Stato:** Accettata — sostituisce [DD-003](#dd-003--due-branch-main-stabile-develop-per-il-lavoro) - -**Contesto** -DD-003 prevedeva di lavorare su `develop` e portare su `main` solo dopo i test. Nella pratica -è successo il contrario: `main` è arrivato a 43 commit di vantaggio su `develop`, che è rimasto -fermo. Una regola che nessuno segue è peggio di nessuna regola, perché rende inaffidabile tutto -il resto del documento — e con più assistenti AI in gioco il rischio vero non era il branch -sbagliato, ma un agente che committa o pusha per conto suo. - -**Decisione** -È l'utente a dire su quale branch va un commit. L'assistente può **consigliare** un branch -dedicato quando la modifica è rischiosa o parallela ad altro lavoro, ma non cambia branch, non -committa, non fa push e non apre PR di propria iniziativa. In assenza di indicazioni si lavora -dove si trova il repository, di fatto `main`. - -**Alternative scartate** - -- Tenere DD-003 e riallineare `develop` → si sarebbe rotta di nuovo alla prima fretta. -- Dismettere `develop` → si perderebbero le preview Vercel, utili quando servono davvero. - -**Conseguenze** - -- `main` è insieme produzione e branch di lavoro: ogni commit deve lasciare l'app funzionante, - quindi la rete di sicurezza sono i test (vedi DD-020), non il branch. -- `develop` esiste ancora ma è indietro: la sua preview Vercel non rappresenta lo stato attuale - finché non viene riallineata. - -**Riesame** -Se il team cresce oltre una persona che scrive codice, o se un lavoro lungo ha bisogno di stare -fuori produzione per più di qualche giorno. - ---- - -### DD-020 — Una funzione modificata senza test non è finita - -**Data:** 4 settembre 2026 -**Stato:** Accettata - -**Contesto** -Con `main` come branch di lavoro (DD-019) non c'è più un ambiente di prova tra il codice e i -giocatori. La suite in `test/` esisteva già ma scriverla era di fatto facoltativo, e i difetti -trovati dai test sono arrivati a posteriori (la sessione Scout Live che non scadeva mai, le -serie di presenze ferme a zero per settimane). - -**Decisione** -Chi aggiunge o modifica una funzione scrive o aggiorna il test nello stesso lavoro, e i test -devono essere verdi prima di consegnare. Non si commenta un test che fallisce né si indebolisce -un'asserzione per farla passare: se il comportamento voluto è cambiato, si aggiorna il test -dicendo perché. - -**Alternative scartate** - -- Test solo sui moduli critici → il confine «critico» si sposta a ogni fretta. -- Introdurre un framework di test → la suite bun con `node:assert` funziona e non aggiunge - dipendenze (vedi [test/README.md](../test/README.md)). - -**Conseguenze** - -- La logica di dominio va tenuta separabile dagli hook (`*-core.ts`), altrimenti non è - testabile in `test/unit/` senza rete. -- Le modifiche costano un po' di più; le regressioni in produzione costano di più. - -**Riesame** -Se comparisse un ambiente di staging stabile che rende superflua parte della copertura. - ---- - -### DD-021 — Molle interrompibili al posto delle animazioni a durata fissa - -**Data:** 5 settembre 2026 -**Stato:** Accettata - -**Contesto** -Il movimento era fatto con `@keyframes` CSS e transizioni a durata fissa. Funzionava, ma -nessuna di quelle animazioni può essere interrotta: se l'utente tocca o scorre a metà, la -sequenza va avanti per conto suo, e per ripartire deve prima finire. Mancava del tutto -qualsiasi gesto: il calendario si cambiava solo con due frecce. - -Contemporaneamente 43 componenti su 45 in `src/components/ui/` non erano importati da nessuna -parte, e con loro ~45 dipendenze (tutti i `@radix-ui/*`, `recharts`, `react-hook-form`, -`date-fns`, `embla`, `cmdk`, …): superficie di aggiornamento e di sicurezza pagata a vuoto. - -**Decisione** -Aggiungere **una** libreria di animazione — `motion` — e toglierne ~45 inutilizzate. I -parametri stanno in `src/lib/molla.ts` e sono i due di Apple (_Designing Fluid Interfaces_): -rimbalzo e durata, non massa/rigidità/smorzamento. `molla.ui` (nessun sorpasso) è il default; -`molla.slancio` si usa **solo** dopo un gesto che portava già inerzia. - -**Alternative scartate** - -- Tenere solo CSS con easing `linear()` e View Transitions → copre le comparse, non i gesti: - niente handoff di velocità, niente ripartenza dal valore corrente. -- GSAP → più grande e orientato alla timeline, cioè al modello prescritto che stiamo lasciando. - -**Conseguenze** - -- Le animazioni partono dal valore _a schermo_: un dato che cambia a metà transizione non - produce salti. -- Il movimento è ora codice JavaScript: senza JS non c'è comparsa (l'app già non funziona - senza, per auth e dati). -- `src/lib/motion.ts` resta solo per il movimento ridotto e i coriandoli. -- `src/components/ui/` non è più una libreria: aggiungere una primitiva shadcn significa - installarla, non pescarla da lì. - -**Riesame** -Se il peso del bundle client diventasse un problema misurato, o se il web recuperasse -nativamente l'interrompibilità (`ScrollTimeline` e `linear()` sono un primo passo). - ---- - -### DD-022 — L'app è solo chiara - -**Data:** 5 settembre 2026 -**Stato:** Accettata - -**Contesto** -`styles.css` conteneva un blocco `.dark` completo che non veniva mai applicato: nessun -interruttore, nessun `prefers-color-scheme`. Peggio, era incoerente. In `.dark` l'accento -diventava grigio-blu — il rosso del brand spariva — e mancavano del tutto `--success`, -`--warning`, `--info`, `--training`, i metalli dei badge, i due gradienti e le due ombre. Un -terzo stato: presente, sbagliato, morto. - -**Decisione** -CrAPP è un'app solo chiara. Il blocco `.dark` è rimosso e `:root` dichiara -`color-scheme: light`, così anche i controlli nativi restano coerenti. - -**Alternative scartate** - -- Completare il tema scuro → è lavoro vero (gradienti, ombre, i due colori dei metalli, le - superfici traslucide) per una richiesta che nessuno ha fatto. -- Lasciare il blocco lì «per dopo» → un tema mai attivato non si accorge di rompersi. - -**Conseguenze** - -- Chi riaprirà il tema scuro parte da zero, ma da zero onesto: la palette chiara ha ora - contrasti verificati e i token con suffisso `-testo` per i colori che come testo non - reggono. -- La barra di stato (`theme-color`) e lo splash del manifest sono allineati al fondo chiaro. - -**Riesame** -Se arriva una richiesta reale dalla squadra, o se si gioca in palestre al buio abbastanza -spesso da rendere il tema scuro una funzione e non un vezzo. - -### DD-023 — Ogni scrittura è limitata a chi la fa - -**Data:** 6 settembre 2026 -**Stato:** Accettata - -**Contesto** -Le tabelle preesistenti sono nate con policy `USING (true)` per `anon, authenticated`. M4 -(DD-011) ha tolto il GRANT ad `anon`, e la cosa è stata letta come «ora è chiuso». Non lo -era: per gli autenticati non c'era rimasto nessun limite. Verificato sul database locale con -un utente appena creato, senza ruolo e senza slot nella rosa: `POST /rest/v1/eventi_app` → -201, `DELETE` → 200. Qualsiasi giocatore loggato poteva svuotare il calendario della squadra -o riscrivere il voto pagella di un altro, parlando direttamente con PostgREST — senza -passare dall'interfaccia, che quei pulsanti glieli nasconde. - -Il permesso viveva quindi solo nei componenti (`useIsAdmin()`, `io.id`), cioè nel posto che -un attaccante non usa. - -**Decisione** -Le policy rispecchiano quello che l'interfaccia già fa. Tre gruppi: - -- **solo admin**: `eventi_app`. Nell'app li scrive unicamente la rotta `/eventi`, che è già - riservata agli amministratori. -- **solo la propria riga**: `risposte_presenze`, `cacche_partita` (per `giocatore_id`), - `pagelle_voti`, `mvp_voti`, `badge_social_voti` (per `votante_id`). L'admin resta incluso, - perché DD-017 gli riconosce già il diritto di agire al posto del giocatore. -- **invariate**: `turni_palloni` e le tre tabelle scout. Nell'interfaccia non hanno nessun - gate — il turno palloni se lo passa chiunque, e chiunque può aprire lo Scout Live — - quindi stringerle sarebbe una funzionalità nuova, non una messa in sicurezza. - -L'identità è lo slot di `giocatori_squadra` collegato all'account, espresso con lo stesso -`EXISTS` che usano le policy dei profili — la funzione `mio_giocatore_id()` di M2 è stata -rimossa dalla migration di correzione e non si reintroduce. Regge perché `io.id` non è una -scelta libera: dopo DD-011 e DD-018 il `localStorage` viene forzato sullo slot collegato -all'account (`benvenuto.tsx`), e senza slot non si entra. - -**Alternative scartate** - -- Lasciare tutto aperto e documentarlo → si può difendere per una squadra di venti persone - che si conoscono, ma non regge il primo account che passa di mano o il primo telefono - perso, e rende ogni bug indistinguibile da un dispetto. -- Controllare i permessi nelle route server → CrAPP scrive dal client con supabase-js. Ci - vorrebbe un livello API che oggi non esiste, per ottenere quello che la RLS fa da sola. -- Stringere anche palloni e scout → cambierebbe come funziona la squadra, e nessuno l'ha - chiesto. - -**Conseguenze** - -- Un giocatore che non ha ancora collegato lo slot non scrive più niente: l'`EXISTS` non - trova nessuna riga. È lo stesso muro di `/benvenuto`, ora applicato anche al database. -- La lettura resta aperta a tutti gli autenticati, pagelle comprese: chi interroga PostgREST - può ancora vedere **chi** ha votato cosa. L'anonimato delle pagelle è una scelta di - interfaccia, non una garanzia del database, e questa migration non lo cambia. -- I test in `test/integration/permessi.test.ts` diventano la definizione eseguibile di questa - tabella dei permessi. - -**Riesame** -Se l'anonimato delle pagelle deve diventare reale (servirebbe una vista aggregata e la -chiusura della lettura riga per riga), o se turni e scout acquistano un gate -nell'interfaccia: allora le loro policy devono seguirlo. - -### DD-024 — Le route che avvisano la squadra chiedono le credenziali - -**Data:** 6 settembre 2026 -**Stato:** Accettata - -**Contesto** -Le route in `src/routes/api/public/` girano con la service role e saltano la RLS: DD-023 non -le tocca. Nessuna di loro faceva un controllo di accesso — cercando `authorization` in quella -cartella l'unico header era lo User-Agent con cui `csi.ts` chiama il portale CSI. Chiunque -conoscesse l'URL poteva quindi far suonare i telefoni di tutta la squadra: -`promemoria-palloni` accetta perfino una POST con il corpo vuoto. - -La difesa apparente delle altre due — «serve un id evento valido» — non è una difesa: l'id è -`e` + il timestamp in base 36 (`nuovoIdEvento()`), compare negli URL che la squadra si -scambia, ed è elencabile da qualsiasi utente loggato. - -Il danno non è furto di dati: i testi delle notifiche li costruisce il server. È molestia e -consumo della quota push. Non è però una ragione per lasciare la porta aperta. - -**Decisione** -Le tre route che inviano notifiche chiedono le credenziali, con due controlli diversi perché -i chiamanti sono diversi (`src/lib/auth-route.server.ts`): - -- `apri-sondaggio` e `sollecita-presenze` → `richiediAdmin`: token della sessione Supabase - verificato con `auth.getUser`, poi ruolo `admin` letto da `user_roles`, la stessa fonte di - `src/lib/ruoli.ts` (DD-011). `401` senza token valido, `403` con token ma senza ruolo. Il - controllo viene **prima** della validazione dell'input, così la risposta non rivela - nemmeno se un evento esiste. -- `promemoria-palloni` → all'inizio `richiediSegreto`, con un segreto da cron; sostituito - subito dopo da `richiediAdmin` quando la route è diventata manuale (DD-025). - -`csi`, `push-config` e `push-subscribe` restano aperte: le chiama il browser prima del login, -dove qualsiasi segreto sarebbe pubblico. (`push-messaggio` esisteva per lo stesso motivo ed è -sparita con DD-026.) - -**Alternative scartate** - -- Un segreto condiviso anche per le due route dell'app → finirebbe nel bundle JavaScript, - cioè pubblico. -- Il middleware `requireSupabaseAuth` già presente nel repository → è - `createMiddleware({ type: "function" })`, protegge le server function di TanStack Start. In - CrAPP `createServerFn` non compare da nessuna parte: quel file non è mai stato eseguito, - e non si applica comunque alle route in `src/routes/api/`. -- Fidarsi dell'id evento come credenziale → è un timestamp in un URL condiviso. - -**Conseguenze** - -- Nessuna variabile d'ambiente da configurare: dopo DD-025 tutte e tre le route usano lo - stesso controllo sul ruolo. -- I due pulsanti dell'app mandano ora il token con `intestazioniAutenticate()` - (`src/lib/auth.ts`), letto al momento della chiamata e non da uno stato React, così non si - spedisce un token scaduto. -- `api.test.ts` non può più verificare la validazione dell'input di `sollecita-presenze`, - che ora sta dietro all'accesso: quel pezzo si è spostato in - `test/integration/permessi-route.test.ts`, che gira sullo stack locale perché ha bisogno - di utenti veri. - -**Riesame** -Se un giorno l'app userà `createServerFn`, il middleware già presente diventa la strada -naturale e questi controlli vanno riletti alla sua luce. - -### DD-025 — Il promemoria palloni lo manda l'admin, per un evento - -**Data:** 6 settembre 2026 -**Stato:** Accettata - -**Contesto** -`promemoria-palloni` era disegnata per un cron quotidiano: calcolava chi è di turno **oggi** e -gli mandava una push. DD-024 l'aveva chiusa con un segreto condiviso, coerente con quel -disegno. Ma nel repository non c'è nessun cron, e `docs/modules/palloni.md` lo annotava già -come «da verificare lato hosting»: nei fatti quel promemoria non è mai partito. Una route che -funziona e che nessuno chiama. - -Nel frattempo l'app aveva già il precedente giusto: `apri-sondaggio` è manuale fin dall'inizio -(«Nessun cron: l'invio è manuale», CHANGELOG). - -**Decisione** -Il promemoria lo fa partire un amministratore dal pulsante «Avvisa chi è di turno», dentro il -riquadro palloni della pagina evento. La route accetta un `eventoId` e avvisa i destinatari di -**quell'evento** — chi deve prendere i palloni e chi deve riportarli — invece della giornata -corrente. Il controllo di accesso diventa `richiediAdmin` come le altre due, e -`richiediSegreto` con la sua variabile `CRON_SEGRETO` spariscono. - -Il testo dell'avviso viaggia dentro la push, cifrato nel payload (DD-026): il service worker -lo mostra così com'è, quindi un avviso mandato il martedì per il sabato arriva col testo -giusto. - -**Alternative scartate** - -- Un pulsante «manda il promemoria di oggi» in Dashboard → rispecchia la route com'era, ma - premuto un martedì qualsiasi risponderebbe «inviate: 0» e sembrerebbe rotto. L'admin - ragiona per evento, non per giornata. -- Tenere il cron e configurarlo davvero → più lavoro, una variabile d'ambiente da gestire in - ogni ambiente, e nessuno l'aveva chiesto. Un pulsante che funziona batte uno scheduler che - non esiste. -- Calcolare il testo al volo sulla giornata corrente → funziona solo se l'avviso parte il - giorno stesso, cioè proprio il vincolo da cui volevamo uscire. - -**Conseguenze** - -- Il promemoria è ora una scelta consapevole di un admin, non un automatismo: se nessuno preme - il pulsante, non parte niente. È un passo indietro rispetto all'idea originale, ma un passo - avanti rispetto alla realtà, dove non partiva mai. -- `destinatariPromemoriaPalloni()` non è più usata dalla route ma resta in `palloni-core.ts`. -- Sparisce `CRON_SEGRETO`: nessuna variabile d'ambiente nuova da configurare in nessun - ambiente. - -**Riesame** -Se la squadra si accorge che l'admin si dimentica di premere il pulsante. A quel punto il cron -torna utile, e con lui il segreto: DD-024 descrive già come farlo. - ---- - -**Aggiornamento (30 settembre 2026)** — DD-040 affianca al pulsante un invio automatico a 12, 6 e 3 ore -prima dell'evento, per gli eventi con un incaricato confermato; il pulsante e la route restano. - -### DD-026 — Il testo della notifica viaggia dentro la push - -**Stato:** accettata · **Data:** settembre 2026 - -**Contesto** -Le push partivano senza payload: il service worker, appena svegliato, chiedeva a -`/api/public/push-messaggio` che cosa mostrare. Ad app aperta funzionava; ad app chiusa e -telefono bloccato non arrivava niente. È lo scenario per cui il browser dà al worker pochi -secondi di vita: una fetch verso un endpoint che interroga Supabase è esattamente ciò che non -riesce a chiudersi in tempo, e senza `showNotification` non compare nulla. `Urgency: high` e -un timeout di 3 secondi sul recupero del testo avevano attenuato il sintomo, non la causa. - -**Decisione** -Titolo e testo viaggiano cifrati nel corpo della push (`aes128gcm`, RFC 8188/8291) con le -chiavi `p256dh` e `auth` già salvate in `push_subscriptions`. Il service worker legge -`event.data.json()` e mostra la notifica: zero rete, zero attesa. - -Cadono di conseguenza la route `push-messaggio`, la coda `promemoria_push` (con la sua -scadenza a 12 ore), `messaggioPalloniOggi()` e il timeout nel service worker: tutti pezzi che -esistevano solo per rimediare al payload vuoto. Tutti e tre i mittenti -(`apri-sondaggio`, `sollecita-presenze`, `promemoria-palloni`) avevano già il testo pronto -prima di inviare. - -**Alternative scartate** - -- Usare la libreria `web-push` → dipende da `node:crypto`, e il build nitro ha come target - Cloudflare. La cifratura sta in ~40 righe di Web Crypto, già disponibile ovunque. -- Allungare ancora il timeout della fetch → allunga anche il tempo in cui il worker può - morire prima di mostrare qualcosa. Il problema era la fetch, non la sua durata. - -**Conseguenze** - -- La notifica non dipende più dalla rete al momento del risveglio, e nemmeno da una funzione - serverless che risponda in fretta a freddo. -- Sparisce l'unico punto in cui chiunque conoscesse un endpoint push poteva leggere il - messaggio destinato a quel dispositivo. -- Il payload sta sotto i 4 KB: i testi attuali sono ampiamente dentro, ma un messaggio molto - lungo andrebbe accorciato. -- La tabella `promemoria_push` resta nel database, ora inutilizzata: va rimossa con una - migrazione quando si tocca lo schema. - -**Riesame** -Se servisse mandare payload più grandi del limite del protocollo, o se un servizio push -smettesse di accettare corpi cifrati (nessuno lo fa: è lo standard). - -### DD-027 — Chi vota deve essere convocato, non solo autenticato come sé stesso - -**Stato:** accettata · **Data:** 8 settembre 2026 - -**Contesto** -Un audit del modulo Badge (`docs/modules/badge.md`) ha trovato due filtri rimasti solo -applicativi dopo DD-023: la policy di M11 garantisce che `votante_id` sia lo slot collegato -all'account di chi scrive, ma non controlla che **votante e votato fossero convocati** -all'evento — un utente che scrive direttamente su PostgREST (bypassando l'interfaccia) poteva -votare o essere votato in una partita a cui non aveva partecipato, gonfiando `mediaVoto`, -`mvp` o un badge social a piacere. Allo stesso modo, `eventi_app.pagelle_chiuse` nascondeva -solo i bottoni in UI: un voto pagella "fuori tempo" restava tecnicamente possibile. - -**Decisione** -La policy "Ognuno gestisce i propri voti ..." di `pagelle_voti`, `mvp_voti` e -`badge_social_voti` (M11) guadagna un controllo aggiuntivo tramite la funzione -`evento_permette_voto()` (migration `m13_convocati_e_pagelle_chiuse`): verifica che sia -`votante_id` sia `votato_id` compaiano in `eventi_app.convocati` per quel `match_id` -(`convocati` vuoto = tutta la rosa, la stessa convenzione di `convocatiEvento()` in -`eventi.ts`), e — solo per le pagelle — che `pagelle_chiuse` sia falso. Le policy admin -restano invariate e permissive: un amministratore deve poter correggere un voto anche fuori -convocazione o dopo la chiusura. - -Il controllo si ferma alla **convocazione**, non alla **presenza reale**: per l'MVP, ad -esempio, l'interfaccia limita già il voto ai soli presenti/in ritardo -(`usePresenzeEvento`), un filtro più stretto che resta solo applicativo — un convocato ma -assente passa ancora a livello database. Stringere fino a quel punto avrebbe richiesto -leggere `risposte_presenze` dentro la policy, un salto di complessità non giustificato -dall'audit che ha originato questa decisione. - -**Alternative scartate** - -- Un trigger `BEFORE INSERT/UPDATE` invece di RLS → si applicherebbe anche alla service key - e agli admin, bloccando correzioni legittime fuori convocazione; la RLS, applicata solo - alla policy non-admin, li esclude naturalmente. -- Controllare anche la presenza reale (`risposte_presenze`), non solo la convocazione → - scope maggiore del gap trovato in audit, e specifico dell'MVP (pagelle e badge social non - hanno un concetto di "presente" distinto da "convocato" nell'interfaccia attuale). - -**Conseguenze** - -- Un evento senza `convocati` esplicito (lista vuota, il caso più comune oggi) non cambia - comportamento: tutta la rosa resta votabile, come prima. -- I test di `test/integration/permessi.test.ts` sono la definizione eseguibile anche di - questa parte della tabella dei permessi (voti non convocati rifiutati, `pagelle_chiuse` - rifiutata a database, controlli positivi che provano che un voto legittimo passa ancora). -- Resta un gap conosciuto e documentato (non quello risolto qui): che il votante fosse - davvero presente, non solo convocato, per MVP/pagelle/badge social. - -**Riesame** -Se un giorno servisse bloccare anche il voto di un convocato-ma-assente a livello database, -non solo in UI. - -### DD-028 — Soglia minima di campione per Media voto e MVP in home - -**Data:** 9 settembre 2026 -**Stato:** Accettata - -**Contesto** -Un audit della sezione «Colpo d'occhio» in home (`index.tsx`, StatTile Presenze/Media -voto/MVP) ha trovato che due delle tre statistiche non avevano nessun minimo campionario: -`mediePagelle()` calcola una media aritmetica pura, così un giocatore con un solo voto da 10 -mostrava "10" in home, più alto di un titolare con 40 voti e media 7.2 — lo stesso problema -che il badge Pagellone già risolve con `VOTI_MINIMI_PAGELLA` (badge.md), ma applicato solo al -badge, non alla StatTile home. Allo stesso modo `mvpVintiPerGiocatore()`/`vincitoriMvp()` -assegnavano un MVP di partita anche con un solo voto totale: bastava che un solo giocatore -votasse perché il votato "vincesse" nettamente, senza nessun quorum di partecipazione. - -**Decisione** - -- **Media voto** in home usa la stessa soglia del badge Pagellone: sotto `VOTI_MINIMI_PAGELLA` - (5) voti ricevuti, la StatTile mostra `—` invece della media, tramite la funzione pura - `mediaVotoColpoDOcchio()` (`pagelle.ts`), estratta dalla route per restare testabile (DD-020). - La funzione `mediePagelle()` non cambia: il filtro resta lato chiamante, come già faceva il - badge. -- **MVP**: `conteggioPartita`'s aggregazione, tramite `vincitoriMvp()` e - `mvpVintiPerGiocatore()`, richiede ora un quorum minimo di voti totali sulla partita - (`VOTI_MINIMI_MVP = 2`, `mvp-voti.ts`) prima di assegnare un vincitore, oltre alla regola già - esistente del margine netto tra primo e secondo. Un solo voto non basta più a incoronare - nessuno, nemmeno in assenza di concorrenza. - -**Alternative scartate** - -- Alzare la soglia MVP oltre 2 (es. metà dei convocati) → serve conoscere i convocati - dell'evento dentro una funzione che oggi lavora solo sui voti; complessità non giustificata - per il gap trovato in audit. -- Lasciare l'MVP senza quorum e limitarsi al fix della Media voto → il problema di fondo - (un numero esiguo di voti che decide una statistica mostrata come solida) resterebbe aperto - per l'MVP. - -**Conseguenze** - -- Alcuni MVP di partita già assegnati con un solo voto totale non contano più nel conteggio - `mvp` del giocatore: è una modifica retroattiva al dato mostrato, non solo al calcolo futuro, - perché `mvpVintiPerGiocatore()` deriva sempre il conteggio dai voti grezzi, senza storico - persistito a parte. -- `mediePagelle()` resta invariata: chi la chiama altrove (profilo, squadra) senza applicare la - soglia continua a mostrare la media grezza — non tocca questa decisione, resta il limite già - noto in [pagelle.md](modules/pagelle.md). - -**Riesame** -Se la squadra segnala che il quorum di 2 voti per l'MVP è troppo permissivo o troppo severo, o -se si vuole applicare la stessa soglia di Media voto anche alle StatTile di profilo e squadra. - -### DD-029 — Cancellare un evento pulisce a cascata i dati collegati - -**Data:** 9 settembre 2026 -**Stato:** Accettata - -**Contesto** -Un audit del modulo Obiettivi ha verificato che gli obiettivi in sé non hanno bisogno di -nessuna pulizia quando un evento viene cancellato: sono ricalcolati a runtime sull'elenco -eventi corrente (`obiettivi.ts`), quindi un evento sparito da `eventi_app` semplicemente -smette di contare. Il problema è un livello sotto: `useEliminaEvento()` -(`src/lib/eventi.ts`) cancella solo la riga in `eventi_app`. Nessuna delle otto tabelle -collegate (`risposte_presenze`, `cacche_partita`, `mvp_voti`, `pagelle_voti`, -`badge_social_voti`, `turni_palloni`, `scout_sessioni`, `scout_live`, `scout_partite`) ha mai -avuto una foreign key verso `eventi_app(id)`: le loro righe restavano orfane a database. - -Oggi è innocuo per le statistiche, perché nessun calcolo legge quelle tabelle se non partendo -dall'elenco eventi corrente. Ma è un rischio latente: un id evento futuro identico a uno -passato (generato come `"e" + timestamp`, collisione improbabile ma non impossibile) -erediterebbe dati vecchi non suoi; e un calcolo futuro che iterasse direttamente una di quelle -tabelle, invece di partire da `eventi_app`, conterebbe anche le righe orfane. - -**Decisione** -Migration `m14_pulizia_dati_evento_cancellato`: un trigger `AFTER DELETE ON eventi_app` -cancella a cascata le righe corrispondenti (`evento_id`/`match_id = id evento cancellato`) -nelle otto tabelle collegate, tramite una funzione `SECURITY DEFINER`. Agisce a database, non -in `useEliminaEvento()`: protegge anche chi cancella un evento scrivendo direttamente su -PostgREST, non solo chi passa dal bottone dell'app. - -**Alternative scartate** - -- Foreign key con `ON DELETE CASCADE` verso `eventi_app(id)` → non applicabile subito: - `mvp.md` documenta che storicamente `match_id` in `mvp_voti`/`pagelle_voti`/ - `badge_social_voti` a volte conteneva l'id di una sessione Scout o di una partita CSI, non - l'id evento CrAPP. Un vincolo FK avrebbe rifiutato la migration alla prima riga storica - disallineata; il trigger non valida i dati esistenti, solo le cancellazioni da qui in avanti. -- Cancellazione manuale nelle otto tabelle dentro `useEliminaEvento()` → fragile: va tenuta - aggiornata a mano ogni volta che un nuovo modulo aggiunge una tabella con `evento_id`, e non - protegge chi scrive/cancella direttamente su PostgREST. -- Bonificare anche le righe orfane già esistenti nella stessa migration → rimandato: tocca dati - reali già scritti, è un intervento più delicato che merita una migration a sé, non urgente - perché quelle righe sono già invisibili a ogni calcolo attuale. - -**Conseguenze** - -- Da questa migration in poi, cancellare un evento (da qualunque punto, app o REST diretto) - ripulisce automaticamente tutte le tabelle collegate. -- Le righe orfane generate da cancellazioni **precedenti** a questa migration restano nel - database: il trigger previene il problema da qui in avanti, non ripulisce lo storico. -- `test/integration/pulizia-evento.test.ts` è la definizione eseguibile del comportamento: - scrive una riga in ciascuna delle otto tabelle, cancella l'evento e verifica che spariscano - tutte. - -**Aggiornamento (9 settembre 2026, stesso giorno)** — la bonifica dello storico prevista sopra -come "riesame" è stata fatta subito dopo, migration `m15_bonifica_dati_evento_orfani`: righe -orfane in `risposte_presenze`, `cacche_partita`, `turni_palloni`, `scout_sessioni`, -`scout_live`, `scout_partite` identificate confrontando `evento_id` con `eventi_app`. Per -`mvp_voti`/`pagelle_voti`/`badge_social_voti` il confronto si applica **solo** ai `match_id` -nel formato id evento CrAPP (`^e[0-9a-z]+$`, quello di `nuovoIdEvento()`): i vecchi voti su id -Scout (prefisso `s` + timestamp decimale) o CSI (numerico o `data-squadra-squadra`) non sono -orfani, sono dati storici legittimi mai collegati a un evento CrAPP (vedi contesto sopra), e la -migration non li tocca. Verificato manualmente sul database locale prima di applicarla: un voto -di test su id Scout è sopravvissuto alla bonifica, un voto di test su id evento CrAPP orfano è -stato rimosso. - -**Aggiornamento (9 settembre 2026)** — la verifica manuale di M15 non lasciava nessuna rete di -sicurezza automatica per il futuro, a differenza del resto del progetto (DD-020). Migration -`m16_funzione_bonifica_dati_evento_orfani` rende lo stesso corpo una funzione -`bonifica_dati_evento_orfani()` (riservata al `service_role`, non richiamabile dall'app), -coperta da `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. Non serve -richiamarla di nuovo ora (M15 ha già ripulito lo storico): resta pronta come intervento di -manutenzione se in futuro il trigger di M14 smettesse di funzionare o emergesse un altro batch -di orfani. - -**Riesame** -Se una nuova tabella collegata a un evento non viene aggiunta al trigger quando creata (va -aggiornata a mano, non c'è un meccanismo che lo forzi). - -### DD-030 — Centro notifiche in-app: storico separato dalla push, letto solo con RLS - -**Data:** 15 settembre 2026 -**Stato:** Accettata - -**Contesto** -Le notifiche esistenti (`docs/modules/notifiche.md`) sono solo push OS/browser: arrivano se -il dispositivo ha attivato l'interruttore in Profilo, sono effimere (nessuno storico) e non -hanno uno stato letto/non letto — chi non attiva le push, o cambia dispositivo, semplicemente -non le vede mai. Serviva un canale visibile sempre, dentro l'app, con un badge accanto al -profilo che segnali quante cose non lette ci sono. - -**Decisione** -Nuova tabella `notifiche_utente` (migration `m17_notifiche_utente`), popolata da quattro -sorgenti lato database — mai dal client: - -1. messaggio libero dell'admin (`notifica-personalizzata.ts`, in parallelo alla push); -2. promemoria automatico prima di un evento, 24h e 3h prima, due `cron.schedule` - (estensione `pg_cron`, abilitata dal 31/07/2026 e usata per la prima volta qui); -3. turno palloni (`promemoria-palloni.ts`), in parallelo alla push; -4. sollecito presenze (`sollecita-presenze.ts`), in parallelo alla push. - -Lettura e scrittura (segna come letta, elimina) passano dal client Supabase autenticato con -RLS (`useNotificheMie()`/`useSegnaLette()`/`useEliminaNotifica()` in -`src/lib/notifiche-utente.ts`), senza una route API dedicata — stesso pattern di -`useSalvaEvento()` in `eventi.ts`. Nessuna policy INSERT per `authenticated`: le righe nascono -solo da funzioni `SECURITY DEFINER` o dalla service role, mai da un client. - -Pallino con il conteggio sull'avatar del profilo (`PallinoNotifiche`, -`src/components/crapp/ui-bits.tsx`; in origine una campanella separata, tolta per liberare -l'header: il tap sull'avatar resta l'accesso al profilo, quello sul pallino apre il -pannello), pannello a comparsa costruito a mano (nel progetto non c'è una libreria dropdown). Ogni riga si elimina -con uno swipe o una ×: non c'è pulizia automatica delle notifiche vecchie, quindi è l'unico -modo per un giocatore di toglierle di torno per sempre. - -**Alternative scartate** - -- Notificare anche sulla sola creazione di un evento, non solo sull'avvicinarsi della data → - scartata su richiesta esplicita: conta quando l'evento sta per succedere, non il momento in - cui è stato messo in calendario. La prima versione aveva un trigger `AFTER INSERT ON -eventi_app` per questo, rimosso prima del rilascio. -- Una route API dedicata per leggere/segnare come lette (come le route push esistenti) → - scartata: qui non serve la service role (niente da nascondere al giocatore stesso), quindi - la RLS basta ed evita di duplicare la logica di lettura già scritta lato client per gli - eventi. -- Pulizia automatica delle notifiche vecchie con un cron di retention (es. elimina le lette - dopo 30 giorni) → scartata per ora a favore dello swipe manuale: più diretto da capire per - chi usa l'app, e la tabella resta comunque piccola per una sola squadra. Resta un'estensione - naturale se in futuro servisse. -- Semplice `INSERT` invece di `upsert` per turno palloni e sollecito presenze → scartata: un - admin che preme due volte lo stesso pulsante per lo stesso evento avrebbe urtato contro il - vincolo `UNIQUE (giocatore_id, evento_id, tipo)`. L'upsert aggiorna la riga esistente - (testo e `creato_il` freschi, `letta` di nuovo `false`), così ripremere il pulsante si - comporta come un rinvio vero, non un errore silenzioso. - -**Conseguenze** - -- Il pallino sull'avatar riflette solo ciò che sta in `notifiche_utente`: un dispositivo senza - push attive vede comunque tutto, perché il canale è del tutto indipendente. -- `eventi_app.ora` è testo libero e non validato dal form (`` si può - svuotare): la funzione `genera_promemoria_eventi()` usa `ora_evento_a_time()`, che assorbe - l'errore di cast riga per riga invece di far fallire l'intera generazione dei promemoria per - tutti gli eventi quando uno solo ha l'ora scritta male. -- `test/integration/notifiche-utente.test.ts` è la definizione eseguibile del comportamento: - promemoria 24h/3h e deduplica, tolleranza a un'ora malformata, RLS su lettura/segna - letta/elimina, e le tre route (`notifica-personalizzata`, `promemoria-palloni`, - `sollecita-presenze`) che scrivono lo storico in-app. - -**Riesame** -Se la tabella crescesse in modo sensibile (squadre molto più numerose, o giocatori che non -fanno mai swipe), va aggiunta la pulizia automatica scartata sopra. - -### DD-031 — Data di nascita pubblica: una colonna sincronizzata, non una seconda RLS aperta - -**Data:** 15 settembre 2026 -**Stato:** Accettata - -**Contesto** -In "Squadra" la data di nascita mostrava **"Invalid Date"** per chi l'aveva inserita da -solo dal proprio Profilo. Causa: due fonti mai collegate. I 17 giocatori del seed storico -hanno la nascita in `nascitaPerId`, una mappa statica pubblica bundlata nel client -(`src/lib/crapp-data.ts`) — il commento sulla riga la descriveva esplicitamente come -stopgap: _"`giocatori_squadra` non ha ancora questa colonna (DD-015 follow-up)"_. Un -giocatore aggiunto dopo la migrazione DD-015 inserisce invece la nascita dal form Profilo, -che scrive su `profili_giocatore.data_nascita` — tabella con RLS "solo il proprio profilo o -admin" (M2), mai letta da `useRosa()`/`useAnagraficaRosa()` (`src/lib/rosa.ts`), e comunque -non visibile ai compagni per via della RLS. Per quel giocatore `nascitaPerId[g.id]` era -`undefined` → `""` → `formatData("")` in `src/routes/squadra.tsx` produceva `Invalid Date`. - -**Decisione** -Migration `m18_nascita_pubblica_giocatori_squadra`: nuova colonna `giocatori_squadra.nascita` -(quella prevista da DD-015), pubblica a tutta la rosa attiva con la stessa policy di lettura -già in vigore per le altre colonne. Un trigger `SECURITY DEFINER` su `profili_giocatore` -(`sincronizza_nascita_pubblica`, AFTER INSERT/UPDATE/DELETE) la tiene allineata a -`data_nascita`: un solo input nel form Profilo (`CampiProfilo`), due destinazioni — quella -amministrativa esistente (RLS invariata: sé stesso o admin) e questo specchio pubblico. -`profili_giocatore` resta l'unica tabella che il client scrive per la nascita; il client non -tocca mai `giocatori_squadra.nascita` direttamente. - -Essendo `SECURITY DEFINER` di proprietà dell'owner della tabella, la scrittura del trigger -bypassa la RLS di `giocatori_squadra` automaticamente — nessuna nuova policy lì. Serve però -un nuovo ramo in `enforce_giocatori_squadra_update()` (il trigger `BEFORE UPDATE` che oggi -rifiuta qualunque modifica a uno slot già collegato da parte di chi non è admin): accetta un -update dove cambia **solo** `nascita` e il chiamante è il proprietario dello slot -(`OLD.auth_user_id = auth.uid()`). Senza quel ramo l'update annidato del trigger di sync -fallirebbe con l'eccezione già in vigore. - -Backfill una tantum dei 17 storici con gli stessi valori di `nascitaPerId`, poi rimozione di -quell'export (`crapp-data.ts`): era descritto fin dall'inizio come temporaneo, in attesa -esattamente di questa colonna. - -**Alternative scartate** - -- Allentare la RLS di `profili_giocatore` per rendere `data_nascita` leggibile a tutta la - squadra → scartata: quella tabella porta anche indirizzo, telefono, documento d'identità - nello stesso record — aprirne la lettura avrebbe esposto molto più della sola nascita. -- Un secondo campo "data di nascita pubblica" nel form Profilo, distinto da quello - amministrativo → scartata: due campi per lo stesso dato reale avrebbero richiesto - spiegare all'utente perché ne esistono due, con rischio di disallineamento se ne compila - uno solo. -- Far scrivere `giocatori_squadra.nascita` direttamente dal client (nuova RLS policy per il - proprietario dello slot) invece di sincronizzarla da `profili_giocatore` → scartata: la - RLS di update per un non-admin resta `USING (auth_user_id IS NULL)` (solo reclamare uno - slot libero); aprirla anche a righe già collegate avrebbe ampliato la superficie scrivibile - dal client su una tabella con un trigger di sicurezza già delicato (DD-016/DD-023), per un - guadagno che il trigger di sync ottiene comunque senza toccare la RLS. - -**Conseguenze** - -- Chiunque compili la propria data di nascita dal Profilo la vede comparire in Squadra (e - nei compleanni del Calendario) esattamente come i 17 storici — non più un dato "di serie - B". -- Cancellare il profilo (da sé o da un admin) azzera anche la nascita pubblica: non resta un - valore pubblico orfano di un profilo che non esiste più. -- Un giocatore senza nascita nota (profilo mai compilato) non genera più "Invalid Date": - `squadra.tsx` mostra la riga della data di nascita solo se `g.nascita` è valorizzata, - stesso criterio già usato da `compleanniEventi()` per escludere chi non ha ancora un dato. -- `test/integration/nascita-pubblica.test.ts` è la definizione eseguibile del comportamento: - sync alla scrittura, visibilità a un giocatore terzo (non solo l'interessato o l'admin), - azzeramento alla cancellazione, blocco della scrittura diretta dal client. - -**Riesame** -Se in futuro serve differenziare "chi può vedere la nascita" (es. solo squadra, non -pubblico) va rivista insieme alla policy di lettura generale di `giocatori_squadra`, non solo -per questa colonna. - -**Aggiornamento (15 settembre 2026, stesso giorno)** — dopo aver applicato M18 in produzione -(`supabase db push`), la nascita in Squadra compariva solo per i 17 giocatori storici, non -per chi aveva già compilato il proprio profilo prima della migration: il trigger di sync -copre solo le scritture _future_ su `profili_giocatore`, non retroattivamente, e M18 -backfillava a mano solo il seed storico. Migration `m19_backfill_nascita_da_profili_esistenti` -allinea una tantum `giocatori_squadra.nascita` a `profili_giocatore.data_nascita` per tutti i -profili già esistenti, con la stessa disattivazione temporanea del trigger di sicurezza usata -in M18. Da qui in poi il gap non si può ripresentare: ogni scrittura successiva passa dal -trigger di sync, questo backfill serve solo per lo storico antecedente a M18. - -A differenza della bonifica eventi orfani (DD-029, M15→M16), qui il backfill non è stato -reso una funzione richiamabile: resta SQL diretto nella migration. La copertura di test -(`test/integration/backfill-nascita-esistenti.test.ts`) verifica comunque lo stesso -comportamento eseguendo lo stesso pattern SQL via `psql` nel container Postgres locale — -scelta necessaria perché senza una funzione RPC, PostgREST non espone un update -multi-tabella come questo. - -### DD-032 — La stagione CSI si ricava dalle date delle gare - -**Data:** 24 settembre 2026 -**Stato:** Accettata - -**Contesto** -La pagina Campionato (`/classifica`) non diceva a quale stagione si riferissero classifica e -storico partite. Il portale CSI non espone il nome della stagione da nessuna parte: -`project-sheets.php`, `league_details.php`, `team-main.php` e il feed -`getEventsByTeamId.php` riportano solo il nome della competizione (verificato il -24/09/2026). Il `project_id` cambia a ogni stagione ed è aggiornato a mano in -`csi-core.ts`. - -**Decisione** -`stagioneDi()` in `src/lib/csi-core.ts` ricava la stagione ("2025/26") dalla data di una -gara, con l'anno che cambia ad agosto; `stagioneDaPartite()` usa la prima gara del -campionato letto (tutte le gare, non solo quelle giocate, così a inizio stagione basta il -calendario). L'header di `/classifica` mostra quella stagione, e lo storico partite è -diviso per stagione con `raggruppaPerStagione()`: le gare dello Scout Live locale, usate -quando il CSI non ne ha, possono essere di stagioni diverse. - -**Alternative scartate** - -- Una costante `CSI_STAGIONE` accanto a `CSI_PROJECT_ID` → un secondo valore da ricordarsi - di aggiornare a ogni stagione, che può divergere dal `project_id`; ricavata dai dati - segue da sola. -- Usare la stagione "in corso" per la squadra (la data di oggi) → sbaglierebbe proprio nel - periodo di passaggio: a settembre 2026 il CSI mostra ancora i dati 2025/26. - -**Conseguenze** - -- Oggi Campionato mostra «Stagione 2025/26» mentre Squadra e Calendario, che hanno la - stagione scritta nel codice, mostrano «Stagione 2026/27»: il CSI non ha ancora creato le - pagine della nuova stagione. È voluto — Campionato riporta la stagione dei dati che - mostra (dettagli in [collegamento-csi.md](modules/collegamento-csi.md)). -- Quando si aggiornano i `project_id` l'etichetta si aggiorna senza toccare altro codice. - -**Riesame** -Se il CSI iniziasse a esporre il nome della stagione, leggerlo da lì; se una stagione CSI -dovesse iniziare prima di agosto, spostare la soglia in `stagioneDi()`. - -### DD-033 — Gestione eventi: solo calendario, giorno fissato dal tocco - -**Data:** 24 settembre 2026 -**Stato:** Accettata - -**Contesto** -`/eventi` mostrava sotto la griglia mensile anche la lista cronologica di tutti gli eventi -("Eventi in calendario"). La lista non aveva azioni proprie (toccare una riga apriva lo -stesso drawer del giorno) e allungava la pagina con informazioni già nel calendario. Nel -form, poi, il campo «Data» era già precompilato con il giorno toccato ma sembrava da -compilare: l'amministratore doveva scegliere solo l'ora. - -**Decisione** -La pagina mostra solo il calendario, che si scorre di mese in mese con le frecce o con lo -swipe (stessa fisica a molla di `/calendario`, disattivata con `useMotoRidotto()`). Nel form -il giorno compare come testo fisso e resta da scegliere l'ora; il campo data compare solo -toccando «Cambia», per spostare un evento in un altro giorno. - -**Alternative scartate** - -- Togliere del tutto il campo data → non si potrebbe più spostare un evento esistente, una - funzione che oggi c'è (regola anti-regressione). -- Tenere la lista ma più corta (solo i prossimi eventi) → i prossimi eventi sono già in home - e in `/calendario`; qui sarebbe stata un doppione. - -**Conseguenze** - -- Per trovare un evento lontano bisogna scorrere fino al suo mese; il totale degli eventi - resta nel sottotitolo dell'header. -- Caricamento ed errore di `useEventi()` compaiono dentro la card del calendario, dove prima - stava la lista. - -**Riesame** -Se gli eventi crescono tanto da rendere scomodo lo scorrere dei mesi, valutare una ricerca -per titolo invece di reintrodurre la lista completa. - -### DD-034 — L'allenatore è uno slot della squadra con tipo diverso, non un giocatore - -**Data:** 24 settembre 2026 -**Stato:** Accettata (implementata con le migration M20 e M21) - -**Contesto** -Serve un account per l'allenatore: deve gestire gli eventi (oggi solo admin, `m11`) ma non è -un giocatore. Non ha statistiche, badge né stagione, non vota e non viene votato. L'admin -vuole registrarlo come i giocatori, inserendo la sua email Gmail. Specifica completa in -[modules/allenatore.md](modules/allenatore.md). - -**Decisione** -L'allenatore è una riga di `giocatori_squadra` con una nuova colonna `tipo = 'allenatore'` -(default `'giocatore'`). Così riusa senza cambiarli il collegamento per email (DD-018), -`profili_giocatore` per i dati personali, il bucket avatar e il compleanno sincronizzato -(DD-031). Il permesso di gestire gli eventi arriva da un nuovo valore `allenatore` di -`app_role` in `user_roles` (DD-011), scritto da una funzione `SECURITY DEFINER` quando si -collega uno slot di tipo allenatore: è quindi l'admin a concederlo, registrando lo slot. -"Rosa" diventa `attivo e tipo = 'giocatore'` ovunque. - -**Alternative scartate** - -- Una tabella `staff` separata → isola l'allenatore da voti e rosa senza toccare i filtri, ma - duplica collegamento per email, profilo, avatar e form admin. Resta l'alternativa se i - filtri sulla rosa si rivelano troppi o fragili. -- Registrare l'allenatore come admin → gli darebbe dati personali e documenti di tutta la - squadra (DD-017), molto più del necessario. -- Distinguere l'allenatore dal campo `ruolo` (ruolo in campo) → mescola due concetti e il - campo è testo libero. - -**Conseguenze** - -- Ogni lettura della rosa (client, server, SQL di voto e promemoria) va filtrata per `tipo`: - un punto dimenticato rende l'allenatore votabile o lo conta tra i convocati. L'elenco sta - nella specifica e va coperto dai test di `permessi.test.ts`. -- Il trigger sugli slot cambia: `tipo` lo scrive solo l'admin; nome e cognome li scrive anche - l'allenatore sul proprio slot. -- `numero` diventa nullabile a database, ma solo per gli allenatori (vincolo di M21); nell'app - resta un numero, 0 per l'allenatore, per non toccare i tipi di tutta la rosa. -- Due fonti distinte, ciascuna per il suo scopo: i permessi (eventi, sollecito) da - `user_roles`, la visibilità (badge, cacche, stagione) dal tipo del proprio slot, che è - immediato e non aspetta la query dei ruoli. -- L'allenatore non vede le tab Stagione e Badge del Profilo, né Obiettivi e Badge di Squadra. -- In Squadra compare nella tab Rosa con «Allenatore» al posto del ruolo, ricavato da `tipo`; - riceve i promemoria degli eventi (dal 29/09/2026 non più: DD-039) e può sollecitare le presenze, - non il turno palloni. -- Non vede badge né classifiche o sondaggi sulle cacche, in nessuna schermata. - -**Riesame** -Se arriva un secondo tipo di staff (dirigente, segreteria) o se i filtri sulla rosa diventano -fonte di bug, passare alla tabella `staff` separata. - -### DD-035 — Avviso certificati: calcolato in Home, visibile solo al titolare e agli admin - -**Data:** 24 settembre 2026 -**Stato:** Accettata (implementata, versione 1.1.0) - -**Contesto** -Il certificato medico ha una data di scadenza (`profili_giocatore.certificato_scadenza`), ma -oggi ci si accorge che è scaduto solo aprendo la tab Profili di `/admin`: spesso quando il -giocatore è già fuori regola. La richiesta è un avviso in Home, giallo nei 7 giorni prima -della scadenza e nero dopo, con i nomi dei giocatori, che sparisca da solo quando il -certificato viene rinnovato. Specifica completa in -[modules/profilo-giocatore.md](modules/profilo-giocatore.md#avviso-certificati). - -**Decisione** - -1. L'avviso si **calcola al volo** dalla data già salvata, con una funzione pura in - `profili-core.ts`: nessuna tabella, nessuno stato «avvisato», nessuna migration. -2. Lo vedono **solo gli admin**, con i nomi di tutta la rosa, e **il giocatore interessato**, - riferito solo a sé. L'allenatore e i compagni non lo vedono. -3. L'avviso dello staff **non è cliccabile**; quello personale apre - `/profilo?tab=documenti`. -4. Soglia fissa di **7 giorni** in una costante; il giorno della scadenza il certificato vale - ancora (come `statoScadenza()`). -5. **Niente push**: l'avviso vive solo in Home. - -**Alternative scartate** - -- Mostrarlo anche all'allenatore → il certificato è un dato sanitario che oggi l'allenatore - non vede (DD-034); servirebbe una funzione `SECURITY DEFINER` apposta per un'informazione - che all'allenatore non serve ad agire. -- Salvare gli avvisi in una tabella o nel centro notifiche (DD-030) → uno stato da tenere - allineato alla data, che si disallinea appena il giocatore corregge la scadenza; calcolarlo - è gratis e sempre giusto. -- Avviso dello staff cliccabile verso `/admin?tab=profili` → l'admin non può caricare il file - al posto del giocatore (DD-017), quindi il tocco non gli farebbe fare nulla in più, e - richiederebbe di aggiungere a `/admin` la lettura della tab dall'URL. -- Mostrare al massimo alcuni nomi e un «e altri N» → a inizio stagione ne scadono molti - insieme ed è proprio lì che l'elenco completo serve. -- Push quotidiana a chi ha il certificato in scadenza → servirebbe un cron, che oggi non - esiste per nessuna push (DD-025), e le push vanno riservate agli eventi importanti - ([EFFICIENZA_CLOUD.md](EFFICIENZA_CLOUD.md)). -- Includere i certificati mancanti → è un problema diverso (profilo incompleto), già coperto - dal widget «Completa il tuo profilo» e dalla tab Profili. - -**Conseguenze** - -- Nessuna query nuova: si riusa `useProfili()`, già in cache per Profilo e `/admin`, e le RLS - esistenti decidono quanti profili arrivano (tutti all'admin, il proprio al giocatore). -- Il rinnovo non richiede nessuna azione di chiusura: cambiata la data, l'avviso si aggiorna - da solo. -- Chi non apre l'app non viene avvisato: l'avviso raggiunge solo chi entra in Home. -- Un admin che è anche giocatore vede solo l'avviso dello staff, dove compare il suo nome. - -**Riesame** -Se gli avvisi in Home non bastano (certificati che scadono comunque senza essere rinnovati), -valutare una push, legata a un cron o a un invio manuale dell'admin come per i palloni. Se 7 -giorni si rivelano pochi per prenotare una visita, alzare la costante o renderla regolabile. - -### DD-036 — Notifiche email via Gmail, da un worker Docker sull'host di casa - -**Data:** 29 settembre 2026 -**Stato:** Accettata - -**Contesto** -Le notifiche di CrAPP arrivano solo con la push (che dipende da Chrome vivo in background, -vedi i limiti in `docs/modules/notifiche.md`) e nel centro notifiche in-app, che si vede solo -aprendo l'app. Chi non ha attivato la push o ha un telefono aggressivo sul risparmio -energetico non riceve nulla ad app chiusa. Serve un canale che arrivi comunque, **gratis**, ai -giocatori registrati, che sono tutti su Gmail perché l'accesso è con Google. L'app gira su -Vercel, che non esegue container né processi sempre attivi. - -**Decisione** -Le email partono da un **worker Docker sull'host di casa** che legge una coda su Supabase e -invia tramite `smtp.gmail.com` con un account Gmail dedicato e una password per app. - -- La coda è `notifiche_email_coda`, riempita da un trigger su `notifiche_utente`: ogni - notifica già esistente, di qualunque tipo e per qualunque destinatario (admin compresi, - con la stessa logica degli altri), diventa anche una mail. Le sorgenti di M17 non cambiano, - non ne nascono di nuove e l'app non conosce le email. Il certificato in scadenza resta - fuori: è una card in Home, non una notifica (DD-035). -- Il worker fa solo polling **in uscita** con la service role: nessuna porta aperta sull'host. -- La coda è una tabella a parte, con RLS senza policy, perché la policy `UPDATE` di - `notifiche_utente` lascerebbe a un client la possibilità di rimettere in coda una mail. -- Canale **opt-out**: acceso di default per tutti, con un interruttore per account in - Profilo → Opzioni. La preferenza sta in una tabella nuova, `preferenze_utente`, una riga - per account (non per slot); l'assenza di riga vale «acceso». -- Specifica completa in [notifiche-email.md](modules/notifiche-email.md); installazione e - gestione in [WORKER_EMAIL.md](WORKER_EMAIL.md). - -**Alternative scartate** - -- Inviare direttamente dal proprio IP con un MTA (Postfix) → scartata: porta 25 bloccata dai - provider residenziali, IP nelle blocklist, niente reverse DNS; le mail finirebbero in spam o - sarebbero rifiutate. -- Relay Brevo o Resend → scartati per ora: il piano gratuito funziona ma per una buona - consegna richiede un dominio con SPF/DKIM/DMARC, che oggi non c'è. Restano l'alternativa - se servirà un mittente della squadra. -- Supabase Edge Function con `pg_cron` e un relay → scartata su richiesta esplicita di un - servizio dockerizzato sull'host; resta la strada senza host acceso, ma dipende comunque da un - relay esterno. -- Vercel Cron → scartato: sul piano gratuito gira al massimo una volta al giorno, troppo poco - per i promemoria a 24h e 3h. -- Chiamare l'host dall'app (webhook verso casa) → scartato: richiederebbe un IP pubblico o un - tunnel e una superficie d'attacco in più; il polling in uscita non ne ha. -- Colonne di stato sulla riga di `notifiche_utente` → scartate per la RLS spiegata sopra. - -**Conseguenze** - -- Costo zero, ma il canale email dipende da un host acceso: se è spento le mail arrivano in - ritardo (mai perse). Consegna _almeno una volta_: in caso di crash tra invio e - registrazione può arrivare un doppione. -- Il tetto di circa 500 destinatari al giorno di Gmail è largo per una squadra, ma è un limite - reale: con molte squadre o riepiloghi frequenti andrebbe rivisto. -- Nuova migration (`m22_notifiche_email`), nuova cartella `mailer/` (worker bun con la sola - dipendenza `nodemailer`, separato dall'app) e un segreto in più (la service role sull'host). - L'app non guadagna nessuna dipendenza e le route di M17 non cambiano. -- Il certificato in scadenza resta fuori dalle email: non è una notifica (DD-035), quindi non - passa da `notifiche_utente`. Renderlo una notifica sarebbe una decisione a parte. -- Il tetto giornaliero è una finestra mobile di 24 ore, `MAIL_LIMITE_GIORNO` (400 di default, - sotto i circa 500 di Gmail). -- Le email dei giocatori si usano per un secondo scopo oltre al login: da qui l'opt-out e - l'indicazione in ogni mail su come disattivarlo. -- Una tabella in più, `preferenze_utente`, pensata per accogliere in futuro anche le - preferenze per tipo di notifica senza toccare `giocatori_squadra`. -- In produzione dal 29/09/2026 (migration e worker su un Raspberry Pi). Dalla prova reale: la - prima mail, da un account Gmail nuovo, è arrivata ma nella cartella spam, e la password per app - non era disponibile finché l'account aveva le sole passkey come secondo passaggio (dettagli in - [WORKER_EMAIL.md](WORKER_EMAIL.md#account-gmail-e-password-per-app)). - -**Riesame** -Se serve un mittente col dominio della squadra, se le mail finiscono in spam anche dopo qualche -giorno di uso, se si supera il tetto giornaliero di Gmail, o se l'host di casa si rivela poco -affidabile: passare a un relay con dominio e, in quel caso, valutare Edge Function più `pg_cron` -al posto del container. - -### DD-037 — I promemoria evento ricordano di essere già stati generati - -**Data:** 29 settembre 2026 -**Stato:** Accettata - -**Contesto** -I promemoria a 24 e a 3 ore (M17, DD-030) evitavano i doppioni con `ON CONFLICT DO NOTHING` sul -vincolo `UNIQUE (giocatore_id, evento_id, tipo)` di `notifiche_utente`: la riga della notifica -era l'unica traccia che il promemoria fosse già stato generato. Il giocatore però può eliminarla -(swipe o ×, DD-030). Senza più la riga, al giro successivo del job (ogni ora per il promemoria a -24 ore, ogni 15 minuti per quello a 3 ore, finché l'evento non inizia) il promemoria veniva -rigenerato come non letto. Con il canale email (DD-036) ogni rigenerazione accodava anche una -mail nuova: un giocatore che eliminava un promemoria ne riceveva una ogni ora fino all'inizio -dell'evento. Il difetto è stato trovato controllando la logica il 29/09/2026, il giorno in cui il -worker email è entrato in produzione, e riprodotto sul database locale. - -**Decisione** -Un registro separato, `promemoria_eventi_generati` (migration `m23_promemoria_gia_generati`), con -una riga per `(giocatore_id, evento_id, tipo)` scritta dalla funzione `genera_promemoria_eventi()`. -La notifica in-app si crea solo per le righe **appena registrate**, quindi eliminarla non fa -ripartire nulla. Il registro è indipendente da `notifiche_utente`, senza policy per i client, e -sparisce con l'evento o con il giocatore (`ON DELETE CASCADE`). I promemoria già presenti al -momento della migration sono stati registrati come generati, altrimenti la prima notifica -eliminata dopo la migration sarebbe tornata come prima. Il vincolo `UNIQUE` di `notifiche_utente` -resta come seconda difesa. Cron, criteri di selezione, testi e destinatari sono invariati. - -**Alternative scartate** - -- Non eliminare davvero la notifica (nasconderla con un flag) → scartata: cambia il significato - dell'eliminazione per tutte le sorgenti, tocca la RLS e le query del client, e la tabella - crescerebbe di righe che il giocatore non vede più. -- Togliere l'eliminazione dai soli promemoria, lasciando «segna come letta» → scartata: a un - giocatore che vuole ripulire l'elenco resterebbero notifiche non eliminabili, incoerenti con - le altre. -- Un flag «già generato» sulla riga dell'evento → scartata: il promemoria è per giocatore, e chi - viene aggiunto ai convocati dopo deve comunque riceverlo. -- Confrontare `creato_il` o lo stato «letta» per capire se ricrearlo → scartata: non distingue - una notifica mai generata da una eliminata. - -**Conseguenze** - -- Eliminare un promemoria lo fa sparire per sempre, e non parte nessuna mail in più. Il numero - massimo di mail automatiche per giocatore ed evento resta due (24 e 3 ore). -- Una tabella in più, che cresce di una riga per giocatore, evento e tipo e si svuota con - l'evento. -- Se un evento viene spostato dopo che il promemoria è partito, non ne parte uno nuovo per la - data nuova: è lo stesso comportamento di prima, ora esplicito. -- `test/integration/promemoria-eventi.test.ts` è la definizione eseguibile: il promemoria - eliminato non ricompare, non riaccoda mail, i due tipi sono indipendenti, un convocato aggiunto - dopo lo riceve comunque, il registro non è leggibile né scrivibile da un client e sparisce con - l'evento. Sul database con la funzione vecchia il test fallisce. - -**Riesame** -Se gli eventi potessero essere spostati con frequenza e servisse un nuovo promemoria per la data -nuova, il registro va agganciato anche alla data di inizio, oltre a giocatore, evento e tipo. - -### DD-038 — Ogni notifica ha tre canali: la push dei promemoria parte dal worker - -**Data:** 29 settembre 2026 -**Stato:** Accettata - -**Contesto** -Il requisito è che ogni notifica arrivi su tutti e tre i canali: push, centro notifiche in-app ed -email. Due casi non lo rispettavano ([catalogo](modules/notifiche.md#catalogo-delle-notifiche)): - -- i **promemoria a 24 e 3 ore** nascono dentro il database (job `pg_cron`, M17), che non può - firmare né cifrare una push Web (VAPID, RFC 8291): arrivavano solo in-app e per email; -- il **sondaggio pre-partita** partiva solo come push e non scriveva in `notifiche_utente`, quindi - non compariva in-app e, dato che le email nascono da quella tabella (DD-036), non arrivava per - email. - -Le altre notifiche (messaggio dello staff, turno palloni, sollecito presenze) partono dall'app su -Vercel e avevano già i tre canali. - -**Decisione** - -1. **Sondaggio pre-partita:** la route `apri-sondaggio` scrive anche `notifiche_utente` (tipo - nuovo `sondaggio_cacche`, un `upsert` per giocatore ed evento come le altre), per i giocatori - attivi che non sono allenatori. La mail parte dal trigger di M22 senza altro lavoro. La push - resta com'è. Il vincolo `CHECK` sul tipo si estende con la migration `m24`. -2. **Push dei promemoria:** la manda il **worker `mailer/`**, con un secondo canale accanto alle - email. Un trigger accoda ogni promemoria a 24 o 3 ore in `notifiche_push_coda` (stessa - struttura di `notifiche_email_coda`); il worker prende il lotto con `FOR UPDATE SKIP LOCKED`, - manda la push a tutti i dispositivi iscritti del giocatore e registra l'esito. Usa **lo stesso - modulo dell'app** (`src/lib/webpush.server.ts`, senza dipendenze) invece di una copia: il - container si costruisce dalla radice del repository. Le iscrizioni che rispondono 404 o 410 - vengono eliminate, come nelle route. Le push in coda da oltre 3 ore si scartano: un promemoria - arrivato a evento iniziato è solo rumore. -3. La push dei promemoria non dipende dall'interruttore «Email»: dipende solo dai dispositivi - iscritti, come le altre push. - -**Alternative scartate** - -- Far chiamare all'app dal database (`pg_net`, già abilitata) una route che manda la push → - scartata: il database dipenderebbe dall'indirizzo pubblico di Vercel e da un segreto condiviso, - e `pg_net` è un'estensione del provider (regola 4 di [PORTABILITA.md](PORTABILITA.md)). -- Vercel Cron → scartato: sul piano gratuito gira una volta al giorno, come già notato in DD-036. -- Copiare il modulo push nel worker → scartata: due versioni da tenere allineate per codice che - cifra e firma. -- Mandare dal worker la push di tutte le notifiche → scartata: le altre partono già dall'app, si - avrebbero push doppie. -- Un unico stato di coda per email e push → scartata: le due consegne hanno esiti, tentativi e - destinatari diversi (una push per dispositivo, un'email per giocatore). - -**Conseguenze** - -- Il worker non fa più solo email: gestisce due canali (il documento operativo resta - [WORKER_EMAIL.md](WORKER_EMAIL.md)). Serve la chiave VAPID privata anche sull'host, un segreto in - più. Senza le chiavi il worker parte lo stesso e manda solo le email, segnalandolo nel log. -- La push dei promemoria dipende dall'host di casa acceso, come le email: se è spento parte alla - ripartenza, ma non oltre le 3 ore dalla creazione. -- Una tabella nuova (`notifiche_push_coda`) e una migration (`m24_push_promemoria_e_sondaggio`). -- Il sondaggio compare nel centro notifiche e arriva per email ai giocatori attivi, non agli - allenatori; la push continua ad andare a tutti i dispositivi iscritti tranne quelli degli - allenatori, anche di giocatori non più attivi. -- Le altre notifiche non cambiano canali e non ricevono push doppie. - -**Aggiornamento (30 settembre 2026)** — DD-040 esclude dalle email le notifiche del turno palloni: per -quei tipi i canali sono due (push e in-app). - -**Riesame** -Se l'host di casa si rivela poco affidabile per una notifica sensibile al tempo come la push, o se -il database dovesse comunque chiamare l'app per altro, valutare di nuovo la chiamata dal database -all'app. - -### DD-039 — L'allenatore non riceve i promemoria degli eventi - -**Data:** 29 settembre 2026 -**Stato:** Accettata - -**Contesto** -DD-034 stabilì che l'allenatore riceve i promemoria a 24 e 3 ore prima di un evento, sempre, convocato -o no (`giocatori_destinatari_evento()` riscritta da M21). Ma un promemoria serve a ricordare di -esserci, e l'allenatore non può rispondere alle presenze, non è convocabile e non compare tra i -partecipanti (DD-034). L'incoerenza si vede negli eventi extra-campo, come una cena di squadra: -riceveva l'avviso (in-app, per email e, dopo M24, come push) senza poter dire se ci sarebbe stato né -comparire nell'elenco. - -**Decisione** -I destinatari dei promemoria sono i **giocatori attivi**: i convocati, o tutta la rosa se -`convocati` è vuoto. L'allenatore non li riceve, per nessun tipo di evento. Cambia solo la funzione -SQL (migration `m25_promemoria_solo_giocatori`, `g.tipo = 'giocatore'` al posto di «o allenatore -sempre»); cron, criteri di selezione, registro dei promemoria (DD-037), code email e push e testi -restano invariati. - -**Alternative scartate** - -- Toglierli solo per gli eventi extra-campo → scartata su indicazione esplicita di toglierli per - gli eventi in generale: anche per un allenamento o una partita l'allenatore non è tra i - partecipanti. -- Farlo rispondere agli eventi extra-campo, così da tenere il promemoria → scartata: servono - risposte presenze e regole del database per una categoria nuova, e il conteggio dei presenti. - -**Conseguenze** - -- Le notifiche che l'allenatore riceve dal server sono i **messaggi dello staff** e nient'altro - (sollecito, turno palloni, sondaggio e notifiche smart non lo riguardano già). -- I promemoria già generati per un allenatore prima della migration non si toccano. -- Un allenatore non è avvisato dell'inizio di un evento che gestisce: sa quando è perché lo ha - creato lui o lo vede in Calendario. -- Sostituisce la parte di DD-034 sui promemoria; il resto di DD-034 resta valido. - -**Riesame** -Se l'allenatore dovesse poter partecipare a certi eventi (per esempio la cena), si riapre insieme -alla sua possibilità di rispondere. - -### DD-040 — Promemoria con «oggi» o «domani», turno palloni e solleciti automatici - -**Data:** 30 settembre 2026 -**Stato:** Accettata (implementata e verificata in locale: migration `m26`, job, route e test; la -migration non è ancora applicata in produzione) - -**Contesto** -Quattro notifiche su sei partivano da un pulsante premuto da un admin (DD-025 per i palloni): se nessuno -lo premeva, nessuno veniva avvisato. I testi erano inoltre brevi e informali, e i promemoria non dicevano -quando e dove. Si vuole che turno palloni e sollecito presenze partano anche da soli, a orari fissi prima -dell'evento, e che i testi siano più formali e completi. I testi nuovi sono nel -[catalogo](modules/notifiche.md#testi-delle-notifiche). - -**Decisione** - -1. **Promemoria a 24 e 3 ore:** nuovo testo con data, ora e luogo. La parola nel titolo e nella riga - «Data» è **«domani»** se la data dell'evento è quella di domani (fuso `Europe/Rome`), **«oggi»** - altrimenti. Vale per entrambi i promemoria, quindi anche quello a 3 ore di un evento dopo la - mezzanotte dice «domani». La parola la calcola la funzione SQL quando genera la notifica. -2. **Turno palloni automatico.** Un job `pg_cron` ogni 15 minuti avvisa per gli eventi di tipo allenamento - o partita, in tre tempi con finestre che non si sovrappongono: **12 ore** (tra 12 e 6 ore prima - dell'inizio), **6 ore** (tra 6 e 3) e **3 ore** (meno di 3). Parte **solo se per l'evento c'è un - incaricato confermato** in `turni_palloni`: le proposte automatiche di `completaTurni()` non salvate non - contano. Gli avvisi sono due, con lo stesso testo nei tre tempi: «incarico assegnato» all'incaricato e - «riconsegna» a chi aveva i palloni all'evento precedente, se è un'altra persona. - - **Pulsante manuale.** «Avvisa chi è di turno» e la route `promemoria-palloni` **restano**: tipo - `turno_palloni`, testo nuovo, push mandata dalla route e non dalla coda. È una notifica in più; - riceverne due (automatica e manuale) va bene. - - **Incaricato cambiato dopo un avviso.** Il nuovo incaricato riceve il suo avviso al giro successivo, - con il tipo della finestra in cui cade. Chi aveva già ricevuto un avviso e non è più destinatario - riceve **una sola volta** «Turno palloni: incarico revocato» (`turno_palloni_revocato`), se - l'evento non è iniziato. Il job lo scopre confrontando i destinatari attuali con il **registro** degli - avvisi automatici già generati (non con le notifiche, che il giocatore può aver eliminato), senza - trigger su `turni_palloni`. Gli avvisi del pulsante non contano: possono riguardare una proposta non - confermata. - - **Niente email.** Gli avvisi palloni (automatici, revoca e pulsante) arrivano come push e nel centro - notifiche, **non per email**: il trigger `notifiche_utente_accoda_email` salta i tipi - `turno_palloni`, `turno_palloni_12h`, `_6h`, `_3h` e `turno_palloni_revocato`. È un'eccezione a - DD-038 («ogni notifica ha tre canali») solo per questi tipi; l'interruttore «Email» non cambia. -3. **Sollecito presenze automatico in tre tempi.** Lo stesso job manda il sollecito **24 ore** (tra 24 e - 12 ore prima), **12 ore** (tra 12 e 6) e **6 ore** (meno di 6) prima, al massimo una notifica per - giocatore e per tipo. Lo riceve solo chi in quel momento non ha risposto o ha risposto «forse» ed è - destinatario dell'evento: i convocati, o tutta la rosa attiva se `convocati` è vuoto, mai l'allenatore. - Chi risponde presente, assente o in ritardo smette di riceverli; chi resta su «forse» li riceve tutti e - tre. Un evento creato a meno di 12 ore dall'inizio riceve solo il sollecito della sua finestra. - - **Pulsante manuale.** «Sollecita» e la route `sollecita-presenze` **restano**, anche per l'allenatore: - tipo `sollecita_presenze`, push mandata dalla route, testo nuovo con in più la riga «Richiesta di: - _Nome_». I destinatari sono **gli stessi dell'automatico** (convocati senza risposta o con «forse»); - oggi la route guarda invece tutti i giocatori attivi. -4. **Stessa infrastruttura dei promemoria** (DD-037, DD-038): le notifiche automatiche nascono nel - database, il registro `promemoria_eventi_generati` impedisce i doppioni anche se il giocatore le - elimina, la push la manda il worker dalla coda `notifiche_push_coda`, l'email (quando c'è) nasce dal - trigger su `notifiche_utente`. Tipi nuovi: `sollecita_presenze_24h`, `_12h`, `_6h`; - `turno_palloni_12h`, `_6h`, `_3h` e `turno_palloni_revocato`. Il `turno_palloni` del pulsante resta. -5. **Migration `m26`:** estende i `CHECK` sui tipi di `notifiche_utente` e del registro, il trigger della - coda push, il trigger della coda email (che salta i tipi dei palloni) e ridefinisce la funzione dei - promemoria. Le funzioni nuove riusano `giocatori_destinatari_evento()`. - -**Alternative scartate** - -- Togliere i pulsanti manuali (turno palloni e sollecito) una volta arrivati gli automatici → scartata su - indicazione esplicita: servono all'admin per rimandare un avviso o sollecitare fuori dagli orari fissi. -- Mandare tutti i solleciti (o gli avvisi palloni) a un evento creato tardi → scartata: più messaggi nel - giro di pochi minuti sono rumore. -- «Domani» e «oggi» fissi nei due testi → scartata: il job delle 24 ore scatta anche per eventi che - iniziano oggi, e quello delle 3 ore attraversa la mezzanotte. -- Calcolare il testo nell'app invece che nel database → scartata: le notifiche nascono nei job `pg_cron`, - che non chiamano l'app (DD-038). -- Spostare nel worker il calcolo di evento precedente e successivo dei palloni → scartata: un secondo - punto che scrive in `notifiche_utente`, per un vantaggio piccolo. -- Ritirare l'avviso già consegnato al vecchio incaricato → scartata: per la push non si può, e in-app - confonde. Si manda la revoca. - -**Conseguenze** - -- Cambia la regola «quattro notifiche su sei dipendono da un gesto manuale»: restano solo manuali il - messaggio dello staff e il sondaggio pre-partita; per turno palloni e sollecito gli automatici si - aggiungono ai pulsanti, e un giocatore può ricevere lo stesso avviso due volte. -- **Logica duplicata:** «evento precedente» e «evento successivo» per i palloni, in `palloni-core.ts` - (usato dal pulsante e dalla schermata), sono riscritti in SQL per i job (`genera_avvisi_palloni_fascia`). - Ordine comune: data, ora, id. Se si cambia una regola in una sola delle due versioni, pulsante e job danno - risposte diverse: `notifiche-automatiche.test.ts` confronta testi e destinatari sugli stessi dati, anche a - parità di data e ora. -- `destinatariSollecito()` riceve anche i convocati e serve al sollecito manuale; un giocatore non - convocato non è più sollecitato. `avvisiPalloniEvento()` resta come riferimento per i test finché il SQL - non la sostituisce del tutto. -- Se un incaricato revocato viene poi riassegnato non riceve un secondo avviso, perché il registro lo ha - già generato per quella finestra: la revoca resta l'ultima notifica che ha letto. -- Le mail automatiche per evento e giocatore sono al massimo **cinque** (due promemoria, tre solleciti), - più quelle dei pulsanti (sollecito, sondaggio). Il turno palloni non ne manda. -- Il testo dei promemoria già generati non cambia (il registro impedisce di rigenerarli). - -**Riesame** -Dopo la prima stagione di uso: se i solleciti o gli avvisi palloni risultano troppi, ridurre le -finestre o escludere chi ha risposto «forse» più di una volta. diff --git a/docs/GLOSSARIO.md b/docs/GLOSSARIO.md new file mode 100644 index 0000000..1812865 --- /dev/null +++ b/docs/GLOSSARIO.md @@ -0,0 +1,29 @@ +# Glossario + +Termini del dominio e del progetto, per chi (persona o assistente AI) non conosce la squadra o +il campionato. Dove serve il dettaglio c'è il rimando al modulo. + +| Termine | Significato | +| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Slot** | Riga di `giocatori_squadra`: la «casella» di una persona della squadra. L'account Google la reclama al primo accesso (`auth_user_id`), collegandosi per email (DD-018). Un admin può liberarla. | +| **Rosa** | I giocatori attivi di gioco (`inRosa()`): esclude gli slot disattivati e l'allenatore (DD-034). La legge `useRosa()`. | +| **Allenatore** | Slot con `tipo = 'allenatore'`: gestisce gli eventi, ma non gioca, non ha numero né riceve i promemoria degli eventi (DD-034, DD-039). Vedi [modules/allenatore.md](modules/allenatore.md). | +| **Admin** | Chi ha il ruolo `admin` in `user_roles`. Gestisce squadra, profili e notifiche; i permessi veri stanno nelle policy RLS ([DATABASE.md](DATABASE.md)). | +| **Evento** | Riga di `eventi_app`: `partita`, `allenamento` o `evento` extra-campo (es. cena). I compleanni compaiono in calendario ma non sono eventi di campo. | +| **Convocati** | Giocatori chiamati a un evento. **`convocati` vuoto = tutta la rosa attiva**: è la convenzione di `convocatiEvento()` e di `evento_permette_voto()`. | +| **Presenza / risposta** | Risposta di un giocatore a un evento (`risposte_presenze`): `presente`, `ritardo`, `assente`, `forse`, `infortunato`. Vedi [modules/presenze.md](modules/presenze.md). | +| **Serie** | Streak di comportamenti positivi consecutivi (allenamenti, partite, conferme entro 24 ore). Vedi [modules/serie-presenze.md](modules/serie-presenze.md). | +| **Pagelle** | Voti 1-10 anonimi ai compagni dopo una partita; solo i convocati votano, finché `pagelle_chiuse = false` (DD-027). Vedi [modules/pagelle.md](modules/pagelle.md). | +| **MVP** | Voto al migliore in campo a fine partita, uno per votante (DD-027). Vedi [modules/mvp.md](modules/mvp.md). | +| **Badge** | Riconoscimenti calcolati a runtime, mai salvati (DD-007): normali (bronzo/argento/oro), segreti, social (votati dai compagni). Vedi [modules/badge.md](modules/badge.md). | +| **Cacche** | Sondaggio pre-partita (`cacche_partita`) usato per statistiche e per il badge segreto `s-cacche`. | +| **Palloni** | Turno a rotazione di chi porta e riporta i palloni a partite e allenamenti. Vedi [modules/palloni.md](modules/palloni.md). | +| **Scout / Scout Live** | Rilevazione in tempo reale delle azioni di una partita, con blocco condiviso («Scout occupato da X»). Solo statistiche di squadra, mai classifiche individuali (DD-008). | +| **CSI** | Centro Sportivo Italiano, l'ente del campionato. Due usi: il **tesseramento** dei giocatori (manuale, DD-009) e il **collegamento** con classifica e risultati ufficiali ([modules/collegamento-csi.md](modules/collegamento-csi.md)). | +| **Stagione CSI** | Si ricava dalle date delle gare, non da un'impostazione (DD-032). | +| **Centro notifiche** | Storico in-app (`notifiche_utente`) sull'avatar del profilo, separato dalla push (DD-030). | +| **Tre canali** | Ogni notifica può uscire come in-app, push ed email (DD-038). Email e push dei promemoria partono dal **worker** (`mailer/`). | +| **Worker** | Il container Docker in `mailer/` che invia email e push dei promemoria con la service role ([WORKER_EMAIL.md](WORKER_EMAIL.md)). | +| **Service role** | Chiave Supabase che bypassa la RLS. Solo server e worker, mai il client ([OPERATIONS.md](OPERATIONS.md)). | +| **DD-XXX** | Decisione registrata in [decisions/](decisions/README.md). | +| **M1…M26** | Numero d'ordine delle migration (`m26_notifiche_automatiche`…) in `supabase/migrations/`. | diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md new file mode 100644 index 0000000..eb4146c --- /dev/null +++ b/docs/OPERATIONS.md @@ -0,0 +1,106 @@ +# Operatività: ambienti, variabili e procedure + +Come si allestisce, si configura e si rilascia CrAPP. Le scelte di fondo sono in +[PORTABILITA.md](PORTABILITA.md) (cosa lega l'app a un fornitore, come spostarla) e in +[ARCHITECTURE.md](ARCHITECTURE.md); il worker delle email ha il suo manuale in +[WORKER_EMAIL.md](WORKER_EMAIL.md). + +## Ambienti + +| Ambiente | Dove | Note | +| ---------- | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| Produzione | Vercel (`main`) + Supabase proprietario | Project Ref `kfkcldwncxqaixetsjes`. Il vecchio `hetycilxgkdmccelwerq` è deprecato: non usarlo. | +| Preview | Vercel, da `develop` e dai branch | `develop` è fermo indietro rispetto a `main` (DD-019): non rappresenta lo stato attuale. | +| Locale | `npm run dev` (8080) + `npx supabase start` | Database in Docker con migration e seed. Dev e produzione possono condividere lo stesso progetto Supabase: vedi «Attenzione». | +| Worker | Raspberry Pi con Docker (`mailer/`) | Email e push dei promemoria. Vedi [WORKER_EMAIL.md](WORKER_EMAIL.md). | + +Lovable Cloud non è più il backend di CrAPP (DD-001). + +## Variabili d'ambiente + +Nomi usati dal codice; i valori stanno in `.env` / `.env.local` (mai nel repository) e nelle +Environment Variables di Vercel (Preview e Production). + +| Variabile | Dove serve | Per cosa | +| ----------------------------------------------------------------- | ---------------- | -------------------------------------------------------------------------------------- | +| `VITE_SUPABASE_URL`, `VITE_SUPABASE_PUBLISHABLE_KEY` | client (browser) | Connessione pubblica a Supabase | +| `SUPABASE_URL`, `SUPABASE_PUBLISHABLE_KEY` | server | Come sopra, lato route | +| `SUPABASE_SERVICE_ROLE_KEY` | server e worker | Bypassa la RLS: solo nelle route `src/routes/api/public/` e nel worker. Mai al client. | +| `VAPID_PUBLIC_KEY`, `VAPID_PRIVATE_KEY`, `VAPID_SUBJECT` | server e worker | Firma delle notifiche push. Le chiavi devono essere le stesse nell'app e nel worker. | +| `SMTP_HOST`, `SMTP_PORT`, `SMTP_SECURE`, `SMTP_USER`, `SMTP_PASS` | worker | Invio email via Gmail (password per app) | +| `MAILER_FILE_SALUTE` | worker | File usato dall'healthcheck del container | +| `SUPABASE_AUTH_GOOGLE_CLIENT_ID`, `SUPABASE_AUTH_GOOGLE_SECRET` | Supabase locale | Solo per provare il login Google sullo stack locale | + +Se aggiungi una variabile, aggiungila anche qui. + +## Migration + +Ordine obbligatorio (vedi anche [AGENTS.md](../AGENTS.md), sezione Database): + +1. si progetta e si documenta in [DATABASE.md](DATABASE.md); +2. si crea una **nuova** migration in `supabase/migrations/` (mai riscrivere quelle applicate); +3. si prova in locale con `npx supabase db reset` e `npm run test:all`; +4. solo dopo `npx supabase db push` verso il cloud; +5. si verifica con `npx supabase migration list` che locale e remoto coincidano. + +## Rilascio + +`main` è la produzione: ogni commit deve lasciare l'app funzionante. Il deploy è quello di Vercel +sul push di `main`; un cron giornaliero (`/api/public/keepalive`, `vercel.json`) evita la pausa +del progetto. Prima di dire «finito» vale la checklist «Fine lavoro» di [AGENTS.md](../AGENTS.md). +Per tornare indietro: promuovere il deployment precedente da Vercel; le migration, essendo +additive, non si annullano ma si correggono con una migration nuova. + +## Allestire un ambiente: autenticazione e primo admin + +In produzione su `main`. **Il login è l'unica via d'accesso** (31/08/2026): la selezione +libera del giocatore non esiste più, senza sessione Google si resta su `/benvenuto`, e i +permessi di amministrazione arrivano solo da `user_roles`. + +**Attenzione all'ordine:** finché il provider Google è spento in Supabase, «Accedi con +Google» risponde + +``` +{"code":400,"error_code":"validation_failed","msg":"Unsupported provider: provider is not enabled"} +``` + +e **nessuno entra nell'app**. Vale ancora per chi allestisce un ambiente nuovo (per esempio +lo stack Supabase locale): il passo 1 qui sotto va fatto per primo. + +Passaggi in ordine, nessuno dei quali è reversibile a metà. **Stato al 04/09/2026: fatti i +passaggi 1, 2, 3 e 5 (M4 applicata); il passaggio 4 è un processo continuo (7 dei 16 giocatori +attivi hanno già fatto il primo accesso).** + +1. **Provider Google in Supabase** — Google Cloud Console: consent screen _External_ (scope + `email` e `profile`, non sensibili: nessuna verifica richiesta, e la modalità _Testing_ + regge fino a 100 utenti, più che sufficiente per la squadra), credenziale + _Web application_ con redirect URI + `https://kfkcldwncxqaixetsjes.supabase.co/auth/v1/callback`. Client ID e + secret in _Authentication → Providers → Google_. In _URL Configuration_: Site URL di + produzione, più `localhost:8080` e il wildcard delle preview Vercel tra i Redirect URLs. + Per provare sullo stack locale invece che sul cloud servono anche `enabled = true` in + `[auth.external.google]` di `supabase/config.toml`, le due variabili + `SUPABASE_AUTH_GOOGLE_*` in `.env` e una credenziale con redirect URI + `http://127.0.0.1:54321/auth/v1/callback`. +2. **Migration M2** (`supabase db push`). È un `CREATE` puro: si può applicare in + produzione senza toccare il comportamento attuale. +3. **Primo admin**, dopo il primo login (l'ID esiste solo da quel momento): + `INSERT INTO public.user_roles (user_id, role) SELECT id, 'admin' FROM auth.users WHERE email = '';` +4. **Collegamento degli account**: ciascuno accede con Google e viene collegato in + automatico al proprio giocatore per email (DD-018) — nessuna scelta manuale. Finché + l'email di un giocatore non è impostata, il suo accesso mostra un errore; da `/admin` si + imposta l'email di un giocatore (nuovo o esistente) senza bisogno di una migration. Da + settembre 2026 tutti i giocatori attivi hanno l'email registrata, ma il collegamento vero + e proprio (`auth_user_id`) avviene solo al primo login di ciascuno, quindi resta un + processo continuo che si ripete a ogni nuovo giocatore aggiunto a stagione in corso. Uno + slot già collegato può essere liberato solo da un admin. +5. **Solo a squadra collegata**: migration `m4_solo_autenticati`, che toglie al ruolo `anon` + l'accesso alle tabelle v1.0. Da lì in poi i dati sono raggiungibili solo con una sessione; + le route in `src/routes/api/public/` usano la service role e continuano a funzionare. + **Applicata in produzione il 03/09/2026** — non è più necessario aspettare che l'intera + rosa abbia già fatto login: il login era già l'unica via d'accesso lato app, quindi i + giocatori non ancora collegati non erano comunque impattati; M4 chiudeva solo un residuo + di accesso diretto al database bypassando l'app. + +Attenzione: dev e produzione condividono lo stesso progetto Supabase. Un account di prova che +collega uno slot lo occupa anche in produzione, e va liberato da un admin. diff --git a/docs/README.md b/docs/README.md index 8658b50..0a500f1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,29 +2,34 @@ Indice della documentazione ufficiale del progetto. Ogni file risponde a una domanda precisa: se l'informazione che cerchi non è nel file indicato, probabilmente non esiste -ancora e va **prima documentata** (vedi [DD-002](DESIGN_DECISIONS.md#dd-002--sviluppo-document-first)). +ancora e va **prima documentata** (vedi [DD-002](decisions/DD-002.md)). ## Dove sta cosa -| Documento | Risponde a | -| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------- | -| [ROADMAP.md](ROADMAP.md) | Cosa è fatto, cosa è previsto, cosa resta un'idea | -| [ARCHITECTURE.md](ARCHITECTURE.md) | Com'è fatta l'app: stack, struttura del codice, flusso di sviluppo | -| [DATABASE.md](DATABASE.md) | Quali tabelle esistono, a cosa servono, chi le usa | -| [DESIGN_DECISIONS.md](DESIGN_DECISIONS.md) | Perché abbiamo scelto così, cosa abbiamo escluso e quando riaprire la scelta | -| [PORTABILITA.md](PORTABILITA.md) | Cosa lega l'app a un fornitore e cosa no, come spostarla su server proprio (backup/disaster recovery incluso) | -| [EFFICIENZA_CLOUD.md](EFFICIENZA_CLOUD.md) | Come tenere basso il consumo cloud: cache, query, push | -| [WORKER_EMAIL.md](WORKER_EMAIL.md) | Come si installa, si gestisce e si diagnostica il worker che invia le notifiche per email | -| [CHANGELOG.md](CHANGELOG.md) | Cosa è cambiato e quando | -| [modules/](modules/) | Specifica funzionale di ogni modulo, una per file | +| Documento | Risponde a | +| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | +| [ROADMAP.md](ROADMAP.md) | Cosa è fatto, cosa è previsto, cosa resta un'idea | +| [ARCHITECTURE.md](ARCHITECTURE.md) | Com'è fatta l'app: stack, struttura del codice, flusso di sviluppo | +| [DATABASE.md](DATABASE.md) | Quali tabelle esistono, a cosa servono, chi le usa | +| [decisions/](decisions/README.md) | Perché abbiamo scelto così, cosa abbiamo escluso e quando riaprire la scelta | +| [PORTABILITA.md](PORTABILITA.md) | Cosa lega l'app a un fornitore e cosa no, come spostarla su server proprio (backup/disaster recovery incluso) | +| [EFFICIENZA_CLOUD.md](EFFICIENZA_CLOUD.md) | Come tenere basso il consumo cloud: cache, query, push | +| [GLOSSARIO.md](GLOSSARIO.md) | Cosa significano i termini del dominio (slot, rosa, convocati, CSI, pagelle…) | +| [OPERATIONS.md](OPERATIONS.md) | Ambienti, variabili d'ambiente, migration, rilascio e allestimento di un ambiente nuovo (login Google, primo admin) | +| [WORKER_EMAIL.md](WORKER_EMAIL.md) | Come si installa, si gestisce e si diagnostica il worker che invia le notifiche per email | +| [CHANGELOG.md](CHANGELOG.md) | Cosa è cambiato e quando | +| [archive/](archive/README.md) | Materiale storico non più in uso: non è fonte di verità | +| [modules/](modules/) | Specifica funzionale di ogni modulo, una per file | Le regole vincolanti per gli assistenti AI stanno in [AGENTS.md](../AGENTS.md); -lo stato corrente del lavoro in [PROJECT_STATE.md](../PROJECT_STATE.md). +lo stato corrente del lavoro in [PROJECT_STATE.md](../PROJECT_STATE.md) (una pagina: a che punto +siamo, non la cronologia). ## Ordine di lettura Prima di modificare il codice, nell'ordine: questo indice → `ROADMAP.md` → `ARCHITECTURE.md` -→ `DATABASE.md` → `DESIGN_DECISIONS.md` → il documento del modulo interessato in `modules/`. +→ `DATABASE.md` → il documento del modulo interessato in `modules/` → solo le decisioni +(`decisions/DD-XXX.md`) che il modulo o l'indice richiamano, non tutte. ## Regole di manutenzione @@ -37,8 +42,8 @@ Ogni informazione ha **una sola casa**, per evitare che le copie divergano: - il lavoro in corso sta solo in `PROJECT_STATE.md`, che rimanda alla roadmap per il resto; - lo schema del database sta solo in `DATABASE.md`, allineato alle migration in `supabase/migrations/`: una tabella nuova si documenta nella stessa modifica che la crea; -- le motivazioni stanno solo in `DESIGN_DECISIONS.md`, in voci `DD-XXX`; per aggiungerne - una si copia [\_template-dd.md](_template-dd.md). +- le motivazioni stanno solo in `decisions/`, un file `DD-XXX.md` per decisione; per + aggiungerne una si copia [\_template-dd.md](_template-dd.md) e si aggiorna l'indice. Convenzioni di scrittura: un solo titolo `#` per file (le sezioni interne partono da `##`), niente `---` come riempitivo tra i paragrafi, tabelle al posto degli elenchi ripetitivi. diff --git a/docs/WORKER_EMAIL.md b/docs/WORKER_EMAIL.md index 9eac5ee..57e03b2 100644 --- a/docs/WORKER_EMAIL.md +++ b/docs/WORKER_EMAIL.md @@ -4,8 +4,8 @@ Come si mette in funzione, si controlla e si diagnostica il worker (`mailer/`) c per email e le push delle notifiche che nascono nel database: promemoria a 24 e 3 ore, turno palloni a 12, 6 e 3 ore con revoca, solleciti presenze a 24, 12 e 6 ore (DD-040). Cosa fa il canale email sta in [modules/notifiche-email.md](modules/notifiche-email.md), la push dei promemoria in [modules/notifiche.md](modules/notifiche.md#push-dei-promemoria-m24); perché è fatto così in -[DD-036](DESIGN_DECISIONS.md#dd-036--notifiche-email-via-gmail-da-un-worker-docker-sullhost-di-casa) -e [DD-038](DESIGN_DECISIONS.md#dd-038--ogni-notifica-ha-tre-canali-la-push-dei-promemoria-parte-dal-worker). +[DD-036](decisions/DD-036.md) +e [DD-038](decisions/DD-038.md). Il nome del documento resta quello delle email, che sono il canale principale. **Stato in produzione:** migration `m22` applicata e worker delle email in funzione dal 29/09/2026, diff --git a/docs/archive/README.md b/docs/archive/README.md new file mode 100644 index 0000000..f7dd4d5 --- /dev/null +++ b/docs/archive/README.md @@ -0,0 +1,8 @@ +# Archivio + +Materiale **storico, non più in uso**: non è fonte di verità e non va letto per capire lo stato +attuale. Lo schema vigente sono le migration in `supabase/migrations/`. + +| Cartella | Contenuto | +| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `migrations/` | Le due prime migration del Profilo Giocatore (m2, m3), sostituite da `m2_profili_giocatore` e da `correzione_schema_profili_giocatore` in `supabase/migrations/`. Conservate solo come riferimento. | diff --git a/docs/decisions/DD-001.md b/docs/decisions/DD-001.md new file mode 100644 index 0000000..f71f9de --- /dev/null +++ b/docs/decisions/DD-001.md @@ -0,0 +1,24 @@ +# DD-001 — CrAPP deve restare indipendente da Lovable + +**Data:** luglio 2026 +**Stato:** Accettata + +**Contesto** +Il progetto nasce come prototipo su Lovable Cloud. Per crescere serve controllo su codice, deploy, database e costi. + +**Decisione** +Spostare lo sviluppo su repository GitHub indipendente, con deploy su Vercel e database Supabase gestito dal team. + +**Alternative scartate** + +- Restare su Lovable come unica piattaforma → troppa dipendenza da un servizio esterno. +- Riscrivere tutto da zero → costo e rischio inutili; il prototipo funzionava già. + +**Conseguenze** + +- Maggiore libertà e responsabilità per il team. +- Restano tracce del passaggio (dipendenze, meta tag): vanno eliminate gradualmente, non in blocco. +- L’app deve poter girare anche fuori dall’ecosistema Lovable (vedi `PORTABILITA.md`). + +**Riesame** +Quando il progetto non userà più alcun componente Lovable. diff --git a/docs/decisions/DD-002.md b/docs/decisions/DD-002.md new file mode 100644 index 0000000..13e84a2 --- /dev/null +++ b/docs/decisions/DD-002.md @@ -0,0 +1,24 @@ +# DD-002 — Sviluppo document-first + +**Data:** agosto 2026 +**Stato:** Accettata + +**Contesto** +Con più persone (e assistenti AI) che lavorano sul codice, serviva un modo per evitare funzionalità “inventate” al volo e incoerenze tra moduli. + +**Decisione** +Ogni nuova funzionalità significativa viene prima **progettata e documentata** in `docs/modules/`, poi implementata. Il flusso ufficiale è: idea → progettazione → documentazione → database → codice → test → release. + +**Alternative scartate** + +- Documentare solo a posteriori → troppo spesso incompleto o assente. +- Affidarsi solo al codice come documentazione → illeggibile per chi non programma. + +**Conseguenze** + +- Rallenta leggermente l’avvio di nuove feature, ma riduce rework e discussioni infinite. +- I moduli preesistenti vanno retro-documentati quando possibile. +- Nessuna feature non documentata entra in produzione. + +**Riesame** +Se il team diventa molto piccolo e la documentazione smette di essere consultata. diff --git a/docs/decisions/DD-003.md b/docs/decisions/DD-003.md new file mode 100644 index 0000000..0e039c2 --- /dev/null +++ b/docs/decisions/DD-003.md @@ -0,0 +1,26 @@ +# DD-003 — Due branch: main stabile, develop per il lavoro + +**Data:** agosto 2026 +**Stato:** Sostituita da [DD-019](DD-019.md) (settembre 2026) + +**Contesto** +Serve separare ciò che i giocatori usano ogni giorno da ciò che è ancora in prova. + +**Decisione** + +- `main` → produzione, sempre funzionante, deploy automatico. +- `develop` → sviluppo e preview, merge su `main` solo dopo test. + +**Alternative scartate** + +- Sviluppare direttamente su `main` → rischio di rotture in produzione. +- Branch per ogni feature → eccessivo per la dimensione attuale del team. + +**Conseguenze** + +- Gli utenti in produzione non vedono lavori incompleti. +- Ogni release su `main` deve includere verifica delle funzionalità esistenti. + +**Riesame** +Sostituita: nella pratica il lavoro è finito direttamente su `main` e `develop` è rimasto +indietro. Vedi DD-019. diff --git a/docs/decisions/DD-004.md b/docs/decisions/DD-004.md new file mode 100644 index 0000000..4d1762c --- /dev/null +++ b/docs/decisions/DD-004.md @@ -0,0 +1,22 @@ +# DD-004 — Ogni versione aggiunge, non riscrive + +**Data:** agosto 2026 +**Stato:** Accettata + +**Contesto** +CrAPP è già usata dalla squadra per presenze, calendario, scout, badge e notifiche. Rischiare regressioni su moduli funzionanti vanifica la fiducia degli utenti. + +**Decisione** +Le nuove versioni **introducono** funzionalità. Non si riscrive un modulo già operativo salvo richiesta esplicita e pianificata. + +**Alternative scartate** + +- Refactoring ampio “per pulire” insieme a ogni release → alto rischio, poco valore immediato per gli utenti. + +**Conseguenze** + +- Coesistono temporaneamente soluzioni vecchie e nuove (es. dati hardcoded accanto a tabelle database). +- Il debito tecnico va gestito con migration dedicate, non di nascosto. + +**Riesame** +Quando un modulo diventa ingestibile o blocca una release importante. diff --git a/docs/decisions/DD-005.md b/docs/decisions/DD-005.md new file mode 100644 index 0000000..e445230 --- /dev/null +++ b/docs/decisions/DD-005.md @@ -0,0 +1,23 @@ +# DD-005 — Mobile-first, pochi click, pochi schermi + +**Data:** origine progetto +**Stato:** Accettata + +**Contesto** +I giocatori usano l’app soprattutto da smartphone, spesso in spogliatoio o in palestra, con poco tempo e poca pazienza. + +**Decisione** +Interfaccia semplice, veloce, ottimizzata per telefono. Navigazione ridotta (barra inferiore). Ogni schermata deve avere uno scopo chiaro. + +**Alternative scartate** + +- Layout da desktop con menu complessi → scomodo in mobilità. +- App nativa iOS/Android → costi e tempi di pubblicazione non giustificati per una squadra amatoriale. + +**Conseguenze** + +- Funzionalità amministrative complesse vanno semplificate o suddivise con cura. +- La PWA è la forma giusta per questo pubblico. + +**Riesame** +Se emergono esigenze desktop forti (es. gestione documenti massiva solo da PC). diff --git a/docs/decisions/DD-006.md b/docs/decisions/DD-006.md new file mode 100644 index 0000000..f48dbec --- /dev/null +++ b/docs/decisions/DD-006.md @@ -0,0 +1,22 @@ +# DD-006 — Intelligenza artificiale solo se porta beneficio reale + +**Data:** origine progetto +**Stato:** Accettata + +**Contesto** +L’AI è attraente ma può complicare l’app, aumentare i costi e creare aspettative irrealistiche. + +**Decisione** +Usare l’AI solo quando riduce lavoro agli admin o migliora concretamente l’esperienza dei giocatori. Non introdurla “perché si può”. + +**Alternative scartate** + +- AI ovunque (chatbot, suggerimenti automatici, analisi predittive) → fuori focus per una squadra amatoriale. + +**Conseguenze** + +- “AI Allenamenti” è tra le voci ancora da fare in roadmap. +- Ogni proposta AI va valutata con la domanda: _chi risparmia tempo e quanto?_ + +**Riesame** +Quando l’AI diventa economica e affidabile per casi d’uso chiari (es. generazione allenamenti). diff --git a/docs/decisions/DD-007.md b/docs/decisions/DD-007.md new file mode 100644 index 0000000..e47111d --- /dev/null +++ b/docs/decisions/DD-007.md @@ -0,0 +1,23 @@ +# DD-007 — Badge calcolati dall’app, non salvati nel database + +**Data:** origine progetto +**Stato:** Accettata + +**Contesto** +I badge dipendono da statistiche già disponibili (presenze, MVP, cacche, ecc.). Salvare ogni badge sbloccato nel database aggiungerebbe complessità senza beneficio immediato. + +**Decisione** +I badge vengono **calcolati al volo** dall’applicazione in base ai dati esistenti. Non esiste una tabella badge dedicata. + +**Alternative scartate** + +- Tabella `badge_sbloccati` con storico → utile in futuro per notifiche retroattive o audit, ma non necessaria ora. + +**Conseguenze** + +- Meno migration e meno sincronizzazione. +- Lo “sblocco” celebrativo usa cache locale per non ripetere animazioni. +- Un eventuale storico badge richiederà una nuova decisione. + +**Riesame** +Se servono badge manuali assegnati dagli admin o storico immutabile. diff --git a/docs/decisions/DD-008.md b/docs/decisions/DD-008.md new file mode 100644 index 0000000..6370795 --- /dev/null +++ b/docs/decisions/DD-008.md @@ -0,0 +1,22 @@ +# DD-008 — Gamification equa tra ruoli + +**Data:** origine progetto +**Stato:** Accettata + +**Contesto** +In pallavolo i ruoli hanno statistiche diverse (un libero non segna punti d’attacco). Confrontare tutti sugli stessi numeri sarebbe ingiusto e scoraggiante. + +**Decisione** +Le statistiche **personali** in profilo e squadra devono essere **eque per tutti i ruoli**. Dati tecnici di reparto (punti, ace, muri) restano nello Scout Live come informazione di squadra, non come leva competitiva individuale. + +**Alternative scartate** + +- Classifiche individuali basate su punti → penalizza libero, palleggiatore, centrale. + +**Conseguenze** + +- Badge e obiettivi usano presenze, MVP, pagelle, serie, cacche — metriche accessibili a tutti. +- Lo scout resta strumento tecnico, non gioco. + +**Riesame** +Se la squadra chiede esplicitamente classifiche tecniche per ruolo. diff --git a/docs/decisions/DD-009.md b/docs/decisions/DD-009.md new file mode 100644 index 0000000..d0716c3 --- /dev/null +++ b/docs/decisions/DD-009.md @@ -0,0 +1,24 @@ +# DD-009 — Tesseramento CSI manuale, integrazione API in una fase successiva + +**Data:** agosto 2026 +**Stato:** Accettata + +**Contesto** +Il modulo Profilo Giocatore deve aiutare gli admin a raccogliere documenti e dati per il tesseramento CSI. Un collegamento automatico al sistema CSI è complesso e non urgente. + +**Decisione** + +- **Prima fase:** profilo completo, dashboard admin, download documenti, export CSV con i campi richiesti dal CSI. +- **Fase successiva:** eventuale collegamento automatico a CSI (calendario, risultati, classifica ufficiale). + +**Alternative scartate** + +- Integrazione CSI già nella prima fase → scope troppo ampio, dipendenza da API esterne non controllate. + +**Conseguenze** + +- Gli admin guadagnano subito tempo (niente più Excel e chat per i documenti). +- L’export CSV deve essere affidabile e completo: è il deliverable chiave di questa prima fase. + +**Riesame** +Quando il CSI mette a disposizione API stabili o quando il volume di tesseramenti giustifica l’automazione. diff --git a/docs/decisions/DD-010.md b/docs/decisions/DD-010.md new file mode 100644 index 0000000..d0bcac5 --- /dev/null +++ b/docs/decisions/DD-010.md @@ -0,0 +1,23 @@ +# DD-010 — Profilo giocatore: niente storico certificati + +**Data:** agosto 2026 +**Stato:** Accettata + +**Contesto** +Il certificato medico va aggiornato ogni stagione. Tenere lo storico di tutte le versioni complica upload, storage e privacy. + +**Decisione** +Il giocatore può **sovrascrivere** certificato e data di scadenza. Lo storico delle versioni precedenti non viene conservato. + +**Alternative scartate** + +- Archivio certificati → utile per audit, rinviato a versioni future. + +**Conseguenze** + +- Implementazione più semplice e veloce. +- Gli admin vedono solo il certificato attuale. +- Va comunicato chiaramente ai giocatori che sostituire il file elimina quello precedente. + +**Riesame** +Se il CSI o il regolamento interno richiedono conservazione storica. diff --git a/docs/decisions/DD-011.md b/docs/decisions/DD-011.md new file mode 100644 index 0000000..84de709 --- /dev/null +++ b/docs/decisions/DD-011.md @@ -0,0 +1,24 @@ +# DD-011 — Autenticazione reale prima del profilo amministrativo completo + +**Data:** agosto 2026 +**Stato:** Accettata + +**Contesto** +Oggi l’app identifica l’utente con la selezione del giocatore da una lista, senza login. Documenti, certificati e dati personali richiedono sapere _chi_ sta operando e impedire accessi non autorizzati. + +**Decisione** +Prima di completare il modulo Profilo Giocatore, introdurre **login con Google o email** tramite Supabase Auth — non tramite Lovable Auth. Dopo il login, il giocatore associa il proprio profilo squadra. + +**Alternative scartate** + +- Continuare solo con selezione da lista → inaccettabile per dati sensibili. +- Lovable Auth → crea dipendenza da piattaforma che stiamo abbandonando. + +**Conseguenze** + +- Tutti dovranno fare login almeno una volta. +- Gli admin useranno ruoli veri (`user_roles`), non una lista di nomi hardcoded. +- È prerequisito per dashboard admin e export CSI. + +**Riesame** +Dopo il rollout auth, se emergono problemi di adozione (giocatori poco digitali). diff --git a/docs/decisions/DD-012.md b/docs/decisions/DD-012.md new file mode 100644 index 0000000..19aee23 --- /dev/null +++ b/docs/decisions/DD-012.md @@ -0,0 +1,22 @@ +# DD-012 — Rimandare la migrazione degli ID giocatore + +**Data:** agosto 2026 +**Stato:** Accettata + +**Contesto** +L’app usa identificativi semplici (`g1`, `g2`, …) collegati a presenze, voti, palloni e altre funzioni già in uso. Nel database esiste anche una tabella `giocatori` con UUID, non collegata al codice attuale. + +**Decisione** +Per ora **non** unificare gli ID. I nuovi dati del profilo si agganciano agli identificativi già in uso. La migrazione verso UUID resta un lavoro separato, pianificato e testato. + +**Alternative scartate** + +- Migrare subito tutto a UUID → rischio alto di rompere presenze, voti, scout e notifiche. + +**Conseguenze** + +- Coesistono due modelli anagrafici fino a migration dedicata. +- `DATABASE.md` va tenuto aggiornato su cosa è “attivo” e cosa è “futuro”. + +**Riesame** +Quando il modulo Profilo Giocatore è stabile e c’è tempo per una migration con checklist regressioni completa. diff --git a/docs/decisions/DD-013.md b/docs/decisions/DD-013.md new file mode 100644 index 0000000..43d96eb --- /dev/null +++ b/docs/decisions/DD-013.md @@ -0,0 +1,22 @@ +# DD-013 — Portabilità: l’app non deve dipendere da servizi esclusivi + +**Data:** luglio 2026 +**Stato:** Accettata + +**Contesto** +La squadra potrebbe voler cambiare hosting, database o fornitore auth in futuro. + +**Decisione** +CrAPP deve poter girare su **Node.js + PostgreSQL standard**. Niente funzionalità bloccate su servizi proprietari. I dati si accedono solo tramite moduli in `src/lib/`, non direttamente dai componenti. + +**Alternative scartate** + +- Accettare lock-in per velocità → contrario alla lunga vita del progetto. + +**Conseguenze** + +- Supabase va bene perché è PostgreSQL e self-hostable. +- Le API push e i job restano endpoint HTTP richiamabili da qualsiasi scheduler. + +**Riesame** +Se si adotta un servizio che viola questa regola. diff --git a/docs/decisions/DD-014.md b/docs/decisions/DD-014.md new file mode 100644 index 0000000..6bce452 --- /dev/null +++ b/docs/decisions/DD-014.md @@ -0,0 +1,16 @@ +# DD-014 — Convergenza schema database (eventi e presenze) + +**Data:** — +**Stato:** In valutazione + +**Contesto** +Esistono due modelli paralleli: tabelle “legacy” usate dall’app (`eventi_app`, `risposte_presenze`) e tabelle “nuove” con autenticazione e vincoli (`eventi`, `presenze`, `giocatori` UUID). + +**Decisione proposta** +Unificare gradualmente sul modello autenticato, dopo auth e profilo stabili. + +**Perché non ora** +Rischio regressioni su calendario e presenze, moduli più usati della squadra. + +**Riesame previsto** +Dopo che il modulo Profilo Giocatore sarà stabile, con migration e test dedicati. diff --git a/docs/decisions/DD-015.md b/docs/decisions/DD-015.md new file mode 100644 index 0000000..4fd4c6b --- /dev/null +++ b/docs/decisions/DD-015.md @@ -0,0 +1,30 @@ +# DD-015 — Rosa anagrafica: da codice hardcoded a database + +**Data:** 3 settembre 2026 +**Stato:** Accettata + +**Contesto** +La lista giocatori viveva nel codice sorgente (`src/lib/crapp-data.ts`). Il database aveva già +`giocatori_squadra` (migration M1) popolata ma non letta da nessuna schermata tranne +`/benvenuto` e `/admin`: «Aggiungi giocatore» e «Disattiva giocatore» della dashboard non +avevano effetto su Squadra, Presenze, Pagelle, Badge e Scout, mantenendo gli stessi ID finché +non si farà DD-012. + +**Decisione** +`useRosa()` (e con lei `useIo`, `useObiettivi`) legge ora `giocatori_squadra` tramite +`useGiocatoriSquadra()`, filtrando solo i giocatori `attivo`. Tutti i punti che prima +importavano la lista statica (`convocatiEvento`, `compleanniEventi`, `completaTurni`, +`csvScoutMatch`, i widget di voto/scout/palloni, le due route API che mandano push) sono stati +agganciati allo stesso hook o, lato server, a `leggiGiocatoriSquadra()` +(`src/lib/giocatori-squadra.server.ts`, stesso pattern di `eventi.server.ts`). + +**Conseguenze** + +- Un giocatore aggiunto o disattivato dalla dashboard admin ora si riflette ovunque, non solo + in `/benvenuto` e `/admin`. +- `giocatori_squadra` non ha ancora una colonna per la data di nascita: per i 17 giocatori + storici resta quella di `crapp-data.ts` (`nascitaPerId`, lookup per id); un giocatore + aggiunto dopo la migrazione non ha nascita nota finché la colonna non esiste. Follow-up da + aprire quando serve davvero. +- `src/lib/crapp-data.ts` resta come seed storico e fallback (`rosaFallback()`), non più come + fonte viva. diff --git a/docs/decisions/DD-016.md b/docs/decisions/DD-016.md new file mode 100644 index 0000000..e01726c --- /dev/null +++ b/docs/decisions/DD-016.md @@ -0,0 +1,45 @@ +# DD-016 — Schema dati Profilo Giocatore (F0) + +**Data:** agosto 2026 +**Stato:** Accettata + +**Contesto** +La progettazione F0 del modulo Profilo Giocatore ha definito come persistere dati personali, documenti e certificati, in coesistenza con l’anagrafica attuale (`g1`…`g17` nel codice) e con la tabella `giocatori` UUID già presente ma non usata. Serviva una scelta chiara su dove salvare i dati, come collegare l’autenticazione e come proteggere documenti sensibili — senza toccare le tabelle preesistenti già operative. + +**Decisione** +Si introducono **due nuove tabelle additive**: + +- **`giocatori_squadra`** — anagrafica squadra con ID testuali (`g1`…`g17`), dati gestiti dagli admin (nome, cognome, numero, ruolo) e collegamento account (`auth_user_id`). +- **`profili_giocatore`** — dati personali, metadati documento identità, certificato medico e path dei file, in relazione 1:1 con `giocatori_squadra`. + +Regole vincolanti: + +1. **`giocatori_squadra` diventa progressivamente la source of truth** per l’anagrafica squadra. Durante la transizione, `crapp-data.ts` resta come **fallback** se il database non è disponibile o i dati non sono ancora migrati. +2. L’associazione **`auth_user_id` ↔ giocatore** è un’operazione **controllata e atomica** (es. al primo accesso da `/benvenuto`, con `UPDATE … WHERE auth_user_id IS NULL`). Il giocatore **non può modificare liberamente** `auth_user_id`; solo un admin può resettarlo in casi eccezionali. +3. I file (documento identità, certificato, foto tessera) vivono nel bucket Storage **`profili-giocatore`**, configurato come **privato**. +4. Documenti personali e sanitari **non devono mai essere esposti tramite URL pubblici**. Accesso solo tramite client autenticato con policy RLS, o signed URL a scadenza breve per download admin. +5. Le **tabelle preesistenti non vengono modificate** (`eventi_app`, `risposte_presenze`, voti, palloni, scout, push, ecc.). Il profilo si aggancia agli ID `g1`…`g17` già in uso, senza migrare verso UUID per ora (coerente con DD-012). +6. La tabella `giocatori` (UUID) resta **invariata e non usata** dal modulo profilo. + +**Alternative scartate** + +- Estendere la tabella `giocatori` UUID → conflitto con ID operativi del codice e rischio di regressioni. +- Salvare file come base64 nel database → ingestibile, difficile da gestire e da scaricare. +- Bucket pubblico con URL permanenti → inaccettabile per dati sanitari e documenti d’identità. +- Permettere al giocatore di cambiare `auth_user_id` liberamente → rischio di impersonazione e race condition. +- Modificare le tabelle preesistenti per aggiungere FK verso il profilo → viola DD-004 e DD-012. + +**Conseguenze** + +- Coesistono temporaneamente tre rappresentazioni dell’anagrafica: `crapp-data.ts` (fallback), `giocatori_squadra` (target), `giocatori` UUID (dormiente). +- `src/lib/rosa.ts` legge dal database e ricade su `crapp-data.ts` in caso di errore o assenza dati (DD-015). +- Il completamento profilo (30/30/30/10) si calcola in app, non si persiste nel database. +- Lo storico certificati non viene conservato (coerente con DD-010). +- Le migration M1–M2 (tabelle, RLS, bucket) restano **additive**: solo `CREATE`, nessun `ALTER`/`DROP` su schema esistente. +- Raffina e attua quanto proposto in DD-015 per la rosa anagrafica, senza sostituire formalmente quella voce. + +**Riesame** + +- Quando `giocatori_squadra` è stabile in produzione e il fallback `crapp-data.ts` non serve più. +- Quando si pianifica la convergenza verso UUID (DD-012, dopo che il profilo sarà stabile). +- Se il CSI o il regolamento richiedono conservazione storica documenti o consensi privacy dedicati. diff --git a/docs/decisions/DD-017.md b/docs/decisions/DD-017.md new file mode 100644 index 0000000..451bf9a --- /dev/null +++ b/docs/decisions/DD-017.md @@ -0,0 +1,37 @@ +# DD-017 — L'amministratore può compilare i dati al posto del giocatore + +**Data:** agosto 2026 +**Stato:** Accettata + +**Contesto** +Il modulo Profilo Giocatore era costruito su un confine netto: ognuno scrive solo la propria riga, l'amministratore legge e scarica. Nella pratica quel confine blocca il lavoro che il modulo doveva togliere: se metà squadra non compila i propri dati, l'export per il tesseramento CSI resta incompleto e l'admin torna a chiedere le informazioni in chat — esattamente ciò che CrAPP deve eliminare. Inoltre le docs assegnavano già agli admin la gestione dei dati squadra (nome, cognome, numero, ruolo) e il reset del collegamento all'account (DD-016 regola 2), senza che esistesse una schermata per farlo. + +**Decisione** +Dalla dashboard amministratore, un admin può: + +1. modificare i **dati squadra** di qualsiasi giocatore (nome, cognome, numero, ruolo); +2. compilare e correggere i **dati personali e del documento** di qualsiasi giocatore; +3. **scollegare** un account da un profilo, liberando lo slot. + +Restano fuori, e non cambiano: + +- i **file** (documento, certificato, foto): l'admin li scarica ma non li carica né li sostituisce. Un documento d'identità lo produce il suo titolare, e la catena di responsabilità deve restare leggibile; +- il **giocatore**, che continua a non poter toccare i propri dati squadra. + +**Alternative scartate** + +- Lasciare tutto al giocatore → l'export CSI resta incompleto e il lavoro amministrativo torna in chat, contro la missione del progetto. +- Dare all'admin anche l'upload dei file → confonde chi ha fornito un documento, su dati sanitari e d'identità dove serve il contrario. +- Un ruolo intermedio (segreteria) per i soli dati personali → un ruolo in più per una squadra sola, con gli stessi tre amministratori di adesso. + +**Conseguenze** + +- Il modello dei permessi non è più "ognuno i suoi": è "ognuno i suoi, più l'admin su tutti, tranne i file". Le policy RLS di M1 e M2 lo consentivano già, quindi non servono migration. +- Un admin può correggere un errore di battitura in un numero di documento senza inseguire il giocatore. +- Un admin vede e scrive dati personali altrui: è un potere reale, dato a tre persone su diciassette. Va assegnato con la stessa cura di prima (una riga in `user_roles`, nessuna auto-promozione). +- Il completamento del profilo smette di essere un indicatore di _chi ha risposto_ e diventa un indicatore di _quali dati mancano_, chiunque li abbia inseriti. + +**Riesame** + +- Se la squadra cresce al punto da rendere sensato un ruolo di sola segreteria. +- Se serve tracciare _chi_ ha modificato un dato: oggi non c'è audit, e con la scrittura condivisa la domanda prima o poi arriva. diff --git a/docs/decisions/DD-018.md b/docs/decisions/DD-018.md new file mode 100644 index 0000000..c045723 --- /dev/null +++ b/docs/decisions/DD-018.md @@ -0,0 +1,25 @@ +# DD-018 — Collegamento automatico giocatore↔account per email + +**Data:** settembre 2026 +**Stato:** Accettata + +**Contesto** +DD-016 regola 2 prevedeva che, al primo accesso, il giocatore scegliesse manualmente il proprio slot libero da un elenco (`/benvenuto`). In pratica ogni giocatore ha un'email nota (o presto nota), quindi far scegliere un nome da una lista è un passaggio superfluo e un rischio: un giocatore può selezionare per errore lo slot di un compagno, e nulla nel flusso lo impedisce a livello di prodotto. + +**Decisione** +Al primo accesso, `giocatori_squadra` viene interrogata per email (case-insensitive, tramite la nuova colonna `email`) invece di mostrare un elenco di slot liberi. Se l'email dell'account Google corrisponde a una riga libera, il collegamento avviene automaticamente. Se non corrisponde a nessuna riga (email non ancora nota, o nessun profilo per quella persona), l'utente vede solo un messaggio d'errore che invita a contattare un amministratore, con un pulsante per uscire e riprovare con un altro account — nessuna selezione manuale di ripiego. Le email sono popolate via migration (`m5_email_giocatori_squadra`) per la rosa iniziale; un'interfaccia in `/admin` per impostarle su nuovi giocatori è arrivata poco dopo (vedi "Alternative scartate"). Il trigger `enforce_giocatori_squadra_update` (DD-016) viene esteso per richiedere anche la corrispondenza email, non solo lo slot libero: il vincolo resta nel database, non solo nella UI. + +**Alternative scartate** + +- Mantenere la selezione manuale come ripiego quando l'email non trova corrispondenza → scartata: vanificherebbe la garanzia "ognuno collega solo il proprio profilo" e reintrodurrebbe il rischio di scelta errata che questa decisione vuole eliminare. +- Un'interfaccia admin per scrivere l'email dei giocatori → rimandata al momento della decisione, poi implementata nel form "Aggiungi giocatore" di `/admin` (`src/routes/admin.tsx`): serviva per collegare i giocatori aggiunti a metà stagione senza passare da una nuova migration ogni volta. + +**Conseguenze** + +- Le righe senza email nota restano bloccate — nessuno può collegarle, nemmeno per errore — finché un admin non la imposta da `/admin` (o, per la rosa iniziale, una migration). Da settembre 2026 tutta la rosa attiva ha l'email registrata. +- `slotLiberi` (funzione ed elenco "slot liberi" in `/benvenuto`) è stato rimosso: non aveva più chiamanti in produzione dopo il cambio. +- Un utente che accede con l'account Google sbagliato resta bloccato su `/benvenuto` finché non esce e riprova con l'account giusto. + +**Riesame** + +- Se in futuro serve un'assistenza admin diretta dal flusso di login invece che da `/admin`. diff --git a/docs/decisions/DD-019.md b/docs/decisions/DD-019.md new file mode 100644 index 0000000..ae07f48 --- /dev/null +++ b/docs/decisions/DD-019.md @@ -0,0 +1,33 @@ +# DD-019 — Il branch dei commit lo decide l'utente + +**Data:** 4 settembre 2026 +**Stato:** Accettata — sostituisce [DD-003](DD-003.md) + +**Contesto** +DD-003 prevedeva di lavorare su `develop` e portare su `main` solo dopo i test. Nella pratica +è successo il contrario: `main` è arrivato a 43 commit di vantaggio su `develop`, che è rimasto +fermo. Una regola che nessuno segue è peggio di nessuna regola, perché rende inaffidabile tutto +il resto del documento — e con più assistenti AI in gioco il rischio vero non era il branch +sbagliato, ma un agente che committa o pusha per conto suo. + +**Decisione** +È l'utente a dire su quale branch va un commit. L'assistente può **consigliare** un branch +dedicato quando la modifica è rischiosa o parallela ad altro lavoro, ma non cambia branch, non +committa, non fa push e non apre PR di propria iniziativa. In assenza di indicazioni si lavora +dove si trova il repository, di fatto `main`. + +**Alternative scartate** + +- Tenere DD-003 e riallineare `develop` → si sarebbe rotta di nuovo alla prima fretta. +- Dismettere `develop` → si perderebbero le preview Vercel, utili quando servono davvero. + +**Conseguenze** + +- `main` è insieme produzione e branch di lavoro: ogni commit deve lasciare l'app funzionante, + quindi la rete di sicurezza sono i test (vedi DD-020), non il branch. +- `develop` esiste ancora ma è indietro: la sua preview Vercel non rappresenta lo stato attuale + finché non viene riallineata. + +**Riesame** +Se il team cresce oltre una persona che scrive codice, o se un lavoro lungo ha bisogno di stare +fuori produzione per più di qualche giorno. diff --git a/docs/decisions/DD-020.md b/docs/decisions/DD-020.md new file mode 100644 index 0000000..e727b81 --- /dev/null +++ b/docs/decisions/DD-020.md @@ -0,0 +1,31 @@ +# DD-020 — Una funzione modificata senza test non è finita + +**Data:** 4 settembre 2026 +**Stato:** Accettata + +**Contesto** +Con `main` come branch di lavoro (DD-019) non c'è più un ambiente di prova tra il codice e i +giocatori. La suite in `test/` esisteva già ma scriverla era di fatto facoltativo, e i difetti +trovati dai test sono arrivati a posteriori (la sessione Scout Live che non scadeva mai, le +serie di presenze ferme a zero per settimane). + +**Decisione** +Chi aggiunge o modifica una funzione scrive o aggiorna il test nello stesso lavoro, e i test +devono essere verdi prima di consegnare. Non si commenta un test che fallisce né si indebolisce +un'asserzione per farla passare: se il comportamento voluto è cambiato, si aggiorna il test +dicendo perché. + +**Alternative scartate** + +- Test solo sui moduli critici → il confine «critico» si sposta a ogni fretta. +- Introdurre un framework di test → la suite bun con `node:assert` funziona e non aggiunge + dipendenze (vedi [test/README.md](../../test/README.md)). + +**Conseguenze** + +- La logica di dominio va tenuta separabile dagli hook (`*-core.ts`), altrimenti non è + testabile in `test/unit/` senza rete. +- Le modifiche costano un po' di più; le regressioni in produzione costano di più. + +**Riesame** +Se comparisse un ambiente di staging stabile che rende superflua parte della copertura. diff --git a/docs/decisions/DD-021.md b/docs/decisions/DD-021.md new file mode 100644 index 0000000..b591164 --- /dev/null +++ b/docs/decisions/DD-021.md @@ -0,0 +1,40 @@ +# DD-021 — Molle interrompibili al posto delle animazioni a durata fissa + +**Data:** 5 settembre 2026 +**Stato:** Accettata + +**Contesto** +Il movimento era fatto con `@keyframes` CSS e transizioni a durata fissa. Funzionava, ma +nessuna di quelle animazioni può essere interrotta: se l'utente tocca o scorre a metà, la +sequenza va avanti per conto suo, e per ripartire deve prima finire. Mancava del tutto +qualsiasi gesto: il calendario si cambiava solo con due frecce. + +Contemporaneamente 43 componenti su 45 in `src/components/ui/` non erano importati da nessuna +parte, e con loro ~45 dipendenze (tutti i `@radix-ui/*`, `recharts`, `react-hook-form`, +`date-fns`, `embla`, `cmdk`, …): superficie di aggiornamento e di sicurezza pagata a vuoto. + +**Decisione** +Aggiungere **una** libreria di animazione — `motion` — e toglierne ~45 inutilizzate. I +parametri stanno in `src/lib/molla.ts` e sono i due di Apple (_Designing Fluid Interfaces_): +rimbalzo e durata, non massa/rigidità/smorzamento. `molla.ui` (nessun sorpasso) è il default; +`molla.slancio` si usa **solo** dopo un gesto che portava già inerzia. + +**Alternative scartate** + +- Tenere solo CSS con easing `linear()` e View Transitions → copre le comparse, non i gesti: + niente handoff di velocità, niente ripartenza dal valore corrente. +- GSAP → più grande e orientato alla timeline, cioè al modello prescritto che stiamo lasciando. + +**Conseguenze** + +- Le animazioni partono dal valore _a schermo_: un dato che cambia a metà transizione non + produce salti. +- Il movimento è ora codice JavaScript: senza JS non c'è comparsa (l'app già non funziona + senza, per auth e dati). +- `src/lib/motion.ts` resta solo per il movimento ridotto e i coriandoli. +- `src/components/ui/` non è più una libreria: aggiungere una primitiva shadcn significa + installarla, non pescarla da lì. + +**Riesame** +Se il peso del bundle client diventasse un problema misurato, o se il web recuperasse +nativamente l'interrompibilità (`ScrollTimeline` e `linear()` sono un primo passo). diff --git a/docs/decisions/DD-022.md b/docs/decisions/DD-022.md new file mode 100644 index 0000000..9b0793a --- /dev/null +++ b/docs/decisions/DD-022.md @@ -0,0 +1,32 @@ +# DD-022 — L'app è solo chiara + +**Data:** 5 settembre 2026 +**Stato:** Accettata + +**Contesto** +`styles.css` conteneva un blocco `.dark` completo che non veniva mai applicato: nessun +interruttore, nessun `prefers-color-scheme`. Peggio, era incoerente. In `.dark` l'accento +diventava grigio-blu — il rosso del brand spariva — e mancavano del tutto `--success`, +`--warning`, `--info`, `--training`, i metalli dei badge, i due gradienti e le due ombre. Un +terzo stato: presente, sbagliato, morto. + +**Decisione** +CrAPP è un'app solo chiara. Il blocco `.dark` è rimosso e `:root` dichiara +`color-scheme: light`, così anche i controlli nativi restano coerenti. + +**Alternative scartate** + +- Completare il tema scuro → è lavoro vero (gradienti, ombre, i due colori dei metalli, le + superfici traslucide) per una richiesta che nessuno ha fatto. +- Lasciare il blocco lì «per dopo» → un tema mai attivato non si accorge di rompersi. + +**Conseguenze** + +- Chi riaprirà il tema scuro parte da zero, ma da zero onesto: la palette chiara ha ora + contrasti verificati e i token con suffisso `-testo` per i colori che come testo non + reggono. +- La barra di stato (`theme-color`) e lo splash del manifest sono allineati al fondo chiaro. + +**Riesame** +Se arriva una richiesta reale dalla squadra, o se si gioca in palestre al buio abbastanza +spesso da rendere il tema scuro una funzione e non un vezzo. diff --git a/docs/decisions/DD-023.md b/docs/decisions/DD-023.md new file mode 100644 index 0000000..78cb5f6 --- /dev/null +++ b/docs/decisions/DD-023.md @@ -0,0 +1,59 @@ +# DD-023 — Ogni scrittura è limitata a chi la fa + +**Data:** 6 settembre 2026 +**Stato:** Accettata + +**Contesto** +Le tabelle preesistenti sono nate con policy `USING (true)` per `anon, authenticated`. M4 +(DD-011) ha tolto il GRANT ad `anon`, e la cosa è stata letta come «ora è chiuso». Non lo +era: per gli autenticati non c'era rimasto nessun limite. Verificato sul database locale con +un utente appena creato, senza ruolo e senza slot nella rosa: `POST /rest/v1/eventi_app` → +201, `DELETE` → 200. Qualsiasi giocatore loggato poteva svuotare il calendario della squadra +o riscrivere il voto pagella di un altro, parlando direttamente con PostgREST — senza +passare dall'interfaccia, che quei pulsanti glieli nasconde. + +Il permesso viveva quindi solo nei componenti (`useIsAdmin()`, `io.id`), cioè nel posto che +un attaccante non usa. + +**Decisione** +Le policy rispecchiano quello che l'interfaccia già fa. Tre gruppi: + +- **solo admin**: `eventi_app`. Nell'app li scrive unicamente la rotta `/eventi`, che è già + riservata agli amministratori. +- **solo la propria riga**: `risposte_presenze`, `cacche_partita` (per `giocatore_id`), + `pagelle_voti`, `mvp_voti`, `badge_social_voti` (per `votante_id`). L'admin resta incluso, + perché DD-017 gli riconosce già il diritto di agire al posto del giocatore. +- **invariate**: `turni_palloni` e le tre tabelle scout. Nell'interfaccia non hanno nessun + gate — il turno palloni se lo passa chiunque, e chiunque può aprire lo Scout Live — + quindi stringerle sarebbe una funzionalità nuova, non una messa in sicurezza. + +L'identità è lo slot di `giocatori_squadra` collegato all'account, espresso con lo stesso +`EXISTS` che usano le policy dei profili — la funzione `mio_giocatore_id()` di M2 è stata +rimossa dalla migration di correzione e non si reintroduce. Regge perché `io.id` non è una +scelta libera: dopo DD-011 e DD-018 il `localStorage` viene forzato sullo slot collegato +all'account (`benvenuto.tsx`), e senza slot non si entra. + +**Alternative scartate** + +- Lasciare tutto aperto e documentarlo → si può difendere per una squadra di venti persone + che si conoscono, ma non regge il primo account che passa di mano o il primo telefono + perso, e rende ogni bug indistinguibile da un dispetto. +- Controllare i permessi nelle route server → CrAPP scrive dal client con supabase-js. Ci + vorrebbe un livello API che oggi non esiste, per ottenere quello che la RLS fa da sola. +- Stringere anche palloni e scout → cambierebbe come funziona la squadra, e nessuno l'ha + chiesto. + +**Conseguenze** + +- Un giocatore che non ha ancora collegato lo slot non scrive più niente: l'`EXISTS` non + trova nessuna riga. È lo stesso muro di `/benvenuto`, ora applicato anche al database. +- La lettura resta aperta a tutti gli autenticati, pagelle comprese: chi interroga PostgREST + può ancora vedere **chi** ha votato cosa. L'anonimato delle pagelle è una scelta di + interfaccia, non una garanzia del database, e questa migration non lo cambia. +- I test in `test/integration/permessi.test.ts` diventano la definizione eseguibile di questa + tabella dei permessi. + +**Riesame** +Se l'anonimato delle pagelle deve diventare reale (servirebbe una vista aggregata e la +chiusura della lettura riga per riga), o se turni e scout acquistano un gate +nell'interfaccia: allora le loro policy devono seguirlo. diff --git a/docs/decisions/DD-024.md b/docs/decisions/DD-024.md new file mode 100644 index 0000000..602be74 --- /dev/null +++ b/docs/decisions/DD-024.md @@ -0,0 +1,60 @@ +# DD-024 — Le route che avvisano la squadra chiedono le credenziali + +**Data:** 6 settembre 2026 +**Stato:** Accettata + +**Contesto** +Le route in `src/routes/api/public/` girano con la service role e saltano la RLS: DD-023 non +le tocca. Nessuna di loro faceva un controllo di accesso — cercando `authorization` in quella +cartella l'unico header era lo User-Agent con cui `csi.ts` chiama il portale CSI. Chiunque +conoscesse l'URL poteva quindi far suonare i telefoni di tutta la squadra: +`promemoria-palloni` accetta perfino una POST con il corpo vuoto. + +La difesa apparente delle altre due — «serve un id evento valido» — non è una difesa: l'id è +`e` + il timestamp in base 36 (`nuovoIdEvento()`), compare negli URL che la squadra si +scambia, ed è elencabile da qualsiasi utente loggato. + +Il danno non è furto di dati: i testi delle notifiche li costruisce il server. È molestia e +consumo della quota push. Non è però una ragione per lasciare la porta aperta. + +**Decisione** +Le tre route che inviano notifiche chiedono le credenziali, con due controlli diversi perché +i chiamanti sono diversi (`src/lib/auth-route.server.ts`): + +- `apri-sondaggio` e `sollecita-presenze` → `richiediAdmin`: token della sessione Supabase + verificato con `auth.getUser`, poi ruolo `admin` letto da `user_roles`, la stessa fonte di + `src/lib/ruoli.ts` (DD-011). `401` senza token valido, `403` con token ma senza ruolo. Il + controllo viene **prima** della validazione dell'input, così la risposta non rivela + nemmeno se un evento esiste. +- `promemoria-palloni` → all'inizio `richiediSegreto`, con un segreto da cron; sostituito + subito dopo da `richiediAdmin` quando la route è diventata manuale (DD-025). + +`csi`, `push-config` e `push-subscribe` restano aperte: le chiama il browser prima del login, +dove qualsiasi segreto sarebbe pubblico. (`push-messaggio` esisteva per lo stesso motivo ed è +sparita con DD-026.) + +**Alternative scartate** + +- Un segreto condiviso anche per le due route dell'app → finirebbe nel bundle JavaScript, + cioè pubblico. +- Il middleware `requireSupabaseAuth` già presente nel repository → è + `createMiddleware({ type: "function" })`, protegge le server function di TanStack Start. In + CrAPP `createServerFn` non compare da nessuna parte: quel file non è mai stato eseguito, + e non si applica comunque alle route in `src/routes/api/`. +- Fidarsi dell'id evento come credenziale → è un timestamp in un URL condiviso. + +**Conseguenze** + +- Nessuna variabile d'ambiente da configurare: dopo DD-025 tutte e tre le route usano lo + stesso controllo sul ruolo. +- I due pulsanti dell'app mandano ora il token con `intestazioniAutenticate()` + (`src/lib/auth.ts`), letto al momento della chiamata e non da uno stato React, così non si + spedisce un token scaduto. +- `api.test.ts` non può più verificare la validazione dell'input di `sollecita-presenze`, + che ora sta dietro all'accesso: quel pezzo si è spostato in + `test/integration/permessi-route.test.ts`, che gira sullo stack locale perché ha bisogno + di utenti veri. + +**Riesame** +Se un giorno l'app userà `createServerFn`, il middleware già presente diventa la strada +naturale e questi controlli vanno riletti alla sua luce. diff --git a/docs/decisions/DD-025.md b/docs/decisions/DD-025.md new file mode 100644 index 0000000..061a06b --- /dev/null +++ b/docs/decisions/DD-025.md @@ -0,0 +1,54 @@ +# DD-025 — Il promemoria palloni lo manda l'admin, per un evento + +**Data:** 6 settembre 2026 +**Stato:** Accettata + +**Contesto** +`promemoria-palloni` era disegnata per un cron quotidiano: calcolava chi è di turno **oggi** e +gli mandava una push. DD-024 l'aveva chiusa con un segreto condiviso, coerente con quel +disegno. Ma nel repository non c'è nessun cron, e `docs/modules/palloni.md` lo annotava già +come «da verificare lato hosting»: nei fatti quel promemoria non è mai partito. Una route che +funziona e che nessuno chiama. + +Nel frattempo l'app aveva già il precedente giusto: `apri-sondaggio` è manuale fin dall'inizio +(«Nessun cron: l'invio è manuale», CHANGELOG). + +**Decisione** +Il promemoria lo fa partire un amministratore dal pulsante «Avvisa chi è di turno», dentro il +riquadro palloni della pagina evento. La route accetta un `eventoId` e avvisa i destinatari di +**quell'evento** — chi deve prendere i palloni e chi deve riportarli — invece della giornata +corrente. Il controllo di accesso diventa `richiediAdmin` come le altre due, e +`richiediSegreto` con la sua variabile `CRON_SEGRETO` spariscono. + +Il testo dell'avviso viaggia dentro la push, cifrato nel payload (DD-026): il service worker +lo mostra così com'è, quindi un avviso mandato il martedì per il sabato arriva col testo +giusto. + +**Alternative scartate** + +- Un pulsante «manda il promemoria di oggi» in Dashboard → rispecchia la route com'era, ma + premuto un martedì qualsiasi risponderebbe «inviate: 0» e sembrerebbe rotto. L'admin + ragiona per evento, non per giornata. +- Tenere il cron e configurarlo davvero → più lavoro, una variabile d'ambiente da gestire in + ogni ambiente, e nessuno l'aveva chiesto. Un pulsante che funziona batte uno scheduler che + non esiste. +- Calcolare il testo al volo sulla giornata corrente → funziona solo se l'avviso parte il + giorno stesso, cioè proprio il vincolo da cui volevamo uscire. + +**Conseguenze** + +- Il promemoria è ora una scelta consapevole di un admin, non un automatismo: se nessuno preme + il pulsante, non parte niente. È un passo indietro rispetto all'idea originale, ma un passo + avanti rispetto alla realtà, dove non partiva mai. +- `destinatariPromemoriaPalloni()` non è più usata dalla route ma resta in `palloni-core.ts`. +- Sparisce `CRON_SEGRETO`: nessuna variabile d'ambiente nuova da configurare in nessun + ambiente. + +**Riesame** +Se la squadra si accorge che l'admin si dimentica di premere il pulsante. A quel punto il cron +torna utile, e con lui il segreto: DD-024 descrive già come farlo. + +--- + +**Aggiornamento (30 settembre 2026)** — DD-040 affianca al pulsante un invio automatico a 12, 6 e 3 ore +prima dell'evento, per gli eventi con un incaricato confermato; il pulsante e la route restano. diff --git a/docs/decisions/DD-026.md b/docs/decisions/DD-026.md new file mode 100644 index 0000000..2009caf --- /dev/null +++ b/docs/decisions/DD-026.md @@ -0,0 +1,44 @@ +# DD-026 — Il testo della notifica viaggia dentro la push + +**Stato:** accettata · **Data:** settembre 2026 + +**Contesto** +Le push partivano senza payload: il service worker, appena svegliato, chiedeva a +`/api/public/push-messaggio` che cosa mostrare. Ad app aperta funzionava; ad app chiusa e +telefono bloccato non arrivava niente. È lo scenario per cui il browser dà al worker pochi +secondi di vita: una fetch verso un endpoint che interroga Supabase è esattamente ciò che non +riesce a chiudersi in tempo, e senza `showNotification` non compare nulla. `Urgency: high` e +un timeout di 3 secondi sul recupero del testo avevano attenuato il sintomo, non la causa. + +**Decisione** +Titolo e testo viaggiano cifrati nel corpo della push (`aes128gcm`, RFC 8188/8291) con le +chiavi `p256dh` e `auth` già salvate in `push_subscriptions`. Il service worker legge +`event.data.json()` e mostra la notifica: zero rete, zero attesa. + +Cadono di conseguenza la route `push-messaggio`, la coda `promemoria_push` (con la sua +scadenza a 12 ore), `messaggioPalloniOggi()` e il timeout nel service worker: tutti pezzi che +esistevano solo per rimediare al payload vuoto. Tutti e tre i mittenti +(`apri-sondaggio`, `sollecita-presenze`, `promemoria-palloni`) avevano già il testo pronto +prima di inviare. + +**Alternative scartate** + +- Usare la libreria `web-push` → dipende da `node:crypto`, e il build nitro ha come target + Cloudflare. La cifratura sta in ~40 righe di Web Crypto, già disponibile ovunque. +- Allungare ancora il timeout della fetch → allunga anche il tempo in cui il worker può + morire prima di mostrare qualcosa. Il problema era la fetch, non la sua durata. + +**Conseguenze** + +- La notifica non dipende più dalla rete al momento del risveglio, e nemmeno da una funzione + serverless che risponda in fretta a freddo. +- Sparisce l'unico punto in cui chiunque conoscesse un endpoint push poteva leggere il + messaggio destinato a quel dispositivo. +- Il payload sta sotto i 4 KB: i testi attuali sono ampiamente dentro, ma un messaggio molto + lungo andrebbe accorciato. +- La tabella `promemoria_push` resta nel database, ora inutilizzata: va rimossa con una + migrazione quando si tocca lo schema. + +**Riesame** +Se servisse mandare payload più grandi del limite del protocollo, o se un servizio push +smettesse di accettare corpi cifrati (nessuno lo fa: è lo standard). diff --git a/docs/decisions/DD-027.md b/docs/decisions/DD-027.md new file mode 100644 index 0000000..fc449b7 --- /dev/null +++ b/docs/decisions/DD-027.md @@ -0,0 +1,52 @@ +# DD-027 — Chi vota deve essere convocato, non solo autenticato come sé stesso + +**Stato:** accettata · **Data:** 8 settembre 2026 + +**Contesto** +Un audit del modulo Badge (`docs/modules/badge.md`) ha trovato due filtri rimasti solo +applicativi dopo DD-023: la policy di M11 garantisce che `votante_id` sia lo slot collegato +all'account di chi scrive, ma non controlla che **votante e votato fossero convocati** +all'evento — un utente che scrive direttamente su PostgREST (bypassando l'interfaccia) poteva +votare o essere votato in una partita a cui non aveva partecipato, gonfiando `mediaVoto`, +`mvp` o un badge social a piacere. Allo stesso modo, `eventi_app.pagelle_chiuse` nascondeva +solo i bottoni in UI: un voto pagella "fuori tempo" restava tecnicamente possibile. + +**Decisione** +La policy "Ognuno gestisce i propri voti ..." di `pagelle_voti`, `mvp_voti` e +`badge_social_voti` (M11) guadagna un controllo aggiuntivo tramite la funzione +`evento_permette_voto()` (migration `m13_convocati_e_pagelle_chiuse`): verifica che sia +`votante_id` sia `votato_id` compaiano in `eventi_app.convocati` per quel `match_id` +(`convocati` vuoto = tutta la rosa, la stessa convenzione di `convocatiEvento()` in +`eventi.ts`), e — solo per le pagelle — che `pagelle_chiuse` sia falso. Le policy admin +restano invariate e permissive: un amministratore deve poter correggere un voto anche fuori +convocazione o dopo la chiusura. + +Il controllo si ferma alla **convocazione**, non alla **presenza reale**: per l'MVP, ad +esempio, l'interfaccia limita già il voto ai soli presenti/in ritardo +(`usePresenzeEvento`), un filtro più stretto che resta solo applicativo — un convocato ma +assente passa ancora a livello database. Stringere fino a quel punto avrebbe richiesto +leggere `risposte_presenze` dentro la policy, un salto di complessità non giustificato +dall'audit che ha originato questa decisione. + +**Alternative scartate** + +- Un trigger `BEFORE INSERT/UPDATE` invece di RLS → si applicherebbe anche alla service key + e agli admin, bloccando correzioni legittime fuori convocazione; la RLS, applicata solo + alla policy non-admin, li esclude naturalmente. +- Controllare anche la presenza reale (`risposte_presenze`), non solo la convocazione → + scope maggiore del gap trovato in audit, e specifico dell'MVP (pagelle e badge social non + hanno un concetto di "presente" distinto da "convocato" nell'interfaccia attuale). + +**Conseguenze** + +- Un evento senza `convocati` esplicito (lista vuota, il caso più comune oggi) non cambia + comportamento: tutta la rosa resta votabile, come prima. +- I test di `test/integration/permessi.test.ts` sono la definizione eseguibile anche di + questa parte della tabella dei permessi (voti non convocati rifiutati, `pagelle_chiuse` + rifiutata a database, controlli positivi che provano che un voto legittimo passa ancora). +- Resta un gap conosciuto e documentato (non quello risolto qui): che il votante fosse + davvero presente, non solo convocato, per MVP/pagelle/badge social. + +**Riesame** +Se un giorno servisse bloccare anche il voto di un convocato-ma-assente a livello database, +non solo in UI. diff --git a/docs/decisions/DD-028.md b/docs/decisions/DD-028.md new file mode 100644 index 0000000..874ba0a --- /dev/null +++ b/docs/decisions/DD-028.md @@ -0,0 +1,50 @@ +# DD-028 — Soglia minima di campione per Media voto e MVP in home + +**Data:** 9 settembre 2026 +**Stato:** Accettata + +**Contesto** +Un audit della sezione «Colpo d'occhio» in home (`index.tsx`, StatTile Presenze/Media +voto/MVP) ha trovato che due delle tre statistiche non avevano nessun minimo campionario: +`mediePagelle()` calcola una media aritmetica pura, così un giocatore con un solo voto da 10 +mostrava "10" in home, più alto di un titolare con 40 voti e media 7.2 — lo stesso problema +che il badge Pagellone già risolve con `VOTI_MINIMI_PAGELLA` (badge.md), ma applicato solo al +badge, non alla StatTile home. Allo stesso modo `mvpVintiPerGiocatore()`/`vincitoriMvp()` +assegnavano un MVP di partita anche con un solo voto totale: bastava che un solo giocatore +votasse perché il votato "vincesse" nettamente, senza nessun quorum di partecipazione. + +**Decisione** + +- **Media voto** in home usa la stessa soglia del badge Pagellone: sotto `VOTI_MINIMI_PAGELLA` + (5) voti ricevuti, la StatTile mostra `—` invece della media, tramite la funzione pura + `mediaVotoColpoDOcchio()` (`pagelle.ts`), estratta dalla route per restare testabile (DD-020). + La funzione `mediePagelle()` non cambia: il filtro resta lato chiamante, come già faceva il + badge. +- **MVP**: `conteggioPartita`'s aggregazione, tramite `vincitoriMvp()` e + `mvpVintiPerGiocatore()`, richiede ora un quorum minimo di voti totali sulla partita + (`VOTI_MINIMI_MVP = 2`, `mvp-voti.ts`) prima di assegnare un vincitore, oltre alla regola già + esistente del margine netto tra primo e secondo. Un solo voto non basta più a incoronare + nessuno, nemmeno in assenza di concorrenza. + +**Alternative scartate** + +- Alzare la soglia MVP oltre 2 (es. metà dei convocati) → serve conoscere i convocati + dell'evento dentro una funzione che oggi lavora solo sui voti; complessità non giustificata + per il gap trovato in audit. +- Lasciare l'MVP senza quorum e limitarsi al fix della Media voto → il problema di fondo + (un numero esiguo di voti che decide una statistica mostrata come solida) resterebbe aperto + per l'MVP. + +**Conseguenze** + +- Alcuni MVP di partita già assegnati con un solo voto totale non contano più nel conteggio + `mvp` del giocatore: è una modifica retroattiva al dato mostrato, non solo al calcolo futuro, + perché `mvpVintiPerGiocatore()` deriva sempre il conteggio dai voti grezzi, senza storico + persistito a parte. +- `mediePagelle()` resta invariata: chi la chiama altrove (profilo, squadra) senza applicare la + soglia continua a mostrare la media grezza — non tocca questa decisione, resta il limite già + noto in [pagelle.md](../modules/pagelle.md). + +**Riesame** +Se la squadra segnala che il quorum di 2 voti per l'MVP è troppo permissivo o troppo severo, o +se si vuole applicare la stessa soglia di Media voto anche alle StatTile di profilo e squadra. diff --git a/docs/decisions/DD-029.md b/docs/decisions/DD-029.md new file mode 100644 index 0000000..52b25d9 --- /dev/null +++ b/docs/decisions/DD-029.md @@ -0,0 +1,77 @@ +# DD-029 — Cancellare un evento pulisce a cascata i dati collegati + +**Data:** 9 settembre 2026 +**Stato:** Accettata + +**Contesto** +Un audit del modulo Obiettivi ha verificato che gli obiettivi in sé non hanno bisogno di +nessuna pulizia quando un evento viene cancellato: sono ricalcolati a runtime sull'elenco +eventi corrente (`obiettivi.ts`), quindi un evento sparito da `eventi_app` semplicemente +smette di contare. Il problema è un livello sotto: `useEliminaEvento()` +(`src/lib/eventi.ts`) cancella solo la riga in `eventi_app`. Nessuna delle otto tabelle +collegate (`risposte_presenze`, `cacche_partita`, `mvp_voti`, `pagelle_voti`, +`badge_social_voti`, `turni_palloni`, `scout_sessioni`, `scout_live`, `scout_partite`) ha mai +avuto una foreign key verso `eventi_app(id)`: le loro righe restavano orfane a database. + +Oggi è innocuo per le statistiche, perché nessun calcolo legge quelle tabelle se non partendo +dall'elenco eventi corrente. Ma è un rischio latente: un id evento futuro identico a uno +passato (generato come `"e" + timestamp`, collisione improbabile ma non impossibile) +erediterebbe dati vecchi non suoi; e un calcolo futuro che iterasse direttamente una di quelle +tabelle, invece di partire da `eventi_app`, conterebbe anche le righe orfane. + +**Decisione** +Migration `m14_pulizia_dati_evento_cancellato`: un trigger `AFTER DELETE ON eventi_app` +cancella a cascata le righe corrispondenti (`evento_id`/`match_id = id evento cancellato`) +nelle otto tabelle collegate, tramite una funzione `SECURITY DEFINER`. Agisce a database, non +in `useEliminaEvento()`: protegge anche chi cancella un evento scrivendo direttamente su +PostgREST, non solo chi passa dal bottone dell'app. + +**Alternative scartate** + +- Foreign key con `ON DELETE CASCADE` verso `eventi_app(id)` → non applicabile subito: + `mvp.md` documenta che storicamente `match_id` in `mvp_voti`/`pagelle_voti`/ + `badge_social_voti` a volte conteneva l'id di una sessione Scout o di una partita CSI, non + l'id evento CrAPP. Un vincolo FK avrebbe rifiutato la migration alla prima riga storica + disallineata; il trigger non valida i dati esistenti, solo le cancellazioni da qui in avanti. +- Cancellazione manuale nelle otto tabelle dentro `useEliminaEvento()` → fragile: va tenuta + aggiornata a mano ogni volta che un nuovo modulo aggiunge una tabella con `evento_id`, e non + protegge chi scrive/cancella direttamente su PostgREST. +- Bonificare anche le righe orfane già esistenti nella stessa migration → rimandato: tocca dati + reali già scritti, è un intervento più delicato che merita una migration a sé, non urgente + perché quelle righe sono già invisibili a ogni calcolo attuale. + +**Conseguenze** + +- Da questa migration in poi, cancellare un evento (da qualunque punto, app o REST diretto) + ripulisce automaticamente tutte le tabelle collegate. +- Le righe orfane generate da cancellazioni **precedenti** a questa migration restano nel + database: il trigger previene il problema da qui in avanti, non ripulisce lo storico. +- `test/integration/pulizia-evento.test.ts` è la definizione eseguibile del comportamento: + scrive una riga in ciascuna delle otto tabelle, cancella l'evento e verifica che spariscano + tutte. + +**Aggiornamento (9 settembre 2026, stesso giorno)** — la bonifica dello storico prevista sopra +come "riesame" è stata fatta subito dopo, migration `m15_bonifica_dati_evento_orfani`: righe +orfane in `risposte_presenze`, `cacche_partita`, `turni_palloni`, `scout_sessioni`, +`scout_live`, `scout_partite` identificate confrontando `evento_id` con `eventi_app`. Per +`mvp_voti`/`pagelle_voti`/`badge_social_voti` il confronto si applica **solo** ai `match_id` +nel formato id evento CrAPP (`^e[0-9a-z]+$`, quello di `nuovoIdEvento()`): i vecchi voti su id +Scout (prefisso `s` + timestamp decimale) o CSI (numerico o `data-squadra-squadra`) non sono +orfani, sono dati storici legittimi mai collegati a un evento CrAPP (vedi contesto sopra), e la +migration non li tocca. Verificato manualmente sul database locale prima di applicarla: un voto +di test su id Scout è sopravvissuto alla bonifica, un voto di test su id evento CrAPP orfano è +stato rimosso. + +**Aggiornamento (9 settembre 2026)** — la verifica manuale di M15 non lasciava nessuna rete di +sicurezza automatica per il futuro, a differenza del resto del progetto (DD-020). Migration +`m16_funzione_bonifica_dati_evento_orfani` rende lo stesso corpo una funzione +`bonifica_dati_evento_orfani()` (riservata al `service_role`, non richiamabile dall'app), +coperta da `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. Non serve +richiamarla di nuovo ora (M15 ha già ripulito lo storico): resta pronta come intervento di +manutenzione se in futuro il trigger di M14 smettesse di funzionare o emergesse un altro batch +di orfani. + +**Riesame** +Se una nuova tabella collegata a un evento non viene aggiunta al trigger quando creata (va +aggiornata a mano, non c'è un meccanismo che lo forzi). diff --git a/docs/decisions/DD-030.md b/docs/decisions/DD-030.md new file mode 100644 index 0000000..b96fe18 --- /dev/null +++ b/docs/decisions/DD-030.md @@ -0,0 +1,71 @@ +# DD-030 — Centro notifiche in-app: storico separato dalla push, letto solo con RLS + +**Data:** 15 settembre 2026 +**Stato:** Accettata + +**Contesto** +Le notifiche esistenti (`docs/modules/notifiche.md`) sono solo push OS/browser: arrivano se +il dispositivo ha attivato l'interruttore in Profilo, sono effimere (nessuno storico) e non +hanno uno stato letto/non letto — chi non attiva le push, o cambia dispositivo, semplicemente +non le vede mai. Serviva un canale visibile sempre, dentro l'app, con un badge accanto al +profilo che segnali quante cose non lette ci sono. + +**Decisione** +Nuova tabella `notifiche_utente` (migration `m17_notifiche_utente`), popolata da quattro +sorgenti lato database — mai dal client: + +1. messaggio libero dell'admin (`notifica-personalizzata.ts`, in parallelo alla push); +2. promemoria automatico prima di un evento, 24h e 3h prima, due `cron.schedule` + (estensione `pg_cron`, abilitata dal 31/07/2026 e usata per la prima volta qui); +3. turno palloni (`promemoria-palloni.ts`), in parallelo alla push; +4. sollecito presenze (`sollecita-presenze.ts`), in parallelo alla push. + +Lettura e scrittura (segna come letta, elimina) passano dal client Supabase autenticato con +RLS (`useNotificheMie()`/`useSegnaLette()`/`useEliminaNotifica()` in +`src/lib/notifiche-utente.ts`), senza una route API dedicata — stesso pattern di +`useSalvaEvento()` in `eventi.ts`. Nessuna policy INSERT per `authenticated`: le righe nascono +solo da funzioni `SECURITY DEFINER` o dalla service role, mai da un client. + +Pallino con il conteggio sull'avatar del profilo (`PallinoNotifiche`, +`src/components/crapp/ui-bits.tsx`; in origine una campanella separata, tolta per liberare +l'header: il tap sull'avatar resta l'accesso al profilo, quello sul pallino apre il +pannello), pannello a comparsa costruito a mano (nel progetto non c'è una libreria dropdown). Ogni riga si elimina +con uno swipe o una ×: non c'è pulizia automatica delle notifiche vecchie, quindi è l'unico +modo per un giocatore di toglierle di torno per sempre. + +**Alternative scartate** + +- Notificare anche sulla sola creazione di un evento, non solo sull'avvicinarsi della data → + scartata su richiesta esplicita: conta quando l'evento sta per succedere, non il momento in + cui è stato messo in calendario. La prima versione aveva un trigger `AFTER INSERT ON +eventi_app` per questo, rimosso prima del rilascio. +- Una route API dedicata per leggere/segnare come lette (come le route push esistenti) → + scartata: qui non serve la service role (niente da nascondere al giocatore stesso), quindi + la RLS basta ed evita di duplicare la logica di lettura già scritta lato client per gli + eventi. +- Pulizia automatica delle notifiche vecchie con un cron di retention (es. elimina le lette + dopo 30 giorni) → scartata per ora a favore dello swipe manuale: più diretto da capire per + chi usa l'app, e la tabella resta comunque piccola per una sola squadra. Resta un'estensione + naturale se in futuro servisse. +- Semplice `INSERT` invece di `upsert` per turno palloni e sollecito presenze → scartata: un + admin che preme due volte lo stesso pulsante per lo stesso evento avrebbe urtato contro il + vincolo `UNIQUE (giocatore_id, evento_id, tipo)`. L'upsert aggiorna la riga esistente + (testo e `creato_il` freschi, `letta` di nuovo `false`), così ripremere il pulsante si + comporta come un rinvio vero, non un errore silenzioso. + +**Conseguenze** + +- Il pallino sull'avatar riflette solo ciò che sta in `notifiche_utente`: un dispositivo senza + push attive vede comunque tutto, perché il canale è del tutto indipendente. +- `eventi_app.ora` è testo libero e non validato dal form (`` si può + svuotare): la funzione `genera_promemoria_eventi()` usa `ora_evento_a_time()`, che assorbe + l'errore di cast riga per riga invece di far fallire l'intera generazione dei promemoria per + tutti gli eventi quando uno solo ha l'ora scritta male. +- `test/integration/notifiche-utente.test.ts` è la definizione eseguibile del comportamento: + promemoria 24h/3h e deduplica, tolleranza a un'ora malformata, RLS su lettura/segna + letta/elimina, e le tre route (`notifica-personalizzata`, `promemoria-palloni`, + `sollecita-presenze`) che scrivono lo storico in-app. + +**Riesame** +Se la tabella crescesse in modo sensibile (squadre molto più numerose, o giocatori che non +fanno mai swipe), va aggiunta la pulizia automatica scartata sopra. diff --git a/docs/decisions/DD-031.md b/docs/decisions/DD-031.md new file mode 100644 index 0000000..b360962 --- /dev/null +++ b/docs/decisions/DD-031.md @@ -0,0 +1,90 @@ +# DD-031 — Data di nascita pubblica: una colonna sincronizzata, non una seconda RLS aperta + +**Data:** 15 settembre 2026 +**Stato:** Accettata + +**Contesto** +In "Squadra" la data di nascita mostrava **"Invalid Date"** per chi l'aveva inserita da +solo dal proprio Profilo. Causa: due fonti mai collegate. I 17 giocatori del seed storico +hanno la nascita in `nascitaPerId`, una mappa statica pubblica bundlata nel client +(`src/lib/crapp-data.ts`) — il commento sulla riga la descriveva esplicitamente come +stopgap: _"`giocatori_squadra` non ha ancora questa colonna (DD-015 follow-up)"_. Un +giocatore aggiunto dopo la migrazione DD-015 inserisce invece la nascita dal form Profilo, +che scrive su `profili_giocatore.data_nascita` — tabella con RLS "solo il proprio profilo o +admin" (M2), mai letta da `useRosa()`/`useAnagraficaRosa()` (`src/lib/rosa.ts`), e comunque +non visibile ai compagni per via della RLS. Per quel giocatore `nascitaPerId[g.id]` era +`undefined` → `""` → `formatData("")` in `src/routes/squadra.tsx` produceva `Invalid Date`. + +**Decisione** +Migration `m18_nascita_pubblica_giocatori_squadra`: nuova colonna `giocatori_squadra.nascita` +(quella prevista da DD-015), pubblica a tutta la rosa attiva con la stessa policy di lettura +già in vigore per le altre colonne. Un trigger `SECURITY DEFINER` su `profili_giocatore` +(`sincronizza_nascita_pubblica`, AFTER INSERT/UPDATE/DELETE) la tiene allineata a +`data_nascita`: un solo input nel form Profilo (`CampiProfilo`), due destinazioni — quella +amministrativa esistente (RLS invariata: sé stesso o admin) e questo specchio pubblico. +`profili_giocatore` resta l'unica tabella che il client scrive per la nascita; il client non +tocca mai `giocatori_squadra.nascita` direttamente. + +Essendo `SECURITY DEFINER` di proprietà dell'owner della tabella, la scrittura del trigger +bypassa la RLS di `giocatori_squadra` automaticamente — nessuna nuova policy lì. Serve però +un nuovo ramo in `enforce_giocatori_squadra_update()` (il trigger `BEFORE UPDATE` che oggi +rifiuta qualunque modifica a uno slot già collegato da parte di chi non è admin): accetta un +update dove cambia **solo** `nascita` e il chiamante è il proprietario dello slot +(`OLD.auth_user_id = auth.uid()`). Senza quel ramo l'update annidato del trigger di sync +fallirebbe con l'eccezione già in vigore. + +Backfill una tantum dei 17 storici con gli stessi valori di `nascitaPerId`, poi rimozione di +quell'export (`crapp-data.ts`): era descritto fin dall'inizio come temporaneo, in attesa +esattamente di questa colonna. + +**Alternative scartate** + +- Allentare la RLS di `profili_giocatore` per rendere `data_nascita` leggibile a tutta la + squadra → scartata: quella tabella porta anche indirizzo, telefono, documento d'identità + nello stesso record — aprirne la lettura avrebbe esposto molto più della sola nascita. +- Un secondo campo "data di nascita pubblica" nel form Profilo, distinto da quello + amministrativo → scartata: due campi per lo stesso dato reale avrebbero richiesto + spiegare all'utente perché ne esistono due, con rischio di disallineamento se ne compila + uno solo. +- Far scrivere `giocatori_squadra.nascita` direttamente dal client (nuova RLS policy per il + proprietario dello slot) invece di sincronizzarla da `profili_giocatore` → scartata: la + RLS di update per un non-admin resta `USING (auth_user_id IS NULL)` (solo reclamare uno + slot libero); aprirla anche a righe già collegate avrebbe ampliato la superficie scrivibile + dal client su una tabella con un trigger di sicurezza già delicato (DD-016/DD-023), per un + guadagno che il trigger di sync ottiene comunque senza toccare la RLS. + +**Conseguenze** + +- Chiunque compili la propria data di nascita dal Profilo la vede comparire in Squadra (e + nei compleanni del Calendario) esattamente come i 17 storici — non più un dato "di serie + B". +- Cancellare il profilo (da sé o da un admin) azzera anche la nascita pubblica: non resta un + valore pubblico orfano di un profilo che non esiste più. +- Un giocatore senza nascita nota (profilo mai compilato) non genera più "Invalid Date": + `squadra.tsx` mostra la riga della data di nascita solo se `g.nascita` è valorizzata, + stesso criterio già usato da `compleanniEventi()` per escludere chi non ha ancora un dato. +- `test/integration/nascita-pubblica.test.ts` è la definizione eseguibile del comportamento: + sync alla scrittura, visibilità a un giocatore terzo (non solo l'interessato o l'admin), + azzeramento alla cancellazione, blocco della scrittura diretta dal client. + +**Riesame** +Se in futuro serve differenziare "chi può vedere la nascita" (es. solo squadra, non +pubblico) va rivista insieme alla policy di lettura generale di `giocatori_squadra`, non solo +per questa colonna. + +**Aggiornamento (15 settembre 2026, stesso giorno)** — dopo aver applicato M18 in produzione +(`supabase db push`), la nascita in Squadra compariva solo per i 17 giocatori storici, non +per chi aveva già compilato il proprio profilo prima della migration: il trigger di sync +copre solo le scritture _future_ su `profili_giocatore`, non retroattivamente, e M18 +backfillava a mano solo il seed storico. Migration `m19_backfill_nascita_da_profili_esistenti` +allinea una tantum `giocatori_squadra.nascita` a `profili_giocatore.data_nascita` per tutti i +profili già esistenti, con la stessa disattivazione temporanea del trigger di sicurezza usata +in M18. Da qui in poi il gap non si può ripresentare: ogni scrittura successiva passa dal +trigger di sync, questo backfill serve solo per lo storico antecedente a M18. + +A differenza della bonifica eventi orfani (DD-029, M15→M16), qui il backfill non è stato +reso una funzione richiamabile: resta SQL diretto nella migration. La copertura di test +(`test/integration/backfill-nascita-esistenti.test.ts`) verifica comunque lo stesso +comportamento eseguendo lo stesso pattern SQL via `psql` nel container Postgres locale — +scelta necessaria perché senza una funzione RPC, PostgREST non espone un update +multi-tabella come questo. diff --git a/docs/decisions/DD-032.md b/docs/decisions/DD-032.md new file mode 100644 index 0000000..8349503 --- /dev/null +++ b/docs/decisions/DD-032.md @@ -0,0 +1,40 @@ +# DD-032 — La stagione CSI si ricava dalle date delle gare + +**Data:** 24 settembre 2026 +**Stato:** Accettata + +**Contesto** +La pagina Campionato (`/classifica`) non diceva a quale stagione si riferissero classifica e +storico partite. Il portale CSI non espone il nome della stagione da nessuna parte: +`project-sheets.php`, `league_details.php`, `team-main.php` e il feed +`getEventsByTeamId.php` riportano solo il nome della competizione (verificato il +24/09/2026). Il `project_id` cambia a ogni stagione ed è aggiornato a mano in +`csi-core.ts`. + +**Decisione** +`stagioneDi()` in `src/lib/csi-core.ts` ricava la stagione ("2025/26") dalla data di una +gara, con l'anno che cambia ad agosto; `stagioneDaPartite()` usa la prima gara del +campionato letto (tutte le gare, non solo quelle giocate, così a inizio stagione basta il +calendario). L'header di `/classifica` mostra quella stagione, e lo storico partite è +diviso per stagione con `raggruppaPerStagione()`: le gare dello Scout Live locale, usate +quando il CSI non ne ha, possono essere di stagioni diverse. + +**Alternative scartate** + +- Una costante `CSI_STAGIONE` accanto a `CSI_PROJECT_ID` → un secondo valore da ricordarsi + di aggiornare a ogni stagione, che può divergere dal `project_id`; ricavata dai dati + segue da sola. +- Usare la stagione "in corso" per la squadra (la data di oggi) → sbaglierebbe proprio nel + periodo di passaggio: a settembre 2026 il CSI mostra ancora i dati 2025/26. + +**Conseguenze** + +- Oggi Campionato mostra «Stagione 2025/26» mentre Squadra e Calendario, che hanno la + stagione scritta nel codice, mostrano «Stagione 2026/27»: il CSI non ha ancora creato le + pagine della nuova stagione. È voluto — Campionato riporta la stagione dei dati che + mostra (dettagli in [collegamento-csi.md](../modules/collegamento-csi.md)). +- Quando si aggiornano i `project_id` l'etichetta si aggiorna senza toccare altro codice. + +**Riesame** +Se il CSI iniziasse a esporre il nome della stagione, leggerlo da lì; se una stagione CSI +dovesse iniziare prima di agosto, spostare la soglia in `stagioneDi()`. diff --git a/docs/decisions/DD-033.md b/docs/decisions/DD-033.md new file mode 100644 index 0000000..d95381f --- /dev/null +++ b/docs/decisions/DD-033.md @@ -0,0 +1,35 @@ +# DD-033 — Gestione eventi: solo calendario, giorno fissato dal tocco + +**Data:** 24 settembre 2026 +**Stato:** Accettata + +**Contesto** +`/eventi` mostrava sotto la griglia mensile anche la lista cronologica di tutti gli eventi +("Eventi in calendario"). La lista non aveva azioni proprie (toccare una riga apriva lo +stesso drawer del giorno) e allungava la pagina con informazioni già nel calendario. Nel +form, poi, il campo «Data» era già precompilato con il giorno toccato ma sembrava da +compilare: l'amministratore doveva scegliere solo l'ora. + +**Decisione** +La pagina mostra solo il calendario, che si scorre di mese in mese con le frecce o con lo +swipe (stessa fisica a molla di `/calendario`, disattivata con `useMotoRidotto()`). Nel form +il giorno compare come testo fisso e resta da scegliere l'ora; il campo data compare solo +toccando «Cambia», per spostare un evento in un altro giorno. + +**Alternative scartate** + +- Togliere del tutto il campo data → non si potrebbe più spostare un evento esistente, una + funzione che oggi c'è (regola anti-regressione). +- Tenere la lista ma più corta (solo i prossimi eventi) → i prossimi eventi sono già in home + e in `/calendario`; qui sarebbe stata un doppione. + +**Conseguenze** + +- Per trovare un evento lontano bisogna scorrere fino al suo mese; il totale degli eventi + resta nel sottotitolo dell'header. +- Caricamento ed errore di `useEventi()` compaiono dentro la card del calendario, dove prima + stava la lista. + +**Riesame** +Se gli eventi crescono tanto da rendere scomodo lo scorrere dei mesi, valutare una ricerca +per titolo invece di reintrodurre la lista completa. diff --git a/docs/decisions/DD-034.md b/docs/decisions/DD-034.md new file mode 100644 index 0000000..75609af --- /dev/null +++ b/docs/decisions/DD-034.md @@ -0,0 +1,51 @@ +# DD-034 — L'allenatore è uno slot della squadra con tipo diverso, non un giocatore + +**Data:** 24 settembre 2026 +**Stato:** Accettata (implementata con le migration M20 e M21) + +**Contesto** +Serve un account per l'allenatore: deve gestire gli eventi (oggi solo admin, `m11`) ma non è +un giocatore. Non ha statistiche, badge né stagione, non vota e non viene votato. L'admin +vuole registrarlo come i giocatori, inserendo la sua email Gmail. Specifica completa in +[modules/allenatore.md](../modules/allenatore.md). + +**Decisione** +L'allenatore è una riga di `giocatori_squadra` con una nuova colonna `tipo = 'allenatore'` +(default `'giocatore'`). Così riusa senza cambiarli il collegamento per email (DD-018), +`profili_giocatore` per i dati personali, il bucket avatar e il compleanno sincronizzato +(DD-031). Il permesso di gestire gli eventi arriva da un nuovo valore `allenatore` di +`app_role` in `user_roles` (DD-011), scritto da una funzione `SECURITY DEFINER` quando si +collega uno slot di tipo allenatore: è quindi l'admin a concederlo, registrando lo slot. +"Rosa" diventa `attivo e tipo = 'giocatore'` ovunque. + +**Alternative scartate** + +- Una tabella `staff` separata → isola l'allenatore da voti e rosa senza toccare i filtri, ma + duplica collegamento per email, profilo, avatar e form admin. Resta l'alternativa se i + filtri sulla rosa si rivelano troppi o fragili. +- Registrare l'allenatore come admin → gli darebbe dati personali e documenti di tutta la + squadra (DD-017), molto più del necessario. +- Distinguere l'allenatore dal campo `ruolo` (ruolo in campo) → mescola due concetti e il + campo è testo libero. + +**Conseguenze** + +- Ogni lettura della rosa (client, server, SQL di voto e promemoria) va filtrata per `tipo`: + un punto dimenticato rende l'allenatore votabile o lo conta tra i convocati. L'elenco sta + nella specifica e va coperto dai test di `permessi.test.ts`. +- Il trigger sugli slot cambia: `tipo` lo scrive solo l'admin; nome e cognome li scrive anche + l'allenatore sul proprio slot. +- `numero` diventa nullabile a database, ma solo per gli allenatori (vincolo di M21); nell'app + resta un numero, 0 per l'allenatore, per non toccare i tipi di tutta la rosa. +- Due fonti distinte, ciascuna per il suo scopo: i permessi (eventi, sollecito) da + `user_roles`, la visibilità (badge, cacche, stagione) dal tipo del proprio slot, che è + immediato e non aspetta la query dei ruoli. +- L'allenatore non vede le tab Stagione e Badge del Profilo, né Obiettivi e Badge di Squadra. +- In Squadra compare nella tab Rosa con «Allenatore» al posto del ruolo, ricavato da `tipo`; + riceve i promemoria degli eventi (dal 29/09/2026 non più: DD-039) e può sollecitare le presenze, + non il turno palloni. +- Non vede badge né classifiche o sondaggi sulle cacche, in nessuna schermata. + +**Riesame** +Se arriva un secondo tipo di staff (dirigente, segreteria) o se i filtri sulla rosa diventano +fonte di bug, passare alla tabella `staff` separata. diff --git a/docs/decisions/DD-035.md b/docs/decisions/DD-035.md new file mode 100644 index 0000000..31c474c --- /dev/null +++ b/docs/decisions/DD-035.md @@ -0,0 +1,57 @@ +# DD-035 — Avviso certificati: calcolato in Home, visibile solo al titolare e agli admin + +**Data:** 24 settembre 2026 +**Stato:** Accettata (implementata, versione 1.1.0) + +**Contesto** +Il certificato medico ha una data di scadenza (`profili_giocatore.certificato_scadenza`), ma +oggi ci si accorge che è scaduto solo aprendo la tab Profili di `/admin`: spesso quando il +giocatore è già fuori regola. La richiesta è un avviso in Home, giallo nei 7 giorni prima +della scadenza e nero dopo, con i nomi dei giocatori, che sparisca da solo quando il +certificato viene rinnovato. Specifica completa in +[modules/profilo-giocatore.md](../modules/profilo-giocatore.md#avviso-certificati). + +**Decisione** + +1. L'avviso si **calcola al volo** dalla data già salvata, con una funzione pura in + `profili-core.ts`: nessuna tabella, nessuno stato «avvisato», nessuna migration. +2. Lo vedono **solo gli admin**, con i nomi di tutta la rosa, e **il giocatore interessato**, + riferito solo a sé. L'allenatore e i compagni non lo vedono. +3. L'avviso dello staff **non è cliccabile**; quello personale apre + `/profilo?tab=documenti`. +4. Soglia fissa di **7 giorni** in una costante; il giorno della scadenza il certificato vale + ancora (come `statoScadenza()`). +5. **Niente push**: l'avviso vive solo in Home. + +**Alternative scartate** + +- Mostrarlo anche all'allenatore → il certificato è un dato sanitario che oggi l'allenatore + non vede (DD-034); servirebbe una funzione `SECURITY DEFINER` apposta per un'informazione + che all'allenatore non serve ad agire. +- Salvare gli avvisi in una tabella o nel centro notifiche (DD-030) → uno stato da tenere + allineato alla data, che si disallinea appena il giocatore corregge la scadenza; calcolarlo + è gratis e sempre giusto. +- Avviso dello staff cliccabile verso `/admin?tab=profili` → l'admin non può caricare il file + al posto del giocatore (DD-017), quindi il tocco non gli farebbe fare nulla in più, e + richiederebbe di aggiungere a `/admin` la lettura della tab dall'URL. +- Mostrare al massimo alcuni nomi e un «e altri N» → a inizio stagione ne scadono molti + insieme ed è proprio lì che l'elenco completo serve. +- Push quotidiana a chi ha il certificato in scadenza → servirebbe un cron, che oggi non + esiste per nessuna push (DD-025), e le push vanno riservate agli eventi importanti + ([EFFICIENZA_CLOUD.md](../EFFICIENZA_CLOUD.md)). +- Includere i certificati mancanti → è un problema diverso (profilo incompleto), già coperto + dal widget «Completa il tuo profilo» e dalla tab Profili. + +**Conseguenze** + +- Nessuna query nuova: si riusa `useProfili()`, già in cache per Profilo e `/admin`, e le RLS + esistenti decidono quanti profili arrivano (tutti all'admin, il proprio al giocatore). +- Il rinnovo non richiede nessuna azione di chiusura: cambiata la data, l'avviso si aggiorna + da solo. +- Chi non apre l'app non viene avvisato: l'avviso raggiunge solo chi entra in Home. +- Un admin che è anche giocatore vede solo l'avviso dello staff, dove compare il suo nome. + +**Riesame** +Se gli avvisi in Home non bastano (certificati che scadono comunque senza essere rinnovati), +valutare una push, legata a un cron o a un invio manuale dell'admin come per i palloni. Se 7 +giorni si rivelano pochi per prenotare una visita, alzare la costante o renderla regolabile. diff --git a/docs/decisions/DD-036.md b/docs/decisions/DD-036.md new file mode 100644 index 0000000..4a6db8d --- /dev/null +++ b/docs/decisions/DD-036.md @@ -0,0 +1,76 @@ +# DD-036 — Notifiche email via Gmail, da un worker Docker sull'host di casa + +**Data:** 29 settembre 2026 +**Stato:** Accettata + +**Contesto** +Le notifiche di CrAPP arrivano solo con la push (che dipende da Chrome vivo in background, +vedi i limiti in `docs/modules/notifiche.md`) e nel centro notifiche in-app, che si vede solo +aprendo l'app. Chi non ha attivato la push o ha un telefono aggressivo sul risparmio +energetico non riceve nulla ad app chiusa. Serve un canale che arrivi comunque, **gratis**, ai +giocatori registrati, che sono tutti su Gmail perché l'accesso è con Google. L'app gira su +Vercel, che non esegue container né processi sempre attivi. + +**Decisione** +Le email partono da un **worker Docker sull'host di casa** che legge una coda su Supabase e +invia tramite `smtp.gmail.com` con un account Gmail dedicato e una password per app. + +- La coda è `notifiche_email_coda`, riempita da un trigger su `notifiche_utente`: ogni + notifica già esistente, di qualunque tipo e per qualunque destinatario (admin compresi, + con la stessa logica degli altri), diventa anche una mail. Le sorgenti di M17 non cambiano, + non ne nascono di nuove e l'app non conosce le email. Il certificato in scadenza resta + fuori: è una card in Home, non una notifica (DD-035). +- Il worker fa solo polling **in uscita** con la service role: nessuna porta aperta sull'host. +- La coda è una tabella a parte, con RLS senza policy, perché la policy `UPDATE` di + `notifiche_utente` lascerebbe a un client la possibilità di rimettere in coda una mail. +- Canale **opt-out**: acceso di default per tutti, con un interruttore per account in + Profilo → Opzioni. La preferenza sta in una tabella nuova, `preferenze_utente`, una riga + per account (non per slot); l'assenza di riga vale «acceso». +- Specifica completa in [notifiche-email.md](../modules/notifiche-email.md); installazione e + gestione in [WORKER_EMAIL.md](../WORKER_EMAIL.md). + +**Alternative scartate** + +- Inviare direttamente dal proprio IP con un MTA (Postfix) → scartata: porta 25 bloccata dai + provider residenziali, IP nelle blocklist, niente reverse DNS; le mail finirebbero in spam o + sarebbero rifiutate. +- Relay Brevo o Resend → scartati per ora: il piano gratuito funziona ma per una buona + consegna richiede un dominio con SPF/DKIM/DMARC, che oggi non c'è. Restano l'alternativa + se servirà un mittente della squadra. +- Supabase Edge Function con `pg_cron` e un relay → scartata su richiesta esplicita di un + servizio dockerizzato sull'host; resta la strada senza host acceso, ma dipende comunque da un + relay esterno. +- Vercel Cron → scartato: sul piano gratuito gira al massimo una volta al giorno, troppo poco + per i promemoria a 24h e 3h. +- Chiamare l'host dall'app (webhook verso casa) → scartato: richiederebbe un IP pubblico o un + tunnel e una superficie d'attacco in più; il polling in uscita non ne ha. +- Colonne di stato sulla riga di `notifiche_utente` → scartate per la RLS spiegata sopra. + +**Conseguenze** + +- Costo zero, ma il canale email dipende da un host acceso: se è spento le mail arrivano in + ritardo (mai perse). Consegna _almeno una volta_: in caso di crash tra invio e + registrazione può arrivare un doppione. +- Il tetto di circa 500 destinatari al giorno di Gmail è largo per una squadra, ma è un limite + reale: con molte squadre o riepiloghi frequenti andrebbe rivisto. +- Nuova migration (`m22_notifiche_email`), nuova cartella `mailer/` (worker bun con la sola + dipendenza `nodemailer`, separato dall'app) e un segreto in più (la service role sull'host). + L'app non guadagna nessuna dipendenza e le route di M17 non cambiano. +- Il certificato in scadenza resta fuori dalle email: non è una notifica (DD-035), quindi non + passa da `notifiche_utente`. Renderlo una notifica sarebbe una decisione a parte. +- Il tetto giornaliero è una finestra mobile di 24 ore, `MAIL_LIMITE_GIORNO` (400 di default, + sotto i circa 500 di Gmail). +- Le email dei giocatori si usano per un secondo scopo oltre al login: da qui l'opt-out e + l'indicazione in ogni mail su come disattivarlo. +- Una tabella in più, `preferenze_utente`, pensata per accogliere in futuro anche le + preferenze per tipo di notifica senza toccare `giocatori_squadra`. +- In produzione dal 29/09/2026 (migration e worker su un Raspberry Pi). Dalla prova reale: la + prima mail, da un account Gmail nuovo, è arrivata ma nella cartella spam, e la password per app + non era disponibile finché l'account aveva le sole passkey come secondo passaggio (dettagli in + [WORKER_EMAIL.md](../WORKER_EMAIL.md#account-gmail-e-password-per-app)). + +**Riesame** +Se serve un mittente col dominio della squadra, se le mail finiscono in spam anche dopo qualche +giorno di uso, se si supera il tetto giornaliero di Gmail, o se l'host di casa si rivela poco +affidabile: passare a un relay con dominio e, in quel caso, valutare Edge Function più `pg_cron` +al posto del container. diff --git a/docs/decisions/DD-037.md b/docs/decisions/DD-037.md new file mode 100644 index 0000000..83eb71c --- /dev/null +++ b/docs/decisions/DD-037.md @@ -0,0 +1,55 @@ +# DD-037 — I promemoria evento ricordano di essere già stati generati + +**Data:** 29 settembre 2026 +**Stato:** Accettata + +**Contesto** +I promemoria a 24 e a 3 ore (M17, DD-030) evitavano i doppioni con `ON CONFLICT DO NOTHING` sul +vincolo `UNIQUE (giocatore_id, evento_id, tipo)` di `notifiche_utente`: la riga della notifica +era l'unica traccia che il promemoria fosse già stato generato. Il giocatore però può eliminarla +(swipe o ×, DD-030). Senza più la riga, al giro successivo del job (ogni ora per il promemoria a +24 ore, ogni 15 minuti per quello a 3 ore, finché l'evento non inizia) il promemoria veniva +rigenerato come non letto. Con il canale email (DD-036) ogni rigenerazione accodava anche una +mail nuova: un giocatore che eliminava un promemoria ne riceveva una ogni ora fino all'inizio +dell'evento. Il difetto è stato trovato controllando la logica il 29/09/2026, il giorno in cui il +worker email è entrato in produzione, e riprodotto sul database locale. + +**Decisione** +Un registro separato, `promemoria_eventi_generati` (migration `m23_promemoria_gia_generati`), con +una riga per `(giocatore_id, evento_id, tipo)` scritta dalla funzione `genera_promemoria_eventi()`. +La notifica in-app si crea solo per le righe **appena registrate**, quindi eliminarla non fa +ripartire nulla. Il registro è indipendente da `notifiche_utente`, senza policy per i client, e +sparisce con l'evento o con il giocatore (`ON DELETE CASCADE`). I promemoria già presenti al +momento della migration sono stati registrati come generati, altrimenti la prima notifica +eliminata dopo la migration sarebbe tornata come prima. Il vincolo `UNIQUE` di `notifiche_utente` +resta come seconda difesa. Cron, criteri di selezione, testi e destinatari sono invariati. + +**Alternative scartate** + +- Non eliminare davvero la notifica (nasconderla con un flag) → scartata: cambia il significato + dell'eliminazione per tutte le sorgenti, tocca la RLS e le query del client, e la tabella + crescerebbe di righe che il giocatore non vede più. +- Togliere l'eliminazione dai soli promemoria, lasciando «segna come letta» → scartata: a un + giocatore che vuole ripulire l'elenco resterebbero notifiche non eliminabili, incoerenti con + le altre. +- Un flag «già generato» sulla riga dell'evento → scartata: il promemoria è per giocatore, e chi + viene aggiunto ai convocati dopo deve comunque riceverlo. +- Confrontare `creato_il` o lo stato «letta» per capire se ricrearlo → scartata: non distingue + una notifica mai generata da una eliminata. + +**Conseguenze** + +- Eliminare un promemoria lo fa sparire per sempre, e non parte nessuna mail in più. Il numero + massimo di mail automatiche per giocatore ed evento resta due (24 e 3 ore). +- Una tabella in più, che cresce di una riga per giocatore, evento e tipo e si svuota con + l'evento. +- Se un evento viene spostato dopo che il promemoria è partito, non ne parte uno nuovo per la + data nuova: è lo stesso comportamento di prima, ora esplicito. +- `test/integration/promemoria-eventi.test.ts` è la definizione eseguibile: il promemoria + eliminato non ricompare, non riaccoda mail, i due tipi sono indipendenti, un convocato aggiunto + dopo lo riceve comunque, il registro non è leggibile né scrivibile da un client e sparisce con + l'evento. Sul database con la funzione vecchia il test fallisce. + +**Riesame** +Se gli eventi potessero essere spostati con frequenza e servisse un nuovo promemoria per la data +nuova, il registro va agganciato anche alla data di inizio, oltre a giocatore, evento e tipo. diff --git a/docs/decisions/DD-038.md b/docs/decisions/DD-038.md new file mode 100644 index 0000000..d48301c --- /dev/null +++ b/docs/decisions/DD-038.md @@ -0,0 +1,68 @@ +# DD-038 — Ogni notifica ha tre canali: la push dei promemoria parte dal worker + +**Data:** 29 settembre 2026 +**Stato:** Accettata + +**Contesto** +Il requisito è che ogni notifica arrivi su tutti e tre i canali: push, centro notifiche in-app ed +email. Due casi non lo rispettavano ([catalogo](../modules/notifiche.md#catalogo-delle-notifiche)): + +- i **promemoria a 24 e 3 ore** nascono dentro il database (job `pg_cron`, M17), che non può + firmare né cifrare una push Web (VAPID, RFC 8291): arrivavano solo in-app e per email; +- il **sondaggio pre-partita** partiva solo come push e non scriveva in `notifiche_utente`, quindi + non compariva in-app e, dato che le email nascono da quella tabella (DD-036), non arrivava per + email. + +Le altre notifiche (messaggio dello staff, turno palloni, sollecito presenze) partono dall'app su +Vercel e avevano già i tre canali. + +**Decisione** + +1. **Sondaggio pre-partita:** la route `apri-sondaggio` scrive anche `notifiche_utente` (tipo + nuovo `sondaggio_cacche`, un `upsert` per giocatore ed evento come le altre), per i giocatori + attivi che non sono allenatori. La mail parte dal trigger di M22 senza altro lavoro. La push + resta com'è. Il vincolo `CHECK` sul tipo si estende con la migration `m24`. +2. **Push dei promemoria:** la manda il **worker `mailer/`**, con un secondo canale accanto alle + email. Un trigger accoda ogni promemoria a 24 o 3 ore in `notifiche_push_coda` (stessa + struttura di `notifiche_email_coda`); il worker prende il lotto con `FOR UPDATE SKIP LOCKED`, + manda la push a tutti i dispositivi iscritti del giocatore e registra l'esito. Usa **lo stesso + modulo dell'app** (`src/lib/webpush.server.ts`, senza dipendenze) invece di una copia: il + container si costruisce dalla radice del repository. Le iscrizioni che rispondono 404 o 410 + vengono eliminate, come nelle route. Le push in coda da oltre 3 ore si scartano: un promemoria + arrivato a evento iniziato è solo rumore. +3. La push dei promemoria non dipende dall'interruttore «Email»: dipende solo dai dispositivi + iscritti, come le altre push. + +**Alternative scartate** + +- Far chiamare all'app dal database (`pg_net`, già abilitata) una route che manda la push → + scartata: il database dipenderebbe dall'indirizzo pubblico di Vercel e da un segreto condiviso, + e `pg_net` è un'estensione del provider (regola 4 di [PORTABILITA.md](../PORTABILITA.md)). +- Vercel Cron → scartato: sul piano gratuito gira una volta al giorno, come già notato in DD-036. +- Copiare il modulo push nel worker → scartata: due versioni da tenere allineate per codice che + cifra e firma. +- Mandare dal worker la push di tutte le notifiche → scartata: le altre partono già dall'app, si + avrebbero push doppie. +- Un unico stato di coda per email e push → scartata: le due consegne hanno esiti, tentativi e + destinatari diversi (una push per dispositivo, un'email per giocatore). + +**Conseguenze** + +- Il worker non fa più solo email: gestisce due canali (il documento operativo resta + [WORKER_EMAIL.md](../WORKER_EMAIL.md)). Serve la chiave VAPID privata anche sull'host, un segreto in + più. Senza le chiavi il worker parte lo stesso e manda solo le email, segnalandolo nel log. +- La push dei promemoria dipende dall'host di casa acceso, come le email: se è spento parte alla + ripartenza, ma non oltre le 3 ore dalla creazione. +- Una tabella nuova (`notifiche_push_coda`) e una migration (`m24_push_promemoria_e_sondaggio`). +- Il sondaggio compare nel centro notifiche e arriva per email ai giocatori attivi, non agli + allenatori; la push continua ad andare a tutti i dispositivi iscritti tranne quelli degli + allenatori, anche di giocatori non più attivi. +- Le altre notifiche non cambiano canali e non ricevono push doppie. + +**Aggiornamento (30 settembre 2026)** — DD-040 esclude dalle email le notifiche del turno palloni: per +quei tipi i canali sono due (push e in-app). + +**Riesame** +Se l'host di casa si rivela poco affidabile per una notifica sensibile al tempo come la push, o se +il database dovesse comunque chiamare l'app per altro, valutare di nuovo la chiamata dal database +all'app. diff --git a/docs/decisions/DD-039.md b/docs/decisions/DD-039.md new file mode 100644 index 0000000..ee48142 --- /dev/null +++ b/docs/decisions/DD-039.md @@ -0,0 +1,40 @@ +# DD-039 — L'allenatore non riceve i promemoria degli eventi + +**Data:** 29 settembre 2026 +**Stato:** Accettata + +**Contesto** +DD-034 stabilì che l'allenatore riceve i promemoria a 24 e 3 ore prima di un evento, sempre, convocato +o no (`giocatori_destinatari_evento()` riscritta da M21). Ma un promemoria serve a ricordare di +esserci, e l'allenatore non può rispondere alle presenze, non è convocabile e non compare tra i +partecipanti (DD-034). L'incoerenza si vede negli eventi extra-campo, come una cena di squadra: +riceveva l'avviso (in-app, per email e, dopo M24, come push) senza poter dire se ci sarebbe stato né +comparire nell'elenco. + +**Decisione** +I destinatari dei promemoria sono i **giocatori attivi**: i convocati, o tutta la rosa se +`convocati` è vuoto. L'allenatore non li riceve, per nessun tipo di evento. Cambia solo la funzione +SQL (migration `m25_promemoria_solo_giocatori`, `g.tipo = 'giocatore'` al posto di «o allenatore +sempre»); cron, criteri di selezione, registro dei promemoria (DD-037), code email e push e testi +restano invariati. + +**Alternative scartate** + +- Toglierli solo per gli eventi extra-campo → scartata su indicazione esplicita di toglierli per + gli eventi in generale: anche per un allenamento o una partita l'allenatore non è tra i + partecipanti. +- Farlo rispondere agli eventi extra-campo, così da tenere il promemoria → scartata: servono + risposte presenze e regole del database per una categoria nuova, e il conteggio dei presenti. + +**Conseguenze** + +- Le notifiche che l'allenatore riceve dal server sono i **messaggi dello staff** e nient'altro + (sollecito, turno palloni, sondaggio e notifiche smart non lo riguardano già). +- I promemoria già generati per un allenatore prima della migration non si toccano. +- Un allenatore non è avvisato dell'inizio di un evento che gestisce: sa quando è perché lo ha + creato lui o lo vede in Calendario. +- Sostituisce la parte di DD-034 sui promemoria; il resto di DD-034 resta valido. + +**Riesame** +Se l'allenatore dovesse poter partecipare a certi eventi (per esempio la cena), si riapre insieme +alla sua possibilità di rispondere. diff --git a/docs/decisions/DD-040.md b/docs/decisions/DD-040.md new file mode 100644 index 0000000..7feb8f3 --- /dev/null +++ b/docs/decisions/DD-040.md @@ -0,0 +1,95 @@ +# DD-040 — Promemoria con «oggi» o «domani», turno palloni e solleciti automatici + +**Data:** 30 settembre 2026 +**Stato:** Accettata (implementata e verificata in locale: migration `m26`, job, route e test; la +migration non è ancora applicata in produzione) + +**Contesto** +Quattro notifiche su sei partivano da un pulsante premuto da un admin (DD-025 per i palloni): se nessuno +lo premeva, nessuno veniva avvisato. I testi erano inoltre brevi e informali, e i promemoria non dicevano +quando e dove. Si vuole che turno palloni e sollecito presenze partano anche da soli, a orari fissi prima +dell'evento, e che i testi siano più formali e completi. I testi nuovi sono nel +[catalogo](../modules/notifiche.md#testi-delle-notifiche). + +**Decisione** + +1. **Promemoria a 24 e 3 ore:** nuovo testo con data, ora e luogo. La parola nel titolo e nella riga + «Data» è **«domani»** se la data dell'evento è quella di domani (fuso `Europe/Rome`), **«oggi»** + altrimenti. Vale per entrambi i promemoria, quindi anche quello a 3 ore di un evento dopo la + mezzanotte dice «domani». La parola la calcola la funzione SQL quando genera la notifica. +2. **Turno palloni automatico.** Un job `pg_cron` ogni 15 minuti avvisa per gli eventi di tipo allenamento + o partita, in tre tempi con finestre che non si sovrappongono: **12 ore** (tra 12 e 6 ore prima + dell'inizio), **6 ore** (tra 6 e 3) e **3 ore** (meno di 3). Parte **solo se per l'evento c'è un + incaricato confermato** in `turni_palloni`: le proposte automatiche di `completaTurni()` non salvate non + contano. Gli avvisi sono due, con lo stesso testo nei tre tempi: «incarico assegnato» all'incaricato e + «riconsegna» a chi aveva i palloni all'evento precedente, se è un'altra persona. + - **Pulsante manuale.** «Avvisa chi è di turno» e la route `promemoria-palloni` **restano**: tipo + `turno_palloni`, testo nuovo, push mandata dalla route e non dalla coda. È una notifica in più; + riceverne due (automatica e manuale) va bene. + - **Incaricato cambiato dopo un avviso.** Il nuovo incaricato riceve il suo avviso al giro successivo, + con il tipo della finestra in cui cade. Chi aveva già ricevuto un avviso e non è più destinatario + riceve **una sola volta** «Turno palloni: incarico revocato» (`turno_palloni_revocato`), se + l'evento non è iniziato. Il job lo scopre confrontando i destinatari attuali con il **registro** degli + avvisi automatici già generati (non con le notifiche, che il giocatore può aver eliminato), senza + trigger su `turni_palloni`. Gli avvisi del pulsante non contano: possono riguardare una proposta non + confermata. + - **Niente email.** Gli avvisi palloni (automatici, revoca e pulsante) arrivano come push e nel centro + notifiche, **non per email**: il trigger `notifiche_utente_accoda_email` salta i tipi + `turno_palloni`, `turno_palloni_12h`, `_6h`, `_3h` e `turno_palloni_revocato`. È un'eccezione a + DD-038 («ogni notifica ha tre canali») solo per questi tipi; l'interruttore «Email» non cambia. +3. **Sollecito presenze automatico in tre tempi.** Lo stesso job manda il sollecito **24 ore** (tra 24 e + 12 ore prima), **12 ore** (tra 12 e 6) e **6 ore** (meno di 6) prima, al massimo una notifica per + giocatore e per tipo. Lo riceve solo chi in quel momento non ha risposto o ha risposto «forse» ed è + destinatario dell'evento: i convocati, o tutta la rosa attiva se `convocati` è vuoto, mai l'allenatore. + Chi risponde presente, assente o in ritardo smette di riceverli; chi resta su «forse» li riceve tutti e + tre. Un evento creato a meno di 12 ore dall'inizio riceve solo il sollecito della sua finestra. + - **Pulsante manuale.** «Sollecita» e la route `sollecita-presenze` **restano**, anche per l'allenatore: + tipo `sollecita_presenze`, push mandata dalla route, testo nuovo con in più la riga «Richiesta di: + _Nome_». I destinatari sono **gli stessi dell'automatico** (convocati senza risposta o con «forse»); + oggi la route guarda invece tutti i giocatori attivi. +4. **Stessa infrastruttura dei promemoria** (DD-037, DD-038): le notifiche automatiche nascono nel + database, il registro `promemoria_eventi_generati` impedisce i doppioni anche se il giocatore le + elimina, la push la manda il worker dalla coda `notifiche_push_coda`, l'email (quando c'è) nasce dal + trigger su `notifiche_utente`. Tipi nuovi: `sollecita_presenze_24h`, `_12h`, `_6h`; + `turno_palloni_12h`, `_6h`, `_3h` e `turno_palloni_revocato`. Il `turno_palloni` del pulsante resta. +5. **Migration `m26`:** estende i `CHECK` sui tipi di `notifiche_utente` e del registro, il trigger della + coda push, il trigger della coda email (che salta i tipi dei palloni) e ridefinisce la funzione dei + promemoria. Le funzioni nuove riusano `giocatori_destinatari_evento()`. + +**Alternative scartate** + +- Togliere i pulsanti manuali (turno palloni e sollecito) una volta arrivati gli automatici → scartata su + indicazione esplicita: servono all'admin per rimandare un avviso o sollecitare fuori dagli orari fissi. +- Mandare tutti i solleciti (o gli avvisi palloni) a un evento creato tardi → scartata: più messaggi nel + giro di pochi minuti sono rumore. +- «Domani» e «oggi» fissi nei due testi → scartata: il job delle 24 ore scatta anche per eventi che + iniziano oggi, e quello delle 3 ore attraversa la mezzanotte. +- Calcolare il testo nell'app invece che nel database → scartata: le notifiche nascono nei job `pg_cron`, + che non chiamano l'app (DD-038). +- Spostare nel worker il calcolo di evento precedente e successivo dei palloni → scartata: un secondo + punto che scrive in `notifiche_utente`, per un vantaggio piccolo. +- Ritirare l'avviso già consegnato al vecchio incaricato → scartata: per la push non si può, e in-app + confonde. Si manda la revoca. + +**Conseguenze** + +- Cambia la regola «quattro notifiche su sei dipendono da un gesto manuale»: restano solo manuali il + messaggio dello staff e il sondaggio pre-partita; per turno palloni e sollecito gli automatici si + aggiungono ai pulsanti, e un giocatore può ricevere lo stesso avviso due volte. +- **Logica duplicata:** «evento precedente» e «evento successivo» per i palloni, in `palloni-core.ts` + (usato dal pulsante e dalla schermata), sono riscritti in SQL per i job (`genera_avvisi_palloni_fascia`). + Ordine comune: data, ora, id. Se si cambia una regola in una sola delle due versioni, pulsante e job danno + risposte diverse: `notifiche-automatiche.test.ts` confronta testi e destinatari sugli stessi dati, anche a + parità di data e ora. +- `destinatariSollecito()` riceve anche i convocati e serve al sollecito manuale; un giocatore non + convocato non è più sollecitato. `avvisiPalloniEvento()` resta come riferimento per i test finché il SQL + non la sostituisce del tutto. +- Se un incaricato revocato viene poi riassegnato non riceve un secondo avviso, perché il registro lo ha + già generato per quella finestra: la revoca resta l'ultima notifica che ha letto. +- Le mail automatiche per evento e giocatore sono al massimo **cinque** (due promemoria, tre solleciti), + più quelle dei pulsanti (sollecito, sondaggio). Il turno palloni non ne manda. +- Il testo dei promemoria già generati non cambia (il registro impedisce di rigenerarli). + +**Riesame** +Dopo la prima stagione di uso: se i solleciti o gli avvisi palloni risultano troppi, ridurre le +finestre o escludere chi ha risposto «forse» più di una volta. diff --git a/docs/decisions/README.md b/docs/decisions/README.md new file mode 100644 index 0000000..db9f954 --- /dev/null +++ b/docs/decisions/README.md @@ -0,0 +1,100 @@ +# Registro delle decisioni di progetto + +Ogni file di questa cartella è **una decisione** (`DD-XXX.md`): una scelta che ha influito sulla +direzione del prodotto, sull'organizzazione del lavoro o su come l'app si evolve. Non descrive +_come_ è fatto il codice (per quello esistono [ARCHITECTURE.md](../ARCHITECTURE.md) e +[DATABASE.md](../DATABASE.md)): risponde a _perché abbiamo scelto così_, _cosa avevamo escluso_ +e _quando conviene riaprire la scelta_. + +**Leggi solo le decisioni che ti servono**: cerca il tema nell'indice qui sotto e apri il file +corrispondente. Non serve leggerle tutte. + +## Come usare questo registro + +Ogni decisione segue lo schema di [../_template-dd.md](../_template-dd.md): + +| Campo | Significato | +| ------------------------ | ------------------------------------------------------ | +| **Data** | Quando la decisione è stata presa o confermata | +| **Stato** | Accettata · In valutazione · Sostituita · Obsoleta | +| **Contesto** | Quale problema o opportunità avevamo di fronte | +| **Decisione** | Cosa abbiamo scelto di fare | +| **Alternative scartate** | Cosa non abbiamo fatto e perché | +| **Conseguenze** | Cosa comporta nel quotidiano (utenti, admin, sviluppo) | +| **Riesame** | Quando ha senso riconsiderarla | + +**Quando aggiungere una voce** + +- una scelta influisce su più moduli o su più release; +- escludiamo un'alternativa non ovvia; +- accettiamo un compromesso consapevole (debito, limitazione, ritardo); +- cambiamo una decisione precedente. + +**Quando non serve** + +- dettagli implementativi locali; +- scelte estetiche minori; +- bugfix o correzioni puntuali. + +**Come registrare una nuova decisione** + +Copiare [../_template-dd.md](../_template-dd.md) in `docs/decisions/DD-XXX.md` con il primo ID +libero (gli ID non si riusano e non si rinumerano), poi aggiungere la riga all'indice qui sotto. +Una decisione sostituita non si cancella: si cambia il suo **Stato** e si rimanda a quella nuova. + +## Indice + +**Accettate** + +| ID | Titolo | +| ------------------- | ------------------------------------- | +| [DD-001](DD-001.md) | Indipendenza da Lovable | +| [DD-002](DD-002.md) | Sviluppo document-first | +| [DD-004](DD-004.md) | Ogni versione aggiunge, non riscrive | +| [DD-005](DD-005.md) | Mobile-first | +| [DD-006](DD-006.md) | AI solo se utile | +| [DD-007](DD-007.md) | Badge calcolati, non in DB | +| [DD-008](DD-008.md) | Gamification equa tra ruoli | +| [DD-009](DD-009.md) | CSI manuale, poi integrazione API | +| [DD-010](DD-010.md) | Niente storico certificati | +| [DD-011](DD-011.md) | Auth reale prima del profilo | +| [DD-012](DD-012.md) | Rimandare la migrazione ID | +| [DD-013](DD-013.md) | Portabilità dello stack | +| [DD-015](DD-015.md) | Rosa da hardcoded a DB | +| [DD-016](DD-016.md) | Schema dati Profilo Giocatore | +| [DD-017](DD-017.md) | L'admin scrive al posto del giocatore | +| [DD-018](DD-018.md) | Collegamento automatico per email | +| [DD-019](DD-019.md) | Il branch lo decide l'utente | +| [DD-020](DD-020.md) | Test obbligatori e verdi | +| [DD-021](DD-021.md) | Molle interrompibili con motion | +| [DD-022](DD-022.md) | App solo chiara | +| [DD-023](DD-023.md) | Scritture limitate per ruolo | +| [DD-024](DD-024.md) | Route di notifica autenticate | +| [DD-025](DD-025.md) | Promemoria palloni manuale | +| [DD-026](DD-026.md) | Payload push cifrato | +| [DD-027](DD-027.md) | Voto limitato ai convocati | +| [DD-028](DD-028.md) | Soglia minima Media voto e MVP | +| [DD-029](DD-029.md) | Pulizia a cascata evento cancellato | +| [DD-030](DD-030.md) | Centro notifiche in-app | +| [DD-031](DD-031.md) | Nascita pubblica sincronizzata | +| [DD-032](DD-032.md) | Stagione CSI dalle date delle gare | +| [DD-033](DD-033.md) | Gestione eventi solo da calendario | +| [DD-034](DD-034.md) | Ruolo allenatore | +| [DD-035](DD-035.md) | Avviso certificati in Home | +| [DD-036](DD-036.md) | Notifiche email via Gmail | +| [DD-037](DD-037.md) | Promemoria evento senza ritorni | +| [DD-038](DD-038.md) | Tre canali per ogni notifica | +| [DD-039](DD-039.md) | Niente promemoria all'allenatore | +| [DD-040](DD-040.md) | Palloni e solleciti automatici | + +**In valutazione** + +| ID | Titolo | +| ------------------- | --------------------- | +| [DD-014](DD-014.md) | Convergenza schema DB | + +**Sostituite** + +| ID | Titolo | +| ------------------- | --------------------- | +| [DD-003](DD-003.md) | Branch main / develop | diff --git a/docs/modules/allenatore.md b/docs/modules/allenatore.md index d0d13a6..6f40313 100644 --- a/docs/modules/allenatore.md +++ b/docs/modules/allenatore.md @@ -1,7 +1,7 @@ # Modulo — Ruolo Allenatore **Stato:** implementato -**Decisione:** [DD-034](../DESIGN_DECISIONS.md#dd-034--lallenatore-è-uno-slot-della-squadra-con-tipo-diverso-non-un-giocatore) +**Decisione:** [DD-034](../decisions/DD-034.md) **Migration:** `m20_ruolo_allenatore_enum`, `m21_ruolo_allenatore` **File principali:** `src/lib/giocatori-squadra.ts` (`tipo`, `inRosa`, `isAllenatore`, `ruoloVisibile`), `src/lib/ruoli.ts`, `src/lib/user-store.ts` (`useSonoAllenatore`, diff --git a/docs/modules/badge.md b/docs/modules/badge.md index 9354fc9..bcc7170 100644 --- a/docs/modules/badge.md +++ b/docs/modules/badge.md @@ -140,259 +140,33 @@ voti ricevuti). ## Copertura test -Verifica badge per badge (fatta rileggendo codice e test riga per riga, non solo per -categoria). Due bug trovati in una sessione di audit dedicata su tutti i 16 badge (dettagli -nelle sezioni sotto e in "Problemi noti"): `s-tiebreak` non applicava la soglia minima di voti -di Pagellone (**corretto**), `s-cacche` prometteva "partite di campionato" senza che il codice -lo verificasse mai (**la descrizione è stata corretta**, il comportamento — qualunque partita -conta — era già quello voluto). Tutti e 16 i badge hanno ora copertura unit **e** integration -end-to-end completa. - -**Badge normali** — `badges.ts` testa la propria funzione pura (soglia → grado, -`badges.test.ts`) sull'output di altri moduli: - -- `mvp`: soglie inclusive verificate (1→bronzo, 3→argento, 99→resta oro, - `badges.test.ts:44-48`), progresso a metà (`:52-56`). -- `pagella`: caso critico delle soglie decimali senza arrotondamento per eccesso — 6.4 → - nessun grado, 6.5 → bronzo (`badges.test.ts:65-67`); un vero 6.49 non diventa "quasi - bronzo". Più la soglia minima di voti (vedi sotto). -- `palloni`: soglie 3/6/10 testate esplicitamente (bronzo/argento/oro, confine incluso e - oltre l'oro resta oro, `badges.test.ts:94-104`), oltre a un caso di progresso non tondo - (5/6 → 83%). Pipeline end-to-end sotto, come `mvp`/`pagella`. -- `presenze`: soglie 5/15/30 testate esplicitamente (confine incluso, oltre l'oro resta oro, - `badges.test.ts:108-116`). Pipeline end-to-end sotto, come `mvp`/`pagella`/`palloni`. -- `serie-allenamenti`: soglie 3/6/10 testate esplicitamente (confine incluso, oltre l'oro - resta oro, `badges.test.ts:118-126`). Pipeline end-to-end sotto, come gli altri badge da - tabella. -- `serie-conferme`: soglie 3/8/15 testate esplicitamente (confine incluso, oltre l'oro resta - oro, `badges.test.ts:128-136`), oltre agli invarianti generali e a - `collezioneBadge`/`prossimoTraguardo` con valori al massimo. Pipeline end-to-end sotto, come - gli altri badge da tabella. - -`mvp`, `pagella`, `palloni`, `presenze`, `serie-allenamenti` e `serie-conferme` sono le -eccezioni con integration dedicato (sotto) perché la loro fonte passa da una tabella di -voto/turni/presenze -letta e ricalcolata dal vivo, non da un contatore già pronto altrove. - -**Badge segreti** — ognuno testato con la propria condizione esatta e il confine appena sotto: -`s-tiebreak` (mvp:1 non basta, mediaVoto 7.9 non basta, sotto `VOTI_MINIMI_PAGELLA` voti non -basta nemmeno con media alta — vedi il bug fix sotto), `s-mai-forfait` (ogni soglia isolata al -confine, non solo "entrambe servono"), `s-infermeria` (2 infortuni non bastano), `s-ritardi` (4 -ritardi non bastano — gap colmato in questa sessione), `s-cacche` (2 cacche non bastano). -Copertura unit completa **e** integration dedicato per tutti e 5 (aggiunto in questa sessione, -vedi sotto): i dati sorgente hanno già i propri test di integrazione nei rispettivi moduli, ma -nessuno prima arrivava fino a `statoBadge()` sul segreto stesso con dati scritti a database. - -**Badge MVP — pipeline end-to-end** (aggiunta in una sessione dedicata a completare la -copertura di questo badge): - -- Unit: `badges.test.ts` (soglie/gradi) + `mvp-voti.test.ts` (conteggio partita, vincitore con - vantaggio netto, parità che non assegna, apertura voto 2h dopo il fischio d'inizio). -- Integration (`npx supabase start` richiesto): - - `scritture.test.ts` — semantica dell'`upsert` di `mvp_voti` (un voto per - partita/votante, l'ultimo sostituisce) e rifiuto dell'autovoto a database - (`mvp_no_autovoto`). - - `permessi.test.ts` — RLS di `m11`: il proprio voto MVP si registra (caso positivo), non - si può votare a nome di un altro (caso negativo); RLS di `m13` (sotto): un votante o un - votato non convocati vengono rifiutati. - - `mvp-badge.test.ts` — end-to-end reale: scrive voti su `mvp_voti`, rilegge via REST come - fa `useVotiMvp()`, calcola `mvpVintiPerGiocatore()` e verifica che `statoBadge()` assegni - il grado corretto (bronzo a 1-2 vittorie nette, argento a 3), incluso un pareggio che non - deve contare come vittoria. - -**Badge Pagellone — pipeline end-to-end e soglia minima di voti** (stessa sessione di sopra, -dopo l'analisi che ha trovato il gap "un voto solo sblocca il badge"): - -- Unit: `badges.test.ts:69-88` — sotto `VOTI_MINIMI_PAGELLA` (5) il badge resta bloccato anche - con `mediaVoto: 10`; esattamente a 5 la media torna a contare; sopra soglia valgono le - normali soglie di grado (`mediaVoto: 6.5` con 5 voti → bronzo, non oro). -- Integration (`npx supabase start` richiesto): - - `scritture.test.ts` — semantica dell'`upsert` di `pagelle_voti` e rifiuto dell'autovoto - (`pagelle_no_autovoto`), già presente prima di questa sessione. - - `permessi.test.ts` — RLS di `m13`: un votante o un votato non convocati vengono rifiutati - (per tutte e tre le tabelle di voto, non solo le pagelle), e un voto pagella dopo - `pagelle_chiuse` viene rifiutato anche a database, non solo nascosto in UI. - - `pagella-badge.test.ts` (nuovo) — end-to-end reale: scrive voti su `pagelle_voti`, rilegge - via REST come fa `usePagelle()`, calcola `mediePagelle()` e verifica che `statoBadge()` - tenga il badge bloccato sotto soglia, lo sblocchi al voto minimo con il grado giusto, e - applichi le soglie normali sopra soglia. - -**Badge Sherpa dei palloni — pipeline end-to-end, ora senza contare le proposte non -confermate** (analisi dedicata: trovato e sistemato il gap "le proposte contano", che -gonfiava il badge di turni mai confermati da nessuno — vedi "Problemi noti da sistemare"): - -- Unit: `badges.test.ts:93-104` — soglie 3/6/10 (confine incluso, oltre l'oro resta oro) + - `palloni-core.test.ts`, già completo prima di questa sessione (`completaTurni()`, - `conteggioTurni()`, rotazione bilanciata su un giro completo di partite, allenamenti mai - proposti in automatico, turno di un giocatore non più in rosa che non rompe il conteggio). -- Integration (`npx supabase start` richiesto): - - `scritture.test.ts` — un turno resta uno per evento (l'upsert sostituisce, non aggiunge). - - `palloni-badge.test.ts` — end-to-end reale: scrive eventi e turni **solo parzialmente - confermati** su `eventi_app`/`turni_palloni`, rilegge via REST come fa `fetchTurni()`/ - `daRiga()` e passa `turniSalvati` (solo confermati, mai l'output di `completaTurni()`) a - `conteggioTurni()` fino a `statoBadge()`: dimostra che un evento passato senza turno - confermato **non conta per nessuno**, anche se un algoritmo di rotazione (usato altrove - per la UI) lo proporrebbe automaticamente; verifica anche che un evento futuro non conti, - pur avendo già una conferma. - -**Badge Presenza fissa — pipeline end-to-end** (analisi dedicata: nessun bug trovato; a -differenza di MVP/pagelle/badge social, per questo badge **non serve** l'estensione RLS di -M13 — vedi sotto): - -- Unit: `badges.test.ts:108-116` — soglie 5/15/30 (confine incluso, oltre l'oro resta oro) + - `presenze.test.ts`, già molto completo prima di questa sessione (`contaPresenzeGiocatore()` - con ritardo che conta come presenza, denominatore uguale per tutti, eventi futuri esclusi, - filtro sui convocati, solo partite/allenamenti). -- Integration (`npx supabase start` richiesto): - - `obiettivi.test.ts` — copre già `contaPresenzeGiocatore()` end-to-end per l'obiettivo - "250 presenze complessive" (o3), la stessa funzione usata dal badge. - - `presenze-badge.test.ts` (nuovo) — end-to-end reale sul badge: scrive eventi e risposte - su `eventi_app`/`risposte_presenze`, rilegge via REST come fa `fetchPresenze()`/`daRiga()` - e verifica che `statoBadge()` attraversi le tre soglie con dati veri (incluso un ritardo - che conta come presenza e un'assenza che non conta). Dimostra anche che una risposta - scritta per un evento senza convocazione **non conta comunque**, perché - `contaPresenzeGiocatore()` filtra già per `convocati` lato applicazione — a differenza di - MVP/pagelle/badge social, qui non serve una policy RLS aggiuntiva: il filtro è nella - funzione pura che il badge consuma, non solo in UI. - -**Badge Sempre in palestra — pipeline end-to-end** (analisi dedicata: nessun bug trovato). -Stessa fonte dati di `presenze` (`risposte_presenze`) ma logica diversa: non un totale, una -**serie consecutiva** che un buco azzera e un infortunio congela. Anche qui, come per -`presenze`, non serve nessuna estensione RLS: il filtro sui convocati è già nella funzione -pura. - -- Unit: `badges.test.ts:118-126` — soglie 3/6/10 (confine incluso, oltre l'oro resta oro) + - `presenze.test.ts`, già completo prima di questa sessione su `serieConsecutiva()` (buco che - azzera, infortunio che congela invece di azzerare, nessuna risposta vale come buco, - convocati che non spezzano la serie di chi non era coinvolto). -- Integration (`npx supabase start` richiesto): - - `serie-allenamenti-badge.test.ts` (nuovo) — end-to-end reale: scrive allenamenti e - risposte su `eventi_app`/`risposte_presenze`, rilegge via REST e verifica che - `statoBadge()` attraversi bronzo/argento/oro con presenze consecutive vere, che - un'assenza dopo 10 presenze di fila azzeri tutto (torna a nessun grado), e — separatamente - — che un infortunio **non** azzeri la serie ma la lasci congelata (3 presenze vere, - un infortunio nel mezzo saltato dal conteggio, poi ancora presente: la serie resta a 3, - non riparte da 1). - -**Badge Risposta lampo — pipeline end-to-end** (analisi dedicata: nessun bug trovato nella -logica di calcolo; l'unico limite è quello già noto e documentato sui dati pre-`m9`, vedi -"Limiti noti"). Il badge dipende da `serieConferme()`, che passa da due colonne facili da -confondere fra loro (`creato_il`/`risposto_il`, vedi [serie-presenze.md](serie-presenze.md)): -un integration test aggiunto per verificare che la mappatura verso `creatoIl`/`tempi` regga con -dati reali, non solo con timestamp scelti a mano — cosa che i test unitari, che non toccano il -database, non possono garantire. - -- Unit: `badges.test.ts:128-136` — soglie 3/8/15 (confine incluso, oltre l'oro resta oro) + - `presenze.test.ts`, esteso in questa sessione su `serieConferme()`: oltre al buco che azzera - e all'evento senza `creatoIl` che viene saltato (già presenti), ora anche un evento convocato - solo per un altro giocatore che non spezza la serie, partite e allenamenti sommati nella - stessa serie, il confronto inclusivo esattamente a 24h (dentro conta, un secondo oltre - azzera), e un evento futuro che non entra ancora nel calcolo. -- Integration (`npx supabase start` richiesto): - - `serie-conferme-badge.test.ts` (nuovo) — end-to-end reale: scrive eventi con `creato_il` - esplicito e risposte con `risposto_il` esplicito su `eventi_app`/`risposte_presenze`, - rilegge via REST come fa `daRiga()`/`fetchPresenze()` e verifica che `statoBadge()` - attraversi bronzo/argento/oro con conferme rapide vere, che una risposta arrivata oltre le - 24h azzeri tutto anche dopo 15 conferme di fila, e — separatamente — che partite e - allenamenti si sommino nella stessa serie senza bisogno di un filtro per tipo. - -**Badge Cliente VIP dell'Infermeria e Aspettate, arrivo! — pipeline end-to-end** (analisi -dedicata: nessun bug trovato). Stessa fonte (`contaInfortuni()`/`contaRitardi()` in -`src/lib/infortuni.ts`, entrambe sopra la stessa `contaStato()` privata) e stessa struttura di -`serie-allenamenti`/`serie-conferme`, ma senza serie: un contatore semplice di eventi passati. - -- Unit: `badges.test.ts` (soglie 3 e 5, confine appena sotto) + `infortuni.test.ts`, esteso in - questa sessione con un giocatore che ha **sia** un infortunio **sia** un ritardo (su eventi - diversi): i due conteggi restano indipendenti, nessuno "ruba" voci all'altro. -- Integration (`npx supabase start` richiesto): - - `s-infermeria-badge.test.ts` / `s-ritardi-badge.test.ts` (nuovi) — end-to-end reali: scrivono - eventi e risposte "infortunato"/"ritardo" su `eventi_app`/`risposte_presenze`, rileggono via - REST e verificano che il segreto resti bloccato appena sotto soglia e si sblocchi - esattamente al confine (3 infortuni, 5 ritardi). - -**Badge Trono di ferro — pipeline end-to-end, descrizione corretta** (analisi dedicata: trovato -un disallineamento fra descrizione e codice, **risolto aggiornando il testo**, non la logica — -vedi "Problemi noti" più sotto per il perché). `statisticheCacche()` (`src/lib/cacche.ts`) non -ha mai distinto partite di campionato da amichevoli: contava (e conta ancora) qualunque partita -con 3+ cacche dichiarate. La vecchia descrizione del badge prometteva "partite di campionato", -cosa che il codice non ha mai verificato — corretta in "partite (campionato o amichevole)". - -- Unit: `badges.test.ts` (soglia 3, confine appena sotto — gap colmato in questa sessione) + - `cacche.test.ts` (già completo su `giornateTop`). -- Integration (`npx supabase start` richiesto): - - `s-cacche-badge.test.ts` (nuovo) — end-to-end reale: scrive 2 giornate da record su partite - di campionato e una su un'amichevole, dimostrando con dati veri che l'amichevole conta - esattamente come le altre — pin del comportamento attuale, così chi in futuro reintroduce un - filtro sul campionato deve accorgersene qui, non scoprirlo in produzione. - -**Badge Uomo tie-break — pipeline end-to-end, bug corretto** (analisi dedicata: trovato e -sistemato il gap "un voto pagella solo sblocca il segreto insieme a 2 MVP"). Il segreto usa -`g.mediaVoto`, lo stesso campo del badge normale `pagella` — che però lo azzera sotto -`VOTI_MINIMI_PAGELLA` (5) voti ricevuti, proprio per evitare che un singolo voto sblocchi/tolga -il badge senza significatività statistica. `s-tiebreak` non applicava lo stesso filtro: ora sì -(`g.mvp >= 2 && g.votiPagella >= VOTI_MINIMI_PAGELLA && g.mediaVoto >= 8`). - -- Unit: `badges.test.ts` — sotto la soglia minima di voti il segreto resta bloccato anche con - media 8 e 2 MVP; un solo MVP non basta (isolato dal resto). -- Integration (`npx supabase start` richiesto): - - `s-tiebreak-badge.test.ts` (nuovo) — end-to-end reale: scrive voti MVP e pagella veri, - dimostra che un solo voto pagella (media alta, 2 MVP) NON sblocca il segreto, e che il quinto - voto lo sblocca — il fix verificato con la stessa pipeline `mvp_voti`/`pagelle_voti` → REST → - `mvpVintiPerGiocatore()`/`mediePagelle()` → `statoBadge()` che userebbe l'app. - -**Badge Mai un forfait — pipeline end-to-end** (analisi dedicata: nessun bug trovato). Unico -segreto a combinare due statistiche indipendenti (`serieConferme()` e -`contaPresenzeGiocatore()`), entrambe già testate a fondo nei rispettivi moduli. - -- Unit: `badges.test.ts`, esteso in questa sessione con ogni soglia isolata al confine - (`serieConferme` appena sotto con `presenze` abbondanti, e viceversa), non solo "insieme non - bastano". -- Integration (`npx supabase start` richiesto): - - `s-mai-forfait-badge.test.ts` (nuovo) — end-to-end reale: scrive eventi con `creato_il` e - risposte con `risposto_il` veri, verifica che il segreto resti bloccato a 9/9 e si sblocchi a - 15/15, e che una risposta lenta azzeri la serie di conferme **senza** azzerare le presenze - già accumulate (le due statistiche restano indipendenti anche a database). - -**Badge social** — nessuna delle 5 categorie ha logica _propria_ nel codice: l'id è solo una -chiave di raggruppamento, `conteggioCategoria`/`vincitoreCategoria`/`badgeSocialVinti` sono -identici per tutte (`badge-social.ts:107-158`). Testare a fondo 2-3 categorie copre l'intero -meccanismo: - -- Unit (`badge-social.test.ts`): conteggio isolato per match+categoria (`:29-32`), vantaggio - netto/parità → nessun vincitore (`:38-41`), vittorie multi-partita (`badgeSocialVinti`, g2 - vince in `m1` e `m2` → `{affidabile: 2}`, `:48`), zero voti → zero badge (`:51`). Estesi in - questa sessione: un voto totale solo basta a vincere, una parità a 3 candidati (i primi due - pari, il terzo staccato) resta senza vincitore, categorie diverse nella stessa partita non si - mischiano in `badgeSocialVinti()`. -- Integration: upsert/sostituzione voto per categoria (`scritture.test.ts:170-202`), autovoto - rifiutato — doppia barriera UI + database (`scritture.test.ts:124-148`), RLS `m11` — un - giocatore firma solo il proprio voto (`permessi.test.ts:344-369`). - - `badge-social.test.ts` (nuovo, in `test/integration/`) — end-to-end reale sulle **5 - categorie effettive** di `categorieSocial` (non più solo 2-3, e non più le categorie - inventate di `scritture.test.ts`): scrive voti veri su `badge_social_voti`, dimostra che - tutte e 5 si contano e si vincono allo stesso modo, e che una parità su una categoria non - tocca il conteggio delle altre 4 nella stessa partita. +Tutti e 16 i badge hanno test unit (`test/unit/badges.test.ts`, `badge-social.test.ts`) e di +integration end-to-end sul database locale (`test/integration/*-badge.test.ts`, +`badge-social.test.ts`, più `scritture` e `permessi`). La logica pura sta in `badges.ts`; i test +verificano soglie inclusive (1→bronzo, soglia decimale 6.49 ≠ 6.5), confini dei badge segreti e +la soglia minima di voti della Pagella (`VOTI_MINIMI_PAGELLA`). Il dettaglio test per test lo +dicono i file, non questa pagina: leggili prima di cambiare una soglia. ### Riepilogo per badge -| # | id | tipo | test unit | test integration | -| --- | ------------------- | ------- | ------------------------------------- | --------------------------------------------- | -| 1 | `mvp` | normale | ✅ | ✅ (`scritture`, `permessi`, `mvp-badge`) | -| 2 | `pagella` | normale | ✅ (incl. soglia minima voti) | ✅ (`scritture`, `permessi`, `pagella-badge`) | -| 3 | `palloni` | normale | ✅ | ✅ (`scritture`, `palloni-badge`) | -| 4 | `presenze` | normale | ✅ | ✅ (`obiettivi`, `presenze-badge`) | -| 5 | `serie-allenamenti` | normale | ✅ | ✅ (`serie-allenamenti-badge`) | -| 6 | `serie-conferme` | normale | ✅ (limite noto sotto) | ✅ (`serie-conferme-badge`) | -| 7 | `s-tiebreak` | segreto | ✅ (bug corretto, vedi sotto) | ✅ (`s-tiebreak-badge`) | -| 8 | `s-mai-forfait` | segreto | ✅ | ✅ (`s-mai-forfait-badge`) | -| 9 | `s-infermeria` | segreto | ✅ | ✅ (`s-infermeria-badge`) | -| 10 | `s-ritardi` | segreto | ✅ | ✅ (`s-ritardi-badge`) | -| 11 | `s-cacche` | segreto | ✅ (descrizione corretta, vedi sotto) | ✅ (`s-cacche-badge`) | -| 12 | `affidabile` | social | ✅ | ✅ (`scritture`, `permessi`, `badge-social`) | -| 13 | `spirito` | social | ✅ (meccanismo generico) | ✅ (meccanismo generico, `badge-social`) | -| 14 | `fairplay` | social | ✅ (meccanismo generico) | ✅ (meccanismo generico, `badge-social`) | -| 15 | `meme` | social | ✅ | ✅ (`badge-social`) | -| 16 | `cuore` | social | ✅ | ✅ (autovoto, `badge-social`) | +| # | id | tipo | test unit | test integration | +| --- | ------------------- | ------- | ----------------------------- | --------------------------------------------- | +| 1 | `mvp` | normale | ✅ | ✅ (`scritture`, `permessi`, `mvp-badge`) | +| 2 | `pagella` | normale | ✅ (incl. soglia minima voti) | ✅ (`scritture`, `permessi`, `pagella-badge`) | +| 3 | `palloni` | normale | ✅ | ✅ (`scritture`, `palloni-badge`) | +| 4 | `presenze` | normale | ✅ | ✅ (`obiettivi`, `presenze-badge`) | +| 5 | `serie-allenamenti` | normale | ✅ | ✅ (`serie-allenamenti-badge`) | +| 6 | `serie-conferme` | normale | ✅ (vedi Limiti noti) | ✅ (`serie-conferme-badge`) | +| 7 | `s-tiebreak` | segreto | ✅ (soglia minima voti) | ✅ (`s-tiebreak-badge`) | +| 8 | `s-mai-forfait` | segreto | ✅ | ✅ (`s-mai-forfait-badge`) | +| 9 | `s-infermeria` | segreto | ✅ | ✅ (`s-infermeria-badge`) | +| 10 | `s-ritardi` | segreto | ✅ | ✅ (`s-ritardi-badge`) | +| 11 | `s-cacche` | segreto | ✅ | ✅ (`s-cacche-badge`) | +| 12 | `affidabile` | social | ✅ | ✅ (`scritture`, `permessi`, `badge-social`) | +| 13 | `spirito` | social | ✅ (meccanismo generico) | ✅ (meccanismo generico, `badge-social`) | +| 14 | `fairplay` | social | ✅ (meccanismo generico) | ✅ (meccanismo generico, `badge-social`) | +| 15 | `meme` | social | ✅ | ✅ (`badge-social`) | +| 16 | `cuore` | social | ✅ | ✅ (autovoto, `badge-social`) | --- diff --git a/docs/modules/calendario.md b/docs/modules/calendario.md index db72e97..709eea5 100644 --- a/docs/modules/calendario.md +++ b/docs/modules/calendario.md @@ -26,7 +26,7 @@ true })` — ci sono anche gli allenatori, DD-034 — non righe vere di `eventi_app`): la spunta della vista `giorniIT`/`mesiIT` colora la cella per tipo di evento, i giorni con più eventi si dividono lo spazio. - **`/eventi`** — "Gestione eventi", riservata ad amministratori e allenatori - (`usePuoGestireEventi()`, [DD-034](../DESIGN_DECISIONS.md#dd-034--lallenatore-è-uno-slot-della-squadra-con-tipo-diverso-non-un-giocatore)): + (`usePuoGestireEventi()`, [DD-034](../decisions/DD-034.md)): crea, modifica ed elimina un evento, sceglie i convocati tra i soli giocatori (`convocatiEvento()`, vuoto = tutta la rosa). Da qui si distingue "partita" da "amichevole" tramite il flag `campionato` (`categoriaEvento()`/`daCategoria()` in `eventi.ts` convertono tra la categoria mostrata @@ -39,7 +39,7 @@ true })` — ci sono anche gli allenatori, DD-034 — non righe eliminare passano solo da lì. Nel form il giorno si legge come testo fisso e resta da scegliere solo l'ora; il campo data compare solo toccando «Cambia», per spostare l'evento. Fino a 0.9.2 sotto la griglia c'era anche la lista cronologica di tutti gli eventi, tolta perché ripeteva il calendario - ([DD-033](../DESIGN_DECISIONS.md#dd-033--gestione-eventi-solo-calendario-giorno-fissato-dal-tocco)). + ([DD-033](../decisions/DD-033.md)). Entrambe leggono la stessa cache (`useEventi()`, `EVENTI_KEY`, `staleTime` 10 minuti: il calendario cambia raramente). `EventoCard.tsx` è la card riusata da entrambe le schermate; @@ -68,10 +68,10 @@ dalle risposte solo sulla scheda evento (vedi `presenze.md`). 1. **Cancellare un evento è distruttivo per tutto ciò che vi era agganciato.** Un trigger (`m14_pulizia_dati_evento_cancellato`, - [DD-029](../DESIGN_DECISIONS.md#dd-029--cancellare-un-evento-pulisce-a-cascata-i-dati-collegati)) + [DD-029](../decisions/DD-029.md)) pulisce a cascata presenze, cacche, voti MVP/pagelle/badge social, turni palloni e scout di quell'evento: non è recuperabile con un annulla, e prima di M14 quelle righe restavano - orfane nel database (bonificate una tantum da M15/M16, vedi `PROJECT_STATE.md`). + orfane nel database (bonificate una tantum da M15/M16, vedi [DATABASE.md](../DATABASE.md)). 2. **Nessuna creazione automatica degli eventi partita dal calendario CSI.** Le gare ufficiali arrivano già come dati (`getEventsByTeamId.php`, vedi `collegamento-csi.md`), ma un amministratore deve comunque creare a mano l'evento corrispondente in `/eventi` diff --git a/docs/modules/collegamento-csi.md b/docs/modules/collegamento-csi.md index 488244f..5f3adfc 100644 --- a/docs/modules/collegamento-csi.md +++ b/docs/modules/collegamento-csi.md @@ -50,7 +50,7 @@ mostra «Stagione 2025/26» mentre Squadra e Calendario, che hanno la stagione s codice, mostrano già «Stagione 2026/27». È voluto: la pagina Campionato riporta la stagione dei dati CSI che sta mostrando, non quella in corso per la squadra. Quando il CSI pubblica la nuova stagione si aggiornano i due `project_id` (vedi sotto) e l'etichetta passa da sola -a 2026/27 ([DD-032](../DESIGN_DECISIONS.md#dd-032--la-stagione-csi-si-ricava-dalle-date-delle-gare)). +a 2026/27 ([DD-032](../decisions/DD-032.md)). Per ritrovare questi id a ogni cambio stagione: `components/team-main.php?team_id=3359` (dietro `team_details.php`) contiene una sezione "Campionati" con un link diff --git a/docs/modules/notifiche-email.md b/docs/modules/notifiche-email.md index 01d9e01..dedd0d8 100644 --- a/docs/modules/notifiche-email.md +++ b/docs/modules/notifiche-email.md @@ -8,7 +8,7 @@ Questo documento descrive **cosa fa** il canale email. Come si installa, si configura e si diagnostica il worker sta in [WORKER_EMAIL.md](../WORKER_EMAIL.md); le motivazioni delle scelte in -[DD-036](../DESIGN_DECISIONS.md#dd-036--notifiche-email-via-gmail-da-un-worker-docker-sullhost-di-casa). +[DD-036](../decisions/DD-036.md). ## Obiettivo diff --git a/docs/modules/squadra.md b/docs/modules/squadra.md index 8cc1e66..481f87f 100644 --- a/docs/modules/squadra.md +++ b/docs/modules/squadra.md @@ -12,7 +12,7 @@ Tenere l'anagrafica della rosa (nome, numero di maglia, ruolo, chi è collegato a quale account) in un unico posto — `giocatori_squadra` — e farla usare a tutte le schermate che hanno bisogno di sapere "chi c'è in squadra", invece di ciascuna avere la propria copia. -Prima di [DD-015](../DESIGN_DECISIONS.md#dd-015--rosa-anagrafica-da-codice-hardcoded-a-database) +Prima di [DD-015](../decisions/DD-015.md) la lista viveva hardcoded in `src/lib/crapp-data.ts`: aggiungere o disattivare un giocatore dalla dashboard admin non aveva alcun effetto sul resto dell'app. @@ -53,19 +53,19 @@ specifica completa sta in [allenatore.md](allenatore.md). ## Gestione dati squadra (solo amministratore) -Da `/admin` un amministratore può ([DD-017](../DESIGN_DECISIONS.md#dd-017--lamministratore-può-compilare-i-dati-al-posto-del-giocatore)): +Da `/admin` un amministratore può ([DD-017](../decisions/DD-017.md)): -| Azione | Hook | Effetto | -| ------------------------------ | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Modificare dati squadra | `useSalvaDatiSquadra()` | Nome, cognome, numero, ruolo, email (usata per il collegamento automatico, non il dato personale del profilo) | -| Aggiungere un giocatore | `useAggiungiGiocatore()` | Nuova riga con id progressivo `g` (`prossimoIdGiocatore()`), non generato dal database; con tipo «Allenatore» senza numero né ruolo (DD-034) | -| Attivare/disattivare | `useImpostaAttivo()` | Non elimina la riga: la storia della stagione resta intatta | -| Scollegare un account | `useScollegaAccount()` | Libera uno slot collegato per errore ([DD-016](../DESIGN_DECISIONS.md#dd-016--schema-dati-profilo-giocatore-f0) regola 2); il giocatore si ricollega al primo accesso successivo | -| Registrare il tesseramento CSI | `useSalvaTesseramento()` | Numero e data tessera, note solo dopo il tesseramento effettivo (vedi `profilo-giocatore.md`) | +| Azione | Hook | Effetto | +| ------------------------------ | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Modificare dati squadra | `useSalvaDatiSquadra()` | Nome, cognome, numero, ruolo, email (usata per il collegamento automatico, non il dato personale del profilo) | +| Aggiungere un giocatore | `useAggiungiGiocatore()` | Nuova riga con id progressivo `g` (`prossimoIdGiocatore()`), non generato dal database; con tipo «Allenatore» senza numero né ruolo (DD-034) | +| Attivare/disattivare | `useImpostaAttivo()` | Non elimina la riga: la storia della stagione resta intatta | +| Scollegare un account | `useScollegaAccount()` | Libera uno slot collegato per errore ([DD-016](../decisions/DD-016.md) regola 2); il giocatore si ricollega al primo accesso successivo | +| Registrare il tesseramento CSI | `useSalvaTesseramento()` | Numero e data tessera, note solo dopo il tesseramento effettivo (vedi `profilo-giocatore.md`) | Il collegamento giocatore↔account, invece, non è manuale: avviene in automatico al primo accesso con Google, per corrispondenza email -([DD-018](../DESIGN_DECISIONS.md#dd-018--collegamento-automatico-giocatoreaccount-per-email)). +([DD-018](../decisions/DD-018.md)). `useCollegaGiocatore()` esiste per completare quel flusso, non per una scelta libera dell'admin. diff --git a/test/integration/pulizia-evento.test.ts b/test/integration/pulizia-evento.test.ts index facef88..2cfb9ab 100644 --- a/test/integration/pulizia-evento.test.ts +++ b/test/integration/pulizia-evento.test.ts @@ -6,7 +6,7 @@ * `cacche_partita`, `mvp_voti`, `pagelle_voti`, `badge_social_voti`, `turni_palloni`, * `scout_sessioni`, `scout_live`, `scout_partite`), cancella l'evento e verifica che il * trigger `eventi_app_pulisci_dati_collegati` le abbia rimosse tutte. Prima di M14 queste - * righe restavano orfane a database (`docs/DESIGN_DECISIONS.md`, DD-029). + * righe restavano orfane a database (`docs/decisions/DD-029.md`). * * Gira solo sullo stack locale (`npx supabase start`): usa id con il prefisso * `test-pulizia-evento`, che nessun dato vero può avere.