Il testo della notifica viaggia ora cifrato dentro la push (aes128gcm, RFC
8188/8291) invece di essere recuperato dal service worker con una fetch al
risveglio. Era quella fetch a non chiudersi in tempo: ad app chiusa il browser
tiene vivo il worker pochi secondi, showNotification non veniva mai chiamata e
non compariva niente, mentre ad app aperta con la rete calda sembrava tutto a
posto. La POST porta anche Urgency: high, che chiede la consegna immediata
invece di far accumulare i messaggi fino al risveglio del dispositivo.
Cadono i pezzi che esistevano solo per rimediare al payload vuoto: la route
push-messaggio, la coda promemoria_push con la sua scadenza a 12 ore,
messaggioPalloniOggi() e il timeout nel worker. Tutti e tre i mittenti avevano
gia il testo pronto prima di inviare.
Il worker si aggiorna da solo all'avvio e a ogni ritorno in primo piano
(mantieniWorkerPushAggiornato): nella webapp installata quello vecchio puo
sopravvivere a lungo, e senza questo un dispositivo resterebbe fermo alla
versione che va a cercare il testo in rete.
Profilo -> Opzioni ha "Mandami una notifica di prova", visibile solo a notifiche
attive: manda una push a questo dispositivo e riporta stato HTTP, corpo della
risposta e se l'endpoint risulta davvero in push_subscriptions. Senza, "non
arriva" era cieco: ogni prova richiedeva un admin, un evento nello stato giusto
e una seconda persona, e la risposta del servizio push veniva buttata via.
inviaPush torna { stato, corpo } e logga il corpo sui rifiuti.
Dalle prove sul campo: a parita di server, iPhone installato da Home riceve ad
app chiusa. Su Android installato come webapp resta da verificare: il WebAPK e
un'app Android a se, con permesso notifiche (Android 13+) e voce batteria
distinti da quelli di Chrome. Annotato nei limiti noti.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
8.6 KiB
Modulo — Notifiche
Stato: implementato — un unico opt-in dispositivo abilita tutto il canale push
File principali: src/lib/notifiche-smart.ts, src/lib/push-client.ts,
src/lib/webpush.server.ts, src/routes/api/public/push-config.ts,
src/routes/api/public/push-subscribe.ts, src/routes/api/public/push-prova.ts,
public/push-sw.js
Obiettivo
Tenere aggiornati i giocatori senza che debbano aprire l'app, con due meccanismi indipendenti:
- Push VAPID — arrivano anche ad app chiusa (turno palloni, sollecito presenze).
- Notifiche smart — notifiche locali mostrate solo ad app aperta, generate da badge, serie e obiettivi appena raggiunti; non è un canale push separato.
Dati
push_subscriptions (un dispositivo per riga, chiave endpoint, con le chiavi p256dh e
auth con cui si cifra il payload per quel dispositivo). La tabella promemoria_push non è
più usata da nessuno: serviva da coda del testo quando la push partiva vuota (DD-026).
Iscrizione alle notifiche push
In Profilo → Opzioni c’è un solo interruttore («Notifiche»). Non esistono preferenze separate per tipo di messaggio: l’iscrizione registra il dispositivo e lo rende destinatario di tutte le push (promemoria palloni, solleciti presenze) e abilita anche le notifiche smart in app, che usano lo stesso service worker.
- Il giocatore attiva «Notifiche» in
/profilo→ richiesta permesso browser. GET /api/public/push-configrestituisce solo la chiave pubblica VAPID.- Registrazione del service worker
public/push-sw.jsepushManager.subscribe(). POST /api/public/push-subscriberegistra endpoint e chiavi inpush_subscriptions(upsert).
All'avvio e quando l'app torna visibile viene richiesto l'aggiornamento della registrazione
push esistente con ServiceWorkerRegistration.update(). Non si chiede un nuovo permesso,
non si ricrea la sottoscrizione e non si cambia l'endpoint: anche chi ha già attivato le
notifiche deve ricevere le correzioni del worker senza spegnere e riaccendere l'interruttore.
Gli aggiornamenti contemporanei sono accorpati; un errore di rete non blocca l'app e si
riprova al ritorno in primo piano. Il worker attende skipWaiting() durante l'installazione.
Il browser controlla anche autonomamente gli aggiornamenti: questa richiesta esplicita
copre in particolare le sessioni lunghe della webapp (vedi il
ciclo di vita del service worker).
Ruolo delle tre route pubbliche
push-config— espone la sola chiave pubblica VAPID.push-subscribe— registra o rimuove l'iscrizione di un dispositivo.push-prova— manda una push al dispositivo che la chiede e riporta stato e corpo della risposta del servizio push, più se l'endpoint risulta inpush_subscriptions. Serve a rendere osservabile un "non arriva": senza, ogni prova richiede un admin, un evento nello stato giusto e una seconda persona. Il pulsante sta in Profilo → Opzioni, sotto l'interruttore, e compare solo a notifiche attive.apri-sondaggio— premuto da un admin dalla pagina partita: manda a tutti i dispositivi iscritti l'avviso di apertura del sondaggio pre-partita (vedi Scout Live).
L'invio effettivo (src/lib/webpush.server.ts, funzione inviaPush) firma un JWT VAPID
(ECDSA P-256), cifra {title, body} per il dispositivo destinatario e fa una POST
all'endpoint push del browser; è riusato identico da sollecita-presenze.ts,
promemoria-palloni.ts e apri-sondaggio.ts.
Il testo viaggia dentro la push, cifrato in aes128gcm (RFC 8188/8291) con le chiavi del
dispositivo: il service worker fa event.data.json() e mostra la notifica senza toccare la
rete. È il punto decisivo per la consegna ad app chiusa — il browser sveglia il worker per
pochi secondi, e una fetch per recuperare il testo lo faceva morire prima di
showNotification (DD-026).
La POST porta Urgency: high. Con l'urgenza predefinita ("normal") un telefono in risparmio
energetico accumula i messaggi fino al risveglio: la notifica arriva solo quando il
dispositivo è già attivo — cioè, nella pratica, solo con l'app aperta.
Chi può farle partire (DD-024, DD-025)
Queste route usano la service role e saltano la RLS, quindi il permesso deve stare nella
route. Tutte e tre partono da un gesto di un amministratore dentro l'app, quindi il controllo
è uno solo (richiediAdmin in src/lib/auth-route.server.ts) e non serve configurare nessuna
variabile d'ambiente.
| Route | Controllo | Chi la chiama |
|---|---|---|
apri-sondaggio, sollecita-presenze, promemoria-palloni |
richiediAdmin — token della sessione Supabase, poi ruolo admin in user_roles |
l'app, da un pulsante riservato agli admin |
csi, push-config, push-subscribe, push-prova |
nessuno | il browser prima del login, che una sessione non ce l'ha ancora |
Notifiche smart
calcolaNotifiche() (notifiche-smart.ts) genera un evento solo quando "c'è qualcosa di
reale": badge appena sbloccato, "sei a un passo" da un traguardo, serie che raggiunge un
traguardo esatto, obiettivo di squadra tra il 90 e il 100%, badge social vinto. Ogni notifica
ha un id deterministico; quelli già mostrati sono salvati in localStorage per non
ripetersi — deduplica puramente locale al dispositivo, non sincronizzata.
Limiti noti
- Non ci sono preferenze granulari (solo palloni / solo presenze / solo smart): un dispositivo è iscritto o no. Separare i canali richiederebbe schema e UI dedicati.
- La tabella
promemoria_pushè rimasta nel database ma non la usa più nessuno (DD-026): va eliminata con una migrazione alla prossima occasione. - Un 2xx dal server push non significa consegnato. FCM accetta con 201 anche verso
registrazioni scadute e poi butta via il messaggio, senza il 404/410 che farebbe pulire
push_subscriptions. Il conteggio "inviate a N dispositivi" va letto come "accettate da N server push", non come "arrivate a N telefoni". - Compatibilità iOS/Safari non gestita esplicitamente nel codice (nessun branch dedicato): serve l'installazione da schermata Home per funzionare, ma l'app non lo segnala esplicitamente. È il primo sospetto quando una notifica non arriva ad app chiusa su iPhone.
- Android con l'app installata (WebAPK) è il caso fragile. A parità di server — stessa push, stesso payload — su iPhone installato da Home arriva ad app chiusa, sullo stesso invio verso un Android installato come webapp no. Il WebAPK è un'app Android a sé: ha un proprio permesso notifiche di sistema. Le restrizioni di sistema possono impedire la visualizzazione o ritardare il risveglio del browser: un 201 dal servizio push non permette di distinguerli. Controllare Impostazioni → App → CrAPP → Notifiche e le eventuali restrizioni di batteria dell'app e del browser.
- Su Motorola verificare anche le restrizioni del browser che ha installato CrAPP e, dove presente, Impostazioni → Batteria → Ottimizzazione standby app. Il produttore documenta la limitazione dei processi in background (guida Motorola). È una possibile causa del sintomo, non una diagnosi verificata sul dispositivo: il codice web non può rimuovere questi vincoli. La verifica richiede un invio da un altro dispositivo mentre CrAPP è chiusa e lo schermo del Motorola è bloccato. Il pulsante di prova invia subito, quindi da solo non dimostra la ricezione in background.
- Le notifiche smart dipendono da un service worker già registrato: se il giocatore non ha
mai attivato le push,
notificaSistema()non ha unrega cui appoggiarsi e la notifica locale non viene mai mostrata, anche con permesso concesso. - Il payload cifrato non può superare i ~4 KB: i testi attuali stanno larghi, ma un messaggio molto lungo verrebbe rifiutato dal servizio push.
Evoluzioni possibili
- Preferenze per canale (palloni, solleciti, smart), se servono davvero alla squadra.
- Eliminare
promemoria_pushcon una migrazione. - Gestire esplicitamente il caso iOS (messaggio se l'app non è installata da Home).