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:
2026-06-12 08:21:55 +02:00
parent ce83d4dbc4
commit 03b0fa734e
13 changed files with 331 additions and 6 deletions
+32
View File
@@ -93,6 +93,13 @@ CREATE TABLE IF NOT EXISTS prediction_snapshots (
market_json TEXT
);
CREATE TABLE IF NOT EXISTS push_subs (
endpoint TEXT PRIMARY KEY,
json TEXT NOT NULL,
team TEXT,
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS inplay_history (
fixture_num INTEGER NOT NULL,
minute INTEGER NOT NULL,
@@ -147,6 +154,31 @@ export function cacheSet(key: string, value: string, ttl: number): void {
.run(key, value, Date.now(), ttl);
}
// ---- Web Push subscriptions (goal/kickoff notifications, opt-in per team) ----
export interface PushSub {
endpoint: string;
json: string;
team: string | null;
}
export function addPushSub(endpoint: string, json: string, team: string | null): void {
db()
.prepare('INSERT OR REPLACE INTO push_subs (endpoint, json, team, created_at) VALUES (?, ?, ?, ?)')
.run(endpoint, json, team, Date.now());
}
export function removePushSub(endpoint: string): void {
db().prepare('DELETE FROM push_subs WHERE endpoint = ?').run(endpoint);
}
export function pushSubsForTeams(teams: string[]): PushSub[] {
if (teams.length === 0) return [];
const marks = teams.map(() => '?').join(',');
return db()
.prepare(`SELECT endpoint, json, team FROM push_subs WHERE team IN (${marks})`)
.all(...teams) as unknown as PushSub[];
}
// ---- in-play win-probability history (the momentum curve on match pages) ----
export interface InplayPoint {
minute: number;