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:
2026-07-31 14:55:27 +02:00
parent 3fb480dd78
commit a101b0c3ec
861 changed files with 75244 additions and 487 deletions

View File

@@ -7,7 +7,26 @@
* config.php ist gitignored und liegt außerhalb des Webroots (zusätzlich per .htaccess gesperrt).
*
* ---------------------------------------------------------------------------
* CRONJOBS (im KAS anlegen, Typ „eigener Cronjob“ / Shell)
* TAKT DER SYNC-SKRIPTE — zwei Wege, je nach Tarif
* ---------------------------------------------------------------------------
* A) Der Tarif hat Cronjobs → Shell-Cronjobs unten verwenden. Sauberster Weg,
* kein Endpoint, kein Dritter im Spiel.
* B) Der Tarif hat KEINE Cronjobs (Stand 27.07.2026 bei uns der Fall, KAS meldet
* „in deinem Tarif nicht verfügbar“) → public/cron.php + externer Cron-Dienst.
* Dann `cron.key` unten setzen und beim Dienst diese URLs im gewünschten Takt
* aufrufen lassen (Schlüssel besser als Header X-Cron-Key als in der URL):
*
* https://www.tsv08kulmbach.de/cron.php?job=matchcenter
* https://www.tsv08kulmbach.de/cron.php?job=instagram
*
* Takt wie unten bei den Cronjobs. Der Dienst muss keine Antwort auswerten:
* 204 = angenommen oder wegen Mindestabstand übersprungen, 403 = Schlüssel oder
* Job falsch (dann ist der Dienst falsch eingerichtet und es syncte nichts).
* Erwarteter Fehlerfall: Instagram sperrt Rechenzentrums-IPs. Einmal von Hand
* prüfen, ob der Scraper auf dem Server überhaupt durchkommt.
*
* ---------------------------------------------------------------------------
* CRONJOBS (im KAS anlegen, Typ „eigener Cronjob“ / Shell) — nur Weg A
* ---------------------------------------------------------------------------
* SITE=/www/htdocs/<KAS-User>/tsv08kulmbach (echten Pfad einsetzen: pwd auf dem Server)
* PHP=/usr/bin/php (All-Inkl ggf. /usr/local/bin/php8.2 — `which php8.2` prüft es)
@@ -22,7 +41,7 @@
* # damit Ergebnisse und Tabelle zeitnah stehen.
* 7,37 13-21 * * 6,0 cd $SITE && $PHP bin/matchcenter-sync.php >> storage/logs/matchcenter.log 2>&1
*
* # Logrotation: sonntags nachts, damit spam.log/php-errors.log nicht unbegrenzt wachsen.
* # Logpflege: sonntags nachts. Rotiert Dateien über 2 MB und löscht Einträge älter als 90 Tage.
* 23 3 * * 0 cd $SITE && $PHP bin/log-rotate.php > /dev/null 2>&1
*
* Alle Skripte sind CLI-only; die Syncs sperren sich per Lock gegen Parallelläufe und lassen
@@ -35,6 +54,14 @@
* php bin/preflight.php Config, Secrets, Schreibrechte, vendor/, PHP-Version prüfen
* php bin/smtp-test.php SMTP-Login testen (ohne Mailversand)
* danach je eine Testmail über /kontakt, /mitglied-werden und /sportheimbuchung
*
* IM BETRIEB
* php bin/logs.php Läuft alles? Fehler der letzten 7 Tage + Stand aller Kanäle
*
* Für die Dateiablage /dateien zusätzlich:
* storage/dateien/uploads und storage/dateien/system müssen beschreibbar sein
* php bin/filesgallery-update.php --check (Patch + self-hosted Assets prüfen)
* php bin/dateien-init.php Ordnerstruktur Jahr → Abteilung anlegen
*/
return [
// Kanonische Basis-URL ohne trailing slash (für Canonical, OG, Sitemap).
@@ -65,12 +92,50 @@ return [
'to' => 'info@tsv08kulmbach.de',
],
// Cron-Endpoint public/cron.php — nur nötig, wenn der Tarif keine Cronjobs hat
// (Weg B oben). Leer oder 'CHANGE_ME' lassen heißt: der Endpoint ist zu und
// antwortet auf jeden Aufruf mit 403.
'cron' => [
// Langes Zufallsgeheimnis, NICHT app_secret wiederverwenden — der Schlüssel
// reist bei einem externen Dienst mit und darf dort nichts anderes aufschließen.
// Erzeugen: php -r 'echo bin2hex(random_bytes(24)), "\n";'
'key' => 'CHANGE_ME',
// Mindestabstand zwischen zwei Läufen desselben Jobs in Sekunden. Schützt bei
// geleaktem Link davor, dass BFV oder Instagram unsere Server-IP sperren.
// Muss kleiner sein als der engste gewünschte Takt (½-stündlich = 1800 s).
'min_interval' => 240,
],
// Instagram-Scraper (bin/instagram-sync.php).
'instagram' => [
'username' => 'tsv08kulmbach',
'max_posts' => 9,
],
// Zentrale News-Seite (app/pages/news.php) — mischt data/news.json,
// Instagram-Cache und Matchcenter-Ergebnisse; hier nur die Obergrenze für
// die Gesamtliste (News allein könnte sonst unbegrenzt wachsen).
'news' => [
'max_items' => 20,
],
// Dateiablage /dateien (Files Gallery, vendored unter public/dateien/) — der Verein
// lädt dort Fotos und Videos hoch, eingearbeitet werden sie danach von Hand.
// Konfiguriert wird die App in public/dateien/_filesconfig.php; hier stehen nur die
// Geheimnisse. Passwort-Hash erzeugen mit:
// php -r "echo password_hash('DAS-PASSWORT', PASSWORD_DEFAULT), PHP_EOL;"
// Klartext-Passwörter würden auch funktionieren (index.php:507) — aber nicht in einer Datei.
// Solange hier CHANGE_ME steht, ist der Zugang gesperrt (kein Passwort = kein Login).
'dateien' => [
'license_key' => 'CHANGE_ME', // Vollversion-Schlüssel von files.gallery
'users' => [
// Gemeinsames Konto zum Hochladen — Passwort im Verein weitergeben.
'verein' => ['password_hash' => 'CHANGE_ME'],
// Verwaltung (Aufräumen, Sortieren, Löschen).
'admin' => ['password_hash' => 'CHANGE_ME'],
],
],
// Matchcenter-Scraper (bin/matchcenter-sync.php) — Spielplan/Ergebnisse/Tabelle
// aus der öffentlichen BFV-Widget-JSON-API. Die IDs sind keine Geheimnisse
// (sie steckten schon im alten BFV-Widget-Embed) und dürfen hier stehen.
@@ -86,11 +151,11 @@ return [
// übernommen wird. compound_id nur pflegen, falls die Auto-Erkennung mal fehlt.
'teams' => [
['key' => 'erste-mannschaft', 'team_id' => '016PKHIPIO000000VV0AG811VUDIC8D7', 'compound_id' => '02TBKEVREG000006VS5489BTVVQ0O654-G'],
// Vorerst deaktiviert (27.07.2026): Die BFV-Mannschaft hinter dieser ID lief in
// der Saison 25/26 als „(SG1) TSV Melkendorf I/ TSV 08 Kulmbach II" und liefert
// in 26/27 nur noch TSV Melkendorf" — also Spiele ohne den TSV 08. Erst nach
// Klärung im Verein wieder aktivieren (ggf. mit neuer team_id).
// ['key' => 'zweite-mannschaft', 'team_id' => '016PCRVRJC000000VV0AG811VTE5EA5R', 'compound_id' => '02TD8CKMD8000006VS5489BTVTLPPK10-G'],
// Zweite = Spielgemeinschaft „(SG1) TSV Melkendorf III/ TSV 08 Kulmbach II".
// team_id ist die Saison-26/27-ID (reaktiviert 29.07.2026 — die alte ID
// 016PCRVRJC… lieferte ab 26/27 nur noch den TSV Melkendorf ohne uns; bei
// SG-Umbauten zur neuen Saison die ID von der BFV-Mannschaftsseite prüfen).
['key' => 'zweite-mannschaft', 'team_id' => '030RNM1V6K000000VS5489BRVVPSQ1LR'],
['key' => 'damen', 'team_id' => '01HLQ7QVES000000VV0AG80NVVKQLCU9', 'compound_id' => '02TK5AGFE0000004VS5489BUVVPGA9RV-G'],
],
],