Fix title-race chart: persist the diary, dedupe restarts, date axis

The chart looked broken because its data was: history lived in memory,
so every deploy wiped it (yesterday's points were gone), and each boot
plus the +5min refit appended a fresh point labeled with the same last
result — leaving two identical flat segments on an index axis.

- model_history table: every diary point persists with the finished-
  result signature it was computed from; restored at boot
- points only append when the finished set actually changes — refit
  and boot recomputes update the snapshot without spamming the diary
- one-time rebuild when nothing is persisted: replay finished results
  in kickoff order, re-running the sim after each with later results
  masked, so the historical points are recreated faithfully
- chart x-axis shows localized dates instead of raw indices

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 12:26:33 +02:00
parent 1ffa9fba36
commit b552bc8ba9
4 changed files with 47 additions and 5 deletions
+31
View File
@@ -139,6 +139,13 @@ CREATE TABLE IF NOT EXISTS inplay_history (
PRIMARY KEY (fixture_num, minute, home_score, away_score)
);
CREATE TABLE IF NOT EXISTS model_history (
t TEXT NOT NULL,
label TEXT NOT NULL,
top TEXT NOT NULL,
key TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS ingest_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source TEXT NOT NULL,
@@ -473,3 +480,27 @@ export function getMatchExt<T = unknown>(fixtureNum: number): { data: T; updated
if (!row) return null;
return { data: JSON.parse(row.json) as T, updatedAt: row.updated_at };
}
// ---- title-race diary (championship odds after each result) ----
// Persisted so the Predict chart survives restarts; `key` is the finished-
// result signature the point was computed from (dedupes boot recomputes).
export interface ModelHistoryRecord {
t: string;
label: string;
top: { team: string; champion: number }[];
key: string;
}
export function saveModelHistory(p: { t: string; label: string; top: { team: string; champion: number }[] }, key: string): void {
db()
.prepare('INSERT INTO model_history (t, label, top, key) VALUES (?, ?, ?, ?)')
.run(p.t, p.label, JSON.stringify(p.top), key);
}
export function allModelHistory(): ModelHistoryRecord[] {
const rows = db()
.prepare('SELECT t, label, top, key FROM model_history ORDER BY t DESC LIMIT 200')
.all() as unknown as { t: string; label: string; top: string; key: string }[];
return rows.reverse().map((r) => ({ t: r.t, label: r.label, top: JSON.parse(r.top) as ModelHistoryRecord['top'], key: r.key }));
}
+3 -2
View File
@@ -10,7 +10,7 @@ import { startScheduler } from './ingest/scheduler';
import { ModelEngine } from './model';
import { buildPreview, buildTeamProfile } from './preview';
import { buildScoreboard, snapshotCheck } from './scoreboard';
import { db, healthAll, oddsFor, latestOddsAll, dbCounts, inplayHistory, recordInplay, getMatchExt, getMatchProvider, allFixtureResults, saveFixtureResult } from './db/db';
import { db, healthAll, oddsFor, latestOddsAll, dbCounts, inplayHistory, recordInplay, getMatchExt, getMatchProvider, allFixtureResults, saveFixtureResult, allModelHistory, saveModelHistory } from './db/db';
import { inPlayProbs } from '../../src/lib/model/inplay';
import { PushNotifier, pushEnabled, subscribe as pushSubscribe, unsubscribe as pushUnsubscribe, vapidPublicKey } from './push';
import type { EspnSummary } from './ingest/espnNormalize';
@@ -45,8 +45,9 @@ export function buildServer() {
if (state.applyScore(r.fixture_num, r.home_score, r.away_score, r.status as MatchStatus, null, 'seed')) restored++;
}
if (restored) console.log(`[state] restored ${restored} fixture result(s) from the DB`);
const model = new ModelEngine(STATIC_DIR);
const model = new ModelEngine(STATIC_DIR, { load: allModelHistory, save: saveModelHistory });
model.refitTeamDc(state.allFixtures()); // current Team-DC from day one (covers restarts mid-tournament)
model.rebuildHistory(state.allFixtures()); // one-time diary replay when nothing is persisted yet
model.recompute(state.allFixtures());
const clients = new Set<WebSocket>();
Binary file not shown.