v3: the lake processed + /data page + glossary terms

- 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>
This commit is contained in:
2026-06-11 17:41:43 +02:00
parent dc52a908c0
commit dda8e1ae42
13 changed files with 430 additions and 7 deletions
+18 -1
View File
@@ -10,7 +10,8 @@ import { startScheduler } from './ingest/scheduler';
import { ModelEngine } from './model';
import { buildPreview, buildTeamProfile } from './preview';
import { buildScoreboard, snapshotCheck } from './scoreboard';
import { db, healthAll, oddsFor, latestOddsAll } from './db/db';
import { db, healthAll, oddsFor, latestOddsAll, dbCounts } from './db/db';
import { loadStatic } from './preview';
import { canonicalTeam } from '../../src/lib/teams';
import type { MatchStatus, ServerMessage } from '../../src/lib/types';
@@ -61,6 +62,22 @@ export function buildServer() {
// Bookmaker odds (benchmark for the Model-vs-Market scoreboard; not a model input)
app.get('/api/odds', async () => ({ odds: latestOddsAll() }));
app.get('/api/scoreboard', async () => buildScoreboard(state));
// The data lake, quantified — counters for the /data page.
app.get('/api/data/stats', async () => {
const safe = <T,>(fn: () => T): T | null => { try { return fn(); } catch { return null; } };
return {
lake: safe(() => loadStatic('lakestats.json')),
archive: safe(() => {
const r = loadStatic<{ matchesProcessed: number; asOf: string }>('ratings.json');
const h = loadStatic<Record<string, unknown>>('h2h.json');
return { results: r.matchesProcessed, asOf: r.asOf, h2hPairings: Object.keys(h).length };
}),
fingerprints: safe(() => Object.keys(loadStatic<{ teams: Record<string, unknown> }>('fingerprints.json').teams).length),
squadValues: safe(() => Object.keys(loadStatic<{ teams: Record<string, unknown> }>('squadvalues.json').teams).length),
db: dbCounts(),
sources: healthAll(),
};
});
app.get('/api/odds/:num', async (req, reply) => {
const num = Number((req.params as { num: string }).num);
if (!Number.isFinite(num)) return reply.code(400).send({ error: 'bad num' });