Golden Boot race from structured ESPN scoring plays
ESPN's keyEvents carry participants[].athlete for goals — no text parsing needed. The normalizer now extracts scorer/scoring/shootout (payload versioned, with a boot backfill that re-stores summaries saved by the old normalizer), /api/goldenboot aggregates per-player goals (own goals and shootout kicks excluded), and the Teams page leads with the top-10 race. Verified against tonight's real goals. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+26
-1
@@ -10,8 +10,9 @@ 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 } from './db/db';
|
||||
import { db, healthAll, oddsFor, latestOddsAll, dbCounts, inplayHistory, recordInplay, getMatchExt } from './db/db';
|
||||
import { inPlayProbs } from '../../src/lib/model/inplay';
|
||||
import type { EspnSummary } from './ingest/espnNormalize';
|
||||
import { loadStatic } from './preview';
|
||||
import { canonicalTeam } from '../../src/lib/teams';
|
||||
import type { MatchStatus, ServerMessage } from '../../src/lib/types';
|
||||
@@ -86,6 +87,30 @@ export function buildServer() {
|
||||
const num = Number((req.params as { num: string }).num);
|
||||
return { points: Number.isFinite(num) ? inplayHistory(num) : [] };
|
||||
});
|
||||
|
||||
// Golden Boot: per-player goals aggregated from structured ESPN scoring
|
||||
// plays across every started match (own goals + shootout kicks excluded).
|
||||
app.get('/api/goldenboot', async () => {
|
||||
const tally = new Map<string, { player: string; team: string; goals: number; latest: string }>();
|
||||
for (const f of state.allFixtures()) {
|
||||
if (f.status === 'scheduled') continue;
|
||||
const ext = getMatchExt<EspnSummary>(f.num);
|
||||
if (!ext) continue;
|
||||
const teamName = new Map(ext.data.teams.map((tm) => [tm.id, tm.name]));
|
||||
for (const e of ext.data.events) {
|
||||
if (!e.scoring || e.shootout || !e.scorer) continue;
|
||||
if (/own goal/i.test(e.type)) continue;
|
||||
const team = (e.teamId && teamName.get(e.teamId)) ?? '';
|
||||
const key = `${e.scorer}|${team}`;
|
||||
const cur = tally.get(key) ?? { player: e.scorer, team, goals: 0, latest: f.kickoff };
|
||||
cur.goals += 1;
|
||||
if (f.kickoff > cur.latest) cur.latest = f.kickoff;
|
||||
tally.set(key, cur);
|
||||
}
|
||||
}
|
||||
const players = [...tally.values()].sort((a, b) => b.goals - a.goals || a.player.localeCompare(b.player)).slice(0, 25);
|
||||
return { players };
|
||||
});
|
||||
app.get('/api/scoreboard', async () => buildScoreboard(state));
|
||||
// The data lake, quantified — counters for the /data page.
|
||||
app.get('/api/data/stats', async () => {
|
||||
|
||||
Reference in New Issue
Block a user