* php bin/stadionzeitung-publish.php --entwurf (zurückziehen: * Status wieder auf Entwurf, Snapshot wird verworfen — die Seiten * rechnen wieder live) * * Sinnvoller Zeitpunkt: am Spieltagmorgen NACH dem letzten Matchcenter-Sync, * dann steht der aktuelle Tabellenstand im Heft. */ if (PHP_SAPI !== 'cli') { exit(1); } require dirname(__DIR__) . '/app/bootstrap.php'; $slug = trim((string) ($argv[1] ?? '')); $unpublish = in_array('--entwurf', $argv, true); if ($slug === '' || str_starts_with($slug, '--')) { fwrite(STDERR, "Nutzung: php bin/stadionzeitung-publish.php [--entwurf]\n"); exit(1); } $log = static function (string $msg, string $level = 'INFO'): void { log_write('stadionzeitung', $level, $msg); echo '[' . date('c') . "] {$level} {$msg}\n"; }; /** * Bilder, auf die der Snapshot zeigt, in den Ausgaben-Ordner kopieren und die Pfade * darin umschreiben. Damit ist eine veröffentlichte Ausgabe wirklich eingefroren. * * Warum das nötig ist: der Snapshot friert die DATEN ein, verwies aber weiter auf * Ordner, die die Sync-Skripte verwalten. bin/matchcenter-sync.php räumt Wappen weg, * die der BFV nicht mehr führt, bin/instagram-sync.php ältere Posts — am 31.07.2026 * nachgewiesen: ein Lauf löschte 18 Wappen und 3 Instagram-Bilder. Bei den Wappen * dauert das eine Saison, bei Instagram Wochen. Danach zeigte eine archivierte * Ausgabe Lücken, und „gedruckt = fixiert" wäre nur die halbe Wahrheit. * * img/news wird mit eingefroren, obwohl News-Bilder nicht maschinenverwaltet sind: * ein Artikelbild kann im Chat ersetzt werden, und dann änderte sich nachträglich * eine gedruckte Ausgabe. Dieselbe Begründung, derselbe Handgriff. * * Fehlt eine Quelldatei (schon weggeräumt), bleibt der Pfad stehen und es gibt eine * WARN — die crest-Komponente fällt auf Initialen zurück, ein fehlendes Bild darf * das Veröffentlichen nicht verhindern. */ $freezeAssets = static function (array $snapshot, string $slug, callable $log): array { $quellen = ['img/crests/', 'img/instagram/', 'img/news/']; // Präfix je Quellordner, damit zwei gleichnamige Dateien sich nicht überschreiben // und man im Ausgaben-Ordner sofort sieht, woher eine Datei kommt. $praefix = ['img/crests/' => 'fix-wappen-', 'img/instagram/' => 'fix-insta-', 'img/news/' => 'fix-news-']; $zielDir = PUBLIC_PATH . '/assets/img/stadionzeitung/' . $slug; if (!is_dir($zielDir)) { mkdir($zielDir, 0755, true); } $kopiert = []; $fehlend = []; $walk = static function (&$node) use (&$walk, $quellen, $praefix, $zielDir, $slug, &$kopiert, &$fehlend): void { if (is_array($node)) { // Varianten-Set {base, widths, ext?} zuerst: `base` ist KEIN Dateipfad, // sondern ein Stamm ohne Endung — jede Breite ist eine eigene Datei. // Ohne diesen Zweig lief img/news/neue-website als „Quelldatei fehlt" auf. if (isset($node['base'], $node['widths']) && is_string($node['base']) && is_array($node['widths'])) { foreach ($quellen as $ordner) { if (!str_starts_with($node['base'], $ordner)) { continue; } $ext = strtolower((string) ($node['ext'] ?? 'jpg')); $neuStamm = $praefix[$ordner] . basename($node['base']); $alleDa = true; foreach ($node['widths'] as $w) { $von = PUBLIC_PATH . "/assets/{$node['base']}-{$w}.{$ext}"; if (!is_file($von)) { $fehlend["{$node['base']}-{$w}.{$ext}"] = true; $alleDa = false; continue; } copy($von, "{$zielDir}/{$neuStamm}-{$w}.{$ext}"); $kopiert["{$node['base']}-{$w}.{$ext}"] = true; } // Nur umschreiben, wenn ALLE Breiten kopiert sind — ein halbes // srcset wäre schlimmer als der alte Pfad. if ($alleDa) { $node['base'] = "img/stadionzeitung/{$slug}/{$neuStamm}"; } return; } } foreach ($node as &$wert) { $walk($wert); } return; } if (!is_string($node)) { return; } foreach ($quellen as $ordner) { if (!str_starts_with($node, $ordner)) { continue; } $datei = basename($node); $neu = $praefix[$ordner] . $datei; $ziel = "{$zielDir}/{$neu}"; if (!isset($kopiert[$node])) { $von = PUBLIC_PATH . '/assets/' . $node; if (!is_file($von)) { $fehlend[$node] = true; return; // Pfad unverändert lassen — besser ein Fallback als ein toter Link } copy($von, $ziel); $kopiert[$node] = true; } $node = "img/stadionzeitung/{$slug}/{$neu}"; return; } }; $walk($snapshot); $log(sprintf( 'Bilder eingefroren: %d Dateien in img/stadionzeitung/%s/ kopiert%s.', count($kopiert), $slug, $fehlend === [] ? '' : ', ' . count($fehlend) . ' Quelldatei(en) fehlten schon' )); foreach (array_keys($fehlend) as $f) { $log("Quelldatei für den Snapshot fehlt, Pfad unverändert: {$f}", 'WARN'); } return $snapshot; }; $cacheFile = DATA_PATH . '/stadionzeitung.json'; $data = is_file($cacheFile) ? (json_decode((string) file_get_contents($cacheFile), true) ?: ['issues' => []]) : ['issues' => []]; $found = false; foreach ($data['issues'] as &$issue) { if (($issue['slug'] ?? '') !== $slug) { continue; } $found = true; if ($unpublish) { $issue['status'] = 'entwurf'; unset($issue['snapshot']); // Die eingefrorenen Bilder gehören zum Snapshot und gehen mit ihm. Sonst // lägen sie als Waisen im Ausgaben-Ordner und ein erneutes Veröffentlichen // legte daneben eine zweite Fassung ab. $weg = 0; foreach (glob(PUBLIC_PATH . "/assets/img/stadionzeitung/{$slug}/fix-*") ?: [] as $f) { if (is_file($f) && unlink($f)) { $weg++; } } $log("Ausgabe '{$slug}' zurückgezogen — Status Entwurf, Snapshot verworfen, {$weg} eingefrorene Bilder entfernt (Live-Seiten rechnen wieder frisch)."); break; } if (($issue['status'] ?? 'veroeffentlicht') === 'veroeffentlicht' && isset($issue['snapshot'])) { $log("Ausgabe '{$slug}' ist bereits veröffentlicht — nichts zu tun. Zum Neu-Einfrieren erst mit --entwurf zurückziehen.", 'WARN'); exit(0); } $snapshot = stadionzeitung_build_snapshot($issue); // Erst die Bilder sichern, dann den Snapshot ablegen: die Pfade darin zeigen // danach in den Ausgaben-Ordner und sind vom Sync unabhängig. $snapshot = $freezeAssets($snapshot, $slug, $log); $issue['snapshot'] = $snapshot; $issue['status'] = 'veroeffentlicht'; $issue['published_at'] = date('c'); $tables = implode(', ', array_keys($snapshot['tabellen'] ?? [])); $log("Ausgabe '{$slug}' veröffentlicht — eingefroren: Tabellen [{$tables}], " . count($snapshot['ergebnisse'] ?? []) . ' Ergebnis-Blöcke, ' . count($snapshot['vorschau'] ?? []) . ' Vorschau-Spiele, ' . count($snapshot['news'] ?? []) . ' News.'); break; } unset($issue); if (!$found) { $known = implode(', ', array_column($data['issues'], 'slug')); fwrite(STDERR, "Unbekannter Slug '{$slug}'. Bekannt: {$known}\n"); exit(1); } $payload = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); $tmp = $cacheFile . '.tmp'; file_put_contents($tmp, $payload); rename($tmp, $cacheFile); echo $unpublish ? "Fertig: /stadionzeitung/{$slug} ist wieder ein Entwurf.\n" : "Fertig: /stadionzeitung/{$slug} ist veröffentlicht und eingefroren.\n";