m23: un registro (promemoria_eventi_generati) ricorda i promemoria a 24 e 3 ore già generati, così eliminare la notifica non li fa rigenerare dal cron né riaccodare la mail a ogni giro. m24: la push dei promemoria, che nascono nel database, la manda il worker mailer/ con lo stesso modulo dell'app (coda notifiche_push_coda, esiti, backoff, iscrizioni scadute eliminate, push scartate oltre 3 ore). Il sondaggio pre-partita scrive anche notifiche_utente (tipo sondaggio_cacche): compare in-app e arriva per email. L'immagine del worker si costruisce dalla radice del repository per includere src/lib/webpush.server.ts. Test: 9 di integrazione per m23, 17 per la coda push, 6 per la route del sondaggio, nuovi casi unitari sul worker; prova end-to-end con un servizio push finto. Documentazione e catalogo delle notifiche aggiornati. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
42 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; 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) |
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.
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. 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). |
giocatori_squadra / giocatori sono usate 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 (<giocatore_id>/<sezione>.<est>). |
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 (<giocatore_id>/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. |
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). |
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). |
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). |
Turni e notifiche
| Tabella | Scopo | Note |
|---|---|---|
turni_palloni |
Gestione dei turni palloni. | Solo turni confermati. Gli allenamenti non ricevono proposta automatica (vedi 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. Gli allenatori attivi sono sempre tra i destinatari dei promemoria evento, convocati o no (giocatori_destinatari_evento() riscritta da M21, DD-034); 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. |
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. |
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. |
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. |
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 partire dai dati esistenti (DD-007).