Features (all from already-stored data):
- Venue pages: /venues hub + /venue/:name with each stadium's schedule,
altitude badge (>1000 m), local timezone. Linked from the match hero,
nav More sheet, and footer.
- Attacking-zones card on the match Stats tab (left/center/right share per
side, from the stored FotMob payload).
- US broadcast networks on the pre-match hero (SUMMARY_V 6).
- Notification preferences: follow multiple teams + an all-matches feed
('*'); push_subs keyed by (endpoint, team) with a safe one-time migration;
device drops its browser subscription only when the last follow is removed.
- FIFA tournament squad card on team pages (26-man list, position groups).
- Player match logs now show DNP rows for unused-squad appearances.
- OG/share meta for /player and /venue.
Polish (adversarial audit: 25 confirmed findings, all fixed):
- Mobile horizontal-scroll eliminated on /teams and /venues (grid-item
min-w-0 blowout); two-line venue fixture rows.
- Truncation actually fires now across team fixtures, road-to-final,
recent-form, H2H meetings, venue rows (min-w-0 on flex children).
- Team hero no longer detaches the standing block on wrap; bell denied-state
no longer overflows the h1.
- AllMatchesBell: 36px touch target, aria-label, own i18n key, even rhythm.
- Server hardening: cached player index (one cross-fixture scan, not per
request), migration rollback guard, 400 (not 500) on malformed name URLs,
all-zero zones render nothing, tidyName fixes shouted particles (DE→De).
- German polish: removed stray English ("anytime", "Build-Geocode"), clearer
squad note.
Gates: 170 tests, build, typecheck:server — all green.
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.