Sposta le route di notifiche push su PocketBase

Le route server in src/routes/api/public/ (promemoria-palloni,
sollecita-presenze, apri-sondaggio, push-subscribe, notifiche-attive)
usano pbAdmin al posto di supabaseAdmin per leggere/scrivere
push_subscriptions e i dati necessari a comporre gli avvisi.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-11 14:58:40 +02:00
co-authored by Claude Sonnet 5
parent 689468141d
commit d125bdfb6a
5 changed files with 90 additions and 67 deletions
+10 -10
View File
@@ -6,6 +6,8 @@ import { inviaPush } from "@/lib/webpush.server";
const schema = z.object({ eventoId: z.string().min(1).max(50) }); const schema = z.object({ eventoId: z.string().min(1).max(50) });
type RigaIscrizione = { id: string; endpoint: string; p256dh: string; auth: string };
/** Avviso "sondaggio pre-partita aperto": lo fa partire un admin dalla pagina partita. */ /** Avviso "sondaggio pre-partita aperto": lo fa partire un admin dalla pagina partita. */
export const Route = createFileRoute("/api/public/apri-sondaggio")({ export const Route = createFileRoute("/api/public/apri-sondaggio")({
server: { server: {
@@ -21,23 +23,21 @@ export const Route = createFileRoute("/api/public/apri-sondaggio")({
const partita = eventi.find((e) => e.id === parsed.data.eventoId); const partita = eventi.find((e) => e.id === parsed.data.eventoId);
if (!partita) return new Response("Evento non trovato", { status: 404 }); if (!partita) return new Response("Evento non trovato", { status: 404 });
const { supabaseAdmin } = await import("@/integrations/supabase/client.server"); const { pbAdmin } = await import("@/integrations/pocketbase/client.server");
const { data: iscrizioni } = await supabaseAdmin const admin = await pbAdmin();
.from("push_subscriptions") const iscrizioni = await admin
.select("endpoint, p256dh, auth"); .collection("push_subscriptions")
.getFullList<RigaIscrizione>();
const titolo = "💩 Sondaggio pre-partita aperto"; const titolo = "💩 Sondaggio pre-partita aperto";
const testo = `${partita.titolo} · ore ${partita.ora}. Quante cacche hai fatto? Rispondi prima del fischio d'inizio.`; const testo = `${partita.titolo} · ore ${partita.ora}. Quante cacche hai fatto? Rispondi prima del fischio d'inizio.`;
let inviate = 0; let inviate = 0;
for (const iscrizione of iscrizioni ?? []) { for (const iscrizione of iscrizioni) {
try { try {
const { stato } = await inviaPush(iscrizione, titolo, testo); const { stato } = await inviaPush(iscrizione, titolo, testo);
if (stato === 404 || stato === 410) { if (stato === 404 || stato === 410) {
await supabaseAdmin await admin.collection("push_subscriptions").delete(iscrizione.id);
.from("push_subscriptions")
.delete()
.eq("endpoint", iscrizione.endpoint);
} else if (stato >= 200 && stato < 300) { } else if (stato >= 200 && stato < 300) {
inviate += 1; inviate += 1;
} }
@@ -46,7 +46,7 @@ export const Route = createFileRoute("/api/public/apri-sondaggio")({
} }
} }
return Response.json({ inviate, destinatari: (iscrizioni ?? []).length }); return Response.json({ inviate, destinatari: iscrizioni.length });
}, },
}, },
}, },
+11 -7
View File
@@ -9,16 +9,20 @@ export const Route = createFileRoute("/api/public/notifiche-attive")({
const negato = await richiediAdmin(request); const negato = await richiediAdmin(request);
if (negato) return negato; if (negato) return negato;
const { supabaseAdmin } = await import("@/integrations/supabase/client.server"); const { pbAdmin } = await import("@/integrations/pocketbase/client.server");
const { data, error } = await supabaseAdmin const admin = await pbAdmin();
.from("push_subscriptions")
.select("giocatore_id"); try {
if (error) { const righe = await admin
.collection("push_subscriptions")
.getFullList<{ giocatore: string }>();
return Response.json({
giocatoreIds: idsConNotificheAttive(righe.map((r) => ({ giocatore_id: r.giocatore }))),
});
} catch (error) {
console.error("notifiche-attive", error); console.error("notifiche-attive", error);
return new Response("Errore lettura", { status: 500 }); return new Response("Errore lettura", { status: 500 });
} }
return Response.json({ giocatoreIds: idsConNotificheAttive(data ?? []) });
}, },
}, },
}, },
+23 -19
View File
@@ -9,6 +9,15 @@ import { leggiEventi } from "@/lib/eventi.server";
const schema = z.object({ eventoId: z.string().min(1).max(50) }); const schema = z.object({ eventoId: z.string().min(1).max(50) });
type RigaTurno = { evento: string; giocatore: string };
type RigaIscrizione = {
id: string;
endpoint: string;
giocatore: string;
p256dh: string;
auth: string;
};
/** /**
* Promemoria del turno palloni per un evento: lo fa partire un admin dalla pagina * Promemoria del turno palloni per un evento: lo fa partire un admin dalla pagina
* dell'evento (DD-025). Il testo viaggia cifrato dentro la push. * dell'evento (DD-025). Il testo viaggia cifrato dentro la push.
@@ -27,13 +36,12 @@ export const Route = createFileRoute("/api/public/promemoria-palloni")({
const evento = eventi.find((e) => e.id === parsed.data.eventoId); const evento = eventi.find((e) => e.id === parsed.data.eventoId);
if (!evento) return new Response("Evento non trovato", { status: 404 }); if (!evento) return new Response("Evento non trovato", { status: 404 });
const { supabaseAdmin } = await import("@/integrations/supabase/client.server"); const { pbAdmin } = await import("@/integrations/pocketbase/client.server");
const admin = await pbAdmin();
const { data: righe } = await supabaseAdmin const righeTurni = await admin.collection("turni_palloni").getFullList<RigaTurno>();
.from("turni_palloni")
.select("evento_id, giocatore_id");
const salvati: Record<string, string> = {}; const salvati: Record<string, string> = {};
for (const riga of righe ?? []) salvati[riga.evento_id] = riga.giocatore_id; for (const riga of righeTurni) salvati[riga.evento] = riga.giocatore;
const squadra = await leggiGiocatoriSquadra(); const squadra = await leggiGiocatoriSquadra();
const rosa = squadra const rosa = squadra
@@ -44,25 +52,21 @@ export const Route = createFileRoute("/api/public/promemoria-palloni")({
const avvisi = avvisiPalloniEvento(turni, eventi, evento.id); const avvisi = avvisiPalloniEvento(turni, eventi, evento.id);
if (avvisi.length === 0) return Response.json({ inviate: 0, destinatari: 0 }); if (avvisi.length === 0) return Response.json({ inviate: 0, destinatari: 0 });
const { data: iscrizioni } = await supabaseAdmin const idsFiltro = avvisi.map((a) => a.giocatoreId);
.from("push_subscriptions") const filtro = idsFiltro.map((_, i) => `giocatore = {:g${i}}`).join(" || ");
.select("endpoint, giocatore_id, p256dh, auth") const parametri = Object.fromEntries(idsFiltro.map((id, i) => [`g${i}`, id]));
.in( const iscrizioni = await admin
"giocatore_id", .collection("push_subscriptions")
avvisi.map((a) => a.giocatoreId), .getFullList<RigaIscrizione>({ filter: admin.filter(filtro, parametri) });
);
let inviate = 0; let inviate = 0;
for (const iscrizione of iscrizioni ?? []) { for (const iscrizione of iscrizioni) {
const avviso = avvisi.find((a) => a.giocatoreId === iscrizione.giocatore_id); const avviso = avvisi.find((a) => a.giocatoreId === iscrizione.giocatore);
if (!avviso) continue; if (!avviso) continue;
try { try {
const { stato } = await inviaPush(iscrizione, avviso.titolo, avviso.testo); const { stato } = await inviaPush(iscrizione, avviso.titolo, avviso.testo);
if (stato === 404 || stato === 410) { if (stato === 404 || stato === 410) {
await supabaseAdmin await admin.collection("push_subscriptions").delete(iscrizione.id);
.from("push_subscriptions")
.delete()
.eq("endpoint", iscrizione.endpoint);
} else if (stato >= 200 && stato < 300) { } else if (stato >= 200 && stato < 300) {
inviate += 1; inviate += 1;
} }
@@ -71,7 +75,7 @@ export const Route = createFileRoute("/api/public/promemoria-palloni")({
} }
} }
return Response.json({ inviate, destinatari: (iscrizioni ?? []).length }); return Response.json({ inviate, destinatari: iscrizioni.length });
}, },
}, },
}, },
+27 -16
View File
@@ -17,17 +17,24 @@ export const Route = createFileRoute("/api/public/push-subscribe")({
const parsed = schemaIscrizione.safeParse(await request.json()); const parsed = schemaIscrizione.safeParse(await request.json());
if (!parsed.success) return new Response("Dati non validi", { status: 400 }); if (!parsed.success) return new Response("Dati non validi", { status: 400 });
const { supabaseAdmin } = await import("@/integrations/supabase/client.server"); const { pbAdmin } = await import("@/integrations/pocketbase/client.server");
const { error } = await supabaseAdmin.from("push_subscriptions").upsert( const { upsertByFilter } = await import("@/integrations/pocketbase/upsert");
{ const admin = await pbAdmin();
endpoint: parsed.data.endpoint,
giocatore_id: parsed.data.giocatoreId, try {
p256dh: parsed.data.p256dh, await upsertByFilter(
auth: parsed.data.auth, "push_subscriptions",
}, "endpoint = {:endpoint}",
{ onConflict: "endpoint" }, { endpoint: parsed.data.endpoint },
); {
if (error) { endpoint: parsed.data.endpoint,
giocatore: parsed.data.giocatoreId,
p256dh: parsed.data.p256dh,
auth: parsed.data.auth,
},
admin,
);
} catch (error) {
console.error("push-subscribe", error); console.error("push-subscribe", error);
return new Response("Errore salvataggio", { status: 500 }); return new Response("Errore salvataggio", { status: 500 });
} }
@@ -37,11 +44,15 @@ export const Route = createFileRoute("/api/public/push-subscribe")({
const parsed = schemaCancellazione.safeParse(await request.json()); const parsed = schemaCancellazione.safeParse(await request.json());
if (!parsed.success) return new Response("Dati non validi", { status: 400 }); if (!parsed.success) return new Response("Dati non validi", { status: 400 });
const { supabaseAdmin } = await import("@/integrations/supabase/client.server"); const { pbAdmin } = await import("@/integrations/pocketbase/client.server");
await supabaseAdmin const admin = await pbAdmin();
.from("push_subscriptions") const esistente = await admin
.delete() .collection("push_subscriptions")
.eq("endpoint", parsed.data.endpoint); .getFirstListItem(
admin.filter("endpoint = {:endpoint}", { endpoint: parsed.data.endpoint }),
)
.catch(() => null);
if (esistente) await admin.collection("push_subscriptions").delete(esistente.id);
return Response.json({ ok: true }); return Response.json({ ok: true });
}, },
}, },
+19 -15
View File
@@ -12,6 +12,9 @@ const schema = z.object({
da: z.string().min(1).max(60).optional(), da: z.string().min(1).max(60).optional(),
}); });
type RigaPresenza = { giocatore: string; stato: string };
type RigaIscrizione = { id: string; endpoint: string; p256dh: string; auth: string };
export const Route = createFileRoute("/api/public/sollecita-presenze")({ export const Route = createFileRoute("/api/public/sollecita-presenze")({
server: { server: {
handlers: { handlers: {
@@ -26,22 +29,26 @@ export const Route = createFileRoute("/api/public/sollecita-presenze")({
const evento = eventi.find((e) => e.id === parsed.data.eventoId); const evento = eventi.find((e) => e.id === parsed.data.eventoId);
if (!evento) return new Response("Evento non trovato", { status: 404 }); if (!evento) return new Response("Evento non trovato", { status: 404 });
const { supabaseAdmin } = await import("@/integrations/supabase/client.server"); const { pbAdmin } = await import("@/integrations/pocketbase/client.server");
const admin = await pbAdmin();
const { data: righe } = await supabaseAdmin const righe = await admin.collection("risposte_presenze").getFullList<RigaPresenza>({
.from("risposte_presenze") filter: admin.filter("evento = {:evento}", { evento: evento.id }),
.select("giocatore_id, stato") });
.eq("evento_id", evento.id);
const squadra = await leggiGiocatoriSquadra(); const squadra = await leggiGiocatoriSquadra();
const destinatari = destinatariSollecito(squadra, righe ?? []); const destinatari = destinatariSollecito(
squadra,
righe.map((r) => ({ giocatore_id: r.giocatore, stato: r.stato })),
);
if (destinatari.length === 0) return Response.json({ inviate: 0, destinatari: 0 }); if (destinatari.length === 0) return Response.json({ inviate: 0, destinatari: 0 });
const { data: iscrizioni } = await supabaseAdmin const filtro = destinatari.map((_, i) => `giocatore = {:g${i}}`).join(" || ");
.from("push_subscriptions") const parametri = Object.fromEntries(destinatari.map((id, i) => [`g${i}`, id]));
.select("endpoint, p256dh, auth") const iscrizioni = await admin
.in("giocatore_id", destinatari); .collection("push_subscriptions")
.getFullList<RigaIscrizione>({ filter: admin.filter(filtro, parametri) });
const titolo = "Manca la tua risposta"; const titolo = "Manca la tua risposta";
const testo = `${evento.titolo} · ${formatData(evento.data)} ore ${evento.ora}. ${ const testo = `${evento.titolo} · ${formatData(evento.data)} ore ${evento.ora}. ${
@@ -49,14 +56,11 @@ export const Route = createFileRoute("/api/public/sollecita-presenze")({
} una conferma: presente, assente o in ritardo?`; } una conferma: presente, assente o in ritardo?`;
let inviate = 0; let inviate = 0;
for (const iscrizione of iscrizioni ?? []) { for (const iscrizione of iscrizioni) {
try { try {
const { stato } = await inviaPush(iscrizione, titolo, testo); const { stato } = await inviaPush(iscrizione, titolo, testo);
if (stato === 404 || stato === 410) { if (stato === 404 || stato === 410) {
await supabaseAdmin await admin.collection("push_subscriptions").delete(iscrizione.id);
.from("push_subscriptions")
.delete()
.eq("endpoint", iscrizione.endpoint);
} else if (stato >= 200 && stato < 300) { } else if (stato >= 200 && stato < 300) {
inviate += 1; inviate += 1;
} }