diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index 87d60fc..7566407 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -25,10 +25,10 @@ da cancellazioni precedenti a M14 sono state bonificate una tantum (M15/M16). - Cursor e Claude Code come ambienti di sviluppo - Vercel configurato; Environment Variables aggiornate al nuovo Supabase (Preview e Production) - Supabase proprietario attivo — Project Ref: `kfkcldwncxqaixetsjes` -- 36 migration in `supabase/migrations/`, fino a `m25_promemoria_solo_giocatori` (29/09/2026). - Le prime 33 (fino a `m22_notifiche_email`) sono applicate in produzione, verificato con - `npx supabase migration list` il 29/09/2026; **`m23`, `m24` e `m25` sono verificate solo in locale - e non ancora applicate in produzione** +- 37 migration in `supabase/migrations/`, fino a `m26_notifiche_automatiche` (30/09/2026). + Le prime 36 (fino a `m25_promemoria_solo_giocatori`) sono applicate in produzione, verificato con + `npx supabase migration list` il 30/09/2026; **`m26` è verificata solo in locale e non ancora + applicata in produzione** - Worker delle notifiche email (`mailer/`) in funzione su un Raspberry Pi con Docker dal 29/09/2026: invia via Gmail le notifiche di `notifiche_utente`. Installazione e gestione in [docs/WORKER_EMAIL.md](docs/WORKER_EMAIL.md); il codice con l'interruttore «Email» in Profilo è @@ -211,10 +211,10 @@ Niente di assegnato: l'avviso certificati in scadenza (DD-035, 1.1.0) e le notif canali (DD-036–DD-039, 1.2.0) sono implementati (vedi `docs/CHANGELOG.md`). Le voci ancora aperte stanno in [docs/ROADMAP.md](docs/ROADMAP.md), sotto «Prossimo». -Specificato ma non ancora implementato: notifiche automatiche per turno palloni (12, 6 e 3 ore -prima, senza email, con revoca dell'incarico), solleciti presenze (24, 12 e 6 ore prima) e promemoria con -«oggi/domani» (DD-040, migration `m26` da scrivere). I pulsanti «Avvisa chi è di turno» e «Sollecita» -restano. +Implementato e verificato in locale, non ancora rilasciato: notifiche automatiche per turno palloni +(12, 6 e 3 ore prima, senza email, con revoca dell'incarico), solleciti presenze (24, 12 e 6 ore prima) e +promemoria con «oggi/domani» (DD-040, migration `m26`, da applicare in produzione). I pulsanti «Avvisa chi +è di turno» e «Sollecita» restano. --- diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index cca31d9..a4f9a34 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -13,6 +13,29 @@ versioni `0.y.z` erano pre-release. ## [Non rilasciato] +Le notifiche di turno palloni e sollecito presenze partono anche da sole, e i testi sono più formali. +La migration `m26` è verificata in locale e va applicata in produzione insieme a questa versione (DD-040). + +### Aggiunto + +- **Turno palloni automatico** — 12, 6 e 3 ore prima di allenamenti e partite con un incaricato confermato, + all'incaricato e a chi aveva i palloni all'evento precedente. Se l'incaricato cambia, chi era stato avvisato + riceve una sola volta «Turno palloni: incarico revocato». Arriva come push e nel centro notifiche, non per + email. +- **Sollecito presenze automatico** — 24, 12 e 6 ore prima dell'evento, ai convocati che in quel momento non + hanno risposto o hanno risposto «forse»; chi risponde smette di riceverlo. Un evento creato tardi riceve solo + il sollecito della sua fascia. +- Nuovo test di integrazione `notifiche-automatiche` sui job veri del database, più test unitari dei testi e + dei destinatari. + +### Modificato + +- **Testi delle notifiche** — promemoria con «domani» o «oggi», data, ora e luogo; turno palloni e sollecito + con righe etichettate (Evento, Data, Ora, Luogo, Incarico, Azione). Stessi testi per i pulsanti e per i job. +- **Sollecito manuale** — il pulsante «Sollecita» ora segue i convocati dell'evento, come l'automatico, invece + di tutti i giocatori attivi; il testo dice la risposta attuale e chi lo ha chiesto. +- Le righe del corpo di una notifica vanno a capo nel centro notifiche. + ## [1.2.0] - 2026-09-29 Le notifiche arrivano su tre canali (push, centro notifiche in-app ed email) e non si ripetono più. diff --git a/docs/DATABASE.md b/docs/DATABASE.md index fe295af..bda377f 100644 --- a/docs/DATABASE.md +++ b/docs/DATABASE.md @@ -75,16 +75,32 @@ La tabella è verificata da `test/integration/permessi.test.ts` contro il databa ## Turni e notifiche -| Tabella | Scopo | Note | -| ---------------------------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `turni_palloni` | Gestione dei turni palloni. | Solo turni **confermati**. Gli allenamenti non ricevono proposta automatica (vedi [palloni.md](modules/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. I promemoria evento arrivano solo ai giocatori attivi: ai convocati, o a tutti se `convocati` è vuoto (`giocatori_destinatari_evento()`, riscritta da M21 per includere sempre gli allenatori e da M25 per escluderli, DD-039); 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](modules/notifiche-email.md). Con DD-040 (migration `m26`, da scrivere) il trigger salta i tipi del turno palloni, che non mandano email. | -| `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. Con DD-040 (migration `m26`, da scrivere) registra anche turno palloni e solleciti presenze a 24, 12 e 6 ore. | -| `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`. Con DD-040 (migration `m26`, da scrivere) accoda anche turno palloni e solleciti presenze. | +| Tabella | Scopo | Note | +| ---------------------------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `turni_palloni` | Gestione dei turni palloni. | Solo turni **confermati**. Gli allenamenti non ricevono proposta automatica (vedi [palloni.md](modules/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. I promemoria evento arrivano solo ai giocatori attivi: ai convocati, o a tutti se `convocati` è vuoto (`giocatori_destinatari_evento()`, riscritta da M21 per includere sempre gli allenatori e da M25 per escluderli, DD-039); 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. Migration `m26_notifiche_automatiche` (DD-040): il vincolo `CHECK` sul tipo ammette anche `turno_palloni_12h`, `_6h`, `_3h`, `turno_palloni_revocato` e `sollecita_presenze_24h`, `_12h`, `_6h`, scritti dai job; `turno_palloni` e `sollecita_presenze` restano i tipi dei pulsanti. Il trigger delle email salta tutti i tipi `turno_palloni*`; quello delle push accoda i tipi automatici ma non i due dei pulsanti. | +| `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](modules/notifiche-email.md). Con DD-040 (migration `m26`) il trigger salta i tipi del turno palloni, che non mandano email. | +| `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. Con DD-040 (migration `m26`) il `CHECK` sul tipo ammette anche i tipi dei palloni (12h, 6h, 3h, revocato) e dei solleciti (24h, 12h, 6h); è anche la memoria che permette la revoca. | +| `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`. Con DD-040 (migration `m26`) accoda anche turno palloni e solleciti presenze. | + +### 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](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. | `promemoria-eventi-24h` (ogni ora), `-3h` (15 min) | +| `genera_avvisi_palloni()` (M26), con `genera_avvisi_palloni_fascia()` | Avvisi del turno palloni a 12, 6 e 3 ore 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), con `..._fascia()` | Solleciti a 24, 12 e 6 ore ai destinatari dell'evento senza risposta o con «forse». | `solleciti-presenze-automatici` (15 min) | +| `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). | — | ## Funzioni speciali diff --git a/docs/DESIGN_DECISIONS.md b/docs/DESIGN_DECISIONS.md index a2649a0..f559a0a 100644 --- a/docs/DESIGN_DECISIONS.md +++ b/docs/DESIGN_DECISIONS.md @@ -1763,8 +1763,8 @@ alla sua possibilità di rispondere. ### DD-040 — Promemoria con «oggi» o «domani», turno palloni e solleciti automatici **Data:** 30 settembre 2026 -**Stato:** Accettata (solo documentata: codice e migration `m26` non ancora scritti; fino al rilascio -l'app si comporta come in 1.2.0) +**Stato:** Accettata (implementata e verificata in locale: migration `m26`, job, route e test; la +migration non è ancora applicata in produzione) **Contesto** Quattro notifiche su sei partivano da un pulsante premuto da un admin (DD-025 per i palloni): se nessuno @@ -1791,8 +1791,10 @@ dell'evento, e che i testi siano più formali e completi. I testi nuovi sono nel - **Incaricato cambiato dopo un avviso.** Il nuovo incaricato riceve il suo avviso al giro successivo, con il tipo della finestra in cui cade. Chi aveva già ricevuto un avviso e non è più destinatario riceve **una sola volta** «Turno palloni: incarico revocato» (`turno_palloni_revocato`), se - l'evento non è iniziato. Il job lo scopre confrontando i destinatari attuali con le notifiche - `turno_palloni*` già scritte per l'evento, senza trigger su `turni_palloni`. + l'evento non è iniziato. Il job lo scopre confrontando i destinatari attuali con il **registro** degli + avvisi automatici già generati (non con le notifiche, che il giocatore può aver eliminato), senza + trigger su `turni_palloni`. Gli avvisi del pulsante non contano: possono riguardare una proposta non + confermata. - **Niente email.** Gli avvisi palloni (automatici, revoca e pulsante) arrivano come push e nel centro notifiche, **non per email**: il trigger `notifiche_utente_accoda_email` salta i tipi `turno_palloni`, `turno_palloni_12h`, `_6h`, `_3h` e `turno_palloni_revocato`. È un'eccezione a @@ -1836,10 +1838,11 @@ dell'evento, e che i testi siano più formali e completi. I testi nuovi sono nel - Cambia la regola «quattro notifiche su sei dipendono da un gesto manuale»: restano solo manuali il messaggio dello staff e il sondaggio pre-partita; per turno palloni e sollecito gli automatici si aggiungono ai pulsanti, e un giocatore può ricevere lo stesso avviso due volte. -- **Logica duplicata:** «evento precedente» e «evento successivo» per i palloni, oggi in `palloni-core.ts` - (usato dal pulsante e dalla schermata), vengono riscritti in SQL per i job. Se si cambia una regola in - una sola delle due versioni, pulsante e job danno risposte diverse: un test di integrazione le confronta - sugli stessi dati. +- **Logica duplicata:** «evento precedente» e «evento successivo» per i palloni, in `palloni-core.ts` + (usato dal pulsante e dalla schermata), sono riscritti in SQL per i job (`genera_avvisi_palloni_fascia`). + Ordine comune: data, ora, id. Se si cambia una regola in una sola delle due versioni, pulsante e job danno + risposte diverse: `notifiche-automatiche.test.ts` confronta testi e destinatari sugli stessi dati, anche a + parità di data e ora. - `destinatariSollecito()` riceve anche i convocati e serve al sollecito manuale; un giocatore non convocato non è più sollecitato. `avvisiPalloniEvento()` resta come riferimento per i test finché il SQL non la sostituisce del tutto. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 13d25f3..6db1304 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -44,12 +44,11 @@ Tutto quello che è rilasciato, fino alla versione 1.0.0 (vedi `CHANGELOG.md`). - [x] Notifiche email — ogni notifica in-app inviata anche per email agli account registrati, da un worker Docker sull'host di casa via Gmail, con interruttore «Email» in Profilo ([specifica](modules/notifiche-email.md), DD-036) +- [x] Notifiche automatiche — promemoria con «oggi/domani», turno palloni 12, 6 e 3 ore prima, sollecito + presenze a 24, 12 e 6 ore (i pulsanti «Avvisa chi è di turno» e «Sollecita» restano) ([specifica](modules/notifiche.md#testi-delle-notifiche), DD-040) ## Prossimo -- [ ] Notifiche automatiche — promemoria con «oggi/domani», turno palloni 12, 6 e 3 ore prima, sollecito - presenze a 24, 12 e 6 ore (i pulsanti «Avvisa chi è di turno» e «Sollecita» restano) ([specifica](modules/notifiche.md#testi-delle-notifiche), DD-040) - - [ ] Calendario ufficiale — i dati delle gare future arrivano già dal feed CSI, la pagina Campionato usa solo quelle giocate - [ ] Database esercizi diff --git a/docs/WORKER_EMAIL.md b/docs/WORKER_EMAIL.md index 14aa663..9eac5ee 100644 --- a/docs/WORKER_EMAIL.md +++ b/docs/WORKER_EMAIL.md @@ -1,7 +1,7 @@ # Worker email: installazione e gestione Come si mette in funzione, si controlla e si diagnostica il worker (`mailer/`) che invia le notifiche -per email e la push dei promemoria a 24 e 3 ore. Cosa fa il canale email sta in +per email e le push delle notifiche che nascono nel database: promemoria a 24 e 3 ore, turno palloni a 12, 6 e 3 ore con revoca, solleciti presenze a 24, 12 e 6 ore (DD-040). Cosa fa il canale email sta in [modules/notifiche-email.md](modules/notifiche-email.md), la push dei promemoria in [modules/notifiche.md](modules/notifiche.md#push-dei-promemoria-m24); perché è fatto così in [DD-036](DESIGN_DECISIONS.md#dd-036--notifiche-email-via-gmail-da-un-worker-docker-sullhost-di-casa) diff --git a/docs/modules/allenatore.md b/docs/modules/allenatore.md index a8180fb..d0d13a6 100644 --- a/docs/modules/allenatore.md +++ b/docs/modules/allenatore.md @@ -114,7 +114,7 @@ a lui (`usePuoGestireEventi()`). Può anche **sollecitare le presenze** di un evento, come l'admin: la route `sollecita-presenze.ts` accetta admin o allenatore (`richiediGestoreEventi()`), e il -sollecito arriva solo ai giocatori che non hanno risposto, mai all'allenatore stesso. Con DD-040 (non ancora implementato) si aggiungono solleciti automatici a 24, 12 e 6 ore prima dell'evento, ma il pulsante resta. +sollecito arriva solo ai giocatori che non hanno risposto, mai all'allenatore stesso. Con DD-040 si aggiungono solleciti automatici a 24, 12 e 6 ore prima dell'evento, ma il pulsante resta. **Restano solo admin:** dashboard `/admin`, dati e documenti dei giocatori, export CSI, notifica personalizzata, promemoria palloni, apertura sondaggio cacche, correzione delle diff --git a/docs/modules/notifiche.md b/docs/modules/notifiche.md index ab6a965..93bde69 100644 --- a/docs/modules/notifiche.md +++ b/docs/modules/notifiche.md @@ -50,10 +50,9 @@ automatiche (1, 2, 4 e 5) nascono nel database e la manda il worker `mailer/` (v promemoria»). Il sondaggio riscrive la notifica esistente se l'admin preme di nuovo il pulsante per lo stesso evento (vedi «Centro notifiche in-app»). -> **Stato delle righe 1, 2, 4 e 5 (DD-040).** Testi, orari e destinatari sono quelli **decisi** il -> 30/09/2026, non ancora in codice: mancano la migration `m26` e le funzioni SQL dei job. I pulsanti -> «Avvisa chi è di turno» e «Sollecita» **restano** e si aggiungono agli avvisi automatici. Fino al rilascio l'app si comporta come in 1.2.0: promemoria -> con il testo breve, turno palloni e sollecito presenze solo da pulsante. Le sezioni più in basso descrivono lo stato attuale; i testi dei due pulsanti diventano quelli nuovi. +> **Righe 1, 2, 4 e 5 (DD-040).** Implementate con la migration `m26` (job `pg_cron`, testi, revoca) e +> verificate in locale; la migration va ancora applicata in produzione. Finché non lo è, l'app si comporta +> come in 1.2.0: promemoria con il testo breve, turno palloni e sollecito presenze solo da pulsante. Cosa riceve l'allenatore, notifica per notifica: [allenatore.md](allenatore.md#notifiche). @@ -74,11 +73,13 @@ disattivazione). Tra `<…>` i valori che cambiano a ogni invio; le date sono `G | 5 | Sollecito presenze | `Conferma di partecipazione richiesta: ` | `Data: `
`Ora: `
`Luogo: `
`Richiesta di: ` (solo nel sollecito manuale, e solo se il nome c'è)
`Risposta attuale: nessuna` (oppure `forse`)
`Azione: indicare presente, assente o in ritardo` — stesso testo a 24, 12 e 6 ore | | 6 | Sondaggio pre-partita | `💩 Sondaggio pre-partita aperto` | ` · ore . Quante cacche hai fatto? Rispondi prima del fischio d'inizio.` | -Le righe 1-2, 4a, 4b e 5 sono i testi nuovi (DD-040, ancora da implementare): nasceranno nelle -funzioni SQL della migration `m26`. Quelli in uso oggi sono: promemoria «Promemoria: _titolo_» con corpo -«_data_ alle _ora_» (migration `m23`, senza luogo e senza «oggi/domani»); turno palloni in -`palloni-core.ts` (`avvisiPalloniEvento`: «Tocca a te prendere i palloni» / «Porta i palloni»); sollecito in -`sollecita-presenze.ts` («Manca la tua risposta»). Le righe 3 e 6 non cambiano: +Le righe 1-2, 4a-4c e 5 nascono nelle funzioni SQL della migration `m26` +(`genera_promemoria_eventi`, `genera_avvisi_palloni_fascia`, `genera_revoche_palloni`, +`genera_solleciti_presenze_fascia`); i pulsanti usano gli stessi testi da `palloni-core.ts` +(`avvisiPalloniEvento`) e `presenze.ts` (`testoSollecito`), e `notifiche-automatiche.test.ts` verifica che siano +identici. In tutte la riga «Luogo» manca se l'evento non ha un luogo. Fino all'applicazione della `m26` in produzione valgono i testi di 1.2.0: promemoria +«Promemoria: _titolo_» con corpo «_data_ alle _ora_», turno palloni «Tocca a te prendere i palloni» / «Porta +i palloni», sollecito «Manca la tua risposta». Le righe 3 e 6 non cambiano: `notifica-personalizzata.ts` e `cacche.ts` (`avvisoSondaggio`). **Notifiche smart (riga 7).** Solo locali: la notifica di sistema ha il titolo preceduto @@ -145,16 +146,16 @@ Sono l'effetto attuale della logica, non scelte documentate altrove: ### Test che verificano il catalogo -| Cosa | Test | -| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -| Promemoria 24 e 3 ore: destinatari (convocati, tutti i giocatori attivi se `convocati` è vuoto, mai l'allenatore), deduplica, eliminazione, coda push ed email | `test/integration/destinatari-notifiche.test.ts`, `promemoria-eventi.test.ts`, `push-promemoria.test.ts`, `notifiche-email.test.ts` | -| Messaggio dello staff: push a tutti i dispositivi (allenatore compreso) o a uno solo, in-app a tutta la rosa attiva, riservato agli admin | `destinatari-notifiche.test.ts`, `notifiche-utente.test.ts` | -| Turno palloni: incaricato e chi li aveva prima, solo allenamenti e partite, riservato agli admin | `destinatari-notifiche.test.ts`, `unit/palloni-core.test.ts` | -| Sollecito presenze: chi non ha risposto o ha detto «forse», mai l'allenatore; permesso ad allenatore e admin, non ai giocatori | `destinatari-notifiche.test.ts`, `unit/presenze.test.ts` | -| Sondaggio pre-partita: push senza allenatori, avviso in-app ed email ai giocatori attivi, `upsert`, riservato agli admin | `destinatari-notifiche.test.ts`, `sondaggio-notifiche.test.ts`, `unit/cacche.test.ts` | -| Promemoria con «oggi/domani», turno palloni a 12·6·3 ore e revoca, solleciti a 24·12·6 ore (manuale compreso): finestre, destinatari, una sola volta, registro (DD-040, **previsti**, da scrivere con la `m26`) | `promemoria-eventi.test.ts` (aggiornato), nuovo `notifiche-automatiche.test.ts`, `unit/palloni-core.test.ts` | -| Worker: esiti, backoff, tetto giornaliero, iscrizioni scadute | `unit/mailer-core.test.ts` | -| L'allenatore è fuori dalla rosa di gioco | `unit/giocatori-squadra.test.ts` (`inRosa`) | +| Cosa | Test | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Promemoria 24 e 3 ore: destinatari (convocati, tutti i giocatori attivi se `convocati` è vuoto, mai l'allenatore), deduplica, eliminazione, coda push ed email | `test/integration/destinatari-notifiche.test.ts`, `promemoria-eventi.test.ts`, `push-promemoria.test.ts`, `notifiche-email.test.ts` | +| Messaggio dello staff: push a tutti i dispositivi (allenatore compreso) o a uno solo, in-app a tutta la rosa attiva, riservato agli admin | `destinatari-notifiche.test.ts`, `notifiche-utente.test.ts` | +| Turno palloni: incaricato e chi li aveva prima, solo allenamenti e partite, riservato agli admin | `destinatari-notifiche.test.ts`, `unit/palloni-core.test.ts` | +| Sollecito presenze: chi non ha risposto o ha detto «forse», mai l'allenatore; permesso ad allenatore e admin, non ai giocatori | `destinatari-notifiche.test.ts`, `unit/presenze.test.ts` | +| Sondaggio pre-partita: push senza allenatori, avviso in-app ed email ai giocatori attivi, `upsert`, riservato agli admin | `destinatari-notifiche.test.ts`, `sondaggio-notifiche.test.ts`, `unit/cacche.test.ts` | +| Promemoria con «oggi/domani», turno palloni a 12·6·3 ore e revoca, solleciti a 24·12·6 ore (manuale compreso): finestre, destinatari, una sola volta, registro (DD-040, `m26`) | `integration/notifiche-automatiche.test.ts` (nuovo), `promemoria-eventi.test.ts` (testo oggi/domani), `destinatari-notifiche.test.ts` (pulsanti), `unit/palloni-core.test.ts`, `unit/presenze.test.ts` | +| Worker: esiti, backoff, tetto giornaliero, iscrizioni scadute | `unit/mailer-core.test.ts` | +| L'allenatore è fuori dalla rosa di gioco | `unit/giocatori-squadra.test.ts` (`inRosa`) | Le push dei test vanno a un servizio push finto in locale: si verifica **a chi** arrivano, non la consegna su un telefono. Non ha un test l'esclusione dell'allenatore dalle **notifiche smart**: la @@ -281,8 +282,8 @@ non richiede che il dispositivo abbia attivato «Notifiche», ha uno storico per stessa cosa delle notifiche smart (quelle restano locali, non salvate a database). Cinque sorgenti scrivono in `notifiche_utente`, mai il client (con DD-040 le sorgenti 3 e 4 hanno in più un -job `pg_cron` come la 2: turno palloni a 12, 6 e 3 ore, sollecito a 24, 12 e 6 ore; i pulsanti restano; -l'elenco descrive lo stato attuale): +job `pg_cron` come la 2, che scrive `turno_palloni_12h`/`_6h`/`_3h`/`_revocato` e `sollecita_presenze_24h`/ +`_12h`/`_6h`; i pulsanti restano e scrivono `turno_palloni` e `sollecita_presenze`): 1. **Messaggio admin** — `notifica-personalizzata.ts` inserisce una riga per destinatario in parallelo all'invio push esistente (a tutta la rosa attiva se `giocatoreId` è omesso). @@ -307,6 +308,9 @@ invece di un semplice insert: se l'admin preme di nuovo il pulsante per lo stess notifica esistente viene aggiornata (testo e `creato_il` freschi, `letta` riportata a false) invece di duplicarsi o fallire per il vincolo `UNIQUE`. +Il corpo delle notifiche automatiche è a righe etichettate (Data, Ora, Luogo…): `RigaNotifica` lo mostra con +`whitespace-pre-line`, e un test di `unit/notifiche-utente.test.ts` verifica che la classe ci sia. + Lettura, "segna come letta" ed eliminazione passano dal client Supabase autenticato con RLS (`useNotificheMie()`/`useSegnaLette()`/`useEliminaNotifica()`), senza una route API dedicata — stesso pattern di `useSalvaEvento()` in `src/lib/eventi.ts`. Aprire il pannello segna tutte diff --git a/docs/modules/palloni.md b/docs/modules/palloni.md index ab8e3ef..fbb7326 100644 --- a/docs/modules/palloni.md +++ b/docs/modules/palloni.md @@ -26,7 +26,7 @@ compaiono. ## Implementazione -- `eventiPalloni()` (`palloni-core.ts`) è il filtro comune a tutta la pipeline (proposte, +- `eventiPalloni()` (`palloni-core.ts`) ordina gli eventi per data, poi ora, poi id (lo stesso ordine dei job SQL di `m26`: «evento precedente» e «successivo» dipendono da qui) ed è il filtro comune a tutta la pipeline (proposte, conteggi, serie, promemoria): include solo `tipo === "partita"` e `tipo === "allenamento"`, scarta eventi extra-campo e compleanni. - `completaTurni()` (`palloni-core.ts`) propone, per ogni **partita** senza turno già salvato, @@ -55,12 +55,12 @@ oggi`, stesso criterio delle presenze): un turno assegnato in anticipo per un al ## Route API pubblica `/api/public/promemoria-palloni` -> **Affiancata da un invio automatico (DD-040, non ancora implementato).** Oltre al pulsante, un job +> **Affiancata da un invio automatico (DD-040, migration `m26`).** Oltre al pulsante, un job > `pg_cron` ogni 15 minuti manda l'avviso **12, 6 e 3 ore prima** dell'evento, solo se per l'evento c'è un > incaricato confermato in `turni_palloni`. Il pulsante e la route **restano**, con tipo > `turno_palloni` e push dalla route. I due destinatari e i testi nuovi («incarico assegnato» / > «riconsegna», validi per il pulsante e per i job, più la revoca se l'incaricato cambia) sono nel -> [catalogo](notifiche.md#testi-delle-notifiche). Il resto della sezione descrive lo stato attuale. +> [catalogo](notifiche.md#testi-delle-notifiche). Il resto della sezione descrive il pulsante. La fa partire un **amministratore** dal pulsante «Avvisa chi è di turno» dentro il riquadro palloni dell'evento (`TurnoPalloni.tsx`), riservato agli admin (DD-025). Riceve l'`eventoId`, diff --git a/docs/modules/presenze.md b/docs/modules/presenze.md index 0c35350..542a7f1 100644 --- a/docs/modules/presenze.md +++ b/docs/modules/presenze.md @@ -68,20 +68,21 @@ compresi) sarebbe fuorviante lì, perché l'MVP si vota solo alle partite. Bottone "Sollecita" (RosaPresenze.tsx) → POST /api/public/sollecita-presenze ↓ src/routes/api/public/sollecita-presenze.ts - ├─ legge l'evento (eventi_app) e le risposte già date + ├─ legge l'evento (eventi_app, con i convocati) e le risposte già date ├─ destinatariSollecito() → src/lib/presenze.ts - │ (attivi senza risposta o con "forse"; funzione pura, testata in unit) + │ (convocati, o tutta la rosa se vuoti, senza risposta o con "forse"; funzione pura) + ├─ testoSollecito() per ciascuno (risposta attuale e «Richiesta di») ├─ per ciascuno invia una push col testo cifrato nel payload │ (src/lib/webpush.server.ts) └─ elimina le iscrizioni push scadute (404/410) ``` -> **Sollecito automatico (DD-040, non ancora implementato).** Il pulsante «Sollecita» e la route +> **Sollecito automatico (DD-040, migration `m26`).** Il pulsante «Sollecita» e la route > **restano**, ma `destinatariSollecito()` passa a guardare anche i convocati (convocati che non hanno > risposto o hanno risposto «forse», o tutta la rosa se `convocati` è vuoto); in più un job `pg_cron` manda il sollecito **24, 12 e 6 ore prima** dell'evento a chi in > quel momento non ha risposto o ha risposto «forse», tra i destinatari dell'evento (convocati, o > tutta la rosa attiva; mai l'allenatore). Testo e finestre nel [catalogo](notifiche.md#catalogo-delle-notifiche). -> Il diagramma sopra descrive lo stato attuale. +> Il diagramma sopra è quello del pulsante, che da DD-040 legge anche `convocati` dell'evento. Un evento conta ai fini delle statistiche di presenza solo se è di tipo `partita` o `allenamento` e il giocatore è tra i convocati (o non ci sono convocati specificati, cioè diff --git a/test/README.md b/test/README.md index 5618be0..2979f9d 100644 --- a/test/README.md +++ b/test/README.md @@ -24,12 +24,12 @@ la consegna effettiva a schermo bloccato richiede un telefono e il servizio push ## Struttura -| Cartella | Cosa verifica | Serve rete? | -| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | -| `unit/` | Logica di dominio pura: badge, serie, palloni, pagelle, MVP, cacche, scout, obiettivi, notifiche, parsing CSI, dati della rosa. Più le funzioni pure isolabili nei moduli con hook/rete (validazione upload, guardie push, JWT VAPID, cattura errori, avatar, guardia admin delle route, lettura del `.env` ed elenco dei bucket dello script di backup, logica del worker in `mailer/` (email e push dei promemoria), preferenza «Email» e avviso del sondaggio) | No | -| `integration/` | Le route `/api/public/*` sul server di sviluppo: risposte, cache, validazione degli input. Più schema e permessi del Profilo Giocatore (`schema-profili`) contro il database configurato; permessi per ruolo, incluse le policy di M11, le deroghe dell'amministratore e le tabelle lasciate aperte di proposito (`permessi`), accesso alle route di notifica (`permessi-route`), semantica degli upsert e vincoli (`scritture`), le letture lato server delle route push (`lettori-server`) e la pulizia a cascata dei dati collegati alla cancellazione di un evento (`pulizia-evento`, M14) la funzione di bonifica delle righe orfane (`bonifica-evento`, M16), il centro notifiche in-app (`notifiche-utente`, M17), la coda delle email e le sue funzioni RPC (`notifiche-email`, M22), i promemoria evento che non ricompaiono se eliminati (`promemoria-eventi`, M23), la coda delle push dei promemoria (`push-promemoria`, M24) e il sondaggio in-app/email (`sondaggio-notifiche`, M24), i destinatari delle notifiche per ruolo (`destinatari-notifiche`: promemoria, messaggio dello staff, sondaggio, sollecito e turno palloni con allenatore e giocatore), la nascita pubblica sincronizzata dal profilo (`nascita-pubblica`, M18) e il suo backfill per i profili preesistenti (`backfill-nascita-esistenti`, M19 — unico test che esegue SQL diretto via `docker exec` nel container Postgres, perché PostgREST non espone quell'update multi-tabella come RPC) sul database locale | Sì | -| `e2e/` | Percorsi completi sull'app servita: schermate, dati CSI fino alla pagina, file PWA, 404 | Sì | -| `helpers/` | Avvio del server di test e mini-harness condiviso | — | +| Cartella | Cosa verifica | Serve rete? | +| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | +| `unit/` | Logica di dominio pura: badge, serie, palloni, pagelle, MVP, cacche, scout, obiettivi, notifiche, parsing CSI, dati della rosa. Più le funzioni pure isolabili nei moduli con hook/rete (validazione upload, guardie push, JWT VAPID, cattura errori, avatar, guardia admin delle route, lettura del `.env` ed elenco dei bucket dello script di backup, logica del worker in `mailer/` (email e push dei promemoria), preferenza «Email» e avviso del sondaggio) | No | +| `integration/` | Le route `/api/public/*` sul server di sviluppo: risposte, cache, validazione degli input. Più schema e permessi del Profilo Giocatore (`schema-profili`) contro il database configurato; permessi per ruolo, incluse le policy di M11, le deroghe dell'amministratore e le tabelle lasciate aperte di proposito (`permessi`), accesso alle route di notifica (`permessi-route`), semantica degli upsert e vincoli (`scritture`), le letture lato server delle route push (`lettori-server`) e la pulizia a cascata dei dati collegati alla cancellazione di un evento (`pulizia-evento`, M14) la funzione di bonifica delle righe orfane (`bonifica-evento`, M16), il centro notifiche in-app (`notifiche-utente`, M17), la coda delle email e le sue funzioni RPC (`notifiche-email`, M22), i promemoria evento che non ricompaiono se eliminati (`promemoria-eventi`, M23), la coda delle push dei promemoria (`push-promemoria`, M24), i job automatici di turno palloni e sollecito presenze con revoca, finestre e testi identici al codice (`notifiche-automatiche`, M26, DD-040) e il sondaggio in-app/email (`sondaggio-notifiche`, M24), i destinatari delle notifiche per ruolo (`destinatari-notifiche`: promemoria, messaggio dello staff, sondaggio, sollecito e turno palloni con allenatore e giocatore), la nascita pubblica sincronizzata dal profilo (`nascita-pubblica`, M18) e il suo backfill per i profili preesistenti (`backfill-nascita-esistenti`, M19 — unico test che esegue SQL diretto via `docker exec` nel container Postgres, perché PostgREST non espone quell'update multi-tabella come RPC) sul database locale | Sì | +| `e2e/` | Percorsi completi sull'app servita: schermate, dati CSI fino alla pagina, file PWA, 404 | Sì | +| `helpers/` | Avvio del server di test e mini-harness condiviso | — | ## Server di sviluppo con Node vecchio