Phase 1: live dashboard — fixtures, scores, group tables, bracket
- Shared domain model (types, team metadata/flags/aliases, FIFA-tiebreak standings) - buildFixtures.ts: openfootball 2026 → normalized fixtures.json (104 matches, ISO-UTC kickoffs, pretty knockout placeholders) - Fastify live layer: seed load, football-data.org poller + SofaScore enhancement (feature-flagged, fallback), dynamic live-window polling, WS snapshot push, REST /api/snapshot, dev /api/dev/score injection - Client: tournament store (REST + WS + static-file fallback), MatchCard, TeamLabel, GroupTable, Live/Groups/Bracket pages, live connection indicator - Verified: WS pushes a dev-injected result; standings recompute live in-browser Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
// Shared domain types — used by the client, the server, and the build scripts.
|
||||
|
||||
export type Stage = 'group' | 'r32' | 'r16' | 'qf' | 'sf' | 'third' | 'final';
|
||||
export type MatchStatus = 'scheduled' | 'live' | 'finished';
|
||||
|
||||
/** One side of a fixture: a known team, or an unresolved knockout placeholder. */
|
||||
export interface TeamSlot {
|
||||
/** Canonical team name when known, else null (unresolved knockout slot). */
|
||||
team: string | null;
|
||||
/** Raw openfootball placeholder: '2A' | 'W74' | 'L101' | '3A/B/C/D/F' | null. */
|
||||
placeholder: string | null;
|
||||
/** Display label: the team name, or a pretty placeholder ("Runner-up A"). */
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface Fixture {
|
||||
/** Canonical 1..104 match number (group matches 1-72, knockout 73-104). */
|
||||
num: number;
|
||||
stage: Stage;
|
||||
/** 'A'..'L' for group stage, else null. */
|
||||
group: string | null;
|
||||
/** Human round label from the source ("Matchday 1", "Round of 32", …). */
|
||||
round: string;
|
||||
/** 1 | 2 | 3 — the team's nth group game, else null. */
|
||||
groupRound: number | null;
|
||||
/** Kickoff as an ISO 8601 UTC timestamp. */
|
||||
kickoff: string;
|
||||
venue: string;
|
||||
home: TeamSlot;
|
||||
away: TeamSlot;
|
||||
status: MatchStatus;
|
||||
homeScore: number | null;
|
||||
awayScore: number | null;
|
||||
/** Live match minute when status === 'live'. */
|
||||
minute: number | null;
|
||||
}
|
||||
|
||||
export interface FixturesFile {
|
||||
season: string;
|
||||
generatedAt: string;
|
||||
groups: string[];
|
||||
venues: string[];
|
||||
fixtures: Fixture[];
|
||||
}
|
||||
|
||||
export interface StandingRow {
|
||||
team: string;
|
||||
played: number;
|
||||
won: number;
|
||||
drawn: number;
|
||||
lost: number;
|
||||
gf: number;
|
||||
ga: number;
|
||||
gd: number;
|
||||
points: number;
|
||||
/** 1-based rank within the group after sorting. */
|
||||
rank: number;
|
||||
}
|
||||
|
||||
/** What the server pushes to clients: fixtures with any live overlay + tables. */
|
||||
export interface Snapshot {
|
||||
season: string;
|
||||
/** When the live layer last refreshed (ISO), or null if seed-only. */
|
||||
updatedAt: string | null;
|
||||
/** Which live source produced the current scores. */
|
||||
source: 'seed' | 'football-data' | 'sofascore';
|
||||
fixtures: Fixture[];
|
||||
/** Group letter → ranked standings. */
|
||||
tables: Record<string, StandingRow[]>;
|
||||
}
|
||||
|
||||
// ---- WebSocket protocol ----
|
||||
// The snapshot is small (~104 fixtures), so the server simply re-pushes the full
|
||||
// snapshot on every change — no diffing, no client-side merge races.
|
||||
|
||||
/** Server → client. */
|
||||
export type ServerMessage = { t: 'snapshot'; snapshot: Snapshot };
|
||||
Reference in New Issue
Block a user