diff --git a/app/[locale]/components/Fixtures.tsx b/app/[locale]/components/Fixtures.tsx
index 92cccc7..4a49b7d 100644
--- a/app/[locale]/components/Fixtures.tsx
+++ b/app/[locale]/components/Fixtures.tsx
@@ -176,10 +176,13 @@ export default function Fixtures({
- {m.venue && 📍 {m.venue}}
- {m.attendance != null && (
- 👥 {m.attendance.toLocaleString(intlLocale)}
- )}
+ {(() => {
+ const stadium = [m.stadiumName, m.stadiumCity].filter(Boolean).join(", ");
+ const crowd = m.attendance != null ? m.attendance.toLocaleString(intlLocale) : "";
+ const sep = stadium && crowd ? " · " : "";
+ const text = stadium + sep + crowd;
+ return text ? {text} : null;
+ })()}
);
diff --git a/app/[locale]/components/Flag.tsx b/app/[locale]/components/Flag.tsx
index d365a9d..4bddde8 100644
--- a/app/[locale]/components/Flag.tsx
+++ b/app/[locale]/components/Flag.tsx
@@ -1,32 +1,27 @@
"use client";
-import { useState } from "react";
import { Team } from "@/lib/types";
+import { TLA_TO_ISO2 } from "@/lib/flags";
-// Zeigt die Flagge/das Wappen eines Teams. Fällt auf ein neutrales Rund
-// zurück, wenn keine crest-URL vorliegt oder das Bild nicht geladen werden kann.
+// Zeigt die Flagge eines Teams aus lokalem circle-flags-Bestand.
+// Fallback: kein Rendering bei fehlendem Team/Code (kein Broken-Image).
export default function Flag({
team, size = 22,
}: { team?: Team; size?: number }) {
- const [imgFailed, setImgFailed] = useState(false);
const style = { width: size, height: size } as const;
- if (team?.crest && !imgFailed) {
+ const iso2 = team?.code ? TLA_TO_ISO2[team.code.toUpperCase()] : undefined;
+ if (iso2) {
return (
setImgFailed(true)}
/>
);
}
- // Fallback: Kreis mit Ländercode
- return (
-
- {team?.code?.slice(0, 2) || "··"}
-
- );
+ // Kein Team / kein Code: nichts rendern (kein Broken-Image)
+ return null;
}
diff --git a/app/api/matches/route.ts b/app/api/matches/route.ts
index 0472c51..4202647 100644
--- a/app/api/matches/route.ts
+++ b/app/api/matches/route.ts
@@ -1,18 +1,17 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
-import { fetchMatchesAndTeams, fetchOdds, attachOdds, attachKOOdds, fetchFifaScores, applyFifaScores, assignKONumbersBySlots, attachFifaGoals } from "@/lib/feeds";
+import { fetchMatchesAndTeamsFifa, fetchOdds, attachOdds, attachKOOdds, fetchFifaScores, applyFifaScores, attachFifaGoals } from "@/lib/feeds";
import { computeGroupTables, computeThirdPlaceTable } from "@/lib/standings";
import { qualifiedThirdGroups, resolveAnnexC } from "@/lib/bracket";
import { securelyQualifiedThirdTeams } from "@/lib/third-place-security";
-// Diese Route wird vom Frontend gepollt. Sie ist der einzige Ort, der die
-// Upstream-Feeds berührt — gecacht, damit Rate Limits eingehalten werden.
+// Diese Route wird vom Frontend gepollt. FIFA-API als Primärquelle.
export const dynamic = "force-dynamic";
export async function GET(request: NextRequest) {
const locale = request.nextUrl.searchParams.get("locale") || "de";
try {
- const { matches: rawMatches, teams } = await fetchMatchesAndTeams();
+ const { matches: rawMatches, teams } = await fetchMatchesAndTeamsFifa(locale);
// Odds sind optional: fällt der Polymarket-Call aus, liefern wir trotzdem.
let matches = rawMatches;
@@ -24,13 +23,11 @@ export async function GET(request: NextRequest) {
console.error("[polymarket] fetchOdds fehlgeschlagen:", err);
}
- // FIFA-Live-Scores (additiv, Fallback auf football-data)
- assignKONumbersBySlots(matches, teams);
-
if (odds) {
matches = attachKOOdds(matches, teams, odds);
}
+ // FIFA-Live-Scores als häufiger gecachtes Overlay
try {
const fifaData = await fetchFifaScores(locale);
matches = applyFifaScores(matches, teams, fifaData);
diff --git a/lib/feeds.ts b/lib/feeds.ts
index 76dc665..d0596db 100644
--- a/lib/feeds.ts
+++ b/lib/feeds.ts
@@ -1,8 +1,9 @@
import { GroupId, GoalEvent, Match, MatchStatus, Team } from "./types";
import { venueFor } from "./venues";
-import { localisedTeamName } from "./team-mappings";
+import { localisedTeamName, TEAM_LOCALIZATION } from "./team-mappings";
import { R32, LATER_ROUNDS, resolveAnnexC, qualifiedThirdGroups } from "./bracket";
import { computeGroupTables, computeThirdPlaceTable } from "./standings";
+import { FIFA_GROUP_MAP, FIFA_STAGE_MAP } from "./fifa-constants";
// ----------------------------------------------------------------------------
// Caching: einfacher In-Memory-Cache mit TTL. Verhindert, dass jede Browser-
@@ -609,6 +610,122 @@ interface FifaScores {
idStage: string | null;
}
+// ----------------------------------------------------------------------------
+// FIFA-API Calendar-Endpoint: Spiele + Teams parallel zur football-data-Quelle
+// ----------------------------------------------------------------------------
+
+interface FifaCalendarTeamBlock {
+ IdTeam: string;
+ Abbreviation: string;
+ TeamName: Array<{ Locale: string; Description: string }>;
+}
+
+interface FifaCalendarMatch {
+ IdMatch: string;
+ IdStage: string;
+ IdGroup: string | null;
+ MatchNumber: number;
+ Date: string;
+ Home: FifaCalendarTeamBlock | null;
+ Away: FifaCalendarTeamBlock | null;
+ HomeTeamScore: number | null;
+ AwayTeamScore: number | null;
+ HomeTeamPenaltyScore: number | null;
+ AwayTeamPenaltyScore: number | null;
+ MatchStatus: number;
+ MatchTime: string | null;
+ ResultType: number | null;
+ Winner?: string | null;
+ Attendance: string | null;
+ Stadium?: {
+ Name: Array<{ Locale: string; Description: string }>;
+ CityName: Array<{ Locale: string; Description: string }>;
+ } | null;
+}
+
+// Holt alle Spiele + Teams von der FIFA-API (parallele Quelle, noch nicht aktiv).
+export async function fetchMatchesAndTeamsFifa(locale: string = "de"): Promise<{ matches: Match[]; teams: Team[] }> {
+ return cached(`fifa:matches:${locale}`, 60_000, async () => {
+ const lang = locale === "en" ? "en" : "de";
+ const url = `${FIFA_BASE}/calendar/matches?language=${lang}&count=500&idSeason=${FIFA_SEASON}`;
+ const res = await fetch(url, {
+ cache: "no-store",
+ headers: { "User-Agent": "wm2026-board/1.0" },
+ });
+ if (!res.ok) throw new Error(`fifa-calendar ${res.status}`);
+ const data = (await res.json()) as { Results: FifaCalendarMatch[] };
+
+ const teamMap = new Map();
+
+ function teamFromBlock(tb: FifaCalendarTeamBlock, idGroup: string | null): Team | null {
+ const id = tb.IdTeam;
+ if (teamMap.has(id)) return teamMap.get(id)!;
+ const code = fifaCodeToAppCode(tb.Abbreviation);
+ const group = idGroup ? FIFA_GROUP_MAP[idGroup] : null;
+ if (!group) return null;
+ const loc = TEAM_LOCALIZATION[code.toUpperCase()];
+ const name = tb.TeamName?.[0]?.Description ?? "";
+ const t: Team = {
+ id,
+ name,
+ code,
+ group,
+ localisedName: loc ? loc[lang === "en" ? "en" : "de"] : name,
+ localisedNames: loc ? { de: loc.de, en: loc.en } : { de: name, en: name },
+ };
+ teamMap.set(id, t);
+ return t;
+ }
+
+ function mapFifaStatus(ms: number): MatchStatus {
+ switch (ms) {
+ case 0: return "FINISHED";
+ case 1: return "SCHEDULED";
+ case 10: return "POSTPONED";
+ default: return "LIVE";
+ }
+ }
+
+ const matches: Match[] = (data.Results ?? []).map((fm) => {
+ const group = fm.IdGroup ? FIFA_GROUP_MAP[fm.IdGroup] ?? null : null;
+ let homeTeam: Team | null = null;
+ let awayTeam: Team | null = null;
+ if (fm.Home) homeTeam = teamFromBlock(fm.Home, fm.IdGroup);
+ if (fm.Away) awayTeam = teamFromBlock(fm.Away, fm.IdGroup);
+ const attendance = fm.Attendance ? parseInt(fm.Attendance, 10) || null : null;
+ const minute = fm.MatchTime ? parseInt(fm.MatchTime, 10) || null : null;
+ const pref = lang === "en" ? "en-GB" : "de-DE";
+ const stadiumName = fm.Stadium?.Name?.find(n => n.Locale === pref)?.Description
+ ?? fm.Stadium?.Name?.[0]?.Description ?? null;
+ const stadiumCity = fm.Stadium?.CityName?.find(n => n.Locale === pref)?.Description
+ ?? fm.Stadium?.CityName?.[0]?.Description ?? null;
+ return {
+ id: fm.IdMatch,
+ group,
+ stage: FIFA_STAGE_MAP[fm.IdStage] ?? "GROUP",
+ matchNumber: fm.MatchNumber,
+ utcDate: fm.Date,
+ status: mapFifaStatus(fm.MatchStatus),
+ minute,
+ homeTeamId: fm.Home?.IdTeam ?? null,
+ awayTeamId: fm.Away?.IdTeam ?? null,
+ homeScore: fm.HomeTeamScore,
+ awayScore: fm.AwayTeamScore,
+ homePenalty: fm.HomeTeamPenaltyScore,
+ awayPenalty: fm.AwayTeamPenaltyScore,
+ winnerTeamId: fm.Winner ?? null,
+ venue: null,
+ stadiumName,
+ stadiumCity,
+ attendance,
+ };
+ });
+
+ const teams = [...teamMap.values()];
+ return { matches, teams };
+ });
+}
+
// Holt Live-Scores von der FIFA-API. Gibt Map MatchNumber → Scores + FIFA-ID→App-Code-Map zurück.
export async function fetchFifaScores(locale: string = "de"): Promise<{
scores: Map;
@@ -660,17 +777,13 @@ export async function fetchFifaScores(locale: string = "de"): Promise<{
}
// Wendet FIFA-Live-Scores auf Matches an (MatchNumber als exakter Schlüssel).
+// Team-IDs sind jetzt FIFA-IDs → winnerTeamId kann direkt gesetzt werden.
export function applyFifaScores(
matches: Match[], teams: Team[],
fifaData: { scores: Map; fifaIdToAppCode: Map },
): Match[] {
- const { scores: fifaMap, fifaIdToAppCode } = fifaData;
+ const { scores: fifaMap } = 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);
@@ -680,20 +793,7 @@ export function applyFifaScores(
if (fs.awayScore != null) r.awayScore = fs.awayScore;
if (fs.homePenalty != null) r.homePenalty = fs.homePenalty;
if (fs.awayPenalty != null) r.awayPenalty = fs.awayPenalty;
- // FIFA-ID → App-Team-ID auflösen
- if (fs.winnerTeamId) {
- const appCode = fifaIdToAppCode.get(fs.winnerTeamId);
- if (appCode) {
- const appTeamId = appCodeToId.get(appCode.toLowerCase());
- if (appTeamId) {
- r.winnerTeamId = appTeamId;
- } else {
- console.warn("[fifa] Winner-Team-Code nicht in App-Teams:", appCode, "| matchNumber:", m.matchNumber);
- }
- } else {
- console.warn("[fifa] FIFA-Winner-ID nicht in Team-Map:", fs.winnerTeamId, "| matchNumber:", m.matchNumber);
- }
- }
+ if (fs.winnerTeamId) r.winnerTeamId = fs.winnerTeamId;
if (fs.matchTime) {
const min = parseInt(fs.matchTime, 10);
if (!isNaN(min)) r.minute = min;
diff --git a/lib/fifa-constants.ts b/lib/fifa-constants.ts
new file mode 100644
index 0000000..5d87c4d
--- /dev/null
+++ b/lib/fifa-constants.ts
@@ -0,0 +1,14 @@
+import { GroupId, Match } from "./types";
+
+// FIFA IdGroup → App-GroupId (verifiziert, fortlaufend 289275-289286)
+export const FIFA_GROUP_MAP: Record = {
+ "289275": "A", "289276": "B", "289277": "C", "289278": "D",
+ "289279": "E", "289280": "F", "289281": "G", "289282": "H",
+ "289283": "I", "289284": "J", "289285": "K", "289286": "L",
+};
+
+// FIFA IdStage → App-Stage (verifiziert)
+export const FIFA_STAGE_MAP: Record = {
+ "289273": "GROUP", "289287": "R32", "289288": "R16",
+ "289289": "QF", "289290": "SF", "289291": "3RD", "289292": "FINAL",
+};
diff --git a/lib/flags.ts b/lib/flags.ts
new file mode 100644
index 0000000..0e61cb3
--- /dev/null
+++ b/lib/flags.ts
@@ -0,0 +1,52 @@
+// TLA (3-Buchstaben, wie in TEAM_LOCALIZATION) → circle-flags ISO-2-Dateiname.
+// Vollständig für alle Teams aus TEAM_LOCALIZATION.
+export const TLA_TO_ISO2: Record = {
+ ALG: "dz",
+ ARG: "ar",
+ AUS: "au",
+ AUT: "at",
+ BEL: "be",
+ BIH: "ba",
+ BRA: "br",
+ CAN: "ca",
+ CHE: "ch",
+ CIV: "ci",
+ COD: "cd",
+ COL: "co",
+ CPV: "cv",
+ CZE: "cz",
+ ECU: "ec",
+ EGY: "eg",
+ ENG: "gb-eng",
+ ESP: "es",
+ FRA: "fr",
+ GER: "de",
+ GHA: "gh",
+ HAI: "ht",
+ HRV: "hr",
+ IRN: "ir",
+ IRQ: "iq",
+ JOR: "jo",
+ JPN: "jp",
+ KOR: "kr",
+ KSA: "sa",
+ MAR: "ma",
+ MEX: "mx",
+ NED: "nl",
+ NOR: "no",
+ NZL: "nz",
+ PAN: "pa",
+ PAR: "py",
+ PRT: "pt",
+ QAT: "qa",
+ RSA: "za",
+ SCO: "gb-sct",
+ SEN: "sn",
+ SWE: "se",
+ TUN: "tn",
+ TUR: "tr",
+ URU: "uy",
+ USA: "us",
+ UZB: "uz",
+ CUW: "cw",
+};
diff --git a/lib/types.ts b/lib/types.ts
index 13854a0..82612a5 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -51,8 +51,10 @@ export interface Match {
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
- attendance?: number | null; // Zuschauerzahl, falls verfügbar
+ venue?: string | null; // Austragungsort (alt: football-data)
+ stadiumName?: string | null; // FIFA-Stadionname
+ stadiumCity?: string | null; // FIFA-Stadt
+ attendance?: number | null; // Zuschauerzahl
goals?: GoalEvent[] | null; // Torereignisse
}
diff --git a/public/flags/9460.svg b/public/flags/9460.svg
new file mode 100644
index 0000000..f2c9853
--- /dev/null
+++ b/public/flags/9460.svg
@@ -0,0 +1,16 @@
+
diff --git a/public/flags/ar.svg b/public/flags/ar.svg
new file mode 100644
index 0000000..ce73165
--- /dev/null
+++ b/public/flags/ar.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/at.svg b/public/flags/at.svg
new file mode 100644
index 0000000..73a87c8
--- /dev/null
+++ b/public/flags/at.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/au.svg b/public/flags/au.svg
new file mode 100644
index 0000000..bccaab3
--- /dev/null
+++ b/public/flags/au.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/ba.svg b/public/flags/ba.svg
new file mode 100644
index 0000000..62babc7
--- /dev/null
+++ b/public/flags/ba.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/be.svg b/public/flags/be.svg
new file mode 100644
index 0000000..3825681
--- /dev/null
+++ b/public/flags/be.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/br.svg b/public/flags/br.svg
new file mode 100644
index 0000000..44aacbd
--- /dev/null
+++ b/public/flags/br.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/ca.svg b/public/flags/ca.svg
new file mode 100644
index 0000000..3348f9e
--- /dev/null
+++ b/public/flags/ca.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/cd.svg b/public/flags/cd.svg
new file mode 100644
index 0000000..17ef2e2
--- /dev/null
+++ b/public/flags/cd.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/ch.svg b/public/flags/ch.svg
new file mode 100644
index 0000000..3943b1f
--- /dev/null
+++ b/public/flags/ch.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/ci.svg b/public/flags/ci.svg
new file mode 100644
index 0000000..1d21f10
--- /dev/null
+++ b/public/flags/ci.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/co.svg b/public/flags/co.svg
new file mode 100644
index 0000000..ac562e3
--- /dev/null
+++ b/public/flags/co.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/cv.svg b/public/flags/cv.svg
new file mode 100644
index 0000000..a49dc54
--- /dev/null
+++ b/public/flags/cv.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/cw.svg b/public/flags/cw.svg
new file mode 100644
index 0000000..5e8772d
--- /dev/null
+++ b/public/flags/cw.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/cz.svg b/public/flags/cz.svg
new file mode 100644
index 0000000..70668af
--- /dev/null
+++ b/public/flags/cz.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/de.svg b/public/flags/de.svg
new file mode 100644
index 0000000..efde341
--- /dev/null
+++ b/public/flags/de.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/dz.svg b/public/flags/dz.svg
new file mode 100644
index 0000000..4916175
--- /dev/null
+++ b/public/flags/dz.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/ec.svg b/public/flags/ec.svg
new file mode 100644
index 0000000..c0a0b22
--- /dev/null
+++ b/public/flags/ec.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/eg.svg b/public/flags/eg.svg
new file mode 100644
index 0000000..7c00ce5
--- /dev/null
+++ b/public/flags/eg.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/es.svg b/public/flags/es.svg
new file mode 100644
index 0000000..55d3ab5
--- /dev/null
+++ b/public/flags/es.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/fr.svg b/public/flags/fr.svg
new file mode 100644
index 0000000..0fe5619
--- /dev/null
+++ b/public/flags/fr.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/gb-eng.svg b/public/flags/gb-eng.svg
new file mode 100644
index 0000000..3fdf8c5
--- /dev/null
+++ b/public/flags/gb-eng.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/gb-sct.svg b/public/flags/gb-sct.svg
new file mode 100644
index 0000000..0ccc475
--- /dev/null
+++ b/public/flags/gb-sct.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/gh.svg b/public/flags/gh.svg
new file mode 100644
index 0000000..fed2c37
--- /dev/null
+++ b/public/flags/gh.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/hr.svg b/public/flags/hr.svg
new file mode 100644
index 0000000..5ddd718
--- /dev/null
+++ b/public/flags/hr.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/ht.svg b/public/flags/ht.svg
new file mode 100644
index 0000000..5423c4e
--- /dev/null
+++ b/public/flags/ht.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/iq.svg b/public/flags/iq.svg
new file mode 100644
index 0000000..63ee33e
--- /dev/null
+++ b/public/flags/iq.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/ir.svg b/public/flags/ir.svg
new file mode 100644
index 0000000..883fe5c
--- /dev/null
+++ b/public/flags/ir.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/jo.svg b/public/flags/jo.svg
new file mode 100644
index 0000000..e725de0
--- /dev/null
+++ b/public/flags/jo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/jp.svg b/public/flags/jp.svg
new file mode 100644
index 0000000..31f064f
--- /dev/null
+++ b/public/flags/jp.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/kr.svg b/public/flags/kr.svg
new file mode 100644
index 0000000..1056107
--- /dev/null
+++ b/public/flags/kr.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/ma.svg b/public/flags/ma.svg
new file mode 100644
index 0000000..30cd031
--- /dev/null
+++ b/public/flags/ma.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/mx.svg b/public/flags/mx.svg
new file mode 100644
index 0000000..2671a86
--- /dev/null
+++ b/public/flags/mx.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/nl.svg b/public/flags/nl.svg
new file mode 100644
index 0000000..25b6c91
--- /dev/null
+++ b/public/flags/nl.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/no.svg b/public/flags/no.svg
new file mode 100644
index 0000000..bc702a3
--- /dev/null
+++ b/public/flags/no.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/nz.svg b/public/flags/nz.svg
new file mode 100644
index 0000000..688bb9d
--- /dev/null
+++ b/public/flags/nz.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/pa.svg b/public/flags/pa.svg
new file mode 100644
index 0000000..c29cedd
--- /dev/null
+++ b/public/flags/pa.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/pt.svg b/public/flags/pt.svg
new file mode 100644
index 0000000..32ce13f
--- /dev/null
+++ b/public/flags/pt.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/py.svg b/public/flags/py.svg
new file mode 100644
index 0000000..537af58
--- /dev/null
+++ b/public/flags/py.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/qa.svg b/public/flags/qa.svg
new file mode 100644
index 0000000..7ccf5bd
--- /dev/null
+++ b/public/flags/qa.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/sa.svg b/public/flags/sa.svg
new file mode 100644
index 0000000..87c4d19
--- /dev/null
+++ b/public/flags/sa.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/se.svg b/public/flags/se.svg
new file mode 100644
index 0000000..c8520bc
--- /dev/null
+++ b/public/flags/se.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/sn.svg b/public/flags/sn.svg
new file mode 100644
index 0000000..f634dca
--- /dev/null
+++ b/public/flags/sn.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/tn.svg b/public/flags/tn.svg
new file mode 100644
index 0000000..56d5f3a
--- /dev/null
+++ b/public/flags/tn.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/tr.svg b/public/flags/tr.svg
new file mode 100644
index 0000000..6d22c9b
--- /dev/null
+++ b/public/flags/tr.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/us.svg b/public/flags/us.svg
new file mode 100644
index 0000000..e3560c6
--- /dev/null
+++ b/public/flags/us.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/uy.svg b/public/flags/uy.svg
new file mode 100644
index 0000000..93a40d3
--- /dev/null
+++ b/public/flags/uy.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/uz.svg b/public/flags/uz.svg
new file mode 100644
index 0000000..5607ade
--- /dev/null
+++ b/public/flags/uz.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/flags/za.svg b/public/flags/za.svg
new file mode 100644
index 0000000..4bb7bd5
--- /dev/null
+++ b/public/flags/za.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/scripts/download-flags.ts b/scripts/download-flags.ts
new file mode 100644
index 0000000..f7b7989
--- /dev/null
+++ b/scripts/download-flags.ts
@@ -0,0 +1,50 @@
+// download-flags.ts — Lädt circle-flags SVGs für alle Teams aus TLA_TO_ISO2.
+// Ausführung: npx tsx scripts/download-flags.ts
+
+import { TLA_TO_ISO2 } from "../lib/flags";
+import * as fs from "fs";
+import * as path from "path";
+
+const BASE = "https://hatscripts.github.io/circle-flags/flags";
+const OUT = path.join(__dirname, "..", "public", "flags");
+
+async function main() {
+ const codes = Object.values(TLA_TO_ISO2);
+ const unique = [...new Set(codes)];
+ console.log(`Lade ${unique.length} Flaggen (${codes.length} Team-Codes)...`);
+
+ if (!fs.existsSync(OUT)) fs.mkdirSync(OUT, { recursive: true });
+
+ let ok = 0;
+ let failed = 0;
+ for (const iso2 of unique) {
+ const url = `${BASE}/${iso2}.svg`;
+ try {
+ const res = await fetch(url, {
+ headers: { "User-Agent": "circle-flags-download/1.0" },
+ });
+ if (!res.ok) {
+ console.warn(` ❌ ${iso2}: HTTP ${res.status}`);
+ failed++;
+ continue;
+ }
+ const svg = await res.text();
+ // Prüfen: keine leere/Fehler-SVG (<10 Bytes = kaputt)
+ if (svg.trim().length < 10) {
+ console.warn(` ❌ ${iso2}: leere SVG (${svg.length} Bytes)`);
+ failed++;
+ continue;
+ }
+ fs.writeFileSync(path.join(OUT, `${iso2}.svg`), svg);
+ ok++;
+ } catch (err) {
+ console.warn(` ❌ ${iso2}: ${err instanceof Error ? err.message : err}`);
+ failed++;
+ }
+ }
+
+ console.log(`\n✅ ${ok} OK, ❌ ${failed} fehlgeschlagen`);
+ if (failed > 0) process.exit(1);
+}
+
+main();
diff --git a/scripts/fifa-eval.ts b/scripts/fifa-eval.ts
new file mode 100644
index 0000000..8ff3467
--- /dev/null
+++ b/scripts/fifa-eval.ts
@@ -0,0 +1,119 @@
+// fifa-eval.ts — Analyse-Skript: IdGroup, MatchNumber, Attendance, MatchStatus
+// Ausführung: npx ts-node --compiler-options '{"module":"commonjs","target":"es2020"}' scripts/fifa-eval.ts
+// Oder mit Deno, Bun: bun run scripts/fifa-eval.ts
+
+const FIFA_BASE = "https://api.fifa.com/api/v3";
+const FIFA_SEASON = "285023";
+
+interface RawMatch {
+ MatchNumber: number;
+ MatchStatus: number;
+ IdGroup: string | null;
+ IdStage: string;
+ StageName: Array<{ Locale: string; Description: string }>;
+ GroupName?: Array<{ Locale: string; Description: string }>;
+ Home: { IdTeam: string; Abbreviation: string; TeamName?: Array<{ Locale: string; Description: string }> } | null;
+ Away: { IdTeam: string; Abbreviation: string; TeamName?: Array<{ Locale: string; Description: string }> } | null;
+ HomeTeamScore: number | null;
+ AwayTeamScore: number | null;
+ Attendance: string | null;
+ Date: string;
+ LocalDate: string;
+}
+
+async function main() {
+ console.log("📡 Rufe FIFA Calendar-Endpoint ab...");
+ const res = await fetch(
+ `${FIFA_BASE}/calendar/matches?language=de&count=500&idSeason=${FIFA_SEASON}`,
+ { headers: { "User-Agent": "fifa-eval/1.0" } },
+ );
+ if (!res.ok) { console.error("Fehler:", res.status, res.statusText); process.exit(1); }
+ const data = (await res.json()) as { Results: RawMatch[] };
+ const matches = data.Results ?? [];
+ console.log(`✅ ${matches.length} Matches geladen\n`);
+
+ // ── A) IdGroup → GroupName Mapping ──
+ console.log("═══ A) IdGroup → Gruppe (A-L) ═══");
+ const groupMap = new Map();
+ for (const m of matches) {
+ if (!m.IdGroup || !m.GroupName) continue;
+ const desc = m.GroupName.find(g => g.Locale === "de-DE")?.Description
+ ?? m.GroupName[0]?.Description
+ ?? `ID:${m.IdGroup}`;
+ if (!groupMap.has(m.IdGroup)) groupMap.set(m.IdGroup, desc);
+ }
+ console.log(`Anzahl distinct IdGroup-Werte: ${groupMap.size}`);
+ const sorted = [...groupMap.entries()].sort((a, b) => parseInt(a[0]) - parseInt(b[0]));
+ for (const [id, name] of sorted) {
+ console.log(` ${id} → "${name}"`);
+ }
+ // Als kopierbare Konstante
+ console.log("\n// Kopierbare Konstante:");
+ console.log("const FIFA_IDGROUP_TO_GROUP: Record = {");
+ for (const [id, name] of sorted) {
+ const letter = name.replace(/^Gruppe\s+/, "");
+ console.log(` "${id}": "${letter}", // ${name}`);
+ }
+ console.log("};");
+
+ // ── B) MatchNumber-Konsistenz ──
+ console.log("\n═══ B) MatchNumber-Konsistenz ═══");
+ const groupMatches = matches.filter(m => m.IdGroup != null).sort((a, b) => a.MatchNumber - b.MatchNumber);
+ const koMatches = matches.filter(m => m.IdGroup == null).sort((a, b) => a.MatchNumber - b.MatchNumber);
+ console.log(`Gruppenspiele: ${groupMatches.length} (MatchNumber ${groupMatches[0]?.MatchNumber}–${groupMatches[groupMatches.length-1]?.MatchNumber})`);
+ console.log(`K.o.-Spiele: ${koMatches.length} (MatchNumber ${koMatches[0]?.MatchNumber}–${koMatches[koMatches.length-1]?.MatchNumber})`);
+
+ // Stichproben für K.o.-Slots
+ const checkNums = [73, 74, 76, 89, 104];
+ for (const n of checkNums) {
+ const m = matches.find(x => x.MatchNumber === n);
+ if (m) {
+ const home = m.Home?.Abbreviation ?? "?";
+ const away = m.Away?.Abbreviation ?? "?";
+ console.log(` #${n}: ${home} vs ${away} | Stage=${m.StageName?.[0]?.Description ?? "?"} | Status=${m.MatchStatus}`);
+ } else {
+ console.log(` #${n}: NICHT GEFUNDEN`);
+ }
+ }
+
+ // ── C) Attendance + Scheduled ──
+ console.log("\n═══ C) Attendance + Scheduled-Matches ═══");
+ const withAttendance = matches.filter(m => m.Attendance != null);
+ console.log(`Matches mit Attendance: ${withAttendance.length}`);
+ if (withAttendance.length > 0) {
+ const ex = withAttendance[0];
+ console.log(` Beispiel: #${ex.MatchNumber} → ${ex.Attendance}`);
+ }
+ const scheduled = matches.filter(m => m.MatchStatus === 1);
+ console.log(`Scheduled-Matches (Status=1): ${scheduled.length}`);
+ if (scheduled.length > 0) {
+ const ex = scheduled[0];
+ console.log(` Beispiel: #${ex.MatchNumber} ${ex.Home?.Abbreviation ?? "?"} vs ${ex.Away?.Abbreviation ?? "?"} | Date=${ex.Date}`);
+ }
+
+ // ── D) MatchStatus-Werte ──
+ console.log("\n═══ D) Distinct MatchStatus-Werte ═══");
+ const statuses = new Map();
+ for (const m of matches) {
+ statuses.set(m.MatchStatus, (statuses.get(m.MatchStatus) ?? 0) + 1);
+ }
+ const statusLabels: Record = { 0: "finished", 1: "scheduled", 10: "postponed", 3: "live?" };
+ for (const [s, c] of [...statuses.entries()].sort((a, b) => a[0] - b[0])) {
+ console.log(` MatchStatus ${s}: ${c} Matches ${statusLabels[s] ? `(${statusLabels[s]})` : ""}`);
+ }
+
+ // ── E) Stage-Werte ──
+ console.log("\n═══ E) Distinct Stages ═══");
+ const stages = new Map();
+ for (const m of matches) {
+ const sn = m.StageName?.[0]?.Description ?? String(m.IdStage);
+ stages.set(sn, (stages.get(sn) ?? 0) + 1);
+ }
+ for (const [s, c] of stages.entries()) {
+ console.log(` "${s}" (IdStage=${matches.find(m => (m.StageName?.[0]?.Description ?? "") === s)?.IdStage}): ${c}`);
+ }
+
+ console.log("\n✅ Analyse abgeschlossen.");
+}
+
+main().catch(console.error);