From d982ff1068b984be8b35bcb208f3e1e335083209 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Mon, 29 Jun 2026 23:30:26 -0500 Subject: [PATCH] KO Bracket fixed --- app/api/matches/route.ts | 4 +- app/components/Bracket.tsx | 5 +++ lib/feeds.ts | 75 ++++++++++++++++++++++++++++++++++---- lib/resolve-bracket.ts | 35 +++++++++++++++++- lib/types.ts | 1 + 5 files changed, 110 insertions(+), 10 deletions(-) diff --git a/app/api/matches/route.ts b/app/api/matches/route.ts index bc7e673..62c0801 100644 --- a/app/api/matches/route.ts +++ b/app/api/matches/route.ts @@ -30,8 +30,8 @@ export async function GET() { } try { - const fifaMap = await fetchFifaScores(); - matches = applyFifaScores(matches, fifaMap); + const fifaData = await fetchFifaScores(); + matches = applyFifaScores(matches, teams, fifaData); } catch (err) { console.warn("[fifa] Scores fehlgeschlagen:", err instanceof Error ? err.message : err); } diff --git a/app/components/Bracket.tsx b/app/components/Bracket.tsx index 7427184..c4c7dd6 100644 --- a/app/components/Bracket.tsx +++ b/app/components/Bracket.tsx @@ -75,6 +75,11 @@ function Tie({ + {tie.homePenalty != null && tie.awayPenalty != null && ( +
+ ({tie.homePenalty}:{tie.awayPenalty} i.E.) +
+ )} ); } diff --git a/lib/feeds.ts b/lib/feeds.ts index f251341..9f38236 100644 --- a/lib/feeds.ts +++ b/lib/feeds.ts @@ -561,8 +561,25 @@ export function attachKOOdds(matches: Match[], teams: Team[], odds: ParsedOdds[] const FIFA_BASE = "https://api.fifa.com/api/v3"; const FIFA_SEASON = "285023"; +// FIFA-Code → App-Code (nur Abweichungen; sonst identisch) +const FIFA_CODE_OVERRIDE: Record = { + CRO: "HRV", // Kroatien + POR: "PRT", // Portugal + SUI: "CHE", // Schweiz +}; +function fifaCodeToAppCode(fifaCode: string): string { + return FIFA_CODE_OVERRIDE[fifaCode] ?? fifaCode; +} + +interface FifaTeamBlock { + IdTeam: string; // FIFA-Team-ID + Abbreviation: string; // FIFA-Code (z.B. PAR, CRO) +} + interface FifaMatch { MatchNumber: number; + Home: FifaTeamBlock | null; + Away: FifaTeamBlock | null; HomeTeamScore: number | null; AwayTeamScore: number | null; HomeTeamPenaltyScore: number | null; @@ -570,6 +587,7 @@ interface FifaMatch { MatchStatus: number; // 0=finished, 1=scheduled, else=live MatchTime: string | null; // z.B. "132'" ResultType: number | null; // 1=regular, 2=penalties + Winner?: string | null; // Team-ID des Siegers } interface FifaScores { @@ -580,10 +598,14 @@ interface FifaScores { resultType: number | null; status: MatchStatus; matchTime: string | null; + winnerTeamId: string | null; } -// Holt Live-Scores von der FIFA-API. Gibt Map MatchNumber → Scores zurück. -export async function fetchFifaScores(): Promise> { +// Holt Live-Scores von der FIFA-API. Gibt Map MatchNumber → Scores + FIFA-ID→App-Code-Map zurück. +export async function fetchFifaScores(): Promise<{ + scores: Map; + fifaIdToAppCode: Map; +}> { return cached("fifa:scores", 45_000, async () => { const url = `${FIFA_BASE}/calendar/matches?language=de&count=500&idSeason=${FIFA_SEASON}`; const res = await fetch(url, { @@ -593,15 +615,24 @@ export async function fetchFifaScores(): Promise> { }); if (!res.ok) throw new Error(`fifa ${res.status}`); const data = (await res.json()) as { Results: FifaMatch[] }; - const map = new Map(); + const scores = new Map(); + const fifaIdToAppCode = new Map(); + for (const fm of data.Results ?? []) { + // Team-Mapping sammeln + for (const tb of [fm.Home, fm.Away]) { + if (tb && !fifaIdToAppCode.has(tb.IdTeam)) { + fifaIdToAppCode.set(tb.IdTeam, fifaCodeToAppCode(tb.Abbreviation)); + } + } + let status: MatchStatus; switch (fm.MatchStatus) { case 0: status = "FINISHED"; break; case 1: status = "SCHEDULED"; break; - default: status = "LIVE"; break; // 2,3,... = läuft + default: status = "LIVE"; break; } - map.set(fm.MatchNumber, { + scores.set(fm.MatchNumber, { homeScore: fm.HomeTeamScore, awayScore: fm.AwayTeamScore, homePenalty: fm.HomeTeamPenaltyScore, @@ -609,15 +640,25 @@ export async function fetchFifaScores(): Promise> { resultType: fm.ResultType, status, matchTime: fm.MatchTime, + winnerTeamId: fm.Winner ?? null, }); } - return map; + return { scores, fifaIdToAppCode }; }); } // Wendet FIFA-Live-Scores auf Matches an (MatchNumber als exakter Schlüssel). -export function applyFifaScores(matches: Match[], fifaMap: Map): Match[] { +export function applyFifaScores( + matches: Match[], teams: Team[], + fifaData: { scores: Map; fifaIdToAppCode: Map }, +): Match[] { + const { scores: fifaMap, fifaIdToAppCode } = fifaData; if (fifaMap.size === 0) return matches; + // Baue App-Code → App-Team-ID Map + const appCodeToId = new Map(); + for (const t of teams) { + if (t.code) appCodeToId.set(t.code.toLowerCase(), t.id); + } let applied = 0; const result = matches.map((m) => { const fs = fifaMap.get(m.matchNumber); @@ -627,6 +668,26 @@ export function applyFifaScores(matches: Match[], fifaMap: Map m.winnerTeamId gesetzt:", r.winnerTeamId); if (fs.matchTime) { const min = parseInt(fs.matchTime, 10); if (!isNaN(min)) r.minute = min; diff --git a/lib/resolve-bracket.ts b/lib/resolve-bracket.ts index 126fc72..a490e34 100644 --- a/lib/resolve-bracket.ts +++ b/lib/resolve-bracket.ts @@ -24,6 +24,8 @@ export interface ResolvedTie { away: ResolvedSide; status: string; prob?: { home: number; draw: number | null; away: number } | null; + homePenalty?: number | null; + awayPenalty?: number | null; } // Findet das tatsächliche Spiel (aus dem Feed) zu einer FIFA-Match-Nummer. @@ -34,13 +36,29 @@ function feedMatch(matches: Match[], num: number): Match | undefined { // Bestimmt den Sieger eines abgeschlossenen Spiels (Feed) als Team-ID. function winnerOf(m: Match | undefined): string | null { if (!m || m.status !== "FINISHED") return null; + if (m.winnerTeamId) return m.winnerTeamId; if (m.homeScore == null || m.awayScore == null) return null; if (m.homeScore > m.awayScore) return m.homeTeamId; if (m.awayScore > m.homeScore) return m.awayTeamId; - return null; // Unentschieden -> Elfmeter; Feed liefert i.d.R. Sieger separat + return null; } +let _winnerDbg = true; // Nur einmal loggen +function winnerDbg(m: Match | undefined, result: string | null, ctx: string): void { + if (!_winnerDbg || !m || m.matchNumber !== 74) return; + _winnerDbg = false; + console.log("[WINNER-OF]", ctx, + "| matchNumber:", m.matchNumber, + "| winnerTeamId:", m.winnerTeamId, + "| homeScore:", m.homeScore, "| awayScore:", m.awayScore, + "| status:", m.status, + "| ergebnis (zurückgegebener sieger):", result); +} + function loserOf(m: Match | undefined): string | null { if (!m || m.status !== "FINISHED") return null; + if (m.winnerTeamId) { + return m.homeTeamId === m.winnerTeamId ? m.awayTeamId : m.homeTeamId; + } if (m.homeScore == null || m.awayScore == null) return null; if (m.homeScore > m.awayScore) return m.awayTeamId; if (m.awayScore > m.homeScore) return m.homeTeamId; @@ -159,6 +177,7 @@ export function resolveBracket( const away = sideFrom(a.teamId, slotLabel(rm.away, assignment, winnerGroup), teams, feed, "away", a.provisional, a.tooltip); const feedWinner = winnerOf(feed); + winnerDbg(feed, feedWinner, "resolveR32Slot"); // Sim-Modus: Gewinner NUR aus Scores + Slot-Team-IDs ableiten. // feedWinner (aus feed.homeTeamId/awayTeamId) wird IGNORIERT, // weil matchNumber-Zuweisung (assignNumbersAndVenues) von der @@ -178,9 +197,16 @@ export function resolveBracket( losers.set(rm.matchNumber, loserOf(feed)); } decided.set(rm.matchNumber, feed?.status === "FINISHED"); + if (rm.matchNumber === 74) { + console.log("[PROPAGATE]", "von matchNumber:", rm.matchNumber, + "| feedWinner:", feedWinner, + "| -> in winners map unter key:", rm.matchNumber, + "| ziel z.B. slot 89 fromHome:", 74); + } return { matchNumber: rm.matchNumber, stage: "R32", home, away, status: feed?.status ?? "SCHEDULED", prob: feed?.prob, + homePenalty: feed?.homePenalty, awayPenalty: feed?.awayPenalty, }; }); @@ -190,6 +216,12 @@ export function resolveBracket( const src = km.losers ? losers : winners; const homeId = src.get(km.fromHome) ?? null; const awayId = src.get(km.fromAway) ?? null; + if (km.matchNumber === 89) { + console.log("[PROPAGATE]", "ziel slot:", km.matchNumber, + "| fromHome:", km.fromHome, "| fromAway:", km.fromAway, + "| homeId (aus winners):", homeId, + "| awayId (aus winners):", awayId); + } // Ein Folgeteam ist fix, wenn das speisende Spiel beendet ist. const homeProv = !(decided.get(km.fromHome) ?? false); @@ -217,6 +249,7 @@ export function resolveBracket( later[km.matchNumber] = { matchNumber: km.matchNumber, stage: km.stage, home, away, status: feed?.status ?? "SCHEDULED", prob: feed?.prob, + homePenalty: feed?.homePenalty, awayPenalty: feed?.awayPenalty, }; } diff --git a/lib/types.ts b/lib/types.ts index 0e00cef..c966dd7 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -41,6 +41,7 @@ export interface Match { awayScore: number | null; homePenalty?: number | null; // Elfmeterschießen (FIFA-API) awayPenalty?: number | null; + winnerTeamId?: string | null; // FIFA-Winner (auch bei Elfmeterschießen) // Wahrscheinlichkeiten 0..1 (Polymarket), falls vorhanden prob?: { home: number; draw: number; away: number } | null; venue?: string | null; // Austragungsort