Commit Graph

16 Commits

Author SHA1 Message Date
fs
f88b5f1dd1 Stadionzeitung gegen ASV Marktschorgast vorbereiten 2026-08-28 13:11:43 +02:00
fs
c4ffe4a1f4 Turn-Anfrageformular: Feld „Geburtsdatum des Kindes" wieder da
Auf der alten Seite gab es das Feld, beim Neubau ist es untergegangen. Gegen
2024.tsv08kulmbach.de geprüft: dort hieß es input_alter_kind, war type="date",
OPTIONAL und trug das Label „Geburtsdatum des Kindes" — genau so wieder
eingebaut. Die Abteilung braucht es, weil die Gruppen nach Alter eingeteilt sind.

Nur in der Turn-Variante. In der allgemeinen wäre es sinnlos und würde ohne Not
personenbezogene Daten Minderjähriger erheben. Dafür drei Stellen:
- contact_variant(): Schalter ask_birthdate (true nur für turnen)
- contact-form.php: Feld definiert, bedingt gerendert, direkt nach der
  Gruppenauswahl wie auf der alten Seite
- contact-submit.php: Validierung UND Rücksetzen auf leer, wenn die Variante das
  Feld nicht anbietet — sonst könnte ein manipulierter POST es an der allgemeinen
  Variante vorbei einschmuggeln

Validierung serverseitig, nicht nur per type=date: echtes Datum, nicht in der
Zukunft, nicht vor 1900. createFromFormat mit Rückvergleich, weil PHP sonst
Scheindaten wie 2019-02-31 stillschweigend auf den 3. März verschiebt.
Geprüft mit ausreichend altem Token (die Time-Trap verlangt 3 s, meine ersten
Versuche liefen deshalb als Bot durch): 2019-02-31 abgewiesen, 2030-01-01
abgewiesen, gültiges Datum passiert. Ohne JS bleibt der Wert bei HTTP 422
erhalten (value="2019-03-12" im Re-Render nachgewiesen).

Kein neues Markup: form-field kann type=date und min/max längst, die Prop für
den Hilfetext heißt hint (nicht help, das war mein erster Fehlgriff). Docblock
der Komponente um date und min/max ergänzt.

Datenschutzerklärung mitgezogen, wie die Projektregel es für neue Formularfelder
verlangt — mit Zweckbindung (Zuordnung zur Altersgruppe) und dem ausdrücklichen
Hinweis, dass es keine Pflichtangabe ist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 21:10:47 +02:00
fs
913c058c93 send_mail: Erfolg als INFO ins mail-Log
Bisher schrieb nur der Fehlerfall. Damit war ein funktionierender Versand von
einem stummen nicht zu unterscheiden: beim Deploy am 31.07.2026 zeigte
bin/logs.php gar keinen mail-Kanal, was "nichts abgeschickt" und "einwandfrei
versandt" gleichzeitig bedeuten konnte.

Geloggt wird nur der Kontext (welches Formular) — die Werte sind feste Labels
wie "Kontaktanfrage" oder "Sportheimbuchung", nie Empfänger, Absender oder
Inhalt. Das verbietet die Logging-Regel, und für "läuft der Versand?" braucht es
davon auch nichts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 17:07:51 +02:00
fs
2066b43a57 Cover-Spielerfoto: Varianten statt 12-MB-Original (iOS Safari)
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
2026-07-31 16:58:43 +02:00
fs
2f8619e78d Gedruckte Adressen aus club.website statt base_url
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
2026-07-31 15:40:59 +02:00
fs
a101b0c3ec 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
2026-07-31 14:55:27 +02:00
fs
8304095dc3 Design-System: Token-Ebene für Spacing, Fokus und Tap-Targets
Vorbereitung für die Konsolidierung. tokens.css war für Farben und Typo schon
die Single Source of Truth, für Abstände aber nur mit --space-section und
--container-pad besetzt — dem standen ~450 Spacing-Deklarationen mit 39
verschiedenen rem-Werten gegenüber.

Neu: 14-stufige Spacing-Skala auf 4px-Raster, aus dem Ist-Bestand geclustert
statt frei erfunden. Die neun häufigsten Werte der Codebase (1, 1.5, 1.25, 2.5,
0.75, 0.5, 1.75, 2, 3rem — zusammen ~60% aller Deklarationen) liegen bereits
exakt auf Stufen, die Migration ist dort verlustfrei. Nach oben wird das Raster
gröber (3 → 4 → 5 → 7rem), weil große Abstände das vertragen. --space-4xs (2px)
ist ausdrücklich nur für optische Korrekturen an Icon-/Baseline-Nudges.

Dazu --tap-min (44px, WCAG 2.5.5) und --focus-ring/--focus-offset. Der Fokus
wird auf Weiß festgeschrieben: 15,9:1 auf --clr-bg gegenüber 3,2:1 für Rot,
das WCAG 1.4.11 nur knapp schafft und als einziger Indikator zu schwach ist.

--container-pad und --space-section verweisen jetzt auf die Skala statt eigene
Werte zu führen. Entfernt: --clr-bg-light und --clr-surface-raised, beide
projektweit 0× verwendet.

Consumer folgen in den nächsten Commits — dieser Schritt ändert die Darstellung
noch nicht.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSsVdooFLgPA8gPFp7HwZU
2026-07-27 10:07:31 +02:00
fs
66c402d9f7 Matchcenter: „Nächste Spiele" als Band statt Autoplay-Slider
Der Bereich wirkte deplatziert: der Track war auf 620px begrenzt und zeigte
eine Folie, wodurch die Karte auf breiten Fenstern in ~840px Totraum schwebte;
dazu derselbe Hintergrund wie der Hero direkt darüber, kein Akzent, und ein
einsames „:" zwischen den Wappen, das wie ein fehlendes Ergebnis aussah.

Neu als abgesetztes Band (.section--sunken, nutzt endlich das dafür
vorgesehene, bisher unbenutzte Token --clr-bg-deep) mit roter Oberkante:
links der Aufmacher (match-feature), rechts die folgenden Termine als
verlinkte match-row-Zeilen ohne Kartenfläche.

Der Aufmacher ist die große Variante der Terminzeile — Heim über Gast,
linksbündig, gleiche Struktur wie die Zeilen daneben. Bewusst nur drei
typografische Rollen (Meta-Zeile · Vereinsnamen in Coolvetica · Anstoßzeile
mit Countdown) statt der acht der ersten Fassung, ohne Badges, Icons oder
Button. Die eigene Mannschaft wird über Helligkeit markiert, nicht über
font-weight — Coolvetica hat nur einen Schnitt, bold wäre synthetisch.

match-row bekommt optionale $href/$meta-Props; Raster und Innenabstand liegen
jetzt im __link-Wrapper, damit als Link die ganze Zeilenfläche klickbar ist.
Neue Helper match_when() (ISO + deutscher Countdown, explizit Europe/Berlin,
weil PHP global auf UTC läuft) und match_competition_label().

match-slider.php und match-card.php entfallen — Letztere wurde
ausschließlich vom Slider genutzt. carousel.js treibt damit nur noch den
Banner-Slider der Startseite und wird im Matchcenter nicht mehr geladen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 00:39:57 +02:00
fs
819bdf6173 SEO/GEO-Basics: Descriptions & Titles normalisiert, Outline-Fixes, CLS-Maße
Audit aller 23 indexierbaren Routen (lokaler Crawl) und Fixes:
- Descriptions auf ~130-160 Zeichen; Turnen-Disziplinseiten hatten den
  kurzen Hero-Teaser als Description, jetzt eigene SERP-Texte
- Titles: team-erste gekürzt, sportheimbuchung ohne doppelten Brand
- h1->h3-Sprünge behoben: visually-hidden h2 aus JSON-Section-Titeln
  (sportheimbuchung, partner-werden), sichtbarer choice.title auf
  mitglied-werden; historie bekommt Breadcrumb-Schema
- Neuer Helper img_intrinsic_attrs() liefert width/height aus der
  Bilddatei (PNG via getimagesize, SVG via viewBox) für Partner-Grid-
  und Banner-Slider-Logos — kein Layout-Shift mehr

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 00:27:19 +02:00
fs
9722d8eccc SEO/GEO-Feinschliff: og:image:alt, areaServed, WebSite-Knoten, Team-Descriptions
- og:image:alt (Seitentitel) in meta.php — Social-/AI-Cards + A11y
- areaServed (Kulmbach + Landkreis Kulmbach) am SportsClub-JSON-LD-Knoten
- WebSite-Knoten auf der Startseite via neuem Helper website_schema(), publisher -> #club
- Explizite, Kulmbach-haltige Team-Descriptions (<=155 Zeichen) statt roher Hero-Texte
  (zweite-mannschaft war 214 Zeichen und wurde im Snippet abgeschnitten)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 23:17:53 +02:00
fs
2c0594811c Mitmachen: Ehrenamtsseite mit Stellen-Filter
Neue Seite /mitmachen, die offene Ehrenamtsstellen aus data/mitmachen.json als
position-card-Komponente listet, mit clientseitigem Bereichsfilter
(position-filter.js, progressive enhancement) und JobPosting-Schema
(job_posting_schema(), employmentType VOLUNTEER). Route ergänzt.
Enthält zudem kleine person-card__contact-Stilanpassungen (gleiche CSS-Sektion).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HGzc6GhWhmLJt1jC2q1SRZ
2026-06-20 20:47:13 +02:00
fs
e7801ca6ee Spam-Schutz: Rate-Limiting, Link-Heuristik & Spam-Logging
Mehrschichtiger Formular-Schutz ohne externe Dienste, ergänzend zu Honeypot
und HMAC-Time-Trap:
- rate_limit_ok(): pro IP+Route (5/10 min), Tages-Cap (100/Tag), Token-Replay
- client_ip() / log_spam() (abgewiesene Versuche -> storage/logs/spam.log)
- Honeypot-Feld website -> company_url umbenannt (contact-/membership-form)
- Ratelimit-Hinweis in form.js
- storage/ratelimit/ (gitignored bis auf .gitkeep)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HGzc6GhWhmLJt1jC2q1SRZ
2026-06-20 20:41:50 +02:00
fs
185365dc79 Matchcenter: Spielplan, Ergebnisse & Tabellen (eigene Seite statt BFV-Widget)
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
2026-06-19 10:57:14 +02:00
fs
10b4ede8f1 SEO-Schema-Helper: Breadcrumb, FAQPage, SportsTeam
breadcrumb_schema(), faq_schema(), page_schema() und team_schema() bauen
JSON-LD-Knoten für $meta['schema']. team_schema verweist via #club auf den
Organization-Knoten. Leere FAQ-Paare werden übersprungen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDQZ6FuchNUh2bxSW9PeNv
2026-06-19 08:12:05 +02:00
fs
5170dcb606 Icon-System: icon()-Helper, lokale Bootstrap-Icons, icons-add.php
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
2026-06-19 08:11:58 +02:00
0f19729a88 Neubau Fundament + Startseite: Designsystem, PHP-Komponenten, Brevo-Formular, Instagram-Sync
- 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>
2026-06-11 21:16:44 +02:00