v2 Phase 2: match previews + team profiles

- buildPreviewData.ts: precomputes from 150y of results → h2h.json (825 WC-team
  pairings, full records + recent meetings) + scorers.json (top-10 all-time
  scorers per team). Committed goalscorers.csv.
- server/src/preview.ts: assembles MatchPreview / TeamProfile from three layers —
  historical (h2h, scorers), DB enrichment (ESPN form/lineups/venue), live model
  (W/D/L, xG, odds, Elo). /api/preview/:num + /api/team/:name.
- Client: rich /match/:num page (model expectation, recent form chips, H2H record
  + recent meetings, lineups-or-key-players) and /team/:name profile (Elo, odds,
  standing, fixtures, all-time scorers). Match cards now link to previews;
  cross-linking between matches and teams.
- Verified: Mexico-SA preview (81% W, 2-0, form, H2H 2-1-1, Borgetti 37) and
  Argentina profile (Messi 63) render; 22 tests pass; build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 15:34:54 +02:00
parent b6f62679c9
commit 7f4838d032
12 changed files with 48216 additions and 5 deletions
+43
View File
@@ -117,6 +117,49 @@ export interface ModelSnapshot {
history: OddsHistoryPoint[];
}
// ---- match previews / team profiles ----
export interface H2HMeeting { date: string; home: string; away: string; homeScore: number; awayScore: number }
export interface H2HSummary {
games: number;
/** From THIS fixture's home/away perspective. */
homeWins: number;
awayWins: number;
draws: number;
homeGoals: number;
awayGoals: number;
last: H2HMeeting[];
}
export interface KeyPlayer { name: string; goals: number; pens: number }
export interface LineupPlayer { name: string; position: string | null; starter: boolean; number: number | null }
export interface MatchPreview {
num: number;
fixture: Fixture;
venue: string | null;
prediction: MatchPrediction | null;
/** Recent results per side, most recent first ('W'|'D'|'L'). */
form: { home: string[]; away: string[] };
h2h: H2HSummary | null;
lineups: { home: LineupPlayer[]; away: LineupPlayer[] } | null;
keyPlayers: { home: KeyPlayer[]; away: KeyPlayer[] };
/** Honest note on which data layers are populated yet. */
dataNote: string;
updatedAt: number | null;
}
export interface TeamProfile {
team: string;
elo: number;
group: string | null;
standing: StandingRow | null;
fixtures: Fixture[];
keyPlayers: KeyPlayer[];
championOdds: number | null;
qualifyOdds: number | null;
}
// ---- data-story viz (StatsBomb open data, a fixed historical match) ----
export interface VizShot {