Web Push: goal, kickoff and full-time alerts for a followed team
Fully progressive: without VAPID keys in the environment the API says 404, the bell never renders, and the service worker addition (a push handler via workbox importScripts — no SW strategy change) is inert. With keys: a bell on each team page requests permission, subscribes (one team per device) and the server diffs fixture states on every broadcast to push Kickoff / Goal / Full time to that team's followers. Dead endpoints (404/410) self-clean. Round-trip verified locally with dev VAPID keys; disabled mode verified too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import { buildPreview, buildTeamProfile } from './preview';
|
||||
import { buildScoreboard, snapshotCheck } from './scoreboard';
|
||||
import { db, healthAll, oddsFor, latestOddsAll, dbCounts, inplayHistory, recordInplay, getMatchExt } from './db/db';
|
||||
import { inPlayProbs } from '../../src/lib/model/inplay';
|
||||
import { PushNotifier, pushEnabled, subscribe as pushSubscribe, unsubscribe as pushUnsubscribe, vapidPublicKey } from './push';
|
||||
import type { EspnSummary } from './ingest/espnNormalize';
|
||||
import { loadStatic } from './preview';
|
||||
import { canonicalTeam } from '../../src/lib/teams';
|
||||
@@ -53,9 +54,11 @@ export function buildServer() {
|
||||
};
|
||||
|
||||
/** Re-push the snapshot, and predictions too if the finished-result set moved. */
|
||||
const notifier = new PushNotifier();
|
||||
const broadcast = (): void => {
|
||||
sendAll(snapshotMessage());
|
||||
if (model.recompute(state.allFixtures())) sendAll(predictionsMessage());
|
||||
try { notifier.tick(state.allFixtures()); } catch { /* never block a broadcast */ }
|
||||
};
|
||||
|
||||
// Record the in-play win probability per game state while matches run —
|
||||
@@ -88,6 +91,25 @@ export function buildServer() {
|
||||
return { points: Number.isFinite(num) ? inplayHistory(num) : [] };
|
||||
});
|
||||
|
||||
// Web Push (feature-flagged on VAPID env): key discovery + subscriptions.
|
||||
app.get('/api/push/key', async (_req, reply) =>
|
||||
pushEnabled ? { key: vapidPublicKey } : reply.code(404).send({ error: 'push disabled' }),
|
||||
);
|
||||
app.post('/api/push/subscribe', async (req, reply) => {
|
||||
if (!pushEnabled) return reply.code(404).send({ error: 'push disabled' });
|
||||
const b = (req.body ?? {}) as { subscription?: { endpoint?: string }; team?: string };
|
||||
if (!b.subscription?.endpoint || !b.team) return reply.code(400).send({ error: 'subscription + team required' });
|
||||
pushSubscribe(b.subscription as { endpoint: string }, b.team);
|
||||
return { ok: true };
|
||||
});
|
||||
app.post('/api/push/unsubscribe', async (req, reply) => {
|
||||
if (!pushEnabled) return reply.code(404).send({ error: 'push disabled' });
|
||||
const b = (req.body ?? {}) as { endpoint?: string };
|
||||
if (!b.endpoint) return reply.code(400).send({ error: 'endpoint required' });
|
||||
pushUnsubscribe(b.endpoint);
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
// Golden Boot: per-player goals aggregated from structured ESPN scoring
|
||||
// plays across every started match (own goals + shootout kicks excluded).
|
||||
app.get('/api/goldenboot', async () => {
|
||||
|
||||
Reference in New Issue
Block a user