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>
This commit is contained in:
2026-06-11 13:39:58 +02:00
parent dd8156376d
commit 9c7605391c
3 changed files with 115 additions and 16 deletions
+80
View File
@@ -0,0 +1,80 @@
# 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**.