Promemoria senza ritorni e tre canali per ogni notifica (DD-037, DD-038)

m23: un registro (promemoria_eventi_generati) ricorda i promemoria a 24 e 3 ore
già generati, così eliminare la notifica non li fa rigenerare dal cron né
riaccodare la mail a ogni giro.

m24: la push dei promemoria, che nascono nel database, la manda il worker
mailer/ con lo stesso modulo dell'app (coda notifiche_push_coda, esiti, backoff,
iscrizioni scadute eliminate, push scartate oltre 3 ore). Il sondaggio
pre-partita scrive anche notifiche_utente (tipo sondaggio_cacche): compare
in-app e arriva per email. L'immagine del worker si costruisce dalla radice
del repository per includere src/lib/webpush.server.ts.

Test: 9 di integrazione per m23, 17 per la coda push, 6 per la route del
sondaggio, nuovi casi unitari sul worker; prova end-to-end con un servizio
push finto. Documentazione e catalogo delle notifiche aggiornati.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5.5 committed 2026-09-29 14:19:54 +02:00
1 parent 58eba642f7
commit e9013b703b
27 files changed
+2076 -92

No files matched your search

+23 -5
View File
@@ -1,6 +1,8 @@
import { createFileRoute } from "@tanstack/react-router";
import type { SupabaseClient } from "@supabase/supabase-js";
import { z } from "zod";
import { richiediAdmin } from "@/lib/auth-route.server";
import { avvisoSondaggio, destinatariSondaggio } from "@/lib/cacche";
import { leggiEventi } from "@/lib/eventi.server";
import { inviaPush } from "@/lib/webpush.server";
import { isAllenatore } from "@/lib/giocatori-squadra";
@@ -27,14 +29,30 @@ export const Route = createFileRoute("/api/public/apri-sondaggio")({
const { data: tutte } = await supabaseAdmin
.from("push_subscriptions")
.select("endpoint, p256dh, auth, giocatore_id");
const squadra = await leggiGiocatoriSquadra();
// Il sondaggio cacche non riguarda l'allenatore, che non le vede (DD-034).
const allenatori = new Set(
(await leggiGiocatoriSquadra()).filter(isAllenatore).map((g) => g.id),
);
const allenatori = new Set(squadra.filter(isAllenatore).map((g) => g.id));
const iscrizioni = (tutte ?? []).filter((i) => !allenatori.has(i.giocatore_id));
const titolo = "💩 Sondaggio pre-partita aperto";
const testo = `${partita.titolo} · ore ${partita.ora}. Quante cacche hai fatto? Rispondi prima del fischio d'inizio.`;
const { titolo, testo } = avvisoSondaggio(partita);
// Storico in-app ed email (DD-038): indipendente dai dispositivi iscritti alla push. Un
// upsert perché ripremere il pulsante per la stessa partita deve riportare la notifica a
// non letta, non fallire per il vincolo UNIQUE; la mail riparte dal trigger di M22.
// `types.ts` non include ancora `notifiche_utente`, stesso aggiramento delle altre route.
const client = supabaseAdmin as unknown as SupabaseClient;
await client.from("notifiche_utente").upsert(
destinatariSondaggio(squadra).map((giocatoreId) => ({
giocatore_id: giocatoreId,
tipo: "sondaggio_cacche",
titolo,
corpo: testo,
evento_id: partita.id,
letta: false,
creato_il: new Date().toISOString(),
})),
{ onConflict: "giocatore_id,evento_id,tipo" },
);
let inviate = 0;
for (const iscrizione of iscrizioni) {