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
This commit is contained in:
@@ -3,14 +3,18 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Logrotation für storage/logs/*.log — NUR CLI, per Cron 1×/Woche.
|
||||
* Logpflege für storage/logs/ — NUR CLI, per Cron 1×/Woche.
|
||||
*
|
||||
* Wird eine Datei größer als das Limit, wandert sie nach <name>.log.1 (eine
|
||||
* vorhandene .1 wird ersetzt) und das aktive Log startet leer. Damit bleiben
|
||||
* höchstens zwei Generationen pro Log liegen — genug zum Nachsehen, ohne dass
|
||||
* spam.log oder php-errors.log auf dem Shared Hosting unbegrenzt wachsen.
|
||||
* Zwei Aufgaben:
|
||||
* 1. Rotation nach Größe: wird eine Datei größer als das Limit, wandert sie nach
|
||||
* <name>.log.1 (eine vorhandene .1 wird ersetzt) und das aktive Log startet leer.
|
||||
* Es bleiben also höchstens zwei Generationen pro Log liegen.
|
||||
* 2. Aufbewahrungsfrist: Einträge, die älter als --keep-days sind, werden aus den
|
||||
* aktiven Logs entfernt; rotierte .1-Dateien jenseits der Frist gelöscht. Damit
|
||||
* gibt es eine echte Löschfrist statt nur einer Größengrenze — auf einer ruhigen
|
||||
* Vereinsseite läge ein Eintrag sonst jahrelang herum.
|
||||
*
|
||||
* Aufruf: php bin/log-rotate.php [--max-mb=2]
|
||||
* Aufruf: php bin/log-rotate.php [--max-mb=2] [--keep-days=90] [-v]
|
||||
*/
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
exit(1);
|
||||
@@ -19,30 +23,108 @@ if (PHP_SAPI !== 'cli') {
|
||||
require dirname(__DIR__) . '/app/bootstrap.php';
|
||||
|
||||
$maxMb = 2.0;
|
||||
$keepDays = 90;
|
||||
$verbose = in_array('-v', $argv, true);
|
||||
foreach ($argv as $arg) {
|
||||
if (preg_match('/^--max-mb=([0-9.]+)$/', $arg, $m) === 1) {
|
||||
$maxMb = (float) $m[1];
|
||||
} elseif (preg_match('/^--keep-days=(\d+)$/', $arg, $m) === 1) {
|
||||
$keepDays = max(1, (int) $m[1]);
|
||||
}
|
||||
}
|
||||
$limit = (int) ($maxMb * 1024 * 1024);
|
||||
$cutoff = time() - $keepDays * 86400;
|
||||
$logDir = STORAGE_PATH . '/logs';
|
||||
|
||||
/**
|
||||
* Zeitstempel am Zeilenanfang: `[ISO]` (log_write), `[27-Jul-2026 08:11:45 UTC]`
|
||||
* (PHP selbst) oder ein nackter ISO-Stempel. Null = keiner.
|
||||
*/
|
||||
$stampOf = static function (string $line): ?int {
|
||||
if (preg_match('/^\[([^\]]+)\]/', $line, $m) === 1 || preg_match('/^(\d{4}-\d{2}-\d{2}\S*)/', $line, $m) === 1) {
|
||||
$ts = strtotime($m[1]);
|
||||
return $ts === false ? null : $ts;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Einträge vor $cutoff entfernen. Zeilen ohne eigenen Zeitstempel (z. B. PHP-Stacktraces)
|
||||
* gehören zum vorherigen Eintrag und teilen dessen Schicksal. Geschrieben wird über eine
|
||||
* temporäre Datei + rename; ein Append genau in diesem Moment ginge verloren — bei einem
|
||||
* wöchentlichen Lauf nachts ein akzeptables Risiko, dafür bleibt die Datei immer konsistent.
|
||||
*/
|
||||
$prune = static function (string $file) use ($cutoff, $stampOf): int {
|
||||
$raw = (string) @file_get_contents($file);
|
||||
if ($raw === '') {
|
||||
return 0;
|
||||
}
|
||||
$keep = [];
|
||||
$dropping = false;
|
||||
$removed = 0;
|
||||
foreach (explode("\n", $raw) as $line) {
|
||||
$ts = $stampOf($line);
|
||||
if ($ts !== null) {
|
||||
$dropping = $ts < $cutoff;
|
||||
}
|
||||
if ($dropping) {
|
||||
if (trim($line) !== '') {
|
||||
$removed++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$keep[] = $line;
|
||||
}
|
||||
if ($removed === 0) {
|
||||
return 0;
|
||||
}
|
||||
$tmp = $file . '.tmp';
|
||||
if (@file_put_contents($tmp, implode("\n", $keep)) === false || !@rename($tmp, $file)) {
|
||||
@unlink($tmp);
|
||||
return 0;
|
||||
}
|
||||
@chmod($file, 0664);
|
||||
return $removed;
|
||||
};
|
||||
|
||||
$rotated = 0;
|
||||
foreach (glob(STORAGE_PATH . '/logs/*.log') ?: [] as $log) {
|
||||
$pruned = 0;
|
||||
$deleted = 0;
|
||||
|
||||
foreach (glob($logDir . '/*.log') ?: [] as $log) {
|
||||
$name = basename($log);
|
||||
|
||||
// 1. Alte Einträge raus (vor der Größenprüfung — spart oft die Rotation).
|
||||
$removed = $prune($log);
|
||||
if ($removed > 0) {
|
||||
$pruned += $removed;
|
||||
if ($verbose) {
|
||||
echo "gekürzt: {$name} ({$removed} Einträge älter als {$keepDays} Tage)\n";
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Rotation nach Größe.
|
||||
$size = (int) @filesize($log);
|
||||
if ($size <= $limit) {
|
||||
continue;
|
||||
if ($size > $limit) {
|
||||
if (@rename($log, $log . '.1')) {
|
||||
@touch($log);
|
||||
@chmod($log, 0664);
|
||||
echo "rotiert: {$name} (" . round($size / 1048576, 1) . " MB)\n";
|
||||
$rotated++;
|
||||
} else {
|
||||
fwrite(STDERR, "Rotation fehlgeschlagen: {$name}\n");
|
||||
}
|
||||
}
|
||||
if (!@rename($log, $log . '.1')) {
|
||||
fwrite(STDERR, 'Rotation fehlgeschlagen: ' . basename($log) . "\n");
|
||||
continue;
|
||||
}
|
||||
// Neue, leere Datei mit denselben Rechten anlegen, damit PHP/Cron weiterschreiben können.
|
||||
@touch($log);
|
||||
@chmod($log, 0664);
|
||||
echo 'rotiert: ' . basename($log) . ' (' . round($size / 1048576, 1) . " MB)\n";
|
||||
$rotated++;
|
||||
}
|
||||
|
||||
echo $rotated === 0
|
||||
? "Nichts zu rotieren (alle Logs unter {$maxMb} MB).\n"
|
||||
: "{$rotated} Log(s) rotiert.\n";
|
||||
// 3. Rotierte Generationen jenseits der Frist löschen.
|
||||
foreach (glob($logDir . '/*.log.1') ?: [] as $old) {
|
||||
if ((int) @filemtime($old) < $cutoff && @unlink($old)) {
|
||||
echo 'gelöscht: ' . basename($old) . " (älter als {$keepDays} Tage)\n";
|
||||
$deleted++;
|
||||
}
|
||||
}
|
||||
|
||||
echo $rotated === 0 && $pruned === 0 && $deleted === 0
|
||||
? "Nichts zu tun (alle Logs unter {$maxMb} MB, keine Einträge älter als {$keepDays} Tage).\n"
|
||||
: "Fertig: {$rotated} rotiert, {$pruned} alte Einträge entfernt, {$deleted} Altdatei(en) gelöscht.\n";
|
||||
|
||||
Reference in New Issue
Block a user