Fully progressive: without VAPID keys in the environment the API says
404, the bell never renders, and the service worker addition (a push
handler via workbox importScripts — no SW strategy change) is inert.
With keys: a bell on each team page requests permission, subscribes
(one team per device) and the server diffs fixture states on every
broadcast to push Kickoff / Goal / Full time to that team's followers.
Dead endpoints (404/410) self-clean. Round-trip verified locally with
dev VAPID keys; disabled mode verified too.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
While a match runs, every poll records the in-play model probability
per game state (insert-if-new on minute+score) into inplay_history.
The match page draws it as a stacked area chart — home fills from the
bottom, away from the top, the gap is the draw — with minute ticks and
a 50% guide. Shows live (under the win-probability bar) and after full
time as its own card. EN/DE strings included.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first prod snapshot landed as a 0-byte file with a stale journal:
SQLite needs byte-range locking on the VACUUM INTO destination, which
the CIFS storage box refuses. Now: vacuum into a local temp file, then
a plain sequential copy to the archive (always CIFS-safe), cleaning up
the temp and any leftover journal.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 'external drive' turned out to be a CIFS-mounted Hetzner storage
box — SQLite cannot run on a network mount (WAL shared memory and
locking break), and the live DB is only ~7MB anyway. So: the hot
database stays on the local volume, and a nightly VACUUM INTO snapshot
(transactionally consistent, a plain sequential file write — perfect
for CIFS) lands in /data/archive, which compose binds to the storage
box via HOST_ARCHIVE_DIR. 14-day retention, plus a snapshot at boot.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
HOST_DATA_DIR in .env can now point /data at a host path (the 1TB
external drive) instead of the named volume, so odds history, caches
and snapshots stop eating the 128GB root disk. A daily prune drops
cache entries stale for over a week and ingest-log rows older than 14
days — faster polling no longer means unbounded DB growth.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- scripts/processStatsbomb.ts: one pass over the 17GB StatsBomb lake (4,235
matches) -> lakestats.json (12.82GB events / 14.9M events / 6,919 players),
scorestate.json (goal-rate multipliers by game state — shipped as an insight,
NOT wired into in-play: the raw effect is selection-biased toward stronger
teams and would distort a calibrated model), fingerprints.json (40/48 nations'
shots/xG/set-piece share from real event data). Artifacts committed; raw lake
stays local (gitignored).
- /data page + /api/data/stats: the gigabytes made visible — lake counters,
historical archive, live-collection counts (odds lines, frozen forecasts,
enrichment), per-source health with freshness. Footer links.
- Term component: plain-language glossary tooltips (RPS, Brier, ECE, Elo, xG,
de-vig, ensemble) attached where metrics appear.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- prediction_snapshots: model + de-vigged market frozen ≤15min pre-kickoff
(or at first sight of a live match, flagged 'late' and excluded from totals).
- src/lib/model/scoring.ts: ONE shared RPS/Brier/log-loss implementation for
the backtest and the live scoreboard.
- /api/scoreboard + /scoreboard page: per-match model-vs-bookmaker bars, RPS
for both after FT, 'closer' badges, running head-to-head with honest rules
printed (small-sample caveat included). Nav: 'vs Market'.
- Methodology page now shows the three-way split + ensemble bake-off variants.
- Verified end-to-end with a simulated match: snapshot froze model 80.7% and
DraftKings 67.0% (de-vig correct), both scored at FT, late-flag honored.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- docs/V3-GIGA-PLAN.md: the v3 plan (data lake to 7+GB, GIGA ensemble, scoreboard,
UI clarity) with the honest reframe: perfect accuracy = perfect calibration;
'beat the betting sites' = a live public head-to-head, scored with proper rules.
- src/lib/odds.ts: American-ml → decimal, de-vig (normalize away the margin),
overround; 3 vitest cases incl. tonight's real opening lines.
- ESPN core odds adapter (server/src/ingest/espnOdds.ts): captures DraftKings
1X2 moneylines + O/U + spread per fixture; follows $ref pointer items.
- odds_history table (insert-if-changed → clean line-movement history);
scheduler odds sweep every 3h over a 14-day horizon, first sweep at boot;
/api/odds + /api/odds/:num. Odds are a benchmark ONLY — never a model input.
- Boot speed-up: fixture↔event mapping skips dates already fully mapped.
- Verified: 54 fixtures captured with real moneylines locally.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Persistence via Node's built-in node:sqlite (zero native deps) on a Docker
volume: response cache, source health, fixture↔provider id map, per-fixture
enrichment, ingest log. Runtime bumped to node:24-slim + --experimental-sqlite.
- Resilient fetcher: DB cache + per-source rate-limit + jittered backoff +
circuit-breaker (blocked sources trip + skip; UI reads DB, never breaks).
- ESPN hidden API as the PRIMARY rich source (works from the VPS where SofaScore
403s): scoreboard (live scores w/ clock) + summary (venue, H2H, recent form,
lineups, team stats). football-data / SofaScore are fallbacks.
- Scheduler: maps all 72 group fixtures to ESPN event ids, polls live on a
dynamic cadence, enriches imminent fixtures into match_ext. /api/sources/health.
- Verified: DB populates (real H2H + form for opening matches), 22 tests pass,
Docker image runs node:sqlite on the volume and persists across restart.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>