Files
cup26/README.md
T
NilsBriggen 9c7605391c Phase 4: polish + deploy-ready — mobile nav, README, Docker validation
- Mobile bottom tab bar (icon tabs) with the top nav hidden on small screens;
  content padding so it never overlaps
- data: scripts run via bun (not tsx/node) so the oven/bun build image needs no
  node — fixes the Dockerfile data:build step
- README: features, data sources, dev/test/build/deploy, env vars
- Validated the production Docker image end-to-end: build succeeds (378MB),
  container serves /healthz, /api/snapshot (104 fixtures), /api/predictions
  (20k sims), the SPA with deep-link fallback, and the story dataset

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 13:39:58 +02:00

81 lines
3.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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**.