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:
@@ -177,6 +177,20 @@ export function healthRecord(source: string, ok: boolean, note?: string, openMs?
|
||||
});
|
||||
}
|
||||
|
||||
/** Row counts for the /data page — everything the live layer has collected. */
|
||||
export function dbCounts(): Record<string, number> {
|
||||
const c = (sql: string) => (db().prepare(sql).get() as { n: number }).n;
|
||||
return {
|
||||
oddsLines: c('SELECT COUNT(*) n FROM odds_history'),
|
||||
oddsFixtures: c('SELECT COUNT(DISTINCT fixture_num) n FROM odds_history'),
|
||||
frozenForecasts: c('SELECT COUNT(*) n FROM prediction_snapshots'),
|
||||
enrichedMatches: c('SELECT COUNT(*) n FROM match_ext'),
|
||||
mappedFixtures: c('SELECT COUNT(*) n FROM source_map'),
|
||||
cachedResponses: c('SELECT COUNT(*) n FROM kv_cache'),
|
||||
ingestEvents: c('SELECT COUNT(*) n FROM ingest_log'),
|
||||
};
|
||||
}
|
||||
|
||||
export function logIngest(source: string, job: string, ok: boolean, ms: number, note?: string): void {
|
||||
db().prepare('INSERT INTO ingest_log (source, job, ok, ms, note, at) VALUES (?, ?, ?, ?, ?, ?)')
|
||||
.run(source, job, ok ? 1 : 0, Math.round(ms), note ?? null, Date.now());
|
||||
|
||||
+18
-1
@@ -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' });
|
||||
|
||||
@@ -17,7 +17,7 @@ interface H2HRecord {
|
||||
last: { date: string; home: string; away: string; hs: number; as: number }[];
|
||||
}
|
||||
|
||||
function loadStatic<T>(name: string): T {
|
||||
export function loadStatic<T>(name: string): T {
|
||||
const candidates = [
|
||||
`${process.env.STATIC_DIR ?? ''}/data/${name}`,
|
||||
`${process.cwd()}/public/data/${name}`,
|
||||
|
||||
Reference in New Issue
Block a user