Self-healing results backfill: re-fetch past match days at boot

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>
This commit is contained in:
2026-06-12 09:22:24 +02:00
parent 517a86233a
commit 66cbbc3dde
+26
View File
@@ -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<void> => {
const today = dateUTC(new Date().toISOString());
const dates = new Set<string>();
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. // One-time backfill: re-store summaries saved by an older normalizer (e.g.
// before structured scorers existed), so started matches stay fully usable. // before structured scorers existed), so started matches stay fully usable.
const backfillExt = async (): Promise<void> => { const backfillExt = async (): Promise<void> => {
@@ -339,6 +364,7 @@ export function startScheduler(
await mapAllFixtures().catch(() => {}); await mapAllFixtures().catch(() => {});
await liveTick().catch(() => {}); await liveTick().catch(() => {});
scheduleLive(); // live cadence starts now; slow one-time sweeps follow 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 // persist whatever the current scoreboard window knows — results learned
// before this table existed would otherwise never be written // before this table existed would otherwise never be written
for (const f of state.allFixtures()) { for (const f of state.allFixtures()) {