Sezione 1.4.0 del CHANGELOG, versione in package.json e package-lock.json, PROJECT_STATE aggiornato. Il test di presenza-modificata copre ora i confini delle 6 ore, ogni stato, allenatore inattivo, admin senza slot, doppio ruolo e ora dell'evento non valida. Documentazione delle notifiche allineata (riga 9, copertura test, email, DATABASE). Nessuna nuova migration: la m32 e la m33 sono applicate in produzione (09/10/2026). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
29 KiB
Database CrAPP
Struttura del database Supabase (PostgreSQL) e ruolo di ogni tabella. Lo schema autoritativo
sono le migration in supabase/migrations/: una tabella nuova va documentata qui nella
stessa modifica che la crea. Le funzionalità future stanno in ROADMAP.md,
non in questo file.
Permessi di scrittura
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, con le eccezioni della
sezione «Lettura» qui sotto; anon non arriva a nessuna di esse da M4 (DD-011).
| Tabella | Chi può scrivere |
|---|---|
eventi_app |
admin e allenatori (nell'app li gestisce la rotta /eventi, riservata a loro) |
risposte_presenze, cacche_partita |
il giocatore sulla propria riga (giocatore_id), mai l'allenatore (M21), più gli admin |
pagelle_voti, mvp_voti, badge_social_voti |
il votante sui propri voti (votante_id), se votante e votato sono convocati all'evento (m13) e nessuno dei due è un allenatore (m21); solo per le pagelle anche pagelle_chiuse = false; gli admin senza questi vincoli |
turni_palloni, scout_sessioni, scout_live, scout_partite |
qualsiasi autenticato: nell'interfaccia non hanno gate |
notifiche_utente |
nessuno scrive da client: le righe nascono da trigger/funzioni SECURITY DEFINER o dalla service role (M17); il giocatore può solo segnare come lette le proprie |
notifiche_email_coda |
nessuno da client, né in lettura né in scrittura: la riempie il trigger accoda_notifica_email e la gestisce solo il worker email con la service role (M22) |
preferenze_utente |
l'utente sulla propria riga (auth_user_id = auth.uid()), in lettura e scrittura (M22) |
promemoria_eventi_generati |
nessuno da client, né in lettura né in scrittura: la scrive solo la funzione genera_promemoria_eventi() dei cron (M23) |
compleanni_notificati |
Registro degli avvisi di compleanno già generati, per destinatario e giorno. |
notifiche_push_coda |
nessuno da client, né in lettura né in scrittura: la riempie il trigger accoda_push_promemoria e la gestisce solo il worker con la service role (M24) |
profili_giocatore |
il giocatore sul proprio profilo, admin su tutti (DD-016, DD-017) |
giocatori_squadra |
admin; il giocatore può solo reclamare uno slot libero (DD-016); l'allenatore anche cambiare nome e cognome del proprio slot (M21) |
user_roles |
solo admin; la riga allenatore la scrive il trigger sincronizza_ruolo_allenatore (M21) |
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: 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). allenatore non si inserisce a mano: lo gestisce il trigger sincronizza_ruolo_allenatore (DD-034). |
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 |
Non più usate dall'interfaccia (DD-048): i valori già inseriti restano, nessuna schermata li mostra o li scrive. Il trigger le include ancora 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à e certificato medico, in cartelle per giocatore (<giocatore_id>/<sezione>.<est>). |
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 (<giocatore_id>/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 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: oggi nessuna statistica lo usa (la serie «Conferme 24h» è stata rimossa il 2026-10-06). 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. |
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
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 |
Turni palloni confermati. | Gli allenamenti non ricevono proposta automatica (vedi 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. |
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. - 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 seconvocatiè vuoto; mai gli allenatori (DD-039). - Il
CHECKsul tipo ammette anchepresenza_modificata(M32, DD-050),compleannoecompleanno_auguri(M31, DD-047),turno_palloni_12h,_6h,_3h,turno_palloni_revocatoesollecita_presenze_24h,_12h,_6h, scritti dai job (DD-040).turno_palloniesollecita_presenzerestano i tipi dei pulsanti manuali. - Il trigger delle email salta tutti i tipi
turno_palloni*; quello delle push accoda i tipi automatici (compleanni epresenza_modificatacompresi) ma non quelli dei pulsanti.presenza_modificatahaevento_idNULL (comeadmin): la UNIQUE non si applica e ogni modifica resta una riga (M32, DD-050).
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 funzioniSECURITY DEFINERriservate aservice_role:prendi_notifiche_email(p_max),esito_notifica_email(...),email_inviate_ultime_24h(),prendi_push_promemoria(p_max),esito_push_promemoria(...). I lotti usanoFOR UPDATE SKIP LOCKEDe recuperano le righe ferme inin_invioda più di 10 minuti. - RLS attiva senza policy e permessi tolti a
anon/authenticated. - Sono tabelle separate da
notifiche_utenteperché la policyUPDATEdel giocatore su quella riga gli permetterebbe di riportare ain_codauna 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
Le notifiche automatiche nascono nel database, da job pg_cron che chiamano funzioni SECURITY DEFINER
riservate alla service role (anon e utenti autenticati non possono chiamarle). Testi e regole sono in
modules/notifiche.md; le decisioni in DD-037, DD-038 e DD-040.
| Funzione (migration) | Cosa fa | Job pg_cron |
|---|---|---|
genera_promemoria_eventi(tipo, finestra) (M17, M23, M26) |
Promemoria a 24 e 3 ore, con «oggi/domani», data, ora e luogo. Non pianificata dalla M28 (DD-043): resta come funzione, nessun job la chiama. | promemoria-eventi-24h (ogni ora), -3h (15 min) |
genera_avvisi_palloni() (M26, M27), con genera_avvisi_palloni_fascia() |
Avviso del turno palloni a 3 ore (M27; in M26 erano 12, 6 e 3) per gli eventi con un incaricato confermato; poi chiama genera_revoche_palloni(). |
avvisi-palloni-automatici (15 min) |
genera_revoche_palloni() (M26) |
Una revoca per chi aveva un avviso automatico e non è più destinatario, per eventi non ancora iniziati; guarda il registro. | (dentro genera_avvisi_palloni()) |
genera_solleciti_presenze() (M26, M30), con ..._fascia() |
Sollecito a 24 ore (M30; in M26 erano 24, 12 e 6) ai destinatari dell'evento senza risposta o con «forse». | solleciti-presenze-automatici (15 min) |
genera_avvisi_compleanno(adesso) (M31) |
Dalle 8:00 (Europe/Rome) manda gli auguri a chi compie gli anni e l'avviso (solo titolo, senza età) agli altri membri attivi, un messaggio a testa; guarda il registro compleanni_notificati. adesso (default now()) serve ai test. 29 febbraio non gestito (DD-047). |
avvisi-compleanno (ogni ora) |
avvisa_presenza_modificata() (M32, M33) |
Trigger su risposte_presenze (insert, update dello stato, delete): se lo stato cambia a 0-6 ore da una partita o un allenamento, scrive una notifica presenza_modificata per ogni admin e allenatore attivo (DD-050; la M32 solo admin), tranne il giocatore della presenza. Usa nome_stato_presenza(stato) per i testi. |
trigger risposte_presenze_avvisa_modifica |
inizio_evento(data, ora) (M26) |
Istante d'inizio di un evento in fuso Europe/Rome; NULL se l'ora non è valida, e l'evento viene solo escluso. |
— |
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). |
— |
Badge
Non esiste una tabella dedicata: i badge vengono calcolati a runtime dall'applicazione a partire dai dati esistenti (DD-007).