NilsBriggen f58d6566fb Upset meter: the results the model found least likely
The scoreboard gains a Biggest surprises card — finished matches whose
actual outcome the frozen model priced under 30%, sorted by shock
value, each linking to its match page. Pure client derivation from
data the scoreboard already had.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 01:10:02 +02:00

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.

S
Description
World Cup 2026 data dashboard — live scores, prediction model, StatsBomb data-story viz
Readme 4.7 MiB
Languages
TypeScript 98.1%
JavaScript 1%
CSS 0.7%
Dockerfile 0.1%