- Neue generische Live-Seite `aufruf` (erste Nutzung: Sportfest-Helferaufruf):
Komponente, Dispatcher, Skript-Einbindung (Planung im Aufschlag, QR aus
aufruf.link, Feld-Carryover), CSS, optionales kleines Poster rechts.
- Ausgabe 2026-08-15-tsv-koednitz als ENTWURF: Vorwort (Benedikt, Revanche
dezent), Interview (Ali, 3 Fragen), Sportfest-Helferseite mit Kontakten aus
ansprechpartner.json; Rückseite/Cover wie Hutschdorf wiederverwendet.
- Interview-Frage größer/prominenter gesetzt.
- Sportfest-Plakat-Anzeige deaktiviert (redundant zur Helferseite) -> 40 Seiten.
Sync-Dateien (instagram/matchcenter) bleiben gitignored, Snapshot wird beim
Veröffentlichen auf dem Server eingefroren.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFnFYLm2MoBskyk9mc84ZW
Der Snapshot fror die Daten ein, verwies aber weiter auf Ordner, die die
Sync-Skripte verwalten: matchcenter-sync räumt Wappen weg, die der BFV nicht mehr
führt (eine Saison), instagram-sync ältere Posts (Wochen). Am 31.07.2026
nachgewiesen — ein Lauf löschte 18 Wappen und 3 Bilder. Eine archivierte Ausgabe
wäre also mit der Zeit von selbst zerfallen, und "gedruckt = fixiert" war damit
nur die halbe Wahrheit.
bin/stadionzeitung-publish.php kopiert jetzt alles, worauf der Snapshot zeigt
(img/crests, img/instagram, img/news), als fix-* in den Ausgaben-Ordner und
schreibt die Pfade um. Varianten-Sets {base, widths} werden über alle Breiten
kopiert und nur umgeschrieben, wenn alle da sind — ein halbes srcset wäre
schlimmer als der alte Pfad; genau daran fiel img/news/neue-website beim ersten
Versuch auf. --entwurf löscht die fix-*-Dateien wieder, sie gehören zum Snapshot.
Flache Dateinamen statt Unterordner, weil die Aufräum-Logik in
stadionzeitung-add.php nur Dateien entfernt und ein Unterordner als Waise bliebe.
bin/preflight.php warnt, wenn eine veröffentlichte Ausgabe noch auf die
flüchtigen Ordner zeigt — sonst verfiele sie still.
Geprüft am Hutschdorf-Heft: 39 Dateien eingefroren, Snapshot verweist nur noch
auf den Ausgaben-Ordner, 88 Bilder in Viewer und Druckfassung erreichbar, keins
fehlt, preflight-Warnung verschwindet. Der Testlauf wurde danach zurückgenommen:
der lokale Matchcenter-Stand ist einen Tag alt, eingefroren wird auf dem Server
nach dem letzten Sync vor dem Druck.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
Korrektur meiner eigenen Annahme von heute früh. Der Tarif hat SSH und
Cronjobs, aber das KAS-Formular kennt nur ein Feld "Protokoll / Pfad" mit
https:// — ein `php bin/…` ist dort nicht eintragbar. Der Endpoint, den ich
vorhin stillgelegt hatte, ist damit wieder der einzige Takt-Geber, und zwar in
einer besseren Form als geplant: Aufrufer ist der Hoster selbst, der Schlüssel
verlässt den Webspace nie, kein Dritter ist beteiligt.
Dritter Job 'logrotate' in der Whitelist, denn ohne ihn liefe die Logpflege nie
automatisch und die 2-MB-Grenze samt 90-Tage-Frist wäre toter Code. Er passt in
denselben Rahmen wie die Syncs: kein Parameter aus der URL, keine Verbindung nach
außen, schreibt nur in storage/logs. bin/log-rotate.php trägt dafür denselben
Riegel wie die Syncs (CRON_HTTP) und require_once. Eine vierte Ausnahme braucht
wieder denselben Aufwand an Begründung.
Geprüft: falscher Schlüssel 403, unbekannter Job 403, gültiger Aufruf 204 mit
leerem Body, sofortige Wiederholung 204 plus WARN, Logdateien unversehrt.
Zeitraster angepasst, weil das KAS Wochentage nicht mit Halbstunden kombiniert:
Matchcenter stündlich statt 2x/Tag plus Wochenende, Instagram als zwei
Tageseinträge, Logpflege täglich nachts. Die Abwägung steht in
config.example.php und docs/deploy.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
Auf dem Server scheiterte der Login mit "Could not authenticate", während
dieselben Zugangsdaten vom Entwicklungsrechner angenommen werden. Beide Namen
(w01ca75d.kasserver.com und der Webserver) zeigen auf dieselbe IP — vom Webspace
aus verbindet sich PHP also mit der eigenen Maschine, und die kann Anmeldungen
anders behandeln als Verbindungen von außen.
--probe testet vier Zugangswege (587/STARTTLS, 465/SSL, localhost:25 mit und
ohne Anmeldung) und nennt den ersten, der trägt. Bewusst nur vier: jeder
Fehlversuch ist ein fehlgeschlagener Login, zu viele sperren ein Postfach.
--frage fragt das Passwort verdeckt ab, damit ein neu gesetztes Postfach-Passwort
geprüft werden kann, BEVOR es in eine Datei auf zwei Rechnern wandert.
Die Fehlerausgabe nennt bei "Could not authenticate" jetzt die drei Ursachen,
die dahinter stecken können.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
Das Cover blieb am iPhone leer, während Chrome am Desktop es zeigte. Ursache
war kein fehlendes Bild: das freigestellte Spielerfoto hatte 2584x5093 Pixel und
12,3 MB. iOS Safari verwirft Bilder, deren dekodierte Größe sein Budget
übersteigt — hier gut 53 MB, und auf einer Ausgabe liegen 86 Bilder.
Dargestellt wird das Foto mit 98cqi Höhe, bei 480px Seitenbreite also etwa
238x470 CSS-Pixel. Es gibt jetzt 320/640/1000 als PNG mit erhaltener
Transparenz (0,2 / 0,8 / 1,9 MB); sizes nennt die echte Breite, sonst zöge ein
Handy mit DPR 3 die größte Variante. Das Original ist gelöscht.
Dafür kann resize_image_variants() nun Alpha erhalten ($alpha = true schreibt
PNG statt JPG) und die img-Komponente eine Endung ($image['ext']). Der Default
bleibt JPG, weil bestehende Aufrufer PNG-Quellen absichtlich zu JPG verkleinern.
bin/stadionzeitung-add.php erzeugt die Varianten künftig selbst. Dort stand
"unverändert kopieren, Felix liefert bereits ein web-taugliches Foto" — genau
diese Annahme ist gerissen.
Außerdem: smtp.password stand in der Produktiv-Config noch auf dem Platzhalter
POSTFACH_PASSWORT (von preflight auf dem Server gefunden, einziger harter
Fehler). Übernommen wurde das Passwort aus der Dev-Config, mit dem der Versand
am 27.07.2026 verifiziert wurde; Host, Postfach und Empfänger sind identisch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
Beim ersten Deploy blieben die Slides des Banner-Sliders schwarz. Ursache war
kein fehlendes Bild, sondern die CSP: public/.htaccess setzt
default-src 'self' ohne style-src, der Browser verwirft damit jedes
style-Attribut — und die Komponente lieferte Bild und Akzentfarbe genau so
(style="--banner-bg: url(…); --banner-accent: #6cbbbc"). Lokal war das
unsichtbar, weil php -S keine .htaccess liest.
Der Hintergrund ist jetzt ein <img class="ad-banner__bg"> über die bestehende
img-Komponente (CSP erlaubt img-src) und bringt srcset mit: das Handy lädt die
640er statt der 1600er, die erste Slide lädt eager mit fetchpriority. Der
dunkle Leseverlauf liegt auf ::before statt im background-image.
Die Akzentfarbe kann als beliebiger Hex-Wert nicht ohne Inline-Style in die
Seite. Sie steht deshalb als Token in tokens.css (wo Farben laut CLAUDE.md
hingehören), components.css bildet den Namen darauf ab, und banners.json nennt
nur noch "accent": "breadcrumb".
bin/preflight.php bricht ab, wenn unter app/ wieder ein style-Attribut
auftaucht — der Fehler wäre sonst erneut erst in Produktion sichtbar. Beim
Bauen dieser Wache zweimal selbst hineingetappt: ein Muster mit [^>]* bricht an
jedem schließenden PHP-Tag ab, und die Zeichenfolge dieses Tags beendet den
PHP-Modus auch in einem //-Kommentar (preflight gab ab dort seinen eigenen
Quelltext aus, php -l meldete nichts).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
QR-Codes und Adresszeilen trugen `http://localhost:8000` ins Druckmaterial, weil
sie über abs_url() liefen. Nach dem Produktivgang hätten alle zehn Codes neu
erzeugt werden müssen — und ein gedruckter Code mit localhost darin ist Altpapier.
Die öffentliche Domain ist Vereinsidentität, keine Umgebungseinstellung: neues
Feld `club.website` in data/club.json plus club_url()/club_domain(). base_url
bleibt umgebungsabhängig, weil Canonical, OG, Sitemap und JSON-LD zur
ausliefernden Adresse passen müssen.
Umgestellt: Cover-QR und die acht geteilten CTA-QRs, die Internet-Zeile im
Heft-Impressum, die Cover-Zeile neben dem QR, die acht Linkzeilen unter den
CTA-QRs der Live-Seiten (die hatte ich beim ersten Durchgang übersehen), das
Web-Feld im Website-Impressum und bin/qr.php.
Nebenbei: club_domain() ohne Pfad hängte einen Wurzel-Slash an
(„tsv08kulmbach.de/"), und der Kopf-Kommentar von stadionzeitung-add.php
beschrieb noch die alten vier Anzeigen-Gruppen.
Nicht verifizierbar und deshalb dokumentiert: die vendorte QR-Bibliothek
arbeitet nicht reproduzierbar (gleiche URL, andere Pixel — anderes Maskenmuster,
beides gültig). Ein PNG-Vergleich beweist nichts, nachweisbar ist nur der
Code-Pfad. Jeden Druck-QR einmal scannen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
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
Zweiter Teil der Konsolidierung. Vorher standen 31 font-size-Werte außerhalb
der Skala — beim Durchgehen stellte sich heraus, dass 20 davon gar keine
Textgrößen sind: der icon()-Helper rendert 1em, also steuerte font-size dort
die Icon-Größe. Dafür gibt es jetzt eine eigene, kleine Skala (--icon-s/m/l/xl
in rem, --icon-inline-sm/--icon-inline/--icon-inline-lg in em für Icons, die im
Textfluss mitwachsen sollen). Elf Ad-hoc-Werte fallen damit auf sieben Stufen.
h4–h6 laufen nicht mehr über Coolvetica. Sie lagen mit 26px und 21px unter der
--fs-650-Grenze aus CLAUDE.md, ab der der Condensed-Schnitt in Uppercase
unleserlich wird — die Regel war also nur dokumentiert, nicht durchsetzbar.
Jetzt tragen h1–h3 Coolvetica (alle über der Grenze) und h4–h6 Abel mit
Sperrung. Bezeichnend: das einzige <h4> im Markup hatte sich genau dieses
Muster bereits von Hand gebaut. Die Basisregel liefert es jetzt, die Komponente
gibt ihre drei Duplikat-Deklarationen ab — sichtbar ändert sich dadurch nichts.
Neue Stufen schließen die Lücken: --fs-550 (fluider Lead), --fs-690 (Karten-
Überschrift) und --fs-1000 (Display-Hero). Vier fast deckungsgleiche clamp()-
Rampen für Karten-Überschriften (bento__heading, bento__stat-value,
group-card__name, team-card__name) laufen jetzt über --fs-690; team-card__name
wächst dabei am Desktop um ~5px, die anderen drei bleiben im 1px-Bereich.
Die Wappen-Initialen im Matchcenter hingen an zwei Mini-Schriftgrößen (10,4 und
11,2px) unterhalb der Skala. Sie sind in Wahrheit ~0,4 × Kreisdurchmesser,
folgen jetzt also --crest-size statt einer eigenen font-size. Nebenbei fallen
drei redundante Doppel-Selektoren weg: .crest--initials trägt immer auch .crest.
Ebenfalls tokenisiert: 27 letter-spacing-Werte mit elf verschiedenen Zahlen auf
vier Stufen (--ls-tight/caps/wide/wider), maximale Abweichung 0,02em.
Danach steht kein font-size- und kein letter-spacing-Wert mehr außerhalb von
tokens.css.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSsVdooFLgPA8gPFp7HwZU
web_profile_info antwortet seit kurzem mit HTTP 400 und einem
Instagram-internen Schema-Fehler ("Asset asset://laser.provider/
ig_business_category_subvertical has been deleted") — kein IP-Block.
Gleichzeitig enthält das Profil-HTML kein edge_owner_to_timeline_media
mehr, womit auch der zweite Fallback tot war und der Cache seit
20.06. nicht mehr aktualisiert wurde.
api/v1/feed/user/{username}/username/ liefert weiterhin 200 mit den
letzten Posts, nur in anderer Form (items[] mit image_versions2 statt
edges[] mit display_url). Neuer Extractor dafür, eingehängt als
Strategie 1; die bisherigen zwei bleiben als Fallback 2 und 3.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Versand läuft jetzt über das KAS-Postfach noreply@tsv08kulmbach.de
(w01ca75d.kasserver.com:587, STARTTLS) — ein Drittanbieter weniger,
SPF der Domain deckt den Server bereits ab. bin/smtp-test.php prüft
Verbindung, TLS und Login ohne eine Mail zu versenden (Debug bewusst
aus, damit keine Credentials im Terminal landen).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Sync schreibt Cache jetzt fehlersicher: json_encode mit JSON_THROW_ON_ERROR
+ Rückgabeprüfung von file_put_contents/rename → ein defektes UTF-8-Zeichen
aus der BFV-API kann den guten Cache nicht mehr leerschreiben (alter Cache
bleibt via $fail unangetastet)
- Vorsaison wird bei erkanntem compoundId-Wechsel einmalig archiviert (previous),
pro Team compound_id + abgeleitetes season-Label + stale-Flag persistiert
- Aktuelle Saison ist immer sichtbar (auch leer), Wechsel zur Vorsaison über
wiederverwendete Stripe-Tabbar (group-tabs); neue Komponente matchcenter-season
- Leere aktuelle Saison zeigt Hinweis statt leerer Fläche
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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
Inline-SVG-Ausgabe via icon('name', 'klasse', 'label?') aus
public/assets/icons/. Dekorativ (aria-hidden) oder semantisch (role=img).
.icon-Basisklasse in utilities.css (1em, currentColor). bin/icons-add.php
holt neue Icons einzeln von icons.getbootstrap.com.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDQZ6FuchNUh2bxSW9PeNv
- Ordnerstruktur mit public/-Docroot, Deny-.htaccess für app/bin/config/data/storage
- CLAUDE.md mit Leitplanken (self-hosted only, Single Source of Truth, Component-first)
- Design-Tokens aus alter Seite extrahiert (Akzent #e20612, Coolvetica/Abel als woff2)
- Front Controller mit Routen-Register, Sitemap, Canonical, JSON-LD (SportsClub)
- Startseite: Hero, Instagram-Feed (lokaler Cache), Historie/Sportheim, Stats, Partner, Kontakt
- Kontaktformular via PHPMailer/Brevo mit Honeypot + HMAC-Time-Trap (kein reCAPTCHA)
- bin/instagram-sync.php: Scraper mit Strategie-Kette, flock, atomarem Cache, lokalen Bildern
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>