From 66cbbc3dde2253075804b875423277932a2442db Mon Sep 17 00:00:00 2001 From: Nils Briggen Date: Fri, 12 Jun 2026 09:22:24 +0200 Subject: [PATCH] Self-healing results backfill: re-fetch past match days at boot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- server/src/ingest/scheduler.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/server/src/ingest/scheduler.ts b/server/src/ingest/scheduler.ts index fbd644a..1ab8d7c 100644 --- a/server/src/ingest/scheduler.ts +++ b/server/src/ingest/scheduler.ts @@ -113,6 +113,31 @@ export function startScheduler( } }; + // Self-healing results backfill: any PAST match day whose fixtures still + // look 'scheduled' (fresh DB, or results lost before persistence existed) + // gets its date-scoped ESPN scoreboard re-fetched and merged. + const resultsBackfill = async (): Promise => { + const today = dateUTC(new Date().toISOString()); + const dates = new Set(); + for (const f of state.allFixtures()) { + const d = dateUTC(f.kickoff); + if (d < today && f.status === 'scheduled' && f.home.team && f.away.team) dates.add(d); + } + for (const date of dates) { + if (stopped) return; + const t0 = Date.now(); + try { + const matches = await fetchEspnScoreboard(date); + const changed = state.mergeLive(matches, 'espn'); + for (const f of changed) saveFixtureResult(f.num, f.homeScore, f.awayScore, f.status); + if (changed.length) onChange(); + logIngest('espn', 'results-backfill', true, Date.now() - t0, `${date}: ${changed.length} restored`); + } catch (e) { + logIngest('espn', 'results-backfill', false, Date.now() - t0, e instanceof Error ? e.message : String(e)); + } + } + }; + // One-time backfill: re-store summaries saved by an older normalizer (e.g. // before structured scorers existed), so started matches stay fully usable. const backfillExt = async (): Promise => { @@ -339,6 +364,7 @@ export function startScheduler( await mapAllFixtures().catch(() => {}); await liveTick().catch(() => {}); scheduleLive(); // live cadence starts now; slow one-time sweeps follow + await resultsBackfill().catch(() => {}); // heal any forgotten past match days // persist whatever the current scoreboard window knows — results learned // before this table existed would otherwise never be written for (const f of state.allFixtures()) {