Changelog — Medienakkreditierung BTV
Format orientiert an Keep a Changelog.
Versionierung nach SemVer.
[1.4.0] — 2026-07-15
Neu
- SAFE-SPORT-Merkblatt „Foto- und Videoaufnahmen" eingebaut (O4 erledigt).
Das PDF (BTJ/BTV, 2 Seiten) liegt unter docs/ außerhalb des Webroots und
wird ausschließlich über die Route /safe-sport-merkblatt ausgeliefert (inline,
nosniff).
- Ansehen ist Pflicht, nicht nur Bestätigen. Der Aufruf der Route wird in der
Session vermerkt; die Kenntnisnahme wird serverseitig nur akzeptiert, wenn
das Merkblatt in dieser Sitzung geöffnet wurde. Ein gesetzter Haken ohne
vorheriges Öffnen wird mit HTTP 422 und einem Hinweis abgewiesen — das greift
auch ohne JavaScript und lässt sich nicht durch manipulierte Formularfelder
umgehen.
- Mit JavaScript ist die Checkbox zusätzlich gesperrt, bis der Link geöffnet
wurde; der Status („noch nicht geöffnet" / „✓ geöffnet") ist direkt sichtbar.
[1.3.0] — 2026-07-15
Datenschutz-Anpassungen nach Vorgaben des Datenschutzbeauftragten.
Geändert
- Keine Datenschutz-Einwilligung mehr im Formular. Die Checkbox „…stimme der
Verarbeitung zu" entfällt (die Verarbeitung ist zur Durchführung der
Akkreditierung erforderlich). Stattdessen steht direkt vor dem Absenden ein gut
sichtbarer Hinweis mit Direktlink: „Informationen zum Schutz Ihrer
personenbezogenen Daten finden Sie in unserer Datenschutzerklärung"
(→ btv-turnen.de/der-btv/ueber-uns/datenschutz). Medienrichtlinien- und
SAFE-SPORT-Checkbox bleiben Pflicht (nicht vorbelegt).
- Upload-Hinweis: „Nicht benötigte Angaben auf Nachweisen dürfen geschwärzt
werden."
- Uploads werden verschlüsselt übertragen (HTTPS) und zugriffsbeschränkt
gespeichert (außerhalb Webroot, Deny-all, nur mit Board-Login, Zufallsnamen).
Neu
- Getrennte Löschung je Antrag: Nachweise, Badge oder der komplette Antrag
lassen sich einzeln entfernen (Nachweise/Badge durch Board-Berechtigte, die
vollständige Löschung nur durch die Administration).
- Automatische Löschung nach Veranstaltungsende (täglicher Cron):
- Stufe 1 (Standard 7 Tage): Nachweise + Badge-PDFs gelöscht, Badge-/Verify-Links
entwertet.
- Stufe 2 (Standard 6 Monate): kompletter Antrag inkl. interner Bemerkung und
Statusverlauf gelöscht, Mails anonymisiert.
- Anonyme Statistikzahlen (ohne Personenbezug) bleiben erhalten; jede Löschung
wird protokolliert. Fristen und Kill-Switch unter Einstellungen → Aufbewahrung.
[1.2.1] — 2026-07-15
Neu
- Suche in der Veranstaltungs-Verwaltung. Über der Liste „Alle
Veranstaltungen" filtert ein Suchfeld die Tabelle live nach Name, Ort,
Ansprechpartner:in oder Datum. Ohne JavaScript bleibt die komplette Tabelle
sichtbar.
[1.2.0] — 2026-07-15
Neu (Verbesserungsideen aus dem Betrieb)
- Durchsuchbare Veranstaltungsauswahl im Formular. Über dem Dropdown erscheint
ein Suchfeld, das während der Eingabe passende Veranstaltungen nach Name, Ort
oder Datum vorschlägt (Tastatur-Navigation inklusive). Ohne JavaScript bleibt das
native Dropdown voll nutzbar; die Auswahl steuert weiterhin dasselbe Feld, sodass
Ort/Zeitraum wie gehabt übernommen werden.
- Mehrfachauswahl im Steuerungs-Board. Jede Zeile hat eine Checkbox, der
Tabellenkopf einen „Alle auswählen"-Haken. Sobald etwas markiert ist, erscheint
eine Aktionsleiste mit der Anzahl und den Sammelaktionen.
- Sammelversand für Genehmigungen. Ausgewählte Anträge lassen sich in einem
Rutsch genehmigen; die Genehmigungsmails werden gebündelt vorbereitet. Nur noch
offene Anträge werden verarbeitet, bereits entschiedene übersprungen.
- Sammelversand für Badge-Mails. Für die Auswahl werden fehlende Badges
automatisch erzeugt (inkl. Verify-QR) und die Badge-Mails mit dem jeweiligen PDF
im Anhang gebündelt versendet.
- Badge-Versand direkt aus der Übersicht. Die Badge-Sammelaktion funktioniert
auch mit einer einzelnen Auswahl — Badge erstellen und versenden, ohne den Antrag
einzeln öffnen zu müssen.
Alle Sammelaktionen führen zunächst auf eine Vorschau: Der Fachbereich sieht
Empfänger:innen und übersprungene Anträge, passt den Text einmal an (Platzhalter
wie {vorname} werden je Empfänger:in eingesetzt) und löst erst per Klick den
gebündelten Versand aus. Damit bleibt die Regel „kein Automatikversand" gewahrt.
Behoben
- Badge-Erzeugung schlug live fehl („Can't open file … CenturyGothic.ttf").
Der mitgelieferte tFPDF-Font-Cache (*.mtx.php) enthielt den absoluten Pfad vom
Erzeugungsrechner. Er wird jetzt über __DIR__ bestimmt und ist damit portabel —
egal wohin die App deployt oder verschoben wird.
- Zustimmungs-Checkboxen waren kaum sichtbar (winziger Punkt). Sie bekommen
jetzt eine feste Größe (1,35 rem) und die BTV-Akzentfarbe.
Geändert
- Hinterlegte Veranstaltungen bringen Ort und Zeitraum mit. Format je Zeile:
Name | Ort | von | bis (Ort/Datum optional). Wählt jemand im Formular eine
hinterlegte Veranstaltung, werden Ort und Zeitraum automatisch übernommen und als
Info angezeigt — sie müssen nicht erneut eingegeben werden. Bei „Andere
Veranstaltung" erscheinen wie bisher Freitextfelder. Die Werte einer hinterlegten
Veranstaltung kommen serverseitig aus der Liste, nicht aus dem Formular, sodass
manipulierte Felder keine abweichenden Angaben setzen können.
[1.1.1] — 2026-07-10
Behoben / Geändert (Rückmeldungen aus dem Betrieb)
- Badge: Der Verify-QR überdeckte Antragsnummer und Zeitraum. Er sitzt jetzt
rechts unten mit „prüfen"-Label; die Textbreite links ist begrenzt.
- WL-Live-Seite: zeigt jetzt alle offenen Anträge (auch „Eingegangen"),
nicht nur die am Veranstaltungstag; Block heißt „In Bearbeitung / Vor-Ort-Anfragen".
- WL-Live-Seite: Badge ist herunterladbar — auch wenn er der Person schon per
Mail geschickt wurde (/wl/{token}/badge/{id}, Token-geschützt).
- Board-Filter werden direkt bei Auswahl angewandt (kein „Filtern"-Button mehr);
Freitextsuche mit kurzer Verzögerung.
- Veranstaltungen sind ein eigener Menüpunkt (
/veranstaltungen), nicht mehr
ein Einstellungen-Tab; auch der Fachbereich Kommunikation darf sie pflegen.
- Ansprache: Wettkampfleitung wird mit „du" angesprochen (WL-Seite,
WL-Infomail), Antragstellende weiterhin mit „Sie".
- Das Formular blendet Name/Ort bei ausgewählter Veranstaltung korrekt aus
(bestätigt; die frühere Beobachtung war eine Opcache-Verzögerung nach Deploy).
[1.1.0] — 2026-07-10
Ausbaustufe v1.1 aus Erweiterungskonzept-v1.1.md (E1–E9). Auf frischer
Datenbank (schema + migrate_v1_1 + seed) end-to-end getestet.
Hinzugefügt
- E1 Veranstaltungs-Stammdaten (
events-Tabelle): Ort, Zeitraum,
Ansprechpartner:in + E-Mail, WL-Token. Pflege in den Einstellungen mit
Vorlage-Schnellanlage und Token-Rotation. Excel-Import/-Export (.xlsx)
dependency-frei über ZipArchive + SimpleXML (app/xlsx.php); Import zweistufig
mit Vorschau (neu/aktualisiert/fehlerhaft), Datum tolerant (ISO/TT.MM.JJJJ/
Excel-Serial). Formular nutzt event_id + Text-Snapshot; Vorauswahl per
?veranstaltung={id}.
- E2 Live-Seite Wettkampfleitung
/wl/{token}: vier Blöcke (Zutritt,
Widerrufen, Abgelehnt, Neu eingegangen), Badge-Spalte, 30-s-JSON-Polling,
Vor-Ort-Bestätigung der Badge-Ausgabe. Fremdtoken 404, 7 Tage nach Event-Ende
410, Abgelehnte per Setting abschaltbar.
- E3 QR-Codes je Veranstaltung über phpqrcode (
lib/phpqrcode/) + eigener
Renderer für PNG und SVG (app/qr.php).
- E4 WL-Infomail aus dem Mail-Editor an die Wettkampfleitung, mit QR-Code
und Druckliste als Anhang; setzt „WL informiert".
- E5 Nachreichen-Link
/nachreichen/{token}(14 Tage, einmalig): Upload
ohne Login, Status zurück auf „In Prüfung", Info-Mail an presse@.
- E6 Badge-Verifikation
/verify/{token}: Ampel gültig/widerrufen, QR auf
dem Badge, nachträglicher Badge-Download über den Verify-Link.
- E7 Sammel-Badges & Druckliste je Veranstaltung als PDF.
- E8 SLA-Reminder (
bin/reminder.phpCLI +/cron/reminder?key=Web),
Doppelschutz über reminded_at; Dubletten-Hinweis im Board.
- E9 capture-Attribut (Handy-Kamera), Selfcheck um events/zip/phpqrcode/
Reminder erweitert, CSV um Ansprechpartner.
- Token einheitlich Hex (64 Zeichen), passt zum bestehenden Routing-Muster.
- Mailer auf mehrere Anhänge und
mails.event_iderweitert.
Migration
sql/migrate_v1_1.sql(idempotent):events-Tabelle,requests- und
mails-Erweiterungen. Bestehende Orte werden nicht migriert — die
events-Tabelle startet leer.
[1.0.0] — 2026-07-10
Alle Umsetzungsschritte aus Konzept Abschn. 17 sind fertig. Die 21 prüfbaren
Akzeptanzkriterien (Abschn. 18) laufen lokal grün; offen sind nur Zulieferungen
(O3–O5, O7, O8, O11, O12).
Hinzugefügt
- Echte Anmeldung (Umsetzungsschritt 9):
users-Tabelle, Passwort-Login mit
bcrypt, Einladung und Passwort-Reset über zeitlich begrenzte Token. Der Token
liegt nur als SHA-256-Hash in der Datenbank — wer sie liest, kann daraus keinen
gültigen Link bauen. Passwörter werden nie im Klartext verschickt.
- Brute-Force-Bremse: ab 8 Fehlversuchen je IP innerhalb von 15 Minuten
antwortet die Anmeldung mit 429 (nutzt dieselbe rate_limit-Tabelle wie das
Formular). Die Antwortzeit ist bei unbekannter E-Mail dieselbe wie bei falschem
Passwort, sodass sich nicht ermitteln lässt, welche Adressen ein Konto haben.
- Einstellungen
/einstellungen(Umsetzungsschritt 8, nur Administration):
Auswahllisten (Funktionen, Veranstaltungen), alle sieben Mail-Vorlagen,
interner Empfänger, Aufbewahrungsfrist und Benutzerverwaltung. Konten werden
deaktiviert, nie gelöscht; das eigene Konto lässt sich nicht deaktivieren oder
herabstufen. Unvollständige POSTs leeren keine gepflegten Listen.
- QR-Code
/qr-codeals SVG und PNG, fest auf das Formular gerichtet.
Inhalt zurückgelesen und geprüft.
sql/seed_admin.sqllegt Franz und Stefan als Administratoren ohne Passwort
an; beide setzen es selbst über „Passwort vergessen".
sql/cleanup_abgelaufene.sqlfür das Löschkonzept: zeigt erst an, was betroffen
wäre, anonymisiert das Mail-Protokoll und löscht dann die Anträge. Bewusst
manuell, bis Projekt29 die Frist freigibt (O5).
- Self-Check prüft zusätzlich tFPDF, Badge-Schrift, mitgelieferten Font-Cache
und QR-Code.
README-DEPLOY.mdvollständig: FTPS über den Zertifikats-Hostnamen, Rechte,
Erst-Admin, Graph, Aufbewahrung, Routenübersicht.
Sicherheit
- Bei
app_env=prodist der Demo-Login weder sichtbar noch per direktem POST
nutzbar (403), und der Self-Check verlangt mindestens ein anmeldefähiges Konto.
Behoben
finfo_close()entfernt (seit PHP 8.5 veraltet, Objekte werden selbst
freigegeben). Der Zielserver läuft auf 8.4, lokal 8.5.
[0.6.0] — 2026-07-10
Hinzugefügt
- Vorbefüllter Mail-Editor
/board/{id}/mail(Umsetzungsschritt 6): Betreff
und Text kommen aus der Vorlage, sind editierbar und werden erst per Klick
verschickt. Ein Statuswechsel auf Rückfrage, Genehmigung, Ablehnung oder
Widerruf leitet direkt dorthin — automatisch verschickt wird weiterhin nichts.
- Badge-PDF (Umsetzungsschritt 7) über tFPDF unter
lib/tfpdf/
(LGPL, kein Composer). A6 quer, BTV-Blau, eingebettete Century Gothic.
Enthält Name, Funktion, Redaktion/Verein, Veranstaltung, Ort, Zeitraum und
Logo — nicht Telefon, E-Mail, Nachweis oder interne Bemerkungen (Spec 15.2,
per Textextraktion geprüft). Layout ist ein Platzhalter bis zur
Marketing-Vorlage (O7).
- „Badge erstellen" nur bei Status „Genehmigt"; danach springt der Status auf
„Badge erstellt" und der Mail-Editor öffnet mit der Badge-Vorlage. Erneutes
Erzeugen überschreibt dieselbe Datei (idempotent, für Tippfehler-Korrekturen).
- Badge-Download
/badge/{id}nur für Board-Berechtigte. - Die Badge-Mail führt das PDF als Anhang mit; ohne erzeugtes PDF ist der
Sende-Knopf gesperrt und ein direkter POST wird abgewiesen.
Behoben
imagedestroy()entfernt (seit PHP 8.0 wirkungslos, ab 8.5 veraltet).
Hinweis
- tFPDF kennt kein Seitenformat „A6" — das Format wird über die Maße gesetzt.
[0.5.0] — 2026-07-09
Hinzugefügt
- Steuerungs-Board
/board(Umsetzungsschritt 5, Konzept Abschn. 10):
Liste mit Statuszählern als Schnellfilter, Filter nach Funktion,
Veranstaltung, Zeitraum und Freitextsuche, sortierbare Spalten.
- Detailansicht
/board/{id}: alle Angaben, Zustimmungs-Zeitstempel,
Nachweis-Download, Statuswechsel mit Kommentar, interne Bemerkung, Haken für
„Wettkampfleitung informiert" und „Badge ausgegeben", vollständiger
Statusverlauf und die Mail-Historie des Antrags.
- Statuswechsel verschickt nichts automatisch (Konzept Abschn. 6.2). Bei
Rückfrage, Genehmigung, Ablehnung und Widerruf erscheint ein Hinweis, dass
eine Nachricht vorgesehen ist; der vorbefüllte Mail-Editor folgt in Schritt 6.
decided_at wird bei Genehmigung/Ablehnung gesetzt, der Widerruf setzt
zusätzlich revoked.
- Nachweis-Download
/download/{id}: nur für Board-Berechtigte, Pfad
ausschließlich aus dem Zufallsnamen der Datenbank, Auslieferung immer als
Anhang mit nosniff.
- CSV-Export
/export.csvder gefilterten Liste für die Wettkampfleitung,
die keinen Systemzugang hat. Mit BOM, damit Excel die Umlaute richtig liest.
- Vor-Ort-Nachpflege
/board/neu: Upload optional, dafür Pflichtangabe, wer
den Nachweis vor Ort geprüft hat (im Statusverlauf dokumentiert). Es werden
keine Mails verschickt, und ein bereits vergangener Zeitraum ist zulässig —
anders als im öffentlichen Formular, wo er fast immer ein Tippfehler ist.
- Sortierspalten werden gegen eine feste Liste geprüft, alle Filterwerte gehen
als gebundene Parameter in die Abfrage.
[0.4.0] — 2026-07-09
Hinzugefügt
- Automatische Mails beim Eingang (Umsetzungsschritt 4, Konzept Abschn. 7):
Eingangsbestätigung an die antragstellende Person und interne Information an
presse@btv-turnen.de mit Deeplink zum Antrag im Board. Ein Fehler beim
Versand nimmt den bereits gespeicherten Antrag nicht zurück.
app/mailer.phpmit den Modisimulate(Standard, nur protokolliert),
graph (Microsoft Graph sendMail, App-only) und smtp (mail()-Fallback).
- Graph-Versand um Anhänge erweitert (
#microsoft.graph.fileAttachment,
base64) — das Referenzprojekt kann das nicht. Nötig für die Badge-Mail.
Ist ein verlangter Anhang unlesbar oder größer als 3 MB, wird die Mail
nicht verschickt und nicht protokolliert, sondern ein Fehler geworfen:
Eine Badge-Mail ohne Badge wäre schlimmer als keine Mail.
- Mail-Vorlagen für alle sieben Anlässe (Eingangsbestätigung, intern,
Rückfrage, Genehmigung, Ablehnung, Widerruf, Badge) mit Platzhaltern.
ENTWURF — die Spezifikation mit den wörtlichen Texten liegt nicht vor; der
Fachbereich gibt sie frei, Anpassung später ohne Codeänderung über die
Einstellungen.
- Mail-Protokoll
/mails, nur für Board-Berechtigte (die Mails enthalten
personenbezogene Daten). Zeigt Art, Empfänger, Betreff, Text, Anhang und ob
tatsächlich versendet wurde.
- Self-Check prüft bei
mail.mode = graph, ob die Entra-Zugangsdaten gesetzt
sind — sonst schlüge jeder Versand still fehl (O11).
Behoben
curl_close()entfernt: seit PHP 8.0 wirkungslos, seit 8.5 als veraltet
gemeldet (der Zielserver läuft auf 8.4, lokal 8.5).
[0.3.0] — 2026-07-09
Hinzugefügt
- Öffentliches Formular
/akkreditierung(Umsetzungsschritt 3): alle Felder
aus Konzept Abschn. 4, serverseitige Validierung mit Re-Render und erhaltenen
Eingaben, Bestätigungsseite /danke.
- Upload-Prüfung: Dateiendung und echter MIME-Typ per
finfo. Eine als
ausweis.pdf getarnte PHP-Datei und ein als .jpg getarntes GIF werden beide
abgewiesen. Ablage unter Zufallsnamen in storage/uploads (Rechte 0640),
der Originalname existiert nur in der Datenbank.
- Spam-Schutz: Honeypot-Feld und Zeitfalle verwerfen still (der Bot erhält
dieselbe Seite wie ein Mensch), Rate-Limit von 5 Anträgen je IP und Stunde
antwortet mit 429.
- Antragsnummer erreicht die Bestätigungsseite über die Session, nicht über die
URL — sonst ließen sich fremde Nummern durch Hochzählen bestätigen.
- Veranstaltung als Auswahlliste mit Freitext-Fallback; ohne gepflegte Liste
erscheint direkt ein Freitextfeld.
Behoben
- CSRF-Prüfung ließ Anfragen ganz ohne Token durch. Bei frischer Session ist
$_SESSION['csrf'] leer, und hash_equals('', '') ergibt true. Fehlt das
Token in der Session, wird jetzt abgelehnt. (Derselbe Fehler steckt im
Referenzprojekt btv-Digitalisierungsanforderungen.)
- Zeitzone: PHP lief in UTC, MySQL in Ortszeit. Dadurch standen Eingangsdatum
und Zustimmungs-Zeitstempel zwei Stunden falsch in der Datenbank, und das
Rate-Limit verglich gegen ein verschobenes Zeitfenster — der Spam-Schutz wäre
im Betrieb wirkungslos gewesen. Die App setzt die Zeitzone jetzt selbst
(timezone in der Konfiguration), Zeitfenster berechnet PHP statt MySQL, und
der Self-Check meldet Abweichungen.
- Fehlermeldungen zu Uploads nannten den Dateinamen nicht: In
"„$name“ …"zieht
PHP das schließende Anführungszeichen in den Variablennamen (Bytes ≥ 0x80 sind
dort erlaubt). Variable geklammert.
- Aufräumen alter Rate-Limit-Einträge hat einen eigenen
catch-Block — vorher
hätte ein Fehler dabei die Bremse stillschweigend übersprungen.
[0.2.0] — 2026-07-09
Hinzugefügt
- Datenbankschema
sql/schema.sql(Umsetzungsschritt 2):requests,
request_counter, attachments, status_history, mails, settings,
rate_limit, users. Idempotent (CREATE TABLE IF NOT EXISTS).
- Antragsnummer
AKK-<Jahr>-<lfd>mit jahresweisem Zähler ab 0001.
Vergabe über ein einzelnes atomares INSERT … ON DUPLICATE KEY UPDATE
auf request_counter statt SELECT MAX(seq) … FOR UPDATE — damit keine
Deadlocks bei gleichzeitigen Anträgen. Mit 8 parallelen Prozessen à 25
Nummern geprüft: 200 eindeutige, lückenlose Nummern.
mailsträgt gegenüber dem Referenzprojekt zusätzlichrequest_id
(Mail-Historie je Antrag) und attachment_name (Badge-Mail). Beim Löschen
eines Antrags bleibt das Mail-Protokoll erhalten (ON DELETE SET NULL),
Nachweise und Statusverlauf verschwinden mit (ON DELETE CASCADE).
app/repository.phpmit Zählervergabe, Rate-Limit-Funktionen und
Statusverlauf. sql/cleanup_testdata.sql zum Zurücksetzen.
Geändert
- Self-Check prüft alle acht Tabellen. Die Livegang-Sperre fragt jetzt, ob ein
anmeldefähiges Konto existiert (aktiv, Passwort gesetzt) — die bloße
Existenz der leeren users-Tabelle genügt nicht mehr.
[0.1.0] — 2026-07-09
Hinzugefügt
- Gerüst nach dem Muster des Digitalisierungsportals (Umsetzungsschritt 1):
Front-Controller mit Routentabelle, helpers.php (config, CSRF, Session,
Flash, Changelog-Rendering), PDO-Singleton, settings.php, Zugriffs-Log.
- Landingpage
/mit Ablaufbeschreibung. Die noch offenen Dokumente
(Medienrichtlinien, SAFE-SPORT-Merkblatt, Datenschutz) werden als Text ohne
Link dargestellt, solange keine URL hinterlegt ist.
- Anmeldung Stufe 1: Demo-Login (Rollen-Umschalter ohne Passwort), hart an
app_env=dev gebunden und serverseitig geprüft. Echte Benutzerkonten folgen
in Umsetzungsschritt 9 — vorher kein Livegang.
- Self-Check
/selfcheck: PHP-Erweiterungen, Datenbank, Verzeichnisrechte,
Lage der Speicherverzeichnisse außerhalb des Webroots, Mail-Modus, offene
Platzhalter-Links. Meldet app_env=prod zusammen mit aktivem Demo-Login als
Fehler.
- Changelog-Seite
/changelog, Version wird aus dieser Datei gelesen. - BTV-Look über das UI-Kit (
btv-tokens.css+btv-ui.css), Web-Fonts
Harabara und Century Gothic aktiviert.
- Härtung:
.htaccesssperrtapp/,storage/undlib/; Session-Cookie
httponly/secure/SameSite=Lax.
Bekannte Lücken
- Kein Datenbankschema (Schritt 2), kein Formular (Schritt 3), keine Mails
(Schritt 4), kein Board (Schritt 5).