2026-06-28 11:56:33 -05:00
2026-06-28 11:56:33 -05:00
2026-06-28 11:56:33 -05:00
2026-06-24 22:25:08 -05:00
2026-06-24 22:25:08 -05:00
2026-06-22 15:32:32 -05:00
2026-06-25 12:00:36 -05:00
2026-06-22 15:32:32 -05:00
2026-06-22 15:32:32 -05:00
2026-06-22 15:44:54 -05:00
2026-06-25 14:18:25 -05:00
2026-06-25 14:18:25 -05:00
2026-06-22 15:32:32 -05:00
2026-06-22 15:32:32 -05:00
2026-06-24 22:25:08 -05:00
2026-06-24 22:25:08 -05:00
2026-06-22 15:44:54 -05:00
2026-06-22 15:32:32 -05:00
2026-06-24 17:45:22 -05:00

WM 2026 Board

Live-Gruppentabellen, Drittplatzierten-Wertung und vollständiger K.o.-Baum der FIFA WM 2026 — mit korrekter Annex-C-Zuordnung der acht besten Gruppendritten und optionalen Polymarket-Wahrscheinlichkeiten pro Spiel.

Gebaut als Next.js 15 (App Router) mit serverseitigem Feed-Proxy und Caching, Docker-Standalone-Image, ausgelegt für Caddy als Reverse Proxy.

Warum ein Backend-Proxy?

Das Frontend kann die Feeds nicht direkt aufrufen:

  • football-data.org erlaubt im Free-Tier nur 10 Anfragen/Minute und verlangt einen X-Auth-Token (im Browser nicht sicher unterzubringen).
  • Polymarket Gamma liefert outcomePrices als JSON-String und hat Rate Limits.
  • Beide setzen kein CORS für beliebige Browser-Origins.

Deshalb holt die Route app/api/matches beide Feeds serverseitig, cached sie in-memory (Spiele 60 s, Odds 120 s) und liefert ein fertiges JSON. Das Frontend pollt nur diese eine eigene Route (alle 30 s).

Architektur

Browser ──poll 30s──> /api/matches (Next.js, gecacht)
                          ├── football-data.org/v4/competitions/WC/matches
                          └── gamma-api.polymarket.com/events?slug=...
                          │
                          ├── computeGroupTables()      (FIFA-Tiebreaker)
                          ├── computeThirdPlaceTable()  (beste 8 Dritte)
                          └── resolveAnnexC()           (495 Szenarien, lib/annexc-data.ts)

Die Annex-C-Tabelle (lib/annexc-data.ts) enthält alle 495 möglichen Kombinationen aus den FIFA-Wettbewerbsregeln. Sobald genau 8 Gruppendritte feststehen, ordnet resolveAnnexC() jedem der acht betroffenen Gruppensieger (A, B, D, E, G, I, K, L) seinen Drittplatzierten zu, und der Bracket „verbindet" sich automatisch.

Schnellstart (lokal)

cp .env.example .env      # FOOTBALL_DATA_TOKEN eintragen
npm install
npm run dev               # http://localhost:3000

Token kostenlos registrieren: https://www.football-data.org/client/register

Deployment auf dem VPS (Docker + Caddy)

Das Projekt geht davon aus, dass Caddy bereits als Reverse Proxy in einem externen Docker-Netz läuft. Heißt dein Netz anders, in docker-compose.yml und im Caddy-Snippet anpassen.

# 1. Netz prüfen/anlegen (falls noch nicht vorhanden)
docker network ls | grep caddy_net || docker network create caddy_net

# 2. .env anlegen
cp .env.example .env && nano .env

# 3. Bauen und starten
docker compose up -d --build

# 4. Caddy-Route einbinden
#    Inhalt von Caddyfile.snippet in deinen Caddyfile übernehmen,
#    Domain anpassen, dann Caddy neu laden:
docker exec -w /etc/caddy caddy caddy reload

Danach ist die Seite unter https://wm.deine-domain.de erreichbar. Caddy besorgt TLS automatisch.

Konfiguration (.env)

Variable Pflicht Beschreibung
FOOTBALL_DATA_TOKEN ja Token von football-data.org. Ohne ihn liefert die API nur die Wettbewerbsliste.
POLYMARKET_WC_SLUG nein Slug des WM-Events auf Polymarket (Standard world-cup-2026). Den exakten Slug aus der URL nach /event/ ablesen.

Feeds verifizieren

Beide Upstream-Strukturen vor dem Turnierstart kurz gegenchecken (Felder können sich ändern):

# football-data: Gruppenfeld "GROUP_X", score.fullTime, stage
curl -H "X-Auth-Token: $FOOTBALL_DATA_TOKEN" \
  "https://api.football-data.org/v4/competitions/WC/matches" | jq '.matches[0]'

# Polymarket: markets[].outcomes / outcomePrices (JSON-Strings!)
curl "https://gamma-api.polymarket.com/events?slug=world-cup-2026" | jq '.[0].markets[0]'

Stimmen Feldnamen nicht, nur die Adapter in lib/feeds.ts anpassen — der Rest (Tabellen, Annex C, Bracket) ist davon entkoppelt.

Was anpassbar ist

  • Anderer Ergebnis-Feed (API-Football, TheStatsAPI): nur fetchMatchesAndTeams in lib/feeds.ts neu schreiben, sodass es Match[] + Team[] liefert.
  • Odds-Matching: attachOdds verknüpft per Teamname im Markttitel. Bei abweichenden Namen (z. B. „USA" vs. „United States") dort eine Alias-Tabelle ergänzen.
  • Poll-Intervall: in app/page.tsx (setInterval, Standard 30 s) und den Cache-TTLs in lib/feeds.ts.

Korrektheit der Annex-C-Logik

lib/annexc-data.ts wurde gegen die veröffentlichte FIFA-/Wikipedia-Tabelle geprüft. Beispiel Szenario 415 (Dritte aus A,B,C,E,F,H,I,J) ergibt die Zuordnung H J B C A F E I für die Gruppensieger A,B,D,E,G,I,K,L — exakt wie in den offiziellen Regeln.

Lizenz / Hinweise

Kein offizielles FIFA-Produkt. football-data.org ist für nicht-kommerzielle Nutzung kostenlos; bei kommerzieller Nutzung dort anfragen. Polymarket-Zugang und -Daten unterliegen den jeweiligen lokalen Bestimmungen.

Lokal testen (ohne VPS/Caddy)

Für den lokalen Docker-Test gibt es eine separate Compose-Datei ohne externes Caddy-Netz und mit direktem Port-Mapping:

cp .env.example .env        # FOOTBALL_DATA_TOKEN eintragen
docker compose -f docker-compose.local.yml up --build
# -> http://localhost:3000

Die VPS-Variante (docker compose up --build) bleibt davon unberührt.

Tabs & Bedienung

  • Gruppen — alle 12 Tabellen mit Flaggen. Klick auf einen Gruppen-Header öffnet die Spiele dieser Gruppe.
  • Spiele — Anstoßzeit/Ergebnis/Live-Minute, Flaggen, Austragungsort und (falls im Feed) Zuschauerzahl. Der Tab merkt sich die zuletzt gewählte Gruppe und zeigt sie im Label an („Spiele Gruppe H"). Oben ein Chip-Wechsler AL.
  • Drittplatzierte — gruppenübergreifende Wertung, Schnittlinie bei Platz 8/9.
  • K.o.-Baum — fixe Teams stehen voll ausgeschrieben mit grünem Marker; vorläufige (nur nach aktueller Tabelle, Gruppe noch nicht durchgespielt oder Annex C noch offen) erscheinen kursiv mit „≈"-Zeichen. Flaggen inklusive.

Flaggen

Die Flaggen kommen aus dem crest-Feld von football-data.org (SVG-URLs auf crests.football-data.org). Fehlt eine URL, zeigt die App einen neutralen Kreis mit Ländercode als Fallback.

Description
No description provided
Readme 944 KiB
Languages
TypeScript 82.4%
CSS 17%
Dockerfile 0.4%
Vim Snippet 0.1%