- 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 <noreply@anthropic.com>
4.3 KiB
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:
- messaggio libero dell'admin (
notifica-personalizzata.ts, in parallelo alla push); - promemoria automatico prima di un evento, 24h e 3h prima, due
cron.schedule(estensionepg_cron, abilitata dal 31/07/2026 e usata per la prima volta qui); - turno palloni (
promemoria-palloni.ts), in parallelo alla push; - 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_appper 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
INSERTinvece diupsertper turno palloni e sollecito presenze → scartata: un admin che preme due volte lo stesso pulsante per lo stesso evento avrebbe urtato contro il vincoloUNIQUE (giocatore_id, evento_id, tipo). L'upsert aggiorna la riga esistente (testo ecreato_ilfreschi,lettadi nuovofalse), 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 (<input type="time">si può svuotare): la funzionegenera_promemoria_eventi()usaora_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.