diff --git a/app/api/matches/route.ts b/app/api/matches/route.ts
index 62c0801..fd2c530 100644
--- a/app/api/matches/route.ts
+++ b/app/api/matches/route.ts
@@ -1,5 +1,5 @@
import { NextResponse } from "next/server";
-import { fetchMatchesAndTeams, fetchOdds, attachOdds, attachKOOdds, fetchFifaScores, applyFifaScores, assignKONumbersBySlots } from "@/lib/feeds";
+import { fetchMatchesAndTeams, fetchOdds, attachOdds, attachKOOdds, fetchFifaScores, applyFifaScores, assignKONumbersBySlots, attachFifaGoals } from "@/lib/feeds";
import { computeGroupTables, computeThirdPlaceTable } from "@/lib/standings";
import { qualifiedThirdGroups, resolveAnnexC } from "@/lib/bracket";
import { securelyQualifiedThirdTeams } from "@/lib/third-place-security";
@@ -32,6 +32,11 @@ export async function GET() {
try {
const fifaData = await fetchFifaScores();
matches = applyFifaScores(matches, teams, fifaData);
+ try {
+ matches = await attachFifaGoals(matches, fifaData);
+ } catch (err) {
+ console.warn("[fifa] Goals fehlgeschlagen:", err instanceof Error ? err.message : err);
+ }
} catch (err) {
console.warn("[fifa] Scores fehlgeschlagen:", err instanceof Error ? err.message : err);
}
diff --git a/app/components/KoFixtures.tsx b/app/components/KoFixtures.tsx
index 4e6711c..485b76f 100644
--- a/app/components/KoFixtures.tsx
+++ b/app/components/KoFixtures.tsx
@@ -198,10 +198,9 @@ export default function KoFixtures({ teams, matches }: { teams: Team[]; matches:
{m.goals.map((g, i) => (
- {g.team === "home" ? "⬆" : "⬇"}
+
{g.scorer}
diff --git a/lib/feeds.ts b/lib/feeds.ts
index 1e95460..21ae85b 100644
--- a/lib/feeds.ts
+++ b/lib/feeds.ts
@@ -1,4 +1,4 @@
-import { GroupId, Match, MatchStatus, Team } from "./types";
+import { GroupId, GoalEvent, Match, MatchStatus, Team } from "./types";
import { venueFor } from "./venues";
import { localisedTeamName } from "./team-mappings";
import { R32, LATER_ROUNDS, resolveAnnexC, qualifiedThirdGroups } from "./bracket";
@@ -578,6 +578,8 @@ interface FifaTeamBlock {
interface FifaMatch {
MatchNumber: number;
+ IdMatch?: string | null;
+ IdStage?: string | null;
Home: FifaTeamBlock | null;
Away: FifaTeamBlock | null;
HomeTeamScore: number | null;
@@ -599,6 +601,8 @@ interface FifaScores {
status: MatchStatus;
matchTime: string | null;
winnerTeamId: string | null;
+ idMatch: string | null;
+ idStage: string | null;
}
// Holt Live-Scores von der FIFA-API. Gibt Map MatchNumber → Scores + FIFA-ID→App-Code-Map zurück.
@@ -641,6 +645,8 @@ export async function fetchFifaScores(): Promise<{
status,
matchTime: fm.MatchTime,
winnerTeamId: fm.Winner ?? null,
+ idMatch: fm.IdMatch ?? null,
+ idStage: fm.IdStage ?? null,
});
}
return { scores, fifaIdToAppCode };
@@ -692,4 +698,84 @@ export function applyFifaScores(
});
if (applied > 0) console.log("[fifa] scores angewandt:", applied);
return result;
+}
+
+// Holt Tor-Details pro Spiel vom FIFA-Detail-Endpoint.
+interface FifaGoalRaw { scorer: string; minute: string; team: "home" | "away"; type: number | null; }
+
+function locName(arr: Array<{ Locale: string; Description: string }> | undefined): string {
+ if (!arr || arr.length === 0) return "";
+ return (arr.find(x => x.Locale === "de-DE")
+ ?? arr.find(x => x.Locale === "en-GB")
+ ?? arr[0])?.Description ?? "";
+}
+
+function normMinuteStr(min: string | null | undefined): string {
+ return (min ?? "").replace(/'/g, "").replace(/\s+/g, "").trim();
+}
+
+async function fetchFifaGoals(idStage: string, idMatch: string): Promise {
+ return cached(`fifa:detail:${idMatch}`, 60_000, async () => {
+ const url = `${FIFA_BASE}/live/football/17/${FIFA_SEASON}/${idStage}/${idMatch}?language=de`;
+ const res = await fetch(url, {
+ cache: "no-store",
+ headers: { "User-Agent": "wm2026-board/1.0" },
+ signal: AbortSignal.timeout(5000),
+ });
+ if (!res.ok) { console.warn("[fifa-detail] status", res.status, idMatch); return []; }
+ const dj: any = await res.json();
+
+ const buildPlayerMap = (teamBlock: any): Map => {
+ const map = new Map();
+ for (const p of teamBlock?.Players ?? []) {
+ map.set(String(p.IdPlayer), locName(p.PlayerName) || locName(p.ShortName));
+ }
+ return map;
+ };
+ const homeBlock = dj.HomeTeam, awayBlock = dj.AwayTeam;
+ const homePlayers = buildPlayerMap(homeBlock);
+ const awayPlayers = buildPlayerMap(awayBlock);
+
+ const goals: FifaGoalRaw[] = [];
+ for (const [block, players, side] of [
+ [homeBlock, homePlayers, "home"] as const,
+ [awayBlock, awayPlayers, "away"] as const,
+ ]) {
+ for (const g of block?.Goals ?? []) {
+ const min = normMinuteStr(g.Minute);
+ if (min === "" || isNaN(parseInt(min, 10))) continue;
+ goals.push({
+ scorer: players.get(String(g.IdPlayer)) || "?",
+ minute: normMinuteStr(g.Minute),
+ team: side,
+ type: g.Type ?? null,
+ });
+ }
+ }
+ goals.sort((a, b) => parseInt(a.minute, 10) - parseInt(b.minute, 10));
+ return goals;
+ });
+}
+
+// Lädt Tor-Details für beendete/laufende Spiele mit Toren und hängt sie an die Matches an.
+export async function attachFifaGoals(
+ matches: Match[],
+ fifaData: { scores: Map; fifaIdToAppCode: Map },
+): Promise {
+ const targets = matches.filter(m =>
+ (m.status === "FINISHED" || m.status === "LIVE" || m.status === "IN_PLAY") &&
+ ((m.homeScore ?? 0) + (m.awayScore ?? 0)) > 0,
+ );
+ const goalsByMatchId = new Map();
+ await Promise.all(targets.map(async (m) => {
+ const fs = fifaData.scores.get(m.matchNumber);
+ if (!fs?.idMatch || !fs?.idStage) return;
+ const goals = await fetchFifaGoals(fs.idStage, fs.idMatch);
+ if (goals.length) goalsByMatchId.set(m.id, goals);
+ }));
+ if (goalsByMatchId.size === 0) return matches;
+ return matches.map(m => {
+ const g = goalsByMatchId.get(m.id);
+ return g ? { ...m, goals: g.map(({ scorer, minute, team }) => ({ scorer, minute, team })) } : m;
+ });
}
\ No newline at end of file
diff --git a/lib/types.ts b/lib/types.ts
index c966dd7..a975289 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -23,7 +23,7 @@ export type MatchStatus =
export interface GoalEvent {
scorer: string; // Torschützen-Name
- minute: number; // Spielminute
+ minute: string; // Spielminute (z.B. "72", "90+1")
team: "home" | "away";
}