- /player/:name pages assembled entirely from stored data: tournament
totals, per-match log (rating, minutes, G/A, cards), aggregated shot
map, suspension status, next-match scorer line. Either source's
spelling resolves to the same page (token-set + romanization fold).
- Player names are now links everywhere: stats boards, golden boot,
discipline, injuries, all-time scorers, lineups, the pitch dots,
timeline events and assists, and the search palette (diacritic-folded).
- Golden Boot displays the FotMob form of each name — no more
"Hwang In-Beom" and "In-Beom Hwang" on the same page.
- Match pages: detailed full-time stats card (passes, duels, xGOT… from
the stored FotMob report), O/U + spread closing line, "Who scores?"
pre-match props board, and a FIFA-confirmed-lineups badge.
- Stats hub: new boards — xA, chances created, save percentage, saves
per 90.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Penalty shootout panel: shootout-flagged events get a kicker-by-kicker
dot strip on the Summary tab (and stay OFF the timeline/running score).
Parsing is defensive — validate against the first real shootout June 28.
- Road to the final on team profiles: the model's projected knockout run
(teamPath in whatif.ts forces the team through the model bracket) with
per-round reach probabilities and a title-chance chip. Teams the model
doesn't project through still get a hypothetical runner-up road.
- Match videos: ESPN summaries carry per-match clips (SUMMARY_V=5 keeps
headline/duration/thumbnail/link) — a Videos card on the Summary tab,
clips open on ESPN. Tokenless; replaces the parked ScoreBat idea.
- ICS calendar export: /api/calendar.ics[?team=X] (RFC 5545, escaped,
folded, CRLF) + an Add-to-calendar chip on team pages.
- Injuries plumbing (dormant until API_FOOTBALL_KEY is set): twice-daily
API-Football sweep into the injuries table, surfaced on team profiles
and the match availability banner next to suspensions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- /stats: FotMob tournament boards (goals/assists/rating/xG player+team
categories from the data.fotmob.com CDN, capture-first via scheduler),
Golden Boot race with DraftKings anytime-line benchmark from the
already-captured scorer props, and an attack-vs-defence xG scatter.
- Discipline engine (src/lib/discipline.ts): FIFA suspension rules folded
from stored event feeds — red/second-yellow bans, two-yellow accumulation,
post-QF wipe. Surfaces on /stats, team profiles and match lineups tabs.
- In-play win probability now accounts for red cards (10 men: own rate
x0.67, opponent x1.15 per card) on the server tick and the match page.
- Fix ModelDiary infinite render loop (unstable zustand snapshot while the
model loads crashed the Methodology route under React 19).
- Drop dead tables squad + goalscorers; scorer_props is now read.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
'X is shown the red card' explains nothing. ESPN's summary carries the
full post-match report (Reuters-style narrative) that we fetched and
discarded — it has exactly the missing why ('red-carded for bringing
down Gutierrez just outside the box').
- normalizer keeps the report (headline + plain-text story, schema v4;
boot backfill re-stores played matches automatically)
- expanding an event surfaces the report sentences about that player,
scored by event vocabulary so a player's sending-off sentence beats
his goal mention; attribution line marks the source
- the raw no-reason fallback line is suppressed once the report
explains a booking; xG/momentum/VAR layers stay underneath
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
'X is shown a red card' alone isn't insight — the story is in the
surrounding plays. The ESPN summary carries a ~110-line play-by-play
feed (attempts, fouls, corners, VAR checks) we cached but never used:
- normalizer keeps a trimmed commentary list (schema v3; the boot
backfill re-stores older matches automatically)
- previews serve it; expanding a timeline event now shows up to four
feed lines from the three minutes leading in, then the event's own
description — e.g. Korea's saved attempt and corner right before
Czechia's counter-goal header, or the foul behind a booking
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Scheduler now captures FotMob details for fixtures inside 48h, so the
projected XIs exist before kickoff; the Lineups tab shows the pitch
pre-match under a 'Predicted lineups' header with an honest note
(official lineups ~1h before kickoff), switching to the confirmed
view at kickoff
- Finished matches get their forecast back: the preview falls back to
the prediction frozen <=15min before kickoff (the scoreboard's
no-hindsight snapshot), with the most-likely-score chip rebuilt from
the frozen expected goals; the model card now reads 'Model's
pre-match expected goals: 1.60 - 1.02 · final score 2-1', and the
market-movement chart returns to finished matches too
- Upcoming match cards append the model's expected goals to the hint:
'Model: Brazil 64% · 1.3-0.9 goals'
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GET /api/match-rich/:num projects the captured provider data for the
match center: every shot with pitch coords and xG oriented to
home/away (FotMob payloads now carry team ids, v2 + settle-versioned
refetch), team xG sums, the momentum curve, player of the match with
rating, FIFA's official attendance/stadium/referee, and kickoff-hour
weather. Verified on real data: South Korea 2.31-0.83 Czechia on 22
oriented shots, POTM Hwang 8.9, referee Amin Mohamed Omar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first heal attempt missed both matches: a 02:00 UTC kickoff files
under the PREVIOUS day in ESPN's ?dates= param (ET-based), and the
backfill skipped today's UTC date entirely. Now any started-but-
forgotten fixture triggers fetches for its UTC date and the day
before, covering the ET offset in both directions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The persistence table shipped empty just as ESPN's default scoreboard
window rolled past matchday 1 — prod forgot both results. Rather than
patching the DB by hand, boot now re-fetches the date-scoped
scoreboard for any past day whose fixtures still look scheduled and
merges + persists the results. Heals fresh databases and any future
data loss the same way.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four new feeds, all verified against real tournament matches:
- FotMob (unofficial, works from the VPS again): per-shot xG/xGOT with
pitch coords, team xG, player ratings, POTM, momentum, attacking
zones — trimmed payloads (~87KB/match) on a gentle 90s-live/30m-idle
cadence. Capture-first: this source could lock again any day.
- FIFA official v3: confirmed lineups with player IDs, typed XY event
timelines (80 events for the opener), attendance + named referees,
tactics — id-mapped via the calendar, hot while live.
- Open-Meteo: kickoff-hour weather per venue from a build-time geocoded
venues.json (with elevation — Azteca 2,240m); fetched in the enrich
loop inside the 48h horizon.
- DraftKings scorer props (first/last/anytime goalscorer per athlete)
via ESPN's core propBets, insert-if-changed line history, athlete
names resolved through a 30-day cache.
Also fixes a real resilience gap found along the way: fixture results
now persist in SQLite and restore at boot — previously a restart after
ESPN's scoreboard window moved on would silently forget a match day
(standings, model and scoreboard would all regress).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A restart during a live match sat behind the multi-minute odds sweep
before the first scoreboard poll. The live loop now starts immediately
after fixture mapping; odds/backfill/enrich follow on their own
rate-limit keys.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ESPN's keyEvents carry participants[].athlete for goals — no text
parsing needed. The normalizer now extracts scorer/scoring/shootout
(payload versioned, with a boot backfill that re-stores summaries
saved by the old normalizer), /api/goldenboot aggregates per-player
goals (own goals and shootout kicks excluded), and the Teams page
leads with the top-10 race. Verified against tonight's real goals.
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>
The live staleness bug: liveTick refetched ESPN summaries every 20s but
the cache TTL was 5 minutes, so in-play stats/timeline barely moved.
Summaries now take a live flag (15s TTL in play), the scoreboard TTL
drops 20s->5s, and polling gets three tiers (8s in-play / 20s near
kickoff / 10m idle) via a pure, tested cadence module. The
football-data fallback is throttled to one attempt per 15s so the 8s
cadence can never breach its 10 req/min cap. Enrichment runs every 5m
(was 20m); client refetch intervals tighten to 10s (live match page)
and 30s (scoreboard).
Co-Authored-By: Claude Fable 5 <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>
- src/lib/model/inplay.ts: in-play win probability — remaining goals modelled as
time-scaled Poisson processes convolved with the current score; recomputed each
tick. 4 vitest cases (locks at FT, lead grows decisive over time, sums to 1).
- ESPN summary normalizer extended with the live event timeline (keyEvents);
preview now carries liveStats + events. Scheduler refreshes LIVE matches'
summaries every live tick (~20s) for fresh stats/timeline.
- /match/:num is now a live match center: when live it shows in-play win
probability (driven by the WS score/clock), match stats bars and an event
timeline, polling while live; preview content for scheduled games, recap when
finished. Verified with a simulated live state (94% live vs 81% pre-match).
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>