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>
Cup26 — World Cup 2026 dashboard
A local-first PWA for the 2026 FIFA World Cup: live scores, a model-driven prediction engine, and data-story visualizations — all in one TypeScript app.
- Live — fixtures, group tables and a knockout bracket that update over a WebSocket as results come in.
- Predict — an Elo + Dixon-Coles + Monte-Carlo model that simulates the whole tournament: per-match win/draw/loss, championship odds, and a title-race chart that moves with every result.
- Story — StatsBomb open-data visualizations (shot map, xG race, pass networks) of the 2022 World Cup final.
Stack
Vite + React 19 + TypeScript (strict), Tailwind 4, TanStack Router, Zustand, a Fastify WebSocket server, Recharts + custom SVG. Packaged as an installable PWA and a single Docker image. Bun for tooling.
Data sources
| Source | Used for | Notes |
|---|---|---|
| openfootball/worldcup.json | 2026 fixtures + bracket structure | public domain, no key |
| football-data.org | live scores & status | free tier, World Cup included, 10 req/min, delayed |
| SofaScore (unofficial) | near-real-time scores/events | optional, fragile, feature-flagged |
| martj42 international results | Elo / goals model training | 49k matches, 1872→now |
| StatsBomb Open Data | the Story page | 2022 WC final event data |
All external calls happen server-side, so the browser only talks to its own
origin and the client CSP stays locked to 'self'.
Develop
bun install
bun run data:build # generate icons + fixtures.json + ratings.json
bun run dev # web UI on http://localhost:5173 (proxies /api + /ws)
bun run dev:server # the Fastify live/model server on :8787 (separate shell)
Without a FOOTBALL_DATA_TOKEN the app runs in seed-only mode (full schedule,
model and story; no live scores). To exercise the live pipeline in dev:
curl -XPOST localhost:8787/api/dev/score -H content-type:application/json \
-d '{"num":1,"homeScore":2,"awayScore":1,"status":"finished"}'
Test & build
bun run test # vitest (model + standings invariants)
bun run build # tsc -b && vite build (the deploy gate)
Deploy
A self-contained Docker image serves the built PWA and the live/model server.
docker build -t cup26 .
docker run -p 8787:8787 -e FOOTBALL_DATA_TOKEN=... cup26
deploy/cup26.compose.yml is a Traefik-ready Compose project (mirrors the
sibling TTRPG deploy): rsync the repo to the host, drop it in as
docker-compose.yml, set FOOTBALL_DATA_TOKEN, then docker compose up -d --build.
Environment
| Var | Default | Purpose |
|---|---|---|
PORT |
8787 |
server port |
FOOTBALL_DATA_TOKEN |
– | football-data.org API token (enables live scores) |
ENABLE_SOFASCORE |
false |
layer the unofficial real-time source on top |
SIM_ITER |
20000 |
Monte-Carlo iterations per recompute |
ALLOWED_ORIGINS |
– | comma-separated WebSocket origin allowlist |
Odds are model estimates for fun, not betting advice.