- 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>
72 lines
4.3 KiB
Markdown
72 lines
4.3 KiB
Markdown
# 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 (`<input type="time">` 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.
|