Matchcenter: Spielplan, Ergebnisse & Tabellen (eigene Seite statt BFV-Widget)
Neue Unterseite /matchcenter ersetzt die alten BFV-Widget-Embeds (verstoßen
gegen CSP) durch eine self-hostete Lösung: Cron-Scraper holt die Daten aus der
öffentlichen BFV-Widget-JSON-API, schreibt data/matchcenter.json und lädt die
Vereinswappen lokal — Besucher kontaktieren nur unsere Domain.
- bin/matchcenter-sync.php: CLI/Cron-Scraper (Muster wie instagram-sync), JSON-API
team/{id}/matches + competition/{compoundId}/table, Wappen via getLogo lokal,
atomarer Write, Fail-safe. Auto-Saison: Tabellen-compoundId wird aus der
Matches-API abgeleitet (team_id ist saison-stabil). Per-Team-Cache-Fallback bei
Fetch-Fehlern, damit Sektionen nie leeren.
- Seite app/pages/matchcenter.php + Route + Nav-Eintrag (unter Fußball).
- Komponenten: match-slider (nächste Spiele), match-card, match-row, league-table
(mit Auf-/Abstiegszonen), matchcenter-section (kicker-Layout: Tabelle 2/3 +
Spiele 1/3), matchcenter-empty, crest (weißes Chip + Initialen-Fallback).
- carousel.js: banner.js zum generischen [data-carousel]-Antrieb verallgemeinert
(Banner- und Match-Slider teilen ihn); crest.js: Logo-Fallback im Browser.
- helpers.php: sportsevent_nodes() für SportsEvent-JSON-LD.
- Icons calendar-event; config.example + .gitignore + CLAUDE.md ergänzt.
- Responsive geprüft (kein Seiten-Overflow 360–1280, Tabelle scrollt in der Karte).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192vTfGmGpoWeyUse4TuQFj
2026-06-19 10:57:14 +02:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Matchcenter-Sync (BFV-Widget-Ersatz): holt Spielplan, Ergebnisse und Tabelle der
|
|
|
|
|
|
* drei Aktivenmannschaften aus der öffentlichen BFV-Widget-JSON-API, lädt die
|
|
|
|
|
|
* Vereinswappen lokal nach public/assets/img/crests/ und schreibt data/matchcenter.json
|
|
|
|
|
|
* atomar. Bei JEDEM Fehler bleibt der letzte gute Cache unangetastet — die Website
|
|
|
|
|
|
* degradiert nur, sie bricht nie. Reverse-engineert aus dem BFV-Widget (das alte
|
|
|
|
|
|
* <BFVWidget.HTML5…>-Embed lud genau diese Endpunkte client-seitig nach).
|
|
|
|
|
|
*
|
|
|
|
|
|
* Aufruf: php bin/matchcenter-sync.php [-v] [--dump]
|
|
|
|
|
|
* Cron: 37 7,19 * * * cd /pfad/zur/site && /usr/bin/php bin/matchcenter-sync.php >> storage/logs/matchcenter.log 2>&1
|
|
|
|
|
|
* (zusätzlich am Wochenende häufiger, siehe config.example.php)
|
|
|
|
|
|
*
|
|
|
|
|
|
* Datenquelle (Host https://widget-prod.bfv.de, liefert sauberes JSON, kein HTML-Scraping):
|
|
|
|
|
|
* - Spiele/Ergebnisse: /api/service/widget/v1/team/{teamPermanentId}/matches
|
|
|
|
|
|
* - Liga-Tabelle: /api/service/widget/v1/competition/{compoundId}/table
|
|
|
|
|
|
* - Vereinswappen: https://app.bfv.de/export.media/-/action/getLogo/format/0/id/{clubId} (PNG)
|
|
|
|
|
|
* Ohne Browser-typischen User-Agent antwortet der Host mit HTTP 418 — der $fetch-Helfer
|
|
|
|
|
|
* setzt ihn (wie bin/instagram-sync.php).
|
|
|
|
|
|
*/
|
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.
Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.
News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.
Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
(Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.
Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
|
|
|
|
// Nur CLI — oder der authentifizierte Cron-Endpoint public/cron.php, der CRON_HTTP
|
|
|
|
|
|
// setzt (der Tarif kennt keine Cronjobs, siehe CLAUDE.md: Ausnahme zu Regel 3).
|
|
|
|
|
|
if (PHP_SAPI !== 'cli' && !defined('CRON_HTTP')) {
|
Matchcenter: Spielplan, Ergebnisse & Tabellen (eigene Seite statt BFV-Widget)
Neue Unterseite /matchcenter ersetzt die alten BFV-Widget-Embeds (verstoßen
gegen CSP) durch eine self-hostete Lösung: Cron-Scraper holt die Daten aus der
öffentlichen BFV-Widget-JSON-API, schreibt data/matchcenter.json und lädt die
Vereinswappen lokal — Besucher kontaktieren nur unsere Domain.
- bin/matchcenter-sync.php: CLI/Cron-Scraper (Muster wie instagram-sync), JSON-API
team/{id}/matches + competition/{compoundId}/table, Wappen via getLogo lokal,
atomarer Write, Fail-safe. Auto-Saison: Tabellen-compoundId wird aus der
Matches-API abgeleitet (team_id ist saison-stabil). Per-Team-Cache-Fallback bei
Fetch-Fehlern, damit Sektionen nie leeren.
- Seite app/pages/matchcenter.php + Route + Nav-Eintrag (unter Fußball).
- Komponenten: match-slider (nächste Spiele), match-card, match-row, league-table
(mit Auf-/Abstiegszonen), matchcenter-section (kicker-Layout: Tabelle 2/3 +
Spiele 1/3), matchcenter-empty, crest (weißes Chip + Initialen-Fallback).
- carousel.js: banner.js zum generischen [data-carousel]-Antrieb verallgemeinert
(Banner- und Match-Slider teilen ihn); crest.js: Logo-Fallback im Browser.
- helpers.php: sportsevent_nodes() für SportsEvent-JSON-LD.
- Icons calendar-event; config.example + .gitignore + CLAUDE.md ergänzt.
- Responsive geprüft (kein Seiten-Overflow 360–1280, Tabelle scrollt in der Karte).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192vTfGmGpoWeyUse4TuQFj
2026-06-19 10:57:14 +02:00
|
|
|
|
exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.
Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.
News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.
Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
(Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.
Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
|
|
|
|
// require_once, weil cron.php den Bootstrap schon geladen hat: app/bootstrap.php
|
|
|
|
|
|
// definiert Konstanten und lädt helpers.php, ein zweiter Durchlauf wäre ein Fatal.
|
|
|
|
|
|
require_once dirname(__DIR__) . '/app/bootstrap.php';
|
Matchcenter: Spielplan, Ergebnisse & Tabellen (eigene Seite statt BFV-Widget)
Neue Unterseite /matchcenter ersetzt die alten BFV-Widget-Embeds (verstoßen
gegen CSP) durch eine self-hostete Lösung: Cron-Scraper holt die Daten aus der
öffentlichen BFV-Widget-JSON-API, schreibt data/matchcenter.json und lädt die
Vereinswappen lokal — Besucher kontaktieren nur unsere Domain.
- bin/matchcenter-sync.php: CLI/Cron-Scraper (Muster wie instagram-sync), JSON-API
team/{id}/matches + competition/{compoundId}/table, Wappen via getLogo lokal,
atomarer Write, Fail-safe. Auto-Saison: Tabellen-compoundId wird aus der
Matches-API abgeleitet (team_id ist saison-stabil). Per-Team-Cache-Fallback bei
Fetch-Fehlern, damit Sektionen nie leeren.
- Seite app/pages/matchcenter.php + Route + Nav-Eintrag (unter Fußball).
- Komponenten: match-slider (nächste Spiele), match-card, match-row, league-table
(mit Auf-/Abstiegszonen), matchcenter-section (kicker-Layout: Tabelle 2/3 +
Spiele 1/3), matchcenter-empty, crest (weißes Chip + Initialen-Fallback).
- carousel.js: banner.js zum generischen [data-carousel]-Antrieb verallgemeinert
(Banner- und Match-Slider teilen ihn); crest.js: Logo-Fallback im Browser.
- helpers.php: sportsevent_nodes() für SportsEvent-JSON-LD.
- Icons calendar-event; config.example + .gitignore + CLAUDE.md ergänzt.
- Responsive geprüft (kein Seiten-Overflow 360–1280, Tabelle scrollt in der Karte).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192vTfGmGpoWeyUse4TuQFj
2026-06-19 10:57:14 +02:00
|
|
|
|
|
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.
Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.
News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.
Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
(Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.
Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
|
|
|
|
$verbose = in_array('-v', $argv ?? [], true);
|
|
|
|
|
|
$dump = in_array('--dump', $argv ?? [], true);
|
Matchcenter: Spielplan, Ergebnisse & Tabellen (eigene Seite statt BFV-Widget)
Neue Unterseite /matchcenter ersetzt die alten BFV-Widget-Embeds (verstoßen
gegen CSP) durch eine self-hostete Lösung: Cron-Scraper holt die Daten aus der
öffentlichen BFV-Widget-JSON-API, schreibt data/matchcenter.json und lädt die
Vereinswappen lokal — Besucher kontaktieren nur unsere Domain.
- bin/matchcenter-sync.php: CLI/Cron-Scraper (Muster wie instagram-sync), JSON-API
team/{id}/matches + competition/{compoundId}/table, Wappen via getLogo lokal,
atomarer Write, Fail-safe. Auto-Saison: Tabellen-compoundId wird aus der
Matches-API abgeleitet (team_id ist saison-stabil). Per-Team-Cache-Fallback bei
Fetch-Fehlern, damit Sektionen nie leeren.
- Seite app/pages/matchcenter.php + Route + Nav-Eintrag (unter Fußball).
- Komponenten: match-slider (nächste Spiele), match-card, match-row, league-table
(mit Auf-/Abstiegszonen), matchcenter-section (kicker-Layout: Tabelle 2/3 +
Spiele 1/3), matchcenter-empty, crest (weißes Chip + Initialen-Fallback).
- carousel.js: banner.js zum generischen [data-carousel]-Antrieb verallgemeinert
(Banner- und Match-Slider teilen ihn); crest.js: Logo-Fallback im Browser.
- helpers.php: sportsevent_nodes() für SportsEvent-JSON-LD.
- Icons calendar-event; config.example + .gitignore + CLAUDE.md ergänzt.
- Responsive geprüft (kein Seiten-Overflow 360–1280, Tabelle scrollt in der Karte).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192vTfGmGpoWeyUse4TuQFj
2026-06-19 10:57:14 +02:00
|
|
|
|
|
|
|
|
|
|
$apiHost = 'https://widget-prod.bfv.de';
|
|
|
|
|
|
$crestBase = 'https://app.bfv.de/export.media/-/action/getLogo/format/0/id/';
|
|
|
|
|
|
$referer = (string) config('matchcenter.referer', 'https://widget-prod.bfv.de/');
|
|
|
|
|
|
$maxUpcoming = (int) config('matchcenter.max_upcoming', 5);
|
|
|
|
|
|
$maxResults = (int) config('matchcenter.max_results', 5);
|
|
|
|
|
|
$teamsCfg = (array) config('matchcenter.teams', []);
|
|
|
|
|
|
|
|
|
|
|
|
$cacheFile = DATA_PATH . '/matchcenter.json';
|
|
|
|
|
|
$crestDir = PUBLIC_PATH . '/assets/img/crests';
|
|
|
|
|
|
|
|
|
|
|
|
// Anzeigename/Liga/Slug kommen aus der Single Source of Truth (data/teams.json),
|
|
|
|
|
|
// nicht aus der API — der Scraper erfindet keine kuratierten Namen.
|
|
|
|
|
|
$teamsData = json_load('teams');
|
|
|
|
|
|
$slugMap = [
|
|
|
|
|
|
'erste-mannschaft' => 'fussball/erste-mannschaft',
|
|
|
|
|
|
'zweite-mannschaft' => 'fussball/zweite-mannschaft',
|
|
|
|
|
|
'damen' => 'fussball/damen',
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
$tz = new DateTimeZone('Europe/Berlin');
|
|
|
|
|
|
$today = (new DateTimeImmutable('now', $tz))->setTime(0, 0);
|
|
|
|
|
|
$weekdaysDe = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'];
|
|
|
|
|
|
// Wettbewerbsart → kurzes, sprechendes Badge-Label.
|
|
|
|
|
|
$competitionLabels = [
|
|
|
|
|
|
'Meisterschaften' => 'Liga',
|
|
|
|
|
|
'Freundschaftsspiele' => 'Freundschaft',
|
|
|
|
|
|
'Pokalspiele' => 'Pokal',
|
|
|
|
|
|
'Pokal' => 'Pokal',
|
|
|
|
|
|
'Hallenrunde' => 'Halle',
|
|
|
|
|
|
];
|
|
|
|
|
|
|
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.
Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.
News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.
Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
(Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.
Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
|
|
|
|
// Schreibt über die zentrale log_write() (helpers.php) nach storage/logs/matchcenter.log;
|
|
|
|
|
|
// ERROR landet zusätzlich in error.log. Mit -v zusätzlich auf stdout.
|
|
|
|
|
|
$log = static function (string $msg, string $level = 'INFO') use ($verbose): void {
|
|
|
|
|
|
log_write('matchcenter', $level, $msg);
|
Matchcenter: Spielplan, Ergebnisse & Tabellen (eigene Seite statt BFV-Widget)
Neue Unterseite /matchcenter ersetzt die alten BFV-Widget-Embeds (verstoßen
gegen CSP) durch eine self-hostete Lösung: Cron-Scraper holt die Daten aus der
öffentlichen BFV-Widget-JSON-API, schreibt data/matchcenter.json und lädt die
Vereinswappen lokal — Besucher kontaktieren nur unsere Domain.
- bin/matchcenter-sync.php: CLI/Cron-Scraper (Muster wie instagram-sync), JSON-API
team/{id}/matches + competition/{compoundId}/table, Wappen via getLogo lokal,
atomarer Write, Fail-safe. Auto-Saison: Tabellen-compoundId wird aus der
Matches-API abgeleitet (team_id ist saison-stabil). Per-Team-Cache-Fallback bei
Fetch-Fehlern, damit Sektionen nie leeren.
- Seite app/pages/matchcenter.php + Route + Nav-Eintrag (unter Fußball).
- Komponenten: match-slider (nächste Spiele), match-card, match-row, league-table
(mit Auf-/Abstiegszonen), matchcenter-section (kicker-Layout: Tabelle 2/3 +
Spiele 1/3), matchcenter-empty, crest (weißes Chip + Initialen-Fallback).
- carousel.js: banner.js zum generischen [data-carousel]-Antrieb verallgemeinert
(Banner- und Match-Slider teilen ihn); crest.js: Logo-Fallback im Browser.
- helpers.php: sportsevent_nodes() für SportsEvent-JSON-LD.
- Icons calendar-event; config.example + .gitignore + CLAUDE.md ergänzt.
- Responsive geprüft (kein Seiten-Overflow 360–1280, Tabelle scrollt in der Karte).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192vTfGmGpoWeyUse4TuQFj
2026-06-19 10:57:14 +02:00
|
|
|
|
if ($verbose) {
|
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.
Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.
News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.
Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
(Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.
Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
|
|
|
|
echo '[' . date('c') . "] {$level} {$msg}\n";
|
Matchcenter: Spielplan, Ergebnisse & Tabellen (eigene Seite statt BFV-Widget)
Neue Unterseite /matchcenter ersetzt die alten BFV-Widget-Embeds (verstoßen
gegen CSP) durch eine self-hostete Lösung: Cron-Scraper holt die Daten aus der
öffentlichen BFV-Widget-JSON-API, schreibt data/matchcenter.json und lädt die
Vereinswappen lokal — Besucher kontaktieren nur unsere Domain.
- bin/matchcenter-sync.php: CLI/Cron-Scraper (Muster wie instagram-sync), JSON-API
team/{id}/matches + competition/{compoundId}/table, Wappen via getLogo lokal,
atomarer Write, Fail-safe. Auto-Saison: Tabellen-compoundId wird aus der
Matches-API abgeleitet (team_id ist saison-stabil). Per-Team-Cache-Fallback bei
Fetch-Fehlern, damit Sektionen nie leeren.
- Seite app/pages/matchcenter.php + Route + Nav-Eintrag (unter Fußball).
- Komponenten: match-slider (nächste Spiele), match-card, match-row, league-table
(mit Auf-/Abstiegszonen), matchcenter-section (kicker-Layout: Tabelle 2/3 +
Spiele 1/3), matchcenter-empty, crest (weißes Chip + Initialen-Fallback).
- carousel.js: banner.js zum generischen [data-carousel]-Antrieb verallgemeinert
(Banner- und Match-Slider teilen ihn); crest.js: Logo-Fallback im Browser.
- helpers.php: sportsevent_nodes() für SportsEvent-JSON-LD.
- Icons calendar-event; config.example + .gitignore + CLAUDE.md ergänzt.
- Responsive geprüft (kein Seiten-Overflow 360–1280, Tabelle scrollt in der Karte).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192vTfGmGpoWeyUse4TuQFj
2026-06-19 10:57:14 +02:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
$fail = static function (string $msg) use ($log): never {
|
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.
Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.
News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.
Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
(Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.
Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
|
|
|
|
$log("{$msg} — bestehender Cache bleibt unangetastet.", 'ERROR');
|
Matchcenter: Spielplan, Ergebnisse & Tabellen (eigene Seite statt BFV-Widget)
Neue Unterseite /matchcenter ersetzt die alten BFV-Widget-Embeds (verstoßen
gegen CSP) durch eine self-hostete Lösung: Cron-Scraper holt die Daten aus der
öffentlichen BFV-Widget-JSON-API, schreibt data/matchcenter.json und lädt die
Vereinswappen lokal — Besucher kontaktieren nur unsere Domain.
- bin/matchcenter-sync.php: CLI/Cron-Scraper (Muster wie instagram-sync), JSON-API
team/{id}/matches + competition/{compoundId}/table, Wappen via getLogo lokal,
atomarer Write, Fail-safe. Auto-Saison: Tabellen-compoundId wird aus der
Matches-API abgeleitet (team_id ist saison-stabil). Per-Team-Cache-Fallback bei
Fetch-Fehlern, damit Sektionen nie leeren.
- Seite app/pages/matchcenter.php + Route + Nav-Eintrag (unter Fußball).
- Komponenten: match-slider (nächste Spiele), match-card, match-row, league-table
(mit Auf-/Abstiegszonen), matchcenter-section (kicker-Layout: Tabelle 2/3 +
Spiele 1/3), matchcenter-empty, crest (weißes Chip + Initialen-Fallback).
- carousel.js: banner.js zum generischen [data-carousel]-Antrieb verallgemeinert
(Banner- und Match-Slider teilen ihn); crest.js: Logo-Fallback im Browser.
- helpers.php: sportsevent_nodes() für SportsEvent-JSON-LD.
- Icons calendar-event; config.example + .gitignore + CLAUDE.md ergänzt.
- Responsive geprüft (kein Seiten-Overflow 360–1280, Tabelle scrollt in der Karte).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192vTfGmGpoWeyUse4TuQFj
2026-06-19 10:57:14 +02:00
|
|
|
|
exit(1);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if ($teamsCfg === []) {
|
|
|
|
|
|
$fail('Keine Teams in config.matchcenter.teams konfiguriert');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Lock gegen parallele Läufe ---
|
|
|
|
|
|
$lock = fopen(STORAGE_PATH . '/cache/matchcenter.lock', 'c');
|
|
|
|
|
|
if (!$lock || !flock($lock, LOCK_EX | LOCK_NB)) {
|
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.
Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.
News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.
Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
(Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.
Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
|
|
|
|
// Kein ERROR: ein überlappender Cron-Lauf ist kein Ausfall, der nächste Takt holt es nach.
|
|
|
|
|
|
$log('Läuft bereits (Lock belegt) — Lauf übersprungen.', 'WARN');
|
|
|
|
|
|
exit(1);
|
Matchcenter: Spielplan, Ergebnisse & Tabellen (eigene Seite statt BFV-Widget)
Neue Unterseite /matchcenter ersetzt die alten BFV-Widget-Embeds (verstoßen
gegen CSP) durch eine self-hostete Lösung: Cron-Scraper holt die Daten aus der
öffentlichen BFV-Widget-JSON-API, schreibt data/matchcenter.json und lädt die
Vereinswappen lokal — Besucher kontaktieren nur unsere Domain.
- bin/matchcenter-sync.php: CLI/Cron-Scraper (Muster wie instagram-sync), JSON-API
team/{id}/matches + competition/{compoundId}/table, Wappen via getLogo lokal,
atomarer Write, Fail-safe. Auto-Saison: Tabellen-compoundId wird aus der
Matches-API abgeleitet (team_id ist saison-stabil). Per-Team-Cache-Fallback bei
Fetch-Fehlern, damit Sektionen nie leeren.
- Seite app/pages/matchcenter.php + Route + Nav-Eintrag (unter Fußball).
- Komponenten: match-slider (nächste Spiele), match-card, match-row, league-table
(mit Auf-/Abstiegszonen), matchcenter-section (kicker-Layout: Tabelle 2/3 +
Spiele 1/3), matchcenter-empty, crest (weißes Chip + Initialen-Fallback).
- carousel.js: banner.js zum generischen [data-carousel]-Antrieb verallgemeinert
(Banner- und Match-Slider teilen ihn); crest.js: Logo-Fallback im Browser.
- helpers.php: sportsevent_nodes() für SportsEvent-JSON-LD.
- Icons calendar-event; config.example + .gitignore + CLAUDE.md ergänzt.
- Responsive geprüft (kein Seiten-Overflow 360–1280, Tabelle scrollt in der Karte).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192vTfGmGpoWeyUse4TuQFj
2026-06-19 10:57:14 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- HTTP-Helfer mit Browser-typischen Headern (gegen HTTP 418) ---
|
|
|
|
|
|
$fetch = static function (string $url, bool $json = true) use ($referer): string|false {
|
|
|
|
|
|
$ch = curl_init($url);
|
|
|
|
|
|
curl_setopt_array($ch, [
|
|
|
|
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
|
|
|
|
CURLOPT_FOLLOWLOCATION => true,
|
|
|
|
|
|
CURLOPT_MAXREDIRS => 5,
|
|
|
|
|
|
CURLOPT_TIMEOUT => 20,
|
|
|
|
|
|
CURLOPT_ENCODING => '',
|
|
|
|
|
|
CURLOPT_USERAGENT => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
|
|
|
|
|
CURLOPT_HTTPHEADER => [
|
|
|
|
|
|
'Accept: ' . ($json ? 'application/json, text/plain, */*' : '*/*'),
|
|
|
|
|
|
'Accept-Language: de-DE,de;q=0.9',
|
|
|
|
|
|
'Referer: ' . $referer,
|
|
|
|
|
|
'Sec-Fetch-Dest: ' . ($json ? 'empty' : 'image'),
|
|
|
|
|
|
'Sec-Fetch-Mode: ' . ($json ? 'cors' : 'no-cors'),
|
|
|
|
|
|
'Sec-Fetch-Site: cross-site',
|
|
|
|
|
|
],
|
|
|
|
|
|
]);
|
|
|
|
|
|
$body = curl_exec($ch);
|
|
|
|
|
|
$status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
|
|
|
|
|
return ($body !== false && $status === 200) ? $body : false;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/** JSON-Endpoint holen und data-Knoten zurückgeben (oder null). */
|
|
|
|
|
|
$fetchJson = static function (string $url) use ($fetch, $dump, $log): ?array {
|
|
|
|
|
|
$body = $fetch($url, true);
|
|
|
|
|
|
if ($body === false) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($dump) {
|
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.
Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.
News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.
Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
(Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.
Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
|
|
|
|
echo "DUMP {$url}\n" . substr($body, 0, 4000) . "\n";
|
Matchcenter: Spielplan, Ergebnisse & Tabellen (eigene Seite statt BFV-Widget)
Neue Unterseite /matchcenter ersetzt die alten BFV-Widget-Embeds (verstoßen
gegen CSP) durch eine self-hostete Lösung: Cron-Scraper holt die Daten aus der
öffentlichen BFV-Widget-JSON-API, schreibt data/matchcenter.json und lädt die
Vereinswappen lokal — Besucher kontaktieren nur unsere Domain.
- bin/matchcenter-sync.php: CLI/Cron-Scraper (Muster wie instagram-sync), JSON-API
team/{id}/matches + competition/{compoundId}/table, Wappen via getLogo lokal,
atomarer Write, Fail-safe. Auto-Saison: Tabellen-compoundId wird aus der
Matches-API abgeleitet (team_id ist saison-stabil). Per-Team-Cache-Fallback bei
Fetch-Fehlern, damit Sektionen nie leeren.
- Seite app/pages/matchcenter.php + Route + Nav-Eintrag (unter Fußball).
- Komponenten: match-slider (nächste Spiele), match-card, match-row, league-table
(mit Auf-/Abstiegszonen), matchcenter-section (kicker-Layout: Tabelle 2/3 +
Spiele 1/3), matchcenter-empty, crest (weißes Chip + Initialen-Fallback).
- carousel.js: banner.js zum generischen [data-carousel]-Antrieb verallgemeinert
(Banner- und Match-Slider teilen ihn); crest.js: Logo-Fallback im Browser.
- helpers.php: sportsevent_nodes() für SportsEvent-JSON-LD.
- Icons calendar-event; config.example + .gitignore + CLAUDE.md ergänzt.
- Responsive geprüft (kein Seiten-Overflow 360–1280, Tabelle scrollt in der Karte).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192vTfGmGpoWeyUse4TuQFj
2026-06-19 10:57:14 +02:00
|
|
|
|
}
|
|
|
|
|
|
$data = json_decode($body, true);
|
|
|
|
|
|
return is_array($data) ? $data : null;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// --- Wappen-Download: idempotent, normalisiert auf max 96px PNG (Transparenz erhalten) ---
|
|
|
|
|
|
if (!is_dir($crestDir)) {
|
|
|
|
|
|
mkdir($crestDir, 0755, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
$crestKeep = [];
|
|
|
|
|
|
$downloadCrest = static function (?string $clubId) use ($fetch, $crestBase, $crestDir, &$crestKeep, $log): ?string {
|
|
|
|
|
|
$clubId = trim((string) $clubId);
|
|
|
|
|
|
if ($clubId === '' || !preg_match('/^[A-Z0-9-]+$/i', $clubId)) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
$rel = "img/crests/{$clubId}.png";
|
|
|
|
|
|
$file = "{$crestDir}/{$clubId}.png";
|
|
|
|
|
|
$crestKeep["{$clubId}.png"] = true;
|
|
|
|
|
|
if (is_file($file)) {
|
|
|
|
|
|
return $rel;
|
|
|
|
|
|
}
|
|
|
|
|
|
$bin = $fetch($crestBase . $clubId, false);
|
|
|
|
|
|
if ($bin === false) {
|
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.
Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.
News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.
Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
(Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.
Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
|
|
|
|
$log("Wappen-Download fehlgeschlagen: {$clubId}", 'WARN');
|
Matchcenter: Spielplan, Ergebnisse & Tabellen (eigene Seite statt BFV-Widget)
Neue Unterseite /matchcenter ersetzt die alten BFV-Widget-Embeds (verstoßen
gegen CSP) durch eine self-hostete Lösung: Cron-Scraper holt die Daten aus der
öffentlichen BFV-Widget-JSON-API, schreibt data/matchcenter.json und lädt die
Vereinswappen lokal — Besucher kontaktieren nur unsere Domain.
- bin/matchcenter-sync.php: CLI/Cron-Scraper (Muster wie instagram-sync), JSON-API
team/{id}/matches + competition/{compoundId}/table, Wappen via getLogo lokal,
atomarer Write, Fail-safe. Auto-Saison: Tabellen-compoundId wird aus der
Matches-API abgeleitet (team_id ist saison-stabil). Per-Team-Cache-Fallback bei
Fetch-Fehlern, damit Sektionen nie leeren.
- Seite app/pages/matchcenter.php + Route + Nav-Eintrag (unter Fußball).
- Komponenten: match-slider (nächste Spiele), match-card, match-row, league-table
(mit Auf-/Abstiegszonen), matchcenter-section (kicker-Layout: Tabelle 2/3 +
Spiele 1/3), matchcenter-empty, crest (weißes Chip + Initialen-Fallback).
- carousel.js: banner.js zum generischen [data-carousel]-Antrieb verallgemeinert
(Banner- und Match-Slider teilen ihn); crest.js: Logo-Fallback im Browser.
- helpers.php: sportsevent_nodes() für SportsEvent-JSON-LD.
- Icons calendar-event; config.example + .gitignore + CLAUDE.md ergänzt.
- Responsive geprüft (kein Seiten-Overflow 360–1280, Tabelle scrollt in der Karte).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192vTfGmGpoWeyUse4TuQFj
2026-06-19 10:57:14 +02:00
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
$src = @imagecreatefromstring($bin);
|
|
|
|
|
|
if ($src === false) {
|
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.
Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.
News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.
Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
(Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.
Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
|
|
|
|
$log("Wappen unlesbar: {$clubId}", 'WARN');
|
Matchcenter: Spielplan, Ergebnisse & Tabellen (eigene Seite statt BFV-Widget)
Neue Unterseite /matchcenter ersetzt die alten BFV-Widget-Embeds (verstoßen
gegen CSP) durch eine self-hostete Lösung: Cron-Scraper holt die Daten aus der
öffentlichen BFV-Widget-JSON-API, schreibt data/matchcenter.json und lädt die
Vereinswappen lokal — Besucher kontaktieren nur unsere Domain.
- bin/matchcenter-sync.php: CLI/Cron-Scraper (Muster wie instagram-sync), JSON-API
team/{id}/matches + competition/{compoundId}/table, Wappen via getLogo lokal,
atomarer Write, Fail-safe. Auto-Saison: Tabellen-compoundId wird aus der
Matches-API abgeleitet (team_id ist saison-stabil). Per-Team-Cache-Fallback bei
Fetch-Fehlern, damit Sektionen nie leeren.
- Seite app/pages/matchcenter.php + Route + Nav-Eintrag (unter Fußball).
- Komponenten: match-slider (nächste Spiele), match-card, match-row, league-table
(mit Auf-/Abstiegszonen), matchcenter-section (kicker-Layout: Tabelle 2/3 +
Spiele 1/3), matchcenter-empty, crest (weißes Chip + Initialen-Fallback).
- carousel.js: banner.js zum generischen [data-carousel]-Antrieb verallgemeinert
(Banner- und Match-Slider teilen ihn); crest.js: Logo-Fallback im Browser.
- helpers.php: sportsevent_nodes() für SportsEvent-JSON-LD.
- Icons calendar-event; config.example + .gitignore + CLAUDE.md ergänzt.
- Responsive geprüft (kein Seiten-Overflow 360–1280, Tabelle scrollt in der Karte).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192vTfGmGpoWeyUse4TuQFj
2026-06-19 10:57:14 +02:00
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
$w = imagesx($src);
|
|
|
|
|
|
$h = imagesy($src);
|
|
|
|
|
|
$scale = min(96 / $w, 96 / $h, 1.0);
|
|
|
|
|
|
$nw = max(1, (int) round($w * $scale));
|
|
|
|
|
|
$nh = max(1, (int) round($h * $scale));
|
|
|
|
|
|
$out = imagecreatetruecolor($nw, $nh);
|
|
|
|
|
|
imagealphablending($out, false);
|
|
|
|
|
|
imagesavealpha($out, true);
|
|
|
|
|
|
imagefilledrectangle($out, 0, 0, $nw, $nh, imagecolorallocatealpha($out, 0, 0, 0, 127));
|
|
|
|
|
|
imagecopyresampled($out, $src, 0, 0, 0, 0, $nw, $nh, $w, $h);
|
|
|
|
|
|
imagepng($out, $file);
|
|
|
|
|
|
return $rel;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// --- Datums-/Zeit-Helfer (deutsche Quellformate, Europe/Berlin) ---
|
|
|
|
|
|
$parseKickoff = static function (string $date, string $time) use ($tz): ?DateTimeImmutable {
|
|
|
|
|
|
$date = trim($date);
|
|
|
|
|
|
$time = trim($time) !== '' ? trim($time) : '00:00';
|
|
|
|
|
|
if ($date === '') {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
$dt = DateTimeImmutable::createFromFormat('d.m.Y H:i', "{$date} {$time}", $tz);
|
|
|
|
|
|
return $dt ?: null;
|
|
|
|
|
|
};
|
|
|
|
|
|
$dateDisplay = static function (?DateTimeImmutable $dt, string $raw) use ($weekdaysDe): string {
|
|
|
|
|
|
if ($dt === null) {
|
|
|
|
|
|
return $raw; // Roh-String behalten — Anzeige bricht nie weg.
|
|
|
|
|
|
}
|
|
|
|
|
|
return $weekdaysDe[(int) $dt->format('w')] . ', ' . $dt->format('d.m.');
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-21 00:51:14 +02:00
|
|
|
|
// --- Saison-Label (Fußball-Saison läuft Jul–Jun): aus dem jüngsten Spiel ableiten,
|
|
|
|
|
|
// ersatzweise (leere Saison) aus dem heutigen Datum → liefert immer ein Label. ---
|
|
|
|
|
|
$seasonLabel = static function (DateTimeImmutable $d): string {
|
|
|
|
|
|
$y = (int) $d->format('Y');
|
|
|
|
|
|
$start = (int) $d->format('n') >= 7 ? $y : $y - 1;
|
|
|
|
|
|
return sprintf('Saison %d/%02d', $start, ($start + 1) % 100);
|
|
|
|
|
|
};
|
|
|
|
|
|
$seasonFromRows = static function (array ...$rowSets) use ($seasonLabel, $tz, $today): string {
|
|
|
|
|
|
$latest = null;
|
|
|
|
|
|
foreach ($rowSets as $rows) {
|
|
|
|
|
|
foreach ($rows as $r) {
|
|
|
|
|
|
if (empty($r['kickoff'])) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
$dt = DateTimeImmutable::createFromFormat('Y-m-d\TH:i', (string) $r['kickoff'], $tz);
|
|
|
|
|
|
if ($dt && ($latest === null || $dt > $latest)) {
|
|
|
|
|
|
$latest = $dt;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return $seasonLabel($latest ?? $today);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
Matchcenter: Spielplan, Ergebnisse & Tabellen (eigene Seite statt BFV-Widget)
Neue Unterseite /matchcenter ersetzt die alten BFV-Widget-Embeds (verstoßen
gegen CSP) durch eine self-hostete Lösung: Cron-Scraper holt die Daten aus der
öffentlichen BFV-Widget-JSON-API, schreibt data/matchcenter.json und lädt die
Vereinswappen lokal — Besucher kontaktieren nur unsere Domain.
- bin/matchcenter-sync.php: CLI/Cron-Scraper (Muster wie instagram-sync), JSON-API
team/{id}/matches + competition/{compoundId}/table, Wappen via getLogo lokal,
atomarer Write, Fail-safe. Auto-Saison: Tabellen-compoundId wird aus der
Matches-API abgeleitet (team_id ist saison-stabil). Per-Team-Cache-Fallback bei
Fetch-Fehlern, damit Sektionen nie leeren.
- Seite app/pages/matchcenter.php + Route + Nav-Eintrag (unter Fußball).
- Komponenten: match-slider (nächste Spiele), match-card, match-row, league-table
(mit Auf-/Abstiegszonen), matchcenter-section (kicker-Layout: Tabelle 2/3 +
Spiele 1/3), matchcenter-empty, crest (weißes Chip + Initialen-Fallback).
- carousel.js: banner.js zum generischen [data-carousel]-Antrieb verallgemeinert
(Banner- und Match-Slider teilen ihn); crest.js: Logo-Fallback im Browser.
- helpers.php: sportsevent_nodes() für SportsEvent-JSON-LD.
- Icons calendar-event; config.example + .gitignore + CLAUDE.md ergänzt.
- Responsive geprüft (kein Seiten-Overflow 360–1280, Tabelle scrollt in der Karte).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192vTfGmGpoWeyUse4TuQFj
2026-06-19 10:57:14 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* Ein Match aus der API in unser Anzeige-Format überführen.
|
|
|
|
|
|
* @param array<string,mixed> $m Rohes Match aus data.matches[]
|
|
|
|
|
|
* @param string $teamCfgId permanentId der eigenen Mannschaft (für is_home/outcome)
|
|
|
|
|
|
*/
|
|
|
|
|
|
$mapMatch = static function (array $m, string $teamCfgId) use ($parseKickoff, $dateDisplay, $downloadCrest, $competitionLabels, $tz): array {
|
|
|
|
|
|
$rawDate = (string) ($m['kickoffDate'] ?? '');
|
|
|
|
|
|
$rawTime = (string) ($m['kickoffTime'] ?? '');
|
|
|
|
|
|
$dt = $parseKickoff($rawDate, $rawTime);
|
|
|
|
|
|
$isHome = ($m['homeTeamPermanentId'] ?? null) === $teamCfgId;
|
|
|
|
|
|
$type = (string) ($m['competitionType'] ?? '');
|
|
|
|
|
|
|
|
|
|
|
|
$row = [
|
|
|
|
|
|
'kickoff' => $dt?->format('Y-m-d\TH:i'),
|
|
|
|
|
|
'date_display' => $dateDisplay($dt, $rawDate),
|
|
|
|
|
|
'time_display' => $rawTime !== '' ? $rawTime . ' Uhr' : '',
|
|
|
|
|
|
'competition' => $competitionLabels[$type] ?? ($type !== '' ? $type : 'Spiel'),
|
|
|
|
|
|
'home' => (string) ($m['homeTeamName'] ?? ''),
|
|
|
|
|
|
'away' => (string) ($m['guestTeamName'] ?? ''),
|
|
|
|
|
|
'home_crest' => $downloadCrest($m['homeClubId'] ?? null),
|
|
|
|
|
|
'away_crest' => $downloadCrest($m['guestClubId'] ?? null),
|
|
|
|
|
|
'is_home' => $isHome,
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
$result = trim((string) ($m['result'] ?? ''));
|
|
|
|
|
|
if ($result !== '' && preg_match('/^(\d+)\s*:\s*(\d+)/', $result, $mm)) {
|
|
|
|
|
|
$gh = (int) $mm[1];
|
|
|
|
|
|
$ga = (int) $mm[2];
|
|
|
|
|
|
$own = $isHome ? $gh : $ga;
|
|
|
|
|
|
$opp = $isHome ? $ga : $gh;
|
|
|
|
|
|
$row['goals_home'] = $gh;
|
|
|
|
|
|
$row['goals_away'] = $ga;
|
|
|
|
|
|
$row['result'] = $result;
|
|
|
|
|
|
$row['outcome'] = $own > $opp ? 'win' : ($own === $opp ? 'draw' : 'loss');
|
|
|
|
|
|
}
|
|
|
|
|
|
return $row;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// --- Vorheriger Cache: bei Teil-Ausfällen (BFV liefert 418/Fehler) behalten wir
|
|
|
|
|
|
// die alten Daten dieses Teams bzw. Teils, statt die Sektion zu leeren. ---
|
|
|
|
|
|
$prev = [];
|
|
|
|
|
|
if (is_file($cacheFile)) {
|
|
|
|
|
|
$decoded = json_decode((string) file_get_contents($cacheFile), true);
|
|
|
|
|
|
if (is_array($decoded)) {
|
|
|
|
|
|
$prev = $decoded;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
$prevByKey = [];
|
|
|
|
|
|
foreach ($prev['teams'] ?? [] as $pt) {
|
|
|
|
|
|
if (!empty($pt['key'])) {
|
|
|
|
|
|
$prevByKey[$pt['key']] = $pt;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
$cacheFallbackUsed = false;
|
|
|
|
|
|
|
|
|
|
|
|
// Wappen-Dateinamen aus (ggf. aus dem Cache übernommenen) Zeilen in die Keep-Liste
|
|
|
|
|
|
// aufnehmen, damit die GC unten sie nicht löscht.
|
|
|
|
|
|
$keepCrests = static function (array $rows) use (&$crestKeep): void {
|
|
|
|
|
|
foreach ($rows as $r) {
|
|
|
|
|
|
foreach (['home_crest', 'away_crest', 'crest'] as $k) {
|
|
|
|
|
|
if (!empty($r[$k]) && is_string($r[$k])) {
|
|
|
|
|
|
$crestKeep[basename($r[$k])] = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// --- Pro Team: Spiele + Tabelle holen, parsen, Wappen laden (isoliert) ---
|
|
|
|
|
|
$resultTeams = [];
|
|
|
|
|
|
$upcomingAll = [];
|
|
|
|
|
|
|
|
|
|
|
|
foreach ($teamsCfg as $cfg) {
|
|
|
|
|
|
$key = (string) ($cfg['key'] ?? '');
|
|
|
|
|
|
$teamCfgId = (string) ($cfg['team_id'] ?? '');
|
|
|
|
|
|
$cfgCompoundId = (string) ($cfg['compound_id'] ?? '');
|
|
|
|
|
|
if ($key === '' || $teamCfgId === '') {
|
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.
Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.
News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.
Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
(Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.
Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
|
|
|
|
$log("Team-Eintrag ohne key/team_id übersprungen", 'WARN');
|
Matchcenter: Spielplan, Ergebnisse & Tabellen (eigene Seite statt BFV-Widget)
Neue Unterseite /matchcenter ersetzt die alten BFV-Widget-Embeds (verstoßen
gegen CSP) durch eine self-hostete Lösung: Cron-Scraper holt die Daten aus der
öffentlichen BFV-Widget-JSON-API, schreibt data/matchcenter.json und lädt die
Vereinswappen lokal — Besucher kontaktieren nur unsere Domain.
- bin/matchcenter-sync.php: CLI/Cron-Scraper (Muster wie instagram-sync), JSON-API
team/{id}/matches + competition/{compoundId}/table, Wappen via getLogo lokal,
atomarer Write, Fail-safe. Auto-Saison: Tabellen-compoundId wird aus der
Matches-API abgeleitet (team_id ist saison-stabil). Per-Team-Cache-Fallback bei
Fetch-Fehlern, damit Sektionen nie leeren.
- Seite app/pages/matchcenter.php + Route + Nav-Eintrag (unter Fußball).
- Komponenten: match-slider (nächste Spiele), match-card, match-row, league-table
(mit Auf-/Abstiegszonen), matchcenter-section (kicker-Layout: Tabelle 2/3 +
Spiele 1/3), matchcenter-empty, crest (weißes Chip + Initialen-Fallback).
- carousel.js: banner.js zum generischen [data-carousel]-Antrieb verallgemeinert
(Banner- und Match-Slider teilen ihn); crest.js: Logo-Fallback im Browser.
- helpers.php: sportsevent_nodes() für SportsEvent-JSON-LD.
- Icons calendar-event; config.example + .gitignore + CLAUDE.md ergänzt.
- Responsive geprüft (kein Seiten-Overflow 360–1280, Tabelle scrollt in der Karte).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192vTfGmGpoWeyUse4TuQFj
2026-06-19 10:57:14 +02:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
$prevTeam = $prevByKey[$key] ?? null;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2026-06-21 00:51:14 +02:00
|
|
|
|
$prevCompound = (string) ($prevTeam['compound_id'] ?? '');
|
|
|
|
|
|
$previous = $prevTeam['previous'] ?? null;
|
|
|
|
|
|
|
Matchcenter: Spielplan, Ergebnisse & Tabellen (eigene Seite statt BFV-Widget)
Neue Unterseite /matchcenter ersetzt die alten BFV-Widget-Embeds (verstoßen
gegen CSP) durch eine self-hostete Lösung: Cron-Scraper holt die Daten aus der
öffentlichen BFV-Widget-JSON-API, schreibt data/matchcenter.json und lädt die
Vereinswappen lokal — Besucher kontaktieren nur unsere Domain.
- bin/matchcenter-sync.php: CLI/Cron-Scraper (Muster wie instagram-sync), JSON-API
team/{id}/matches + competition/{compoundId}/table, Wappen via getLogo lokal,
atomarer Write, Fail-safe. Auto-Saison: Tabellen-compoundId wird aus der
Matches-API abgeleitet (team_id ist saison-stabil). Per-Team-Cache-Fallback bei
Fetch-Fehlern, damit Sektionen nie leeren.
- Seite app/pages/matchcenter.php + Route + Nav-Eintrag (unter Fußball).
- Komponenten: match-slider (nächste Spiele), match-card, match-row, league-table
(mit Auf-/Abstiegszonen), matchcenter-section (kicker-Layout: Tabelle 2/3 +
Spiele 1/3), matchcenter-empty, crest (weißes Chip + Initialen-Fallback).
- carousel.js: banner.js zum generischen [data-carousel]-Antrieb verallgemeinert
(Banner- und Match-Slider teilen ihn); crest.js: Logo-Fallback im Browser.
- helpers.php: sportsevent_nodes() für SportsEvent-JSON-LD.
- Icons calendar-event; config.example + .gitignore + CLAUDE.md ergänzt.
- Responsive geprüft (kein Seiten-Overflow 360–1280, Tabelle scrollt in der Karte).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192vTfGmGpoWeyUse4TuQFj
2026-06-19 10:57:14 +02:00
|
|
|
|
// Spiele zuerst — daraus leiten wir die aktuelle Wettbewerbs-ID (Tabelle) ab,
|
|
|
|
|
|
// damit eine neue Saison automatisch übernommen wird (die team_id ist stabil,
|
|
|
|
|
|
// die compound_id der Tabelle wechselt pro Saison).
|
|
|
|
|
|
$matchesData = $fetchJson("{$apiHost}/api/service/widget/v1/team/{$teamCfgId}/matches");
|
|
|
|
|
|
$apiCompound = (string) ($matchesData['data']['team']['compoundId'] ?? '');
|
2026-06-21 00:51:14 +02:00
|
|
|
|
|
|
|
|
|
|
// Saisonwechsel: liefert die API eine NEUE compoundId, archivieren wir die eben
|
|
|
|
|
|
// abgelaufene Saison einmalig (verschachteltes previous des Vorgängers wird nicht
|
|
|
|
|
|
// mitkopiert → keine unbegrenzte Schachtelung).
|
|
|
|
|
|
if ($apiCompound !== '' && $prevCompound !== '' && $apiCompound !== $prevCompound && $prevTeam !== null) {
|
|
|
|
|
|
$previous = [
|
|
|
|
|
|
'season' => $prevTeam['season'] ?? null,
|
|
|
|
|
|
'league' => $prevTeam['league'] ?? null,
|
|
|
|
|
|
'last_results' => $prevTeam['last_results'] ?? [],
|
|
|
|
|
|
'table' => $prevTeam['table'] ?? [],
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Beste bekannte compoundId: API > letzter Cache > Config-Fallback. So triggert ein
|
|
|
|
|
|
// vorübergehender Fetch-Ausfall (apiCompound leer) keinen falschen Saisonwechsel.
|
|
|
|
|
|
$compoundId = $apiCompound !== ''
|
|
|
|
|
|
? $apiCompound
|
|
|
|
|
|
: ($prevCompound !== '' ? $prevCompound : $cfgCompoundId);
|
Matchcenter: Spielplan, Ergebnisse & Tabellen (eigene Seite statt BFV-Widget)
Neue Unterseite /matchcenter ersetzt die alten BFV-Widget-Embeds (verstoßen
gegen CSP) durch eine self-hostete Lösung: Cron-Scraper holt die Daten aus der
öffentlichen BFV-Widget-JSON-API, schreibt data/matchcenter.json und lädt die
Vereinswappen lokal — Besucher kontaktieren nur unsere Domain.
- bin/matchcenter-sync.php: CLI/Cron-Scraper (Muster wie instagram-sync), JSON-API
team/{id}/matches + competition/{compoundId}/table, Wappen via getLogo lokal,
atomarer Write, Fail-safe. Auto-Saison: Tabellen-compoundId wird aus der
Matches-API abgeleitet (team_id ist saison-stabil). Per-Team-Cache-Fallback bei
Fetch-Fehlern, damit Sektionen nie leeren.
- Seite app/pages/matchcenter.php + Route + Nav-Eintrag (unter Fußball).
- Komponenten: match-slider (nächste Spiele), match-card, match-row, league-table
(mit Auf-/Abstiegszonen), matchcenter-section (kicker-Layout: Tabelle 2/3 +
Spiele 1/3), matchcenter-empty, crest (weißes Chip + Initialen-Fallback).
- carousel.js: banner.js zum generischen [data-carousel]-Antrieb verallgemeinert
(Banner- und Match-Slider teilen ihn); crest.js: Logo-Fallback im Browser.
- helpers.php: sportsevent_nodes() für SportsEvent-JSON-LD.
- Icons calendar-event; config.example + .gitignore + CLAUDE.md ergänzt.
- Responsive geprüft (kein Seiten-Overflow 360–1280, Tabelle scrollt in der Karte).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192vTfGmGpoWeyUse4TuQFj
2026-06-19 10:57:14 +02:00
|
|
|
|
$tableData = $compoundId !== '' ? $fetchJson("{$apiHost}/api/service/widget/v1/competition/{$compoundId}/table") : null;
|
|
|
|
|
|
|
|
|
|
|
|
// Name/Liga bevorzugt aus teams.json (Single Source), sonst API/alter Cache.
|
|
|
|
|
|
$apiCompetition = (string) ($matchesData['data']['team']['competitionName'] ?? '');
|
|
|
|
|
|
$name = $teamsData[$key]['name'] ?? ($matchesData['data']['team']['name'] ?? ($prevTeam['name'] ?? $key));
|
|
|
|
|
|
$league = $teamsData[$key]['league'] ?? null;
|
|
|
|
|
|
if (!$league) {
|
|
|
|
|
|
$league = $apiCompetition !== '' ? $apiCompetition : ($prevTeam['league'] ?? null);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Spiele-Teil ---
|
|
|
|
|
|
if ($matchesData !== null) {
|
|
|
|
|
|
$matches = $matchesData['data']['matches'] ?? [];
|
|
|
|
|
|
$upcoming = [];
|
|
|
|
|
|
$results = [];
|
|
|
|
|
|
foreach ($matches as $m) {
|
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.
Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.
News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.
Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
(Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.
Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
|
|
|
|
// Spielfreie Runden führt der BFV als Pseudo-Begegnung gegen
|
|
|
|
|
|
// "SPIELFREI" — kein echtes Spiel, gehört weder in "Nächste
|
|
|
|
|
|
// Spiele" noch in Ergebnisse (tauchte sonst bis in den
|
|
|
|
|
|
// Startseiten-Hero und die Stadionzeitung durch).
|
|
|
|
|
|
if (($m['homeTeamName'] ?? '') === 'SPIELFREI' || ($m['guestTeamName'] ?? '') === 'SPIELFREI') {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
Matchcenter: Spielplan, Ergebnisse & Tabellen (eigene Seite statt BFV-Widget)
Neue Unterseite /matchcenter ersetzt die alten BFV-Widget-Embeds (verstoßen
gegen CSP) durch eine self-hostete Lösung: Cron-Scraper holt die Daten aus der
öffentlichen BFV-Widget-JSON-API, schreibt data/matchcenter.json und lädt die
Vereinswappen lokal — Besucher kontaktieren nur unsere Domain.
- bin/matchcenter-sync.php: CLI/Cron-Scraper (Muster wie instagram-sync), JSON-API
team/{id}/matches + competition/{compoundId}/table, Wappen via getLogo lokal,
atomarer Write, Fail-safe. Auto-Saison: Tabellen-compoundId wird aus der
Matches-API abgeleitet (team_id ist saison-stabil). Per-Team-Cache-Fallback bei
Fetch-Fehlern, damit Sektionen nie leeren.
- Seite app/pages/matchcenter.php + Route + Nav-Eintrag (unter Fußball).
- Komponenten: match-slider (nächste Spiele), match-card, match-row, league-table
(mit Auf-/Abstiegszonen), matchcenter-section (kicker-Layout: Tabelle 2/3 +
Spiele 1/3), matchcenter-empty, crest (weißes Chip + Initialen-Fallback).
- carousel.js: banner.js zum generischen [data-carousel]-Antrieb verallgemeinert
(Banner- und Match-Slider teilen ihn); crest.js: Logo-Fallback im Browser.
- helpers.php: sportsevent_nodes() für SportsEvent-JSON-LD.
- Icons calendar-event; config.example + .gitignore + CLAUDE.md ergänzt.
- Responsive geprüft (kein Seiten-Overflow 360–1280, Tabelle scrollt in der Karte).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192vTfGmGpoWeyUse4TuQFj
2026-06-19 10:57:14 +02:00
|
|
|
|
$hasResult = trim((string) ($m['result'] ?? '')) !== '';
|
|
|
|
|
|
$row = $mapMatch($m, $teamCfgId);
|
|
|
|
|
|
if ($hasResult) {
|
|
|
|
|
|
$results[] = $row;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Ohne Ergebnis nur zeigen, wenn Anstoß heute/künftig.
|
|
|
|
|
|
$dt = $row['kickoff'] !== null ? new DateTimeImmutable($row['kickoff'], $tz) : null;
|
|
|
|
|
|
if ($dt === null || $dt->setTime(0, 0) >= $today) {
|
|
|
|
|
|
$upcoming[] = $row;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
usort($upcoming, static fn ($a, $b) => ($a['kickoff'] ?? '') <=> ($b['kickoff'] ?? ''));
|
|
|
|
|
|
usort($results, static fn ($a, $b) => ($b['kickoff'] ?? '') <=> ($a['kickoff'] ?? ''));
|
|
|
|
|
|
$upcoming = array_slice($upcoming, 0, $maxUpcoming);
|
|
|
|
|
|
$results = array_slice($results, 0, $maxResults);
|
|
|
|
|
|
|
|
|
|
|
|
// permanentId → clubId-Karte (für Wappen in der Tabelle).
|
|
|
|
|
|
$clubIdByPermanent = [];
|
|
|
|
|
|
foreach ($matches as $m) {
|
|
|
|
|
|
if (!empty($m['homeTeamPermanentId']) && !empty($m['homeClubId'])) {
|
|
|
|
|
|
$clubIdByPermanent[$m['homeTeamPermanentId']] = $m['homeClubId'];
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!empty($m['guestTeamPermanentId']) && !empty($m['guestClubId'])) {
|
|
|
|
|
|
$clubIdByPermanent[$m['guestTeamPermanentId']] = $m['guestClubId'];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Spiele-Fetch fehlgeschlagen → alten Cache dieses Teams behalten.
|
|
|
|
|
|
$upcoming = $prevTeam['next_matches'] ?? [];
|
|
|
|
|
|
$results = $prevTeam['last_results'] ?? [];
|
|
|
|
|
|
$clubIdByPermanent = [];
|
|
|
|
|
|
$cacheFallbackUsed = true;
|
|
|
|
|
|
$keepCrests($upcoming);
|
|
|
|
|
|
$keepCrests($results);
|
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.
Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.
News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.
Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
(Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.
Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
|
|
|
|
$log("Team {$key}: Spiele-Fetch fehlgeschlagen — alter Cache übernommen", 'WARN');
|
Matchcenter: Spielplan, Ergebnisse & Tabellen (eigene Seite statt BFV-Widget)
Neue Unterseite /matchcenter ersetzt die alten BFV-Widget-Embeds (verstoßen
gegen CSP) durch eine self-hostete Lösung: Cron-Scraper holt die Daten aus der
öffentlichen BFV-Widget-JSON-API, schreibt data/matchcenter.json und lädt die
Vereinswappen lokal — Besucher kontaktieren nur unsere Domain.
- bin/matchcenter-sync.php: CLI/Cron-Scraper (Muster wie instagram-sync), JSON-API
team/{id}/matches + competition/{compoundId}/table, Wappen via getLogo lokal,
atomarer Write, Fail-safe. Auto-Saison: Tabellen-compoundId wird aus der
Matches-API abgeleitet (team_id ist saison-stabil). Per-Team-Cache-Fallback bei
Fetch-Fehlern, damit Sektionen nie leeren.
- Seite app/pages/matchcenter.php + Route + Nav-Eintrag (unter Fußball).
- Komponenten: match-slider (nächste Spiele), match-card, match-row, league-table
(mit Auf-/Abstiegszonen), matchcenter-section (kicker-Layout: Tabelle 2/3 +
Spiele 1/3), matchcenter-empty, crest (weißes Chip + Initialen-Fallback).
- carousel.js: banner.js zum generischen [data-carousel]-Antrieb verallgemeinert
(Banner- und Match-Slider teilen ihn); crest.js: Logo-Fallback im Browser.
- helpers.php: sportsevent_nodes() für SportsEvent-JSON-LD.
- Icons calendar-event; config.example + .gitignore + CLAUDE.md ergänzt.
- Responsive geprüft (kein Seiten-Overflow 360–1280, Tabelle scrollt in der Karte).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192vTfGmGpoWeyUse4TuQFj
2026-06-19 10:57:14 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Tabellen-Teil ---
|
|
|
|
|
|
if ($tableData !== null) {
|
|
|
|
|
|
$tableRows = $tableData['data']['table'] ?? [];
|
|
|
|
|
|
$cfgZone = $tableData['data']['configuration'] ?? [];
|
|
|
|
|
|
$rowCount = count($tableRows);
|
|
|
|
|
|
$promo = (int) ($cfgZone['promotionTeamCount'] ?? 0);
|
|
|
|
|
|
$promoPlayoff = (int) ($cfgZone['promotionPlayoffTeamCount'] ?? 0);
|
|
|
|
|
|
$releg = (int) ($cfgZone['relegationTeamCount'] ?? 0);
|
|
|
|
|
|
$relegPlayoff = (int) ($cfgZone['relegationPlayoffTeamCount'] ?? 0);
|
|
|
|
|
|
|
|
|
|
|
|
$table = [];
|
|
|
|
|
|
foreach ($tableRows as $r) {
|
|
|
|
|
|
$pos = (int) ($r['position'] ?? 0);
|
|
|
|
|
|
$permanentId = (string) ($r['team']['permanentId'] ?? '');
|
|
|
|
|
|
$zone = null;
|
|
|
|
|
|
if ($pos >= 1 && $pos <= $promo) {
|
|
|
|
|
|
$zone = 'promotion';
|
|
|
|
|
|
} elseif ($pos <= $promo + $promoPlayoff) {
|
|
|
|
|
|
$zone = 'promotion-playoff';
|
|
|
|
|
|
} elseif ($releg > 0 && $pos > $rowCount - $releg) {
|
|
|
|
|
|
$zone = 'relegation';
|
|
|
|
|
|
} elseif ($relegPlayoff > 0 && $pos > $rowCount - $releg - $relegPlayoff) {
|
|
|
|
|
|
$zone = 'relegation-playoff';
|
|
|
|
|
|
}
|
|
|
|
|
|
$table[] = [
|
|
|
|
|
|
'rank' => $pos,
|
|
|
|
|
|
'club' => (string) ($r['team']['name'] ?? ''),
|
|
|
|
|
|
'crest' => $downloadCrest($clubIdByPermanent[$permanentId] ?? null),
|
|
|
|
|
|
'played' => (int) ($r['matches'] ?? 0),
|
|
|
|
|
|
'won' => (int) ($r['matchesWon'] ?? 0),
|
|
|
|
|
|
'drawn' => (int) ($r['matchesDrawn'] ?? 0),
|
|
|
|
|
|
'lost' => (int) ($r['matchesLost'] ?? 0),
|
|
|
|
|
|
'goal_diff' => (int) ($r['goalsDiff'] ?? 0),
|
|
|
|
|
|
'points' => (int) ($r['points'] ?? 0),
|
|
|
|
|
|
'zone' => $zone,
|
|
|
|
|
|
'is_own_club' => $permanentId === $teamCfgId,
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Tabellen-Fetch fehlgeschlagen → alte Tabelle dieses Teams behalten.
|
|
|
|
|
|
$table = $prevTeam['table'] ?? [];
|
|
|
|
|
|
$cacheFallbackUsed = true;
|
|
|
|
|
|
$keepCrests($table);
|
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.
Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.
News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.
Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
(Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.
Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
|
|
|
|
$log("Team {$key}: Tabellen-Fetch fehlgeschlagen — alte Tabelle übernommen", 'WARN');
|
Matchcenter: Spielplan, Ergebnisse & Tabellen (eigene Seite statt BFV-Widget)
Neue Unterseite /matchcenter ersetzt die alten BFV-Widget-Embeds (verstoßen
gegen CSP) durch eine self-hostete Lösung: Cron-Scraper holt die Daten aus der
öffentlichen BFV-Widget-JSON-API, schreibt data/matchcenter.json und lädt die
Vereinswappen lokal — Besucher kontaktieren nur unsere Domain.
- bin/matchcenter-sync.php: CLI/Cron-Scraper (Muster wie instagram-sync), JSON-API
team/{id}/matches + competition/{compoundId}/table, Wappen via getLogo lokal,
atomarer Write, Fail-safe. Auto-Saison: Tabellen-compoundId wird aus der
Matches-API abgeleitet (team_id ist saison-stabil). Per-Team-Cache-Fallback bei
Fetch-Fehlern, damit Sektionen nie leeren.
- Seite app/pages/matchcenter.php + Route + Nav-Eintrag (unter Fußball).
- Komponenten: match-slider (nächste Spiele), match-card, match-row, league-table
(mit Auf-/Abstiegszonen), matchcenter-section (kicker-Layout: Tabelle 2/3 +
Spiele 1/3), matchcenter-empty, crest (weißes Chip + Initialen-Fallback).
- carousel.js: banner.js zum generischen [data-carousel]-Antrieb verallgemeinert
(Banner- und Match-Slider teilen ihn); crest.js: Logo-Fallback im Browser.
- helpers.php: sportsevent_nodes() für SportsEvent-JSON-LD.
- Icons calendar-event; config.example + .gitignore + CLAUDE.md ergänzt.
- Responsive geprüft (kein Seiten-Overflow 360–1280, Tabelle scrollt in der Karte).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192vTfGmGpoWeyUse4TuQFj
2026-06-19 10:57:14 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-21 00:51:14 +02:00
|
|
|
|
// Aktuelle Saison immer zeigen, solange der Spiele-Fetch erfolgreich war (auch wenn
|
|
|
|
|
|
// sie noch leer ist) oder Daten/eine Vorsaison vorliegen. Nur überspringen, wenn der
|
|
|
|
|
|
// Fetch fehlschlug UND weder Cache noch Vorsaison existieren.
|
|
|
|
|
|
$hasCurrent = $upcoming !== [] || $results !== [] || $table !== [];
|
|
|
|
|
|
$hasPrevious = $previous !== null
|
|
|
|
|
|
&& (($previous['last_results'] ?? []) !== [] || ($previous['table'] ?? []) !== []);
|
|
|
|
|
|
if (!$hasCurrent && $matchesData === null && !$hasPrevious) {
|
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.
Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.
News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.
Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
(Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.
Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
|
|
|
|
$log("Team {$key}: keine Daten (API leer, kein Cache) — übersprungen", 'WARN');
|
Matchcenter: Spielplan, Ergebnisse & Tabellen (eigene Seite statt BFV-Widget)
Neue Unterseite /matchcenter ersetzt die alten BFV-Widget-Embeds (verstoßen
gegen CSP) durch eine self-hostete Lösung: Cron-Scraper holt die Daten aus der
öffentlichen BFV-Widget-JSON-API, schreibt data/matchcenter.json und lädt die
Vereinswappen lokal — Besucher kontaktieren nur unsere Domain.
- bin/matchcenter-sync.php: CLI/Cron-Scraper (Muster wie instagram-sync), JSON-API
team/{id}/matches + competition/{compoundId}/table, Wappen via getLogo lokal,
atomarer Write, Fail-safe. Auto-Saison: Tabellen-compoundId wird aus der
Matches-API abgeleitet (team_id ist saison-stabil). Per-Team-Cache-Fallback bei
Fetch-Fehlern, damit Sektionen nie leeren.
- Seite app/pages/matchcenter.php + Route + Nav-Eintrag (unter Fußball).
- Komponenten: match-slider (nächste Spiele), match-card, match-row, league-table
(mit Auf-/Abstiegszonen), matchcenter-section (kicker-Layout: Tabelle 2/3 +
Spiele 1/3), matchcenter-empty, crest (weißes Chip + Initialen-Fallback).
- carousel.js: banner.js zum generischen [data-carousel]-Antrieb verallgemeinert
(Banner- und Match-Slider teilen ihn); crest.js: Logo-Fallback im Browser.
- helpers.php: sportsevent_nodes() für SportsEvent-JSON-LD.
- Icons calendar-event; config.example + .gitignore + CLAUDE.md ergänzt.
- Responsive geprüft (kein Seiten-Overflow 360–1280, Tabelle scrollt in der Karte).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192vTfGmGpoWeyUse4TuQFj
2026-06-19 10:57:14 +02:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-21 00:51:14 +02:00
|
|
|
|
$season = $seasonFromRows($upcoming, $results);
|
|
|
|
|
|
|
|
|
|
|
|
$teamBlock = [
|
Matchcenter: Spielplan, Ergebnisse & Tabellen (eigene Seite statt BFV-Widget)
Neue Unterseite /matchcenter ersetzt die alten BFV-Widget-Embeds (verstoßen
gegen CSP) durch eine self-hostete Lösung: Cron-Scraper holt die Daten aus der
öffentlichen BFV-Widget-JSON-API, schreibt data/matchcenter.json und lädt die
Vereinswappen lokal — Besucher kontaktieren nur unsere Domain.
- bin/matchcenter-sync.php: CLI/Cron-Scraper (Muster wie instagram-sync), JSON-API
team/{id}/matches + competition/{compoundId}/table, Wappen via getLogo lokal,
atomarer Write, Fail-safe. Auto-Saison: Tabellen-compoundId wird aus der
Matches-API abgeleitet (team_id ist saison-stabil). Per-Team-Cache-Fallback bei
Fetch-Fehlern, damit Sektionen nie leeren.
- Seite app/pages/matchcenter.php + Route + Nav-Eintrag (unter Fußball).
- Komponenten: match-slider (nächste Spiele), match-card, match-row, league-table
(mit Auf-/Abstiegszonen), matchcenter-section (kicker-Layout: Tabelle 2/3 +
Spiele 1/3), matchcenter-empty, crest (weißes Chip + Initialen-Fallback).
- carousel.js: banner.js zum generischen [data-carousel]-Antrieb verallgemeinert
(Banner- und Match-Slider teilen ihn); crest.js: Logo-Fallback im Browser.
- helpers.php: sportsevent_nodes() für SportsEvent-JSON-LD.
- Icons calendar-event; config.example + .gitignore + CLAUDE.md ergänzt.
- Responsive geprüft (kein Seiten-Overflow 360–1280, Tabelle scrollt in der Karte).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192vTfGmGpoWeyUse4TuQFj
2026-06-19 10:57:14 +02:00
|
|
|
|
'key' => $key,
|
|
|
|
|
|
'name' => $name,
|
|
|
|
|
|
'league' => $league,
|
|
|
|
|
|
'slug' => $slugMap[$key] ?? '',
|
2026-06-21 00:51:14 +02:00
|
|
|
|
'compound_id' => $compoundId,
|
|
|
|
|
|
'season' => $season,
|
Matchcenter: Spielplan, Ergebnisse & Tabellen (eigene Seite statt BFV-Widget)
Neue Unterseite /matchcenter ersetzt die alten BFV-Widget-Embeds (verstoßen
gegen CSP) durch eine self-hostete Lösung: Cron-Scraper holt die Daten aus der
öffentlichen BFV-Widget-JSON-API, schreibt data/matchcenter.json und lädt die
Vereinswappen lokal — Besucher kontaktieren nur unsere Domain.
- bin/matchcenter-sync.php: CLI/Cron-Scraper (Muster wie instagram-sync), JSON-API
team/{id}/matches + competition/{compoundId}/table, Wappen via getLogo lokal,
atomarer Write, Fail-safe. Auto-Saison: Tabellen-compoundId wird aus der
Matches-API abgeleitet (team_id ist saison-stabil). Per-Team-Cache-Fallback bei
Fetch-Fehlern, damit Sektionen nie leeren.
- Seite app/pages/matchcenter.php + Route + Nav-Eintrag (unter Fußball).
- Komponenten: match-slider (nächste Spiele), match-card, match-row, league-table
(mit Auf-/Abstiegszonen), matchcenter-section (kicker-Layout: Tabelle 2/3 +
Spiele 1/3), matchcenter-empty, crest (weißes Chip + Initialen-Fallback).
- carousel.js: banner.js zum generischen [data-carousel]-Antrieb verallgemeinert
(Banner- und Match-Slider teilen ihn); crest.js: Logo-Fallback im Browser.
- helpers.php: sportsevent_nodes() für SportsEvent-JSON-LD.
- Icons calendar-event; config.example + .gitignore + CLAUDE.md ergänzt.
- Responsive geprüft (kein Seiten-Overflow 360–1280, Tabelle scrollt in der Karte).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192vTfGmGpoWeyUse4TuQFj
2026-06-19 10:57:14 +02:00
|
|
|
|
'next_matches' => $upcoming,
|
|
|
|
|
|
'last_results' => $results,
|
|
|
|
|
|
'table' => $table,
|
|
|
|
|
|
];
|
2026-06-21 00:51:14 +02:00
|
|
|
|
if ($hasPrevious) {
|
|
|
|
|
|
$teamBlock['previous'] = $previous;
|
|
|
|
|
|
$keepCrests($previous['last_results'] ?? []);
|
|
|
|
|
|
$keepCrests($previous['table'] ?? []);
|
|
|
|
|
|
}
|
|
|
|
|
|
$resultTeams[] = $teamBlock;
|
Matchcenter: Spielplan, Ergebnisse & Tabellen (eigene Seite statt BFV-Widget)
Neue Unterseite /matchcenter ersetzt die alten BFV-Widget-Embeds (verstoßen
gegen CSP) durch eine self-hostete Lösung: Cron-Scraper holt die Daten aus der
öffentlichen BFV-Widget-JSON-API, schreibt data/matchcenter.json und lädt die
Vereinswappen lokal — Besucher kontaktieren nur unsere Domain.
- bin/matchcenter-sync.php: CLI/Cron-Scraper (Muster wie instagram-sync), JSON-API
team/{id}/matches + competition/{compoundId}/table, Wappen via getLogo lokal,
atomarer Write, Fail-safe. Auto-Saison: Tabellen-compoundId wird aus der
Matches-API abgeleitet (team_id ist saison-stabil). Per-Team-Cache-Fallback bei
Fetch-Fehlern, damit Sektionen nie leeren.
- Seite app/pages/matchcenter.php + Route + Nav-Eintrag (unter Fußball).
- Komponenten: match-slider (nächste Spiele), match-card, match-row, league-table
(mit Auf-/Abstiegszonen), matchcenter-section (kicker-Layout: Tabelle 2/3 +
Spiele 1/3), matchcenter-empty, crest (weißes Chip + Initialen-Fallback).
- carousel.js: banner.js zum generischen [data-carousel]-Antrieb verallgemeinert
(Banner- und Match-Slider teilen ihn); crest.js: Logo-Fallback im Browser.
- helpers.php: sportsevent_nodes() für SportsEvent-JSON-LD.
- Icons calendar-event; config.example + .gitignore + CLAUDE.md ergänzt.
- Responsive geprüft (kein Seiten-Overflow 360–1280, Tabelle scrollt in der Karte).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192vTfGmGpoWeyUse4TuQFj
2026-06-19 10:57:14 +02:00
|
|
|
|
|
|
|
|
|
|
foreach ($upcoming as $u) {
|
|
|
|
|
|
$upcomingAll[] = array_merge($u, ['team_key' => $key, 'team_name' => $name]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-21 00:51:14 +02:00
|
|
|
|
$log("Team {$key}: " . count($upcoming) . ' anstehend, ' . count($results) . ' Ergebnisse, ' . count($table) . ' Tabellenplätze'
|
|
|
|
|
|
. ($hasPrevious ? ' (+ Vorsaison archiviert)' : ''));
|
Matchcenter: Spielplan, Ergebnisse & Tabellen (eigene Seite statt BFV-Widget)
Neue Unterseite /matchcenter ersetzt die alten BFV-Widget-Embeds (verstoßen
gegen CSP) durch eine self-hostete Lösung: Cron-Scraper holt die Daten aus der
öffentlichen BFV-Widget-JSON-API, schreibt data/matchcenter.json und lädt die
Vereinswappen lokal — Besucher kontaktieren nur unsere Domain.
- bin/matchcenter-sync.php: CLI/Cron-Scraper (Muster wie instagram-sync), JSON-API
team/{id}/matches + competition/{compoundId}/table, Wappen via getLogo lokal,
atomarer Write, Fail-safe. Auto-Saison: Tabellen-compoundId wird aus der
Matches-API abgeleitet (team_id ist saison-stabil). Per-Team-Cache-Fallback bei
Fetch-Fehlern, damit Sektionen nie leeren.
- Seite app/pages/matchcenter.php + Route + Nav-Eintrag (unter Fußball).
- Komponenten: match-slider (nächste Spiele), match-card, match-row, league-table
(mit Auf-/Abstiegszonen), matchcenter-section (kicker-Layout: Tabelle 2/3 +
Spiele 1/3), matchcenter-empty, crest (weißes Chip + Initialen-Fallback).
- carousel.js: banner.js zum generischen [data-carousel]-Antrieb verallgemeinert
(Banner- und Match-Slider teilen ihn); crest.js: Logo-Fallback im Browser.
- helpers.php: sportsevent_nodes() für SportsEvent-JSON-LD.
- Icons calendar-event; config.example + .gitignore + CLAUDE.md ergänzt.
- Responsive geprüft (kein Seiten-Overflow 360–1280, Tabelle scrollt in der Karte).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192vTfGmGpoWeyUse4TuQFj
2026-06-19 10:57:14 +02:00
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
|
// Ein fehlschlagendes Team darf die anderen nicht killen — alten Block behalten.
|
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.
Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.
News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.
Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
(Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.
Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
|
|
|
|
$log("Team {$key} übersprungen: " . $e->getMessage(), 'WARN');
|
Matchcenter: Spielplan, Ergebnisse & Tabellen (eigene Seite statt BFV-Widget)
Neue Unterseite /matchcenter ersetzt die alten BFV-Widget-Embeds (verstoßen
gegen CSP) durch eine self-hostete Lösung: Cron-Scraper holt die Daten aus der
öffentlichen BFV-Widget-JSON-API, schreibt data/matchcenter.json und lädt die
Vereinswappen lokal — Besucher kontaktieren nur unsere Domain.
- bin/matchcenter-sync.php: CLI/Cron-Scraper (Muster wie instagram-sync), JSON-API
team/{id}/matches + competition/{compoundId}/table, Wappen via getLogo lokal,
atomarer Write, Fail-safe. Auto-Saison: Tabellen-compoundId wird aus der
Matches-API abgeleitet (team_id ist saison-stabil). Per-Team-Cache-Fallback bei
Fetch-Fehlern, damit Sektionen nie leeren.
- Seite app/pages/matchcenter.php + Route + Nav-Eintrag (unter Fußball).
- Komponenten: match-slider (nächste Spiele), match-card, match-row, league-table
(mit Auf-/Abstiegszonen), matchcenter-section (kicker-Layout: Tabelle 2/3 +
Spiele 1/3), matchcenter-empty, crest (weißes Chip + Initialen-Fallback).
- carousel.js: banner.js zum generischen [data-carousel]-Antrieb verallgemeinert
(Banner- und Match-Slider teilen ihn); crest.js: Logo-Fallback im Browser.
- helpers.php: sportsevent_nodes() für SportsEvent-JSON-LD.
- Icons calendar-event; config.example + .gitignore + CLAUDE.md ergänzt.
- Responsive geprüft (kein Seiten-Overflow 360–1280, Tabelle scrollt in der Karte).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192vTfGmGpoWeyUse4TuQFj
2026-06-19 10:57:14 +02:00
|
|
|
|
if ($prevTeam !== null) {
|
|
|
|
|
|
$resultTeams[] = $prevTeam;
|
|
|
|
|
|
$cacheFallbackUsed = true;
|
|
|
|
|
|
$keepCrests($prevTeam['next_matches'] ?? []);
|
|
|
|
|
|
$keepCrests($prevTeam['last_results'] ?? []);
|
|
|
|
|
|
$keepCrests($prevTeam['table'] ?? []);
|
2026-06-21 00:51:14 +02:00
|
|
|
|
$keepCrests($prevTeam['previous']['last_results'] ?? []);
|
|
|
|
|
|
$keepCrests($prevTeam['previous']['table'] ?? []);
|
Matchcenter: Spielplan, Ergebnisse & Tabellen (eigene Seite statt BFV-Widget)
Neue Unterseite /matchcenter ersetzt die alten BFV-Widget-Embeds (verstoßen
gegen CSP) durch eine self-hostete Lösung: Cron-Scraper holt die Daten aus der
öffentlichen BFV-Widget-JSON-API, schreibt data/matchcenter.json und lädt die
Vereinswappen lokal — Besucher kontaktieren nur unsere Domain.
- bin/matchcenter-sync.php: CLI/Cron-Scraper (Muster wie instagram-sync), JSON-API
team/{id}/matches + competition/{compoundId}/table, Wappen via getLogo lokal,
atomarer Write, Fail-safe. Auto-Saison: Tabellen-compoundId wird aus der
Matches-API abgeleitet (team_id ist saison-stabil). Per-Team-Cache-Fallback bei
Fetch-Fehlern, damit Sektionen nie leeren.
- Seite app/pages/matchcenter.php + Route + Nav-Eintrag (unter Fußball).
- Komponenten: match-slider (nächste Spiele), match-card, match-row, league-table
(mit Auf-/Abstiegszonen), matchcenter-section (kicker-Layout: Tabelle 2/3 +
Spiele 1/3), matchcenter-empty, crest (weißes Chip + Initialen-Fallback).
- carousel.js: banner.js zum generischen [data-carousel]-Antrieb verallgemeinert
(Banner- und Match-Slider teilen ihn); crest.js: Logo-Fallback im Browser.
- helpers.php: sportsevent_nodes() für SportsEvent-JSON-LD.
- Icons calendar-event; config.example + .gitignore + CLAUDE.md ergänzt.
- Responsive geprüft (kein Seiten-Overflow 360–1280, Tabelle scrollt in der Karte).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192vTfGmGpoWeyUse4TuQFj
2026-06-19 10:57:14 +02:00
|
|
|
|
foreach ($prevTeam['next_matches'] ?? [] as $u) {
|
|
|
|
|
|
$upcomingAll[] = array_merge($u, ['team_key' => $key, 'team_name' => $prevTeam['name'] ?? $key]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ($resultTeams === []) {
|
|
|
|
|
|
$fail('Kein einziges Team lieferte Daten (API blockt evtl. die Server-IP oder Struktur geändert)');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Globaler „nächste Spiele"-Slider: teamübergreifend, nach Anstoß sortiert.
|
|
|
|
|
|
usort($upcomingAll, static fn ($a, $b) => ($a['kickoff'] ?? '') <=> ($b['kickoff'] ?? ''));
|
|
|
|
|
|
$upcomingAll = array_slice($upcomingAll, 0, $maxUpcoming);
|
|
|
|
|
|
|
|
|
|
|
|
// --- Nicht mehr referenzierte Wappen aufräumen. Nur, wenn ALLE Teams frisch
|
|
|
|
|
|
// geladen wurden (sonst könnte ein transienter Ausfall ein noch genutztes
|
|
|
|
|
|
// Wappen löschen) und die Keep-Liste nicht leer ist. ---
|
|
|
|
|
|
if (!$cacheFallbackUsed && $crestKeep !== []) {
|
|
|
|
|
|
foreach (glob("{$crestDir}/*.png") ?: [] as $existing) {
|
|
|
|
|
|
if (!isset($crestKeep[basename($existing)])) {
|
|
|
|
|
|
unlink($existing);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-21 00:51:14 +02:00
|
|
|
|
// --- Cache atomar schreiben (tmp + rename). Jeder Schritt ist abgesichert: ein
|
|
|
|
|
|
// defektes UTF-8-Zeichen aus der API (json_encode → Exception) oder ein
|
|
|
|
|
|
// Schreib-/Rename-Fehler darf den bestehenden guten Cache NICHT leerschreiben. ---
|
|
|
|
|
|
try {
|
|
|
|
|
|
$payload = json_encode([
|
|
|
|
|
|
'fetched_at' => time(),
|
|
|
|
|
|
'stale' => $cacheFallbackUsed, // true = mind. ein Teil kam aus dem alten Cache
|
|
|
|
|
|
'teams' => $resultTeams,
|
|
|
|
|
|
'upcoming' => $upcomingAll,
|
|
|
|
|
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
|
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
|
$fail('Cache-Encode fehlgeschlagen: ' . $e->getMessage());
|
|
|
|
|
|
}
|
Matchcenter: Spielplan, Ergebnisse & Tabellen (eigene Seite statt BFV-Widget)
Neue Unterseite /matchcenter ersetzt die alten BFV-Widget-Embeds (verstoßen
gegen CSP) durch eine self-hostete Lösung: Cron-Scraper holt die Daten aus der
öffentlichen BFV-Widget-JSON-API, schreibt data/matchcenter.json und lädt die
Vereinswappen lokal — Besucher kontaktieren nur unsere Domain.
- bin/matchcenter-sync.php: CLI/Cron-Scraper (Muster wie instagram-sync), JSON-API
team/{id}/matches + competition/{compoundId}/table, Wappen via getLogo lokal,
atomarer Write, Fail-safe. Auto-Saison: Tabellen-compoundId wird aus der
Matches-API abgeleitet (team_id ist saison-stabil). Per-Team-Cache-Fallback bei
Fetch-Fehlern, damit Sektionen nie leeren.
- Seite app/pages/matchcenter.php + Route + Nav-Eintrag (unter Fußball).
- Komponenten: match-slider (nächste Spiele), match-card, match-row, league-table
(mit Auf-/Abstiegszonen), matchcenter-section (kicker-Layout: Tabelle 2/3 +
Spiele 1/3), matchcenter-empty, crest (weißes Chip + Initialen-Fallback).
- carousel.js: banner.js zum generischen [data-carousel]-Antrieb verallgemeinert
(Banner- und Match-Slider teilen ihn); crest.js: Logo-Fallback im Browser.
- helpers.php: sportsevent_nodes() für SportsEvent-JSON-LD.
- Icons calendar-event; config.example + .gitignore + CLAUDE.md ergänzt.
- Responsive geprüft (kein Seiten-Overflow 360–1280, Tabelle scrollt in der Karte).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192vTfGmGpoWeyUse4TuQFj
2026-06-19 10:57:14 +02:00
|
|
|
|
|
|
|
|
|
|
$tmp = $cacheFile . '.tmp';
|
2026-06-21 00:51:14 +02:00
|
|
|
|
if (file_put_contents($tmp, $payload) === false) {
|
|
|
|
|
|
$fail("Cache-Schreiben fehlgeschlagen (tmp: {$tmp})");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!rename($tmp, $cacheFile)) {
|
|
|
|
|
|
@unlink($tmp);
|
|
|
|
|
|
$fail("Cache-Rename fehlgeschlagen ({$tmp} → {$cacheFile})");
|
|
|
|
|
|
}
|
Matchcenter: Spielplan, Ergebnisse & Tabellen (eigene Seite statt BFV-Widget)
Neue Unterseite /matchcenter ersetzt die alten BFV-Widget-Embeds (verstoßen
gegen CSP) durch eine self-hostete Lösung: Cron-Scraper holt die Daten aus der
öffentlichen BFV-Widget-JSON-API, schreibt data/matchcenter.json und lädt die
Vereinswappen lokal — Besucher kontaktieren nur unsere Domain.
- bin/matchcenter-sync.php: CLI/Cron-Scraper (Muster wie instagram-sync), JSON-API
team/{id}/matches + competition/{compoundId}/table, Wappen via getLogo lokal,
atomarer Write, Fail-safe. Auto-Saison: Tabellen-compoundId wird aus der
Matches-API abgeleitet (team_id ist saison-stabil). Per-Team-Cache-Fallback bei
Fetch-Fehlern, damit Sektionen nie leeren.
- Seite app/pages/matchcenter.php + Route + Nav-Eintrag (unter Fußball).
- Komponenten: match-slider (nächste Spiele), match-card, match-row, league-table
(mit Auf-/Abstiegszonen), matchcenter-section (kicker-Layout: Tabelle 2/3 +
Spiele 1/3), matchcenter-empty, crest (weißes Chip + Initialen-Fallback).
- carousel.js: banner.js zum generischen [data-carousel]-Antrieb verallgemeinert
(Banner- und Match-Slider teilen ihn); crest.js: Logo-Fallback im Browser.
- helpers.php: sportsevent_nodes() für SportsEvent-JSON-LD.
- Icons calendar-event; config.example + .gitignore + CLAUDE.md ergänzt.
- Responsive geprüft (kein Seiten-Overflow 360–1280, Tabelle scrollt in der Karte).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192vTfGmGpoWeyUse4TuQFj
2026-06-19 10:57:14 +02:00
|
|
|
|
|
|
|
|
|
|
$log('OK: ' . count($resultTeams) . ' Teams gecached, ' . count($crestKeep) . ' Wappen lokal in assets/img/crests/');
|