Sportfest-Ausgabe der Stadionzeitung zum Heimspiel gegen ATS Wartenfels

Neue Live-Seiten, geschaltet vom chat-gepflegten Feld `veranstaltung` (Slug aus
data/veranstaltungen.json) — Programm und Turnierpläne kommen damit aus derselben
Quelle wie die Website, nicht als Kopie ins Heft:

- Programm-Seite (programm[], rendert zugleich auf /veranstaltungen/sportfest-2026)
- vier Turnierplan-Seiten (turniere[]) mit leerer Ergebnisspalte und leerer
  Abschlusstabelle zum Eintragen am Platz
- Gegnerseite je Mannschaft mit Heimspiel am Fest statt nur der tragenden:
  stadionzeitung_gegner_fixtures() ist die einzige Stelle, die das entscheidet,
  Seitenplanung und Snapshot lesen beide dort. `gegner` ist deshalb eine Map je
  Team-Key; die Vorschau überspringt jedes Spiel mit eigener Gegnerseite
- Danke-Seite mit Partner-Logo-Wand aus data/partners.json und QR auf
  /partner-werden, dazu das Anzeigen-Feld `platzierung` für Motive mit festem
  Platz auf einer Live-Seite (Emons über der Logo-Wand). Vorher klebte der Dank
  als leere Fläche unten an einem Anzeigen-Bild
- Cover einer Sonderausgabe: das Fest als Titelgeschichte statt einer der drei
  Begegnungen, dazu das Hauptplakat schräg gelegt an Stelle des Spielerfotos

Ausgabe 2026-09-04-ats-wartenfels als ENTWURF angelegt, 48 Seiten (12 Bögen),
Tabellen auf gerader Seite, längster Anzeigenblock zwei. Veröffentlicht wird auf
dem Server, dort liegt der frische Matchcenter-Stand.

Nebenbei behoben: bin/anzeige-add.php räumte beim Anlegen alle Motive weg, deren
Slug mit demselben Text beginnt (`emons` löschte `emons-partnerdank` und
`emons-stuebinger`). Muster jetzt auf die Breiten-Endung eingeschränkt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KubsajzuV6N6QsSd3xKNjC
This commit is contained in:
2026-09-03 10:57:52 +02:00
parent b4d2cde149
commit f2f193b223
40 changed files with 2273 additions and 77 deletions

View File

@@ -123,7 +123,11 @@ foreach ($sources as $src) {
// Alte Varianten dieses Slugs weg, sonst bleiben bei einem kleineren Motiv
// Breiten von vorher liegen und die JSON verspricht Dateien, die zum Bild
// nicht mehr passen.
foreach (glob("{$destDir}/{$slug}-*.jpg") ?: [] as $alt) {
// Muster bewusst auf die BREITEN-Endung eingeschränkt (`-<zahl>.jpg`): mit
// `{$slug}-*.jpg` löscht ein Slug die Motive jedes Slugs, der ihn als Präfix
// hat — `emons` räumte so die Dateien von `emons-partnerdank` mit weg
// (03.09.2026 beobachtet, dort zufällig gewollt, generell Datenverlust).
foreach (glob("{$destDir}/{$slug}-[0-9]*.jpg") ?: [] as $alt) {
unlink($alt);
}

View File

@@ -326,6 +326,41 @@ if (is_file($szFile)) {
}
}
// --- Stadionzeitung ↔ Veranstaltung (Sportfest-Ausgabe) ---
// Eine Ausgabe mit `veranstaltung`-Feld zieht Programm, Turnierpläne und die
// Gegnerseiten je Heimspiel aus data/veranstaltungen.json. Ein Tippfehler im Slug
// oder im Turnier-Key rendert dort eine LEERE A5-Seite — ohne Fehlermeldung, weil
// die Komponenten bewusst still zurückkehren. Deshalb hier laut.
if (is_file($szFile)) {
$szData = $szData ?? (json_decode((string) file_get_contents($szFile), true) ?: []);
$vBySlug = array_column($veranstaltungRows, null, 'slug');
foreach ($szData['issues'] ?? [] as $ausgabe) {
$vSlug = (string) ($ausgabe['veranstaltung'] ?? '');
if ($vSlug === '') {
continue;
}
if (!isset($vBySlug[$vSlug])) {
$errors[] = "Stadionzeitung '{$ausgabe['slug']}': veranstaltung '{$vSlug}' fehlt in data/veranstaltungen.json.";
continue;
}
$turniere = $vBySlug[$vSlug]['turniere'] ?? [];
foreach ($turniere as $i => $turnier) {
foreach (['key', 'title', 'date', 'teams', 'spiele'] as $feld) {
if (empty($turnier[$feld])) {
$warnings[] = "data/veranstaltungen.json, '{$vSlug}' → turniere[{$i}]: Feld '{$feld}' fehlt oder ist leer.";
}
}
}
$keys = array_column($turniere, 'key');
foreach ($ausgabe['images'] ?? [] as $seite) {
if (($seite['type'] ?? '') === 'live' && ($seite['page'] ?? '') === 'turnier'
&& !in_array((string) ($seite['turnier'] ?? ''), $keys, true)) {
$errors[] = "Stadionzeitung '{$ausgabe['slug']}': Turnierseite '" . ($seite['turnier'] ?? '') . "' hat keinen Eintrag in turniere[] von '{$vSlug}'.";
}
}
}
}
// --- Inline-Styles (die CSP verwirft sie stillschweigend) ---
// Diese Prüfung existiert wegen eines echten Ausfalls am 31.07.2026: der
// Banner-Slider setzte sein Hintergrundbild per style="--banner-bg: url(…)". Auf

View File

@@ -58,6 +58,16 @@ declare(strict_types=1);
* Interview → Aufruf. Für die Aufruf-Seite erzeugt das Skript zusätzlich den
* QR (qr-aufruf.png im Ausgaben-Ordner) aus `aufruf.link`.
*
* Sportfest-Ausgabe (03.09.2026): trägt die Ausgabe ein `veranstaltung`-Feld
* (Slug aus data/veranstaltungen.json, chat-gepflegt), kommen dazu: die
* Programm-Seite im Aufschlag (aus programm[] der Veranstaltung, dieselbe Liste
* wie die Website), ein Block Turnierplan-Seiten (je Eintrag in turniere[] eine
* Seite) direkt danach, und eine Gegnerseite je Mannschaft mit HEIMspiel im
* Zeitraum der Veranstaltung statt nur für die tragende Mannschaft — welche das
* sind, sagt stadionzeitung_gegner_fixtures() (helpers.php), dieselbe Funktion,
* aus der der Snapshot die Daten baut. `cover` ({edition, teaser}) kennzeichnet
* das Cover als Sonderausgabe. Beide Felder überleben erneute Läufe wie `vorwort`.
*
* Erneuter Aufruf mit demselben Slug ersetzt die Ausgabe vollständig (alte
* Seiten-Bilder werden vorher gelöscht) — idempotent für Korrekturen.
*/
@@ -188,6 +198,19 @@ $oldVorwort = $oldIssue['vorwort'] ?? null;
$oldInterview = $oldIssue['interview'] ?? null;
// Aufruf-Seite (z. B. Sportfest-Helferaufruf) ebenso chat-gepflegt.
$oldAufruf = $oldIssue['aufruf'] ?? null;
// Sportfest-Ausgabe: Slug-Verweis auf data/veranstaltungen.json (Programm,
// Turnierpläne, Gegnerseiten je Heimspiel) und Cover-Kennung — chat-gepflegt.
$oldVeranstaltung = $oldIssue['veranstaltung'] ?? null;
$oldCover = $oldIssue['cover'] ?? null;
$veranstaltung = null;
if (is_string($oldVeranstaltung) && $oldVeranstaltung !== '') {
$veranstaltung = veranstaltung_find($oldVeranstaltung);
if ($veranstaltung === null) {
$log("veranstaltung '{$oldVeranstaltung}' steht nicht in data/veranstaltungen.json — Programm-, Turnier- und Zusatz-Gegnerseiten entfallen.", 'WARN');
} else {
$log("Sportfest-Ausgabe: Veranstaltung '{$oldVeranstaltung}' (" . count($veranstaltung['programm'] ?? []) . ' Programmtage, ' . count($veranstaltung['turniere'] ?? []) . ' Turniere).');
}
}
// --- Ziel-Ordner vorbereiten: bei erneutem Lauf alte Seiten dieser Ausgabe wegräumen ---
$destDir = PUBLIC_PATH . "/assets/img/stadionzeitung/{$slug}";
@@ -215,6 +238,28 @@ $widths = [480, 960, 1440];
// erscheint oft. Gepflegt wird der Bestand über bin/anzeige-add.php (Bilder) und
// im Chat (Sponsorenname, Reihenfolge, aktiv). Hier wird nichts gerendert, die
// Bilder liegen schon. ---
// Anzeigen mit `platzierung` haben einen FESTEN Platz auf einer Live-Seite
// (derzeit `danke`: die Emons-Anzeige über der Partner-Logo-Wand) und laufen
// deshalb nicht im Streu-Pool mit — sonst stünden sie zweimal im Heft.
$platzierteAnzeigen = [];
foreach (json_load('anzeigen')['anzeigen'] ?? [] as $a) {
$platz = trim((string) ($a['platzierung'] ?? ''));
if ($platz === '' || ($a['aktiv'] ?? true) === false || empty($a['base'])) {
continue;
}
if (!empty($a['bis']) && (string) $a['bis'] < $date) {
continue;
}
if (isset($platzierteAnzeigen[$platz])) {
continue;
}
$platzierteAnzeigen[$platz] = [
'base' => (string) $a['base'],
'widths' => $a['widths'] ?? [],
'alt' => (string) ($a['alt'] ?? ('Anzeige: ' . trim((string) ($a['sponsor'] ?? '')))),
];
}
$anzeigenSeiten = array_values(array_map(
static function (array $a): array {
$sponsor = trim((string) ($a['sponsor'] ?? ''));
@@ -237,6 +282,7 @@ $anzeigenSeiten = array_values(array_map(
// nächsten Handgriff im Heft zu bleiben.
static fn (array $a): bool => ($a['aktiv'] ?? true)
&& !empty($a['base'])
&& empty($a['platzierung'])
&& (empty($a['bis']) || (string) $a['bis'] >= $date)
)
));
@@ -245,12 +291,8 @@ $anzeigenSeiten = array_values(array_map(
// Ausgaben-Bilder (Vorwort-Fallback, Rückseite) schon. Beide bekommen SPRECHENDE
// Dateinamen statt der früheren Positionsnummern (page-07) — die verschoben sich
// bei jeder Umverteilung. ---
$live = static function (string $page, ?string $team = null): array {
$entry = ['type' => 'live', 'page' => $page];
if ($team !== null) {
$entry['team'] = $team;
}
return $entry;
$live = static function (string $page, array $extra = []): array {
return ['type' => 'live', 'page' => $page] + $extra;
};
$bild = static function (string $src, string $name, string $alt) use ($destDir, $widths, $slug): array {
@@ -293,6 +335,11 @@ try {
} elseif ($vorwort !== null) {
$aufschlag[] = $bild($vorwort, 'vorwort', 'Vorwort der Stadionzeitung');
}
// Programm-Seite der Sportfest-Ausgabe direkt hinter dem Vorwort: aufgeschlagen
// liegen Vorwort (links) und Programm (rechts) als Doppelseite 2/3 nebeneinander.
if ($veranstaltung !== null && !empty($veranstaltung['programm'])) {
$aufschlag[] = $live('programm');
}
// Interview-Seite nur, wenn die Ausgabe ein Interview trägt (chat-gepflegtes
// Feld) — das Cover zeigt dann automatisch die Teaser-Zeile dazu.
if ($oldInterview !== null) {
@@ -308,8 +355,40 @@ try {
$bloecke[] = ['name' => 'aufschlag', 'seiten' => $aufschlag, 'anzeigen_danach' => true];
}
// Turnierplan-Seiten der Sportfest-Ausgabe: je Turnier eine Seite, EIN Block
// (zusammenhängender Sportfest-Teil, keine Anzeige zwischen zwei Turnieren),
// gleich nach dem Aufschlag und vor dem Fußball-Teil.
$turniere = [];
foreach ($veranstaltung['turniere'] ?? [] as $turnier) {
if (is_array($turnier) && !empty($turnier['key'])) {
$turniere[] = $live('turnier', ['turnier' => (string) $turnier['key']]);
}
}
if ($turniere !== []) {
$bloecke[] = ['name' => 'turniere', 'seiten' => $turniere, 'anzeigen_danach' => true];
}
$bloecke[] = ['name' => 'ergebnisse', 'seiten' => [$live('ergebnisse')], 'anzeigen_danach' => true];
$bloecke[] = ['name' => 'gegner', 'seiten' => [$live('gegner')], 'anzeigen_danach' => true];
// Gegnerseite(n): normal nur die tragende Mannschaft; in der Sportfest-Ausgabe
// je Mannschaft mit Heimspiel am Fest eine (drei Heimspiele, drei Seiten), als
// EIN Block. Welche das sind, entscheidet stadionzeitung_gegner_fixtures() —
// dieselbe Funktion, aus der stadionzeitung_build_snapshot() die Daten baut,
// damit nie eine Seite ohne Daten oder Daten ohne Seite entstehen.
$gegnerSeiten = [];
foreach (stadionzeitung_gegner_fixtures([
'date' => $date, 'team_key' => $teamKey, 'opponent' => $opponent,
'veranstaltung' => $oldVeranstaltung,
]) as $gegnerTeam => $fixture) {
$gegnerSeiten[] = $live('gegner', ['team' => (string) $gegnerTeam]);
$log("Gegnerseite {$gegnerTeam}: {$fixture['opponent']} ({$fixture['date']}).");
}
if ($gegnerSeiten === []) {
// Ohne Treffer (Gegner leer) bleibt die Seite der tragenden Mannschaft —
// sie meldet dann selbst, dass keine Vergleichsdaten vorliegen.
$gegnerSeiten[] = $live('gegner', ['team' => $teamKey]);
}
$bloecke[] = ['name' => 'gegner', 'seiten' => $gegnerSeiten, 'anzeigen_danach' => true];
// Eine Tabellen-Seite je Mannschaft, Reihenfolge wie in matchcenter.json
// (= config 'matchcenter.teams'). Kommt eine Mannschaft dazu (z. B. die
@@ -317,15 +396,28 @@ try {
$tabellen = [];
foreach ($mc['teams'] ?? [] as $team) {
if (!empty($team['key'])) {
$tabellen[] = $live('tabelle', (string) $team['key']);
$tabellen[] = $live('tabelle', ['team' => (string) $team['key']]);
}
}
if ($tabellen !== []) {
$bloecke[] = ['name' => 'tabellen', 'seiten' => $tabellen, 'anzeigen_danach' => true];
}
foreach (['vorschau', 'termine', 'news', 'kontakt', 'mitglied', 'historie', 'sportheim', 'partner', 'momente'] as $seite) {
$bloecke[] = ['name' => $seite, 'seiten' => [$live($seite)], 'anzeigen_danach' => true];
foreach (['vorschau', 'termine', 'news', 'kontakt', 'mitglied', 'historie', 'sportheim', 'partner', 'danke', 'momente'] as $seite) {
// Die Danke-Seite trägt ihr Anzeigenmotiv im Seiteneintrag (siehe oben),
// direkt hinter der Partner-werden-Seite: aufgeschlagen liegen damit
// Akquise und Dank als Partner-Doppelseite nebeneinander.
$extra = ($seite === 'danke' && isset($platzierteAnzeigen['danke']))
? ['anzeige' => $platzierteAnzeigen['danke']]
: [];
// Hinter der Partner-werden-Seite bleibt die Lücke leer, damit die
// Danke-Seite direkt folgt: Akquise und Dank gehören zusammen und
// liegen aufgeschlagen im günstigen Fall als Doppelseite nebeneinander.
$bloecke[] = [
'name' => $seite,
'seiten' => [$live($seite, $extra)],
'anzeigen_danach' => $seite !== 'partner',
];
}
$bloecke[] = ['name' => 'impressum', 'seiten' => [$live('impressum')], 'anzeigen_danach' => false];
@@ -651,6 +743,8 @@ $issues[] = [
'vorwort' => $oldVorwort,
'interview' => $oldInterview,
'aufruf' => $oldAufruf,
'veranstaltung' => $oldVeranstaltung,
'cover' => $oldCover,
'verfasser_photo' => $verfasserPhoto,
'termine_snapshot' => $termineSnapshot,
'images' => $images,