La migration m30 riduce il job automatico alla sola fascia delle 24 ore: il sollecito arriva una volta a chi non ha risposto o ha risposto «forse», anche per un evento creato a meno di 24 ore dall'inizio. Il pulsante manuale «Sollecita» non cambia. I tipi sollecita_presenze_12h e _6h restano ammessi per le notifiche già generate. Migration applicata in produzione. Test di integrazione aggiornati; documentati DD-045, moduli notifiche, presenze e allenatore, DATABASE, ROADMAP e changelog. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
20 KiB
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 le push delle notifiche che nascono nel database: turno palloni a 3 ore con revoca, sollecito presenze a 24 ore (DD-040). Cosa fa il canale email sta in
modules/notifiche-email.md, la push dei promemoria in
modules/notifiche.md; perché è fatto così in
DD-036
e DD-038.
Il nome del documento resta quello delle email, che sono il canale principale.
Stato in produzione: migration m22 applicata e worker delle email in funzione dal 29/09/2026,
su un Raspberry Pi con Docker. La push dei promemoria (m24) richiede le chiavi VAPID nel .env del
worker e la ricostruzione del container.
Cosa serve
| Requisito | Note |
|---|---|
| Un host con Docker | Sempre acceso e con la porta 587 in uscita libera. Nessuna porta in entrata, nessun IP pubblico. |
| Account Gmail dedicato | Con verifica in due passaggi e una password per app (vedi sotto). |
| Chiave del progetto | La service role di Supabase, oggi nel .env della root del repo come SUPABASE_SERVICE_ROLE_KEY. |
| Indirizzo dell'app | L'https pubblico a cui puntano i link nelle mail (APP_URL). |
| Chiavi VAPID | Le stesse VAPID_PUBLIC_KEY e VAPID_PRIVATE_KEY dell'app su Vercel: per la push dei promemoria. |
Il container usa l'immagine oven/bun:1-alpine, disponibile anche per ARM (Raspberry Pi). Si
costruisce dalla radice del repository (docker-compose.yml imposta context: ..), perché include
anche src/lib/webpush.server.ts, lo stesso modulo push dell'app: docker compose up -d --build
va comunque lanciato dentro mailer/. mailer/Dockerfile.dockerignore ammette nel contesto solo i
file usati, quindi nemmeno il .env finisce nell'immagine.
Account Gmail e password per app
Il worker non usa la password dell'account: Google la rifiuterebbe. Serve una password per app.
- Meglio un account dedicato e non quello personale: i messaggi partono da quell'indirizzo.
- Attiva la verifica in due passaggi (myaccount.google.com → Sicurezza e accesso).
- Apri direttamente https://myaccount.google.com/apppasswords: la voce non compare nei menu.
- Dai un nome (per esempio «CrAPP mailer») e crea. Google mostra una volta sola una password di 16 caratteri, a gruppi di quattro: copiala.
- Nel
.envva scritta senza spazi (16 caratteri attaccati): con gli spazi il file, letto come script di shell, si rompe. Non va mai incollata in chat né nel repository.
Si può revocare in ogni momento dalla stessa pagina, senza cambiare la password dell'account.
Se la pagina dice «L'impostazione che stai cercando non è disponibile per il tuo account»:
| Causa | Cosa fare |
|---|---|
| Verifica in due passaggi non attiva, o solo passkey come secondo passaggio | Attiva la verifica e aggiungi anche un'app di autenticazione (myaccount.google.com/signinoptions/twosv). |
| Protezione avanzata attiva | Disattivala da myaccount.google.com/advanced-protection. |
| Account Workspace con l'impostazione disattivata dall'amministratore | Usa un account @gmail.com normale. |
Se nulla funziona, l'alternativa è un altro account Gmail creato da zero, oppure un relay con un dominio proprio (vedi DD-036).
Installazione
-
Migration in produzione.
npx supabase db pushapplicam22. È additiva: non modifica nulla di esistente e non invia mail finché il worker non parte. Dal momento in cui è applicata, le nuove notifiche si accodano: avvia il worker subito, nella stessa sessione. Le notifiche accodate e non ancora inviate partirebbero tutte insieme al primo avvio, anche se ormai vecchie (il 29/09/2026 sono partiti così due messaggi di prova dell'admin). -
Configura il worker. In
mailer/:cp .env.example .env,chmod 600 .enve compila:Variabile Default Contenuto SUPABASE_URL— URL del progetto SUPABASE_SERVICE_ROLE_KEY— chiave service role: salta la RLS, va trattata come una password SMTP_USER,SMTP_PASS— account Gmail dedicato e password per app (vanno insieme) APP_URL— base dei link nelle mail, in https e senza slash finale MAIL_FROM,MAIL_FROM_NAMESMTP_USER,CrAPPindirizzo e nome del mittente POLL_SECONDI30 intervallo di polling MAIL_LIMITE_GIORNO400 tetto delle ultime 24 ore, sotto i circa 500 di Gmail MAIL_LOTTO10 mail prese a ogni giro SMTP_HOST,SMTP_PORT,SMTP_SECUREsmtp.gmail.com, 587,falseserver SMTP ( truesolo per la porta 465)VAPID_PUBLIC_KEY,VAPID_PRIVATE_KEY— chiavi per la push dei promemoria (vanno insieme; senza, solo email) VAPID_SUBJECTmailto:crapp@crapvolley.itcontatto nell'intestazione VAPID, come nell'app Le chiavi VAPID devono essere le stesse dell'app: le iscrizioni push dei dispositivi sono legate alla chiave pubblica con cui sono nate, e con chiavi diverse i servizi push rifiutano l'invio (403). Se mancano il worker parte lo stesso e nel log compare «push dei promemoria: DISATTIVATA».
SMTP_USEReSMTP_PASSvanno impostati insieme o nessuno dei due (senza credenziali solo per un server di prova locale). Un numero non valido o una variabile obbligatoria mancante fa uscire il worker con l'elenco degli errori. Il.envè ignorato da git.APP_URL: usa un indirizzo stabile, cioè il dominio di produzione o l'alias del branch (…-git-<branch>-….vercel.app). L'indirizzo di un singolo deploy (…-<codice>-….vercel.app) cambia a ogni pubblicazione e mostra il codice di quel momento. Se il progetto Vercel ha la Deployment Protection attiva, chi apre il link di un'anteprima vede la richiesta di accesso a Vercel. -
Avvia:
docker compose up -d --builddentromailer/. Il container harestart: unless-stopped, quindi riparte da solo dopo un riavvio dell'host. -
Verifica:
docker compose logsdeve mostrare «SMTP raggiungibile e credenziali accettate» e «push dei promemoria: attiva»; dopo circa 30 secondidocker psdeve mostrarehealthy. Poi manda un messaggio a un giocatore dalla dashboard admin e controlla che la riga in coda passi ainviatae la mail arrivi.
Gestione
| Cosa | Comando (dentro mailer/) |
|---|---|
| Vedere il log in diretta | docker compose logs -f |
| Ultime righe del log | docker compose logs --tail 50 |
| Stato e salute | docker ps (colonna STATUS: healthy / unhealthy) |
Dopo aver cambiato .env |
docker compose up -d --force-recreate (il .env è riletto a ogni avvio; non serve ricostruire l'immagine) |
| Dopo aver cambiato il codice | git pull, poi docker compose up -d --build |
| Fermarlo | docker compose down (le mail restano in coda e partono alla ripartenza) |
| Spegnere subito l'accodamento | DROP TRIGGER notifiche_utente_accoda_email ON public.notifiche_utente; da SQL: le notifiche tornano come prima di M22. Per riattivarlo si ricrea il trigger come in m22. |
Il log riporta solo id notifica, tipo ed esito (inviata, da riprovare tra N min, fallita,
ciclo fermato), mai indirizzi né testi.
Vedere cosa è partito
-
Posta inviata dell'account Gmail: le mail complete, come le ha ricevute il destinatario. È il riscontro più sicuro.
-
SQL Editor di Supabase, per stato e contenuto:
select c.stato, c.inviata_il, c.tentativi, c.errore, g.nome, g.cognome, n.tipo, n.titolo, n.corpo from notifiche_email_coda c join notifiche_utente n on n.id = c.notifica_id join giocatori_squadra g on g.id = n.giocatore_id order by n.creato_il desc limit 50;Per un riepilogo:
select stato, count(*) from notifiche_email_coda group by stato;. Lo stesso per le push dei promemoria, con la tabellanotifiche_push_coda(colonnestato,tentativi,errore,inviata_il). Il testo del messaggio sta intitolo(oggetto) ecorpo.
Spostare il worker su un altro host
Il container non conserva nulla: la coda, gli esiti e il conteggio delle ultime 24 ore stanno in Supabase. Cambiare host non fa perdere né duplicare mail.
- Sul nuovo host servono Docker con il plugin Compose e la porta 587 in uscita libera.
L'immagine
oven/bunesiste sia per ARM sia per x86, quindi l'architettura può cambiare. - Porta il codice:
git clonedel repository (ogit pullse c'è già) e usa la cartellamailer/. Se la copi a mano, saltanode_modules: l'immagine si costruisce da sola. - Copia il
.envcon un canale sicuro (per esempioscp), poichmod 600 .env. Non passa da chat né da git: contiene la service role e la password per app. - Spegni il vecchio:
docker compose downsul vecchio host. - Avvia il nuovo:
docker compose up -d --builddentromailer/. Devono comparire «SMTP raggiungibile e credenziali accettate» nel log e, dopo circa 30 secondi,healthyindocker ps.
Non serve riconfigurare nulla: APP_URL, chiave e password restano quelle del .env. Se per un
po' girano entrambi i worker non ci sono mail doppie, perché prendi_notifiche_email usa
FOR UPDATE SKIP LOCKED; conviene comunque spegnere il vecchio subito, per non confondere i log.
Se il passaggio richiede tempo, le notifiche restano in coda e partono tutte insieme all'avvio del
nuovo worker.
Al primo accesso da un IP nuovo Gmail può chiedere una conferma di sicurezza sull'account. La
password per app di solito continua a funzionare; se nel log del nuovo host compare EAUTH,
controlla la posta dell'account mittente e conferma l'accesso.
Diagnosi
| Sintomo | Causa probabile | Cosa fare |
|---|---|---|
Il log dice SMTP non verificato (EAUTH) |
Password per app sbagliata, con spazi, o revocata | Controlla la lunghezza (awk -F= '/^SMTP_PASS=/{print length($2)}' .env deve dare 16), rigenera se serve. |
Container unhealthy |
Credenziali rifiutate, oppure Supabase non raggiungibile | docker compose logs --tail 50. |
Righe che restano in_coda |
Worker fermo, oppure tetto giornaliero raggiunto | docker ps; il tetto si libera da solo, è una finestra mobile di 24 ore. |
Righe saltata |
Slot senza account collegato, senza email, o email spente | Normale: nessun destinatario utile. Verifica giocatori_squadra.email e auth_user_id. |
Righe fallita |
Errore permanente del server di posta | Leggi errore: 550 = indirizzo inesistente o rifiutato. |
| La mail arriva ma in spam | Mittente nuovo, nessuna reputazione | «Non è spam» e aggiungere il mittente ai contatti; vedi i limiti noti del modulo. |
| Il pulsante «Apri CrAPP» porta a una pagina vecchia o di accesso | APP_URL è l'indirizzo di un singolo deploy o di un'anteprima protetta |
Usa un alias stabile e ricrea il container. |
| L'interruttore «Email» non compare in Profilo | L'app pubblicata non contiene ancora il codice del modulo | Pubblica una versione che lo include. |
| Il log dice «push dei promemoria: DISATTIVATA» | Mancano VAPID_PUBLIC_KEY e VAPID_PRIVATE_KEY nel .env |
Aggiungile (le stesse dell'app) e ricrea il container. |
Push dei promemoria in fallita con errore 403 |
Chiavi VAPID diverse da quelle dell'app, o payload rifiutato | Controlla le chiavi; la query sotto mostra l'errore. |
Push dei promemoria saltata |
Il giocatore non ha dispositivi iscritti, o il promemoria aveva oltre 3 ore | Normale se nessuno ha attivato le notifiche o il worker è stato fermo a lungo. |
Prova in locale, senza toccare la produzione
npx supabase start avvia lo stack locale (la migration m22 si applica con npx supabase db reset),
con Mailpit su http://127.0.0.1:54324: le mail non escono dalla macchina. Il worker si può
lanciare senza Docker con cd mailer && set -a && . ./.env && set +a && bun mailer.mjs.
-
mailer/.envper la prova.SUPABASE_URLeSUPABASE_SERVICE_ROLE_KEYsi leggono connpx supabase status -o env(API_URL,SERVICE_ROLE_KEY). Per provare con Gmail vero si usano le credenziali reali e il worker gira anche senza Docker; il database resta quello locale. Per provare con Mailpit bisogna far girare il worker in un container (vedi in fondo): la porta SMTP di Mailpit non è esposta sull'host, solo la sua interfaccia web. In quel caso si omettonoSMTP_USEReSMTP_PASSe si impostaMAIL_FROM.POLL_SECONDI=5accorcia l'attesa. -
Un giocatore con un account collegato. Le mail vanno solo agli slot con
auth_user_ide conemail. In locale:. <(npx supabase status -o env | grep -E '^(API_URL|SERVICE_ROLE_KEY)=') UID_=$(curl -s -X POST "$API_URL/auth/v1/admin/users" \ -H "apikey: $SERVICE_ROLE_KEY" -H "Authorization: Bearer $SERVICE_ROLE_KEY" \ -H 'content-type: application/json' \ -d "{\"email\":\"prova-$(date +%s)@example.test\",\"password\":\"prova-12345\",\"email_confirm\":true}" \ | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])') docker exec supabase_db_<project_id> psql -U postgres -c \ "set session_replication_role = replica; update giocatori_squadra set auth_user_id='$UID_' where id='<slot>';"session_replication_role = replicaserve perché il trigger digiocatori_squadrarifiuta la service key. L'email dello slot deve essere quella che si vuole ricevere; è unica nella tabella (giocatori_squadra_email_unica): se l'indirizzo compare già in un altro slot del seed, si usa quello slot invece di impostarlo su un secondo. -
Una notifica di prova:
docker exec supabase_db_<project_id> psql -U postgres -c \ "insert into notifiche_utente (giocatore_id, tipo, titolo, corpo) values ('<slot>','admin','Prova CrAPP','Messaggio di prova');"Entro un ciclo di polling il log mostra
inviata …e la mail compare in Mailpit (o nella casella). -
Fine prova:
npx supabase db resetriporta il database locale pulito.
Per provare anche l'immagine Docker, il container si collega alla rete di Supabase
(--network supabase_network_<project_id>) e usa http://supabase_kong_<project_id>:8000 come
SUPABASE_URL e supabase_inbucket_<project_id> sulla porta 1025 come SMTP.
Per provare la push dei promemoria senza un telefono: si genera una coppia di chiavi VAPID di
prova, si avvia un piccolo server HTTP locale che finge di essere il servizio push (risponde 201 su un
percorso e 410 su un altro), si inseriscono in push_subscriptions due iscrizioni con chiavi ECDH
valide che puntano a quel server e si crea un promemoria in notifiche_utente. Il worker lo cifra, lo
spedisce con Authorization: vapid …, Content-Encoding: aes128gcm, Urgency: high e TTL, segna
la riga inviata ed elimina l'iscrizione che ha risposto 410. Il 29/09/2026 la prova è stata fatta
così.
L'app in sviluppo (npm run dev) richiede un Node più recente del 18: su un host con Node 18 Vite
non parte, e con lui i test di integrazione che avviano il server. Si può usare bun al posto di Node:
bun --bun x vite dev --port 8080 (con SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY e
SUPABASE_SERVICE_ROLE_KEY dello stack locale) e poi BASE_URL=http://localhost:8080 davanti ai
test. I test del database e del worker non ne hanno bisogno.
Sicurezza
- Il
.envcontiene la service role e la password per app: permessi600, mai in git, mai in chat. - La service role non passa da Vercel: sta solo sull'host del worker. Anche la chiave VAPID privata sta sull'host, oltre che su Vercel: stessa attenzione, mai in git né in chat.
- Il log delle push riporta solo id notifica e numeri: mai gli endpoint, che identificano il dispositivo del giocatore.
- Se la password per app o la service role sono state esposte, si revocano/ruotano subito (Google e
dashboard Supabase) e si aggiorna il
.envcon--force-recreate.