Implements steps 1-3 of the Claude Design handoff: - MatchTimeline: vertical center spine (home left, away right, minute pills on the rail), goal cards with parsed scorer/assist/running score, sub and booking chips, synthetic kick-off/half-time/full-time dividers; single left rail under the sm breakpoint - matchEvents: pure ESPN commentary parser (scorer, assist, penalty, own goal, sub in/out, embedded score) with tests on real feed shapes - Hero scoreboard: panel gradient + accent glow, smallcaps meta, mono score, stat chips (xG / POTM / attendance / referee; possession and shots while live); weather line stays for upcoming matches - Sticky Summary/Timeline/Stats/Lineups sub-nav; all existing cards re-bucketed with no feature loss (in-play win prob, swing chart, xG race, shot map, momentum, odds movement, form, H2H, lineups) 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.