71 lines
4.3 KiB
Markdown
71 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.
|