# 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](https://github.com/openfootball/worldcup.json) | 2026 fixtures + bracket structure | public domain, no key | | [football-data.org](https://www.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](https://github.com/martj42/international_results) | Elo / goals model training | 49k matches, 1872→now | | [StatsBomb Open Data](https://github.com/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 ```bash 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: ```bash curl -XPOST localhost:8787/api/dev/score -H content-type:application/json \ -d '{"num":1,"homeScore":2,"awayScore":1,"status":"finished"}' ``` ## Test & build ```bash 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. ```bash 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**.