Docs Technische Dokumentation
Technische Dokumentation

FightReg — Entwicklerhandbuch

Architektur, API-Referenz, Datenbankschema, Frontend-System und Deployment-Anleitung für Entwickler.

PHP 8.5 MySQL 8 Vanilla JS SPA PWA Docker 59 DB-Tabellen
🧱

1. Tech Stack

Backend

SprachePHP 8.5 (Container und Server; composer.json verlangt ≥ 8.2)
Frameworkeigenes MVC, kein Laravel oder Symfony
DatenbankMariaDB 10.11, PDO — 103 Tabellen
AnmeldungJWT (HS256, fr_token), Einmalkennwort als zweiter Faktor
API611 Routen auf 474 Pfaden · 78 Controller · 53 Dienste
PDFTCPDF — Rechnungen, Quittungen, Urkunden, Karten, Listen
MailSMTP über die Konfiguration, Vorlagen je Veranstaltung
PrüfstandPHPUnit — 317 Tests, dazu 282 Renderfälle

Frontend

JSeigene SPA, kein React und kein Vue
DOMeigener Bauer h()
SymboleTabler-Icons über ic()
CSSeigenes Design-System: vier Themen, Abstands-Token, Schriftskalierung
SchriftDM Sans, selbst ausgeliefert
Sprachenelf, Ausdrucke folgen der Eventsprache
Dateien46 JS-Dateien, davon 40 Seiten

PWA und Betrieb

Service Workerversionierter Zwischenspeicher, aktuell v561
OfflineCache-First für JS, CSS und Anhänge
PushVAPID Web-Push — produktiv, mit Protokoll je Zustellung
AusliefernGitHub Actions, erst Probelauf, dann echt; Migrationen von Hand freigeben
EntwicklungDocker, ein Stack für alle Worktrees, eigenes Schema je Worktree
Externe DiensteElevenLabs, Anthropic Claude — je Veranstaltung hinterlegbar und abschaltbar
📐
Alle Zahlen am Stand nach Sprint 169 im Repository erhoben — 183 Migrationen, 67 Berechtigungen, 33 Prüfläufe.
🏗

2. Architektur

Backend — MVC-Schichten

Request
  └── public/api.php          ← Entry Point, Route-Dispatch
        └── Router             ← URL-Matching, Wildcard-Params
              └── Middleware   ← CorsMiddleware, AuthMiddleware (JWT + RBAC)
                    └── Controller
                          ├── Service(s)    ← Business Logic
                          └── Repository    ← DB-Abfragen (PDO)

Frontend — SPA-Schichten

public/index.php         ← Shell HTML
  └── app.js               ← App-Bootstrap, Router, Auth-State (App.*)
        ├── core.js         ← api.get/post/patch(), t(), ic(), showToast()
        ├── components.js   ← Shared UI-Komponenten, pageHeader(), mkSearchBar()
        └── pages/*.js      ← Page-Module (admin.js, trainer.js, brackets.js …)

Auth-Flow

  1. POST /api/auth/login → Server prüft Credentials → gibt JWT zurück
  2. Frontend speichert JWT in localStorage unter Key fr_token
  3. Jeder API-Request: Authorization: Bearer <token>
  4. AuthMiddleware::authenticate() dekodiert Token, gibt User-Objekt zurück
  5. requirePermission($user, 'modul.aktion', $compId) prüft RBAC
📂

3. Ordnerstruktur

fightReg/
├── config.php                  ← DB, SMTP, JWT-Secret, API-Keys
├── install.php                 ← DB-Setup + Seed (Erstinstallation)
├── composer.json               ← PSR-4 Autoload: FightReg\
├── migrations/                 ← SQL-Migrations (000–069)
├── public/
│   ├── api.php                 ← REST API Entry Point
│   ├── index.php               ← SPA Shell
│   ├── sw.js                   ← Service Worker (v561)
│   ├── manifest.json           ← PWA Manifest
│   ├── locales/
│   │   ├── de.json             ← Deutsche Übersetzungen
│   │   └── en.json             ← Englische Übersetzungen
│   └── assets/
│       ├── css/app.css         ← Design System
│       ├── js/
│       │   ├── app.js          ← App Bootstrap
│       │   ├── core.js         ← Utilities
│       │   ├── components.js   ← Shared Components
│       │   └── pages/          ← Page Modules (~20 Dateien)
│       └── fonts/              ← DM Sans self-hosted
├── src/
│   ├── Config/
│   │   ├── Database.php        ← PDO Singleton
│   │   └── Permissions.php     ← 67 Permission-Slugs (SSOT)
│   ├── Controllers/            ← ~50 Controller-Klassen
│   ├── Services/               ← ~20 Service-Klassen
│   ├── Repositories/           ← ParticipantRepository, CategoryRepository
│   ├── Middleware/             ← AuthMiddleware, CorsMiddleware
│   ├── Helpers/                ← Response, Validator
│   └── Router.php
├── storage/
│   ├── invoices/               ← PDF-Rechnungen
│   └── tts/                    ← Gecachte MP3-Ansagen
├── templates/email/            ← Mail-Templates (DB-basiert)
└── tests/                      ← PHPUnit Test-Suite
🖥

4. Frontend-System

h() — DOM-Builder

FightReg verwendet einen custom h() DOM-Builder statt Template-Strings:

// ✅ Richtig
const btn = h('button', {class:'btn btn-primary', onClick: handler}, 'Speichern');
parent.appendChild(btn);

// ❌ Falsch — ic() gibt DOM-Element zurück, nicht String!
element.innerHTML = ic('check') + ' Gespeichert';

ic() — Tabler Icons

// ic(name, size?, color?) → gibt SVG DOM-Element zurück
const icon = ic('check', 16, '#22c55e');
container.appendChild(icon);

// In h() inline möglich:
h('span', {}, [ic('arrow-right', 14), ' Weiter'])

api.get / api.post / api.patch

// Auth-Token wird automatisch gesetzt (fr_token)
const data = await api.get('/api/competitions');
const res  = await api.post('/api/participants', {name: 'Max'});
const upd  = await api.patch('/api/participants/42', {weight: 68});

// Fehlerbehandlung
try {
  const data = await api.get('/api/something');
} catch (err) {
  showToast('✗ Fehler: ' + err.message);
}

t() — Übersetzungen

// Übersetzungskey aus locales/de.json oder en.json
const label = t('participants.add');

// Mit Interpolation
const msg = t('competition.created', {name: comp.name_de});

showToast() — Benachrichtigungen

// Immer nur Text oder Emoji — ic() NICHT direkt verwenden!
showToast('✓ Gespeichert', 'success');   // grün
showToast('✗ Fehler beim Laden', 'error'); // rot
showToast('ℹ Information', 'info');       // blau

Design System — CSS-Variablen

/* Spacing-Tokens (skalieren mit --fs) */
padding: var(--sp-16);   /* calc(16px * var(--fs)) */
gap: var(--sp-8);

/* Font-Size (IMMER mit var(--fs) skalieren!) */
font-size: calc(14px * var(--fs));

/* Farben */
color: var(--primary);         /* Akzentfarbe */
color: var(--danger);          /* NICHT --primary für Fehler! */
background: var(--bg);         /* Hintergrund */
border-color: var(--border);

Wichtige Anti-Patterns

🚫
localStorage Key ist fr_token — niemals token.
showToast() triggert render() — State immer VOR showToast() setzen.
ic() gibt DOM zurück — nie in String-Kontext verwenden.
Globale Variablen — nie doppelt in mehreren Dateien deklarieren.
⚙️

5. PWA & Service Worker

Der Service Worker nutzt eine versionierte Cache-Strategie. Bei Änderungen an JS/CSS-Dateien muss die Version erhöht werden.

// public/sw.js — Cache-Version erhöhen bei JS-Änderungen
const CACHE_NAME = 'fightreg-v561';   // ← bei jeder JS- oder CSS-Aenderung erhoehen
const ASSETS = [
  '/assets/js/app.js',
  '/assets/js/core.js',
  '/assets/js/components.js',
  // alle page-JS-Dateien...
  '/assets/css/app.css',
];
⚠️
Vergessene SW-Version → deployed Fixes scheinen bei Nutzern nicht anzukommen (stale Cache). Bei jeder JS-Änderung im Sprint die Version inkrementieren.
🔑

6. Auth & JWT

Token-Struktur

{
  "user_id": 42,
  "email": "admin@example.com",
  "role": "admin",
  "competition_ids": [1, 5, 12],  // zugewiesene Wettkämpfe
  "iat": 1710000000,
  "exp": 1710086400               // 24h Laufzeit
}

AuthMiddleware

// JWT prüfen + User-Objekt zurückgeben
$user = AuthMiddleware::authenticate();

// Einfache Rolle prüfen
AuthMiddleware::requireRole($user, ['admin', 'super_admin']);

// Permission + optionaler Wettkampf-Kontext
AuthMiddleware::requirePermission($user, Permissions::BRACKETS_EDIT, $compId);

// Wettkampf-Zugang prüfen (NUR admin/super_admin!)
// ⚠️ NICHT für billing-Routen verwenden!
AuthMiddleware::requireCompetitionAccess($user, $compId);
🔐

7. RBAC-System

Das RBAC-System arbeitet mit der Klasse Permissions.php als Single Source of Truth (67 Slugs in 20 Modulen, verteilt auf zwölf Rollen).

// src/Config/Permissions.php
class Permissions {
  const BRACKETS_EDIT       = 'brackets.edit';
  const BRACKETS_SCORE      = 'brackets.score';
  const BILLING_VIEW        = 'billing.view';
  // ... 67 Slugs insgesamt
}

// Backend-Check
AuthMiddleware::requirePermission($user, Permissions::BILLING_SEND, $compId);

// Frontend-Check (JS)
if (can('billing.send')) {
  // Button anzeigen
}
🔍
RBAC-Audit: GET /api/system/rbac-audit vergleicht alle Permissions.php-Slugs mit den DB-Einträgen und zeigt Inkonsistenzen.
🎛

8. Controller (Übersicht)

78 Controller. Die Liste ist aus src/Controllers erzeugt — von Hand gepflegt wäre sie nach zwei Sprints unvollständig.

KlasseZuständigkeit
AiControllerKI-Schlüssel und Modellwahl
AnalyticsControllerAuswertungen und Nutzungszahlen
AnnouncementQueueControllerWarteschlange der Hallenansage
ApiDocsControllerOpenAPI-Beschreibung ausliefern
AreaControllerFlächen, Status, Belegung, Ergebnisse
AthleteBookingControllerSelbstbuchung des Sportlers
AthleteControllerAthletenkonto und eigene Daten
AuthControllerAnmeldung, Registrierung, Token, OAuth
BackupControllerSicherungen
BaseChatControllergemeinsamer Unterbau der Chat-Endpunkte
BibControllerStartnummern-Vorrat und Vergabe
BookingControllerBuchungen je Wettkampf
BookingTeamControllerMannschaftsbuchungen
BracketControllerBäume erzeugen und verwalten
BracketFightorderControllerKampfreihenfolge
BracketMatchControllereinzelne Kämpfe und Ergebnisse
BracketSettingsControllerEinstellungen je Baum
BracketStandingsControllerTabellen im Rundenspiel
CardControllerTeilnehmerkarte
CardManifestControllerWallet-Manifest der Karte
CategoryControllerKategorien, Zusammenlegen, Verlauf
CategoryImportControllerKategorien aus Datei einlesen
CertificateControllerUrkunden erzeugen
CertificateTemplateControllerUrkundenvorlagen
ChatControllerKI-Assistent für Veranstalter
CheckinControllerCheck-in, Wiegen, Dokumente
CoachControllerCoaches und ihre Vereine
CompetitionAdminControllerWettkampf-Admins
CompetitionControllerWettkämpfe, Kennzahlen, Dashboard
CompetitionEnrollmentControllerMeldung von Verein und Sportler
ConsentControllerEinwilligungen und Protokoll
EligibilityControllerStartberechtigung je Kategorie
EventDataInviteControllerEinladung zur Datenpflege
FederationControllerVerbände und Zuordnungen
FightCallControllerKampfaufruf
FightScoreControllerKampfwertung, Runden, Fahnen
FileControllerDateien, Fotos, Dokumente
FormsControllerFormen: Sitzungen und Wertung
ImportControllerImport von Teilnehmern und Listen
InvoiceControllerRechnungen
InvoiceDesignControllerBelegbild je Rechnungssteller
JudgeControllerKampfrichter-Ansicht
ListPdfControllerListen und Aushänge als PDF
MailTemplateControllerMailvorlagen und Erscheinungsbild
McpControllerMCP-Schnittstelle
MonitoringControllerZustand des Systems
NotificationControllerBenachrichtigungen und Abos
OrganizerControllerVeranstalter-Stammdaten
OtpControllerEinmalkennwort
ParticipantCardControllerKarte über öffentlichen Token
ParticipantControllerTeilnehmer, Athleten-ID, Rückzug
PaymentControllerZahlungen und Quittungen
PayoutRecipientControllerEmpfänger von Auszahlungen
PayoutReportControllerAuszahlungsübersicht
PlanControllerTarif und Kontingente
PricingRuleControllerPreisregeln
QrControllerQR-Codes
RankingControllerRangliste
RankingProfileControllerPunkteprofile der Rangliste
RefereeControllerKampfrichterverwaltung und Meldung
RingControllerRinge und Zuordnung
RoleControllerRollen und Rechte
SchoolControllerVereine und Schulen
ScreenControllerBildschirme, Warteschlange, Token
SettingsControllerEinstellungen je Veranstaltung
SponsorControllerSponsoren, Flächen, Nachweis
StaffControllerMitarbeiter und Einzelrechte
StartlistControllerStartlisten und Freigabe
SurchargeDiscountControllerAufschläge und Rabatte
TrainerChatControllerChat für Trainer
TransferControllerVereinswechsel
TranslationPageControllerÜbersetzungsseite
TtsAnnouncementControllerAnsagetexte
TtsControllerSprachausgabe und Kontingent
TtsSettingsControllerStimmen, Vorlagen, Pausen
UploadControllerHochgeladene Dateien
UserControllerBenutzerverwaltung
VenueMapControllerHallenplan und Objekte
⚙️

9. Services

53 Dienste, erzeugt aus src/Services. Ein Dienst hält fachliche Regeln, die mehr als ein Controller braucht.

KlasseZuständigkeit
AnsageReihenfolgeReihenfolge in der Ansage-Warteschlange
ApiKeyServicewelcher Schlüssel gerade gilt
AthleteIdServiceAthleten-ID vergeben
AuditServiceProtokoll sicherheitsrelevanter Änderungen
AuthServiceToken, Passwörter, Registrierung
BenachrichtigungBenachrichtigungen bündeln
BibServiceStartnummern-Vorrat und Zustände
BookingTeamServiceMannschaften und Seeding
BracketServiceBäume erzeugen, KO und Rundenspiel
CacheServiceZwischenspeicher
CardPdfServiceTeilnehmerkarte als PDF
CategoryImportServiceKategorien einlesen und abgleichen
CertificateServiceUrkunden setzen
ChatServiceWerkzeuge des KI-Assistenten
CircuitBreakerServiceSchutzschalter für externe Dienste
CoachServiceCoaches und Vereinsbindung
DeadlineServiceFristen
DemoMailSperrekein echter Mailversand aus Demos
EligibilityServiceAlter, Gewicht, Graduierung prüfen
Empfaengerkreiswer eine Nachricht bekommt
FederationMembershipServiceMitgliedschaft im Verband
FederationServiceVerbandsstruktur
FightCallServiceKampfaufruf auslösen
FightScoreServiceWertung, Runden, Fahnen
FormsServiceFormen-Sitzungen
FrPdfgemeinsamer PDF-Unterbau
InvoiceAttachmentServiceAnhänge an Rechnungen
InvoiceDesignServiceBelegbild ermitteln
InvoicePdfServiceRechnung als PDF
ListenfreigabeFreigabe der Startlisten
ListPdfServiceListen als PDF
LogServiceProtokolle
MailServiceMailversand und Vorlagen
MergeServiceKategorien zusammenlegen
OtpServiceEinmalkennwörter
PdfLayoutTraitwiederkehrende PDF-Bausteine
PermissionServiceRolle zu Berechtigungen auflösen
PlanServiceTarif und Grenzen
PricingServicePreis berechnen
PrintLanguageServiceSprache des Ausdrucks
PushServiceWeb-Push und Zustellung
RankingServicePunkte der Rangliste
RbacAuditServiceRechte gegen die Datenbank prüfen
ReceiptPdfServiceQuittung als PDF
RedisServiceRedis, sofern vorhanden
RegistrationPolicyist die Anmeldung offen
RoleCatalogServiceRollenkatalog
SponsorFlaechenBuchung je Sponsorenfläche
TeilnehmerFotoTeilnehmerfotos
TrainerChatServiceChat für Trainer
TranslationHelperFeld in der richtigen Sprache
UebersetzungsStandStand der Übersetzungen
UiTranslationServiceOberflächentexte aus der Datenbank
🗄

10. Repository-Layer

Vier Repositories plus Schnittstelle: ParticipantRepository, CategoryRepository, SchoolRepository, BookingRepository.

ℹ️
Der Rest der Controller greift weiterhin unmittelbar über Database::getInstance() zu. Das ist bekannt und steht als offener Punkt in der Liste — kein Versehen, sondern eine Reihenfolge.
🔀

11. Router

// public/api.php — Routen registrieren
$router->get('/api/competitions',           [CompetitionController::class, 'index']);
$router->post('/api/competitions',          [CompetitionController::class, 'store']);
$router->get('/api/competitions/{id}',      [CompetitionController::class, 'show']);
$router->patch('/api/competitions/{id}',    [CompetitionController::class, 'patch']);
$router->delete('/api/competitions/{id}',   [CompetitionController::class, 'destroy']);

// Wichtig: Literale Pfade VOR Wildcard-Routen!
$router->get('/api/rankings/global-points', [RankingController::class, 'getGlobalPoints']);  // ✅ zuerst
$router->get('/api/rankings/athlete/{pid}', [RankingController::class, 'athleteDetail']);    // ✅ dann

// Request-Body auslesen
$body = Router::getBody();  // json_decode(file_get_contents('php://input'), true)
📊

12. Datenbankschema — Übersicht

FightReg nutzt 59 Tabellen in MySQL 8. Alle FK-Spalten nutzen INT UNSIGNED. Soft-Delete via Status-Felder (kein physisches Delete außer purge).

Kernentitäten und ihre Beziehungen

organizer ──────────────────────────────────────────┐
competitions  (53 cols)                              │
  ├── FK organizer_id → organizer                   │
  ├── FK invoice_issuer_id → invoice_issuer         │
  ├── competition_schools  (Schul-Anmeldung)        │
  ├── competition_participants (TN-Anmeldung)       │
  ├── competition_areas → rings → ring_categories   │
  ├── brackets → matches                            │
  ├── main_categories → sub_categories              │
  └── area_category_assignments

schools (19 cols)
  ├── trainers → users (role: trainer)
  └── participants (23 cols)
        ├── FK user_id (Athlet-Account)
        ├── athlete_id (Format: CC-SC-NNNNN)
        └── bookings (sub_category_id)

users (18 cols)
  └── FK role_id → roles → role_permissions → permissions
📝

13. Migrations

Jede Schemaänderung ist eine nummerierte SQL-Datei unter migrations/, idempotent geschrieben. Stand: 183. Was eingespielt ist, steht in schema_migrations — nicht im Gedächtnis.

# einspielen und nachsehen — beide Datenbanken
.\fr.ps1 migrate          # fightreg
.\fr.ps1 migrate-test     # fightreg_test
.\fr.ps1 migrate-status   # welche Datei fehlt wo?

# Jede neue Spalte zusaetzlich in install.php eintragen,
# sonst fehlt sie in jeder frischen Installation.
⚠️
Fremdschlüsselnamen führt InnoDB schemaweit, nicht je Tabelle. Ein Kürzel wie fk_fs_area kollidiert mit einer anderen Tabelle, die dasselbe Kürzel gewählt hat — die Meldung lautet dann errno 121 und klingt nach einem doppelten Datensatz. Der Name trägt deshalb den Tabellennamen aus.
ℹ️
Eine neue Spalte wird im PHP defensiv gelesen ($row['neu'] ?? null): deployter Code steht gelegentlich vor seiner Migration, und eine Notiz mitten in der JSON-Antwort zerlegt sie.
📋

14. Alle Tabellen (103)

Alle Tabellen aus install.php, mit der Zahl ihrer Spalten. Zum Aufklappen anklicken.

🌐

15. API-Referenz — Übersicht

Alle Endpunkte unter /api/. Auth via JWT Bearer-Token erforderlich (außer Public-Endpunkte). Response-Format: JSON.

Response-Format

// Erfolg
{ "success": true, "data": {...}, "message": "OK" }

// Fehler
{ "success": false, "error": "Fehlermeldung", "code": 422 }

// HTTP-Status-Codes
200 OK · 201 Created · 400 Bad Request · 401 Unauthorized
403 Forbidden · 404 Not Found · 422 Validation Error · 500 Server Error
📐
611 Routen auf 474 Pfaden — 236 GET, 170 POST, 87 DELETE, 61 PATCH, 57 PUT. Jede neue Route bringt ihren Eintrag in src/OpenApi/ mit; 156 Bestandsrouten haben noch keinen, und diese Zahl darf nur fallen.
🔑

16. API — Auth-Endpunkte

MethodePfadBeschreibungAuth
POST/api/auth/loginLogin → JWT
POST/api/auth/registerTrainer-Registrierung
POST/api/auth/register/athleteAthlet-Registrierung mit Athlet-ID
POST/api/auth/refreshJWT erneuernJWT
GET/api/auth/meEigenes User-ProfilJWT
PUT/api/auth/profileProfil aktualisierenJWT
PUT/api/auth/passwordPasswort ändernJWT
POST/api/auth/forgot-passwordReset-Link per Mail
POST/api/auth/reset-passwordPasswort mit Reset-Token setzen
GET/api/auth/permissionsEigene Permissions ladenJWT
POST/api/auth/oauthOAuth Login (extern)
DELETE/api/auth/accountAccount löschenJWT
🏆

17. API — Wettkämpfe

MethodePfadBeschreibung
GET/api/competitionsAlle Wettkämpfe auflisten
POST/api/competitionsNeuer Wettkampf
GET/api/competitions/{id}Wettkampf-Details
PUT/api/competitions/{id}Wettkampf vollständig aktualisieren
PATCH/api/competitions/{id}Wettkampf teilweise aktualisieren
DELETE/api/competitions/{id}Wettkampf löschen
POST/api/competitions/{id}/duplicateWettkampf duplizieren
GET/api/competitions/{id}/statsStatistiken
GET/api/competitions/{id}/dashboardDashboard-Daten
GET/api/competitions/{id}/schoolsAngemeldete Schulen
POST/api/competitions/{id}/enrollSchule anmelden
PATCH/api/competitions/{id}/schools/{sid}Schul-Anmeldung bestätigen/ablehnen
GET/api/competitions/{id}/participantsAngemeldete TN
POST/api/competitions/{id}/participantsTN anmelden
DELETE/api/competitions/{id}/participants/{pid}TN abmelden
GET/api/competitions/{id}/categoriesKategorien des Wettkampfs
GET/api/competitions/{id}/bookingsAlle Buchungen
GET/api/competitions/{id}/adminsWettkampf-Admins
POST/api/competitions/{id}/adminsAdmin hinzufügen
GET/api/competitions/{id}/rankingsWettkampf-Ranking
GET/api/competitions/{id}/fightorderKampfreihenfolge
PATCH/api/competitions/{id}/fightorder/reorderReihenfolge ändern
GET/api/competitions/{id}/surchargesAufschläge
GET/api/competitions/{id}/discountsRabatte
👥

18. API — Teilnehmer & Schulen

MethodePfadBeschreibung
GET/api/participantsAlle TN (Admin)
POST/api/participantsNeuer TN
PUT/api/participants/{id}TN aktualisieren
DELETE/api/participants/{id}TN löschen
PATCH/api/participants/{id}/assign-athlete-idAthlet-ID manuell zuweisen
PATCH/api/participants/{id}/ai-excludeKI-Ausschluss toggle
PATCH/api/participants/{id}/self-payerSelbstzahler toggle
GET/api/participants/{id}/eligible-categoriesGeeignete Kategorien
POST/api/participants/{id}/inviteEinladungsmail senden
GET/api/participant-card/{token}Öffentliche TN-Karte (kein Auth)
GET/api/schoolsAlle Schulen
PUT/api/schools/{id}Schule bearbeiten
PATCH/api/schools/{id}/approveSchule freischalten
POST/api/schools/{id}/bulk-assign-athlete-idsBulk Athlet-IDs vergeben
POST/api/import/participantsCSV/Excel-Import
GET/api/import/templateImport-Vorlage herunterladen
🥊

19. API — Brackets & Matches

MethodePfadBeschreibung
GET/api/competitions/{id}/bracketsBrackets eines Wettkampfs
POST/api/bracketsBracket erstellen
GET/api/brackets/{id}Bracket-Details inkl. Matches
POST/api/brackets/{id}/generateBracket generieren (Seeding → Matches)
PATCH/api/brackets/{id}Bracket-Metadaten aktualisieren
DELETE/api/brackets/{id}Bracket löschen
PATCH/api/brackets/{id}/activateBracket aktivieren
PATCH/api/brackets/{id}/arenaArena/Matte zuweisen
PATCH/api/brackets/{id}/swapTN im Bracket tauschen
PATCH/api/bracket-matches/{id}/resultMatch-Ergebnis eintragen
PATCH/api/bracket-matches/{id}/revertMatch-Ergebnis zurücksetzen
GET/api/brackets/{id}/final-standingsPlatzierungen nach Abschluss
POST/api/brackets/{id}/next-roundNächste Runde starten (RR)
💶

20. API — Billing & Rechnungen

MethodePfadBeschreibung
GET/api/competitions/{id}/invoicesRechnungen des Wettkampfs
POST/api/competitions/{id}/invoicesRechnung generieren
GET/api/invoices/{id}Rechnungsdetails
GET/api/invoices/{id}/pdfPDF herunterladen
POST/api/invoices/{id}/sendRechnung per Mail senden
PATCH/api/invoices/{id}/statusRechnungsstatus setzen
GET/api/competitions/{id}/pricing-rulesPreisregeln
POST/api/competitions/{id}/pricing-rulesPreisregel anlegen
GET/api/invoice-issuerRechnungssteller
PUT/api/invoice-issuerRechnungssteller speichern
🔊

21. API — TTS & Ansagen

MethodePfadBeschreibung
POST/api/tts/generateText → MP3 via ElevenLabs
GET/api/tts/settingsTTS-Einstellungen
PUT/api/tts/settingsTTS-Einstellungen speichern
GET/api/tts/templatesAnsage-Templates
PUT/api/tts/templatesTemplates speichern
GET/api/tts/historyAnsage-Verlauf
GET/api/competitions/{id}/announcementsAnsagen eines Wettkampfs
POST/api/competitions/{id}/announcementsNeue Ansage
👔

22. API — Staff & RBAC

MethodePfadBeschreibung
GET/api/competitions/{id}/staffStaff-Liste
POST/api/competitions/{id}/staffStaff hinzufügen
PUT/api/competitions/{id}/staff/{sid}Staff aktualisieren
DELETE/api/competitions/{id}/staff/{sid}Staff entfernen
GET/api/competitions/{id}/staff/matrixPermission-Matrix
GET/api/competitions/{id}/staff/{sid}/permissionsIndividual-Permissions
PUT/api/competitions/{id}/staff/{sid}/permissionsPermissions setzen
GET/api/rolesAlle Rollen
POST/api/rolesRolle erstellen
PUT/api/roles/{id}/permissionsRollen-Permissions setzen
GET/api/system/rbac-auditRBAC-Konsistenzprüfung
GET/api/usersAlle Benutzer
POST/api/users/createBenutzer anlegen (OTP)
DELETE/api/users/{id}Benutzer löschen
📦

23. API — Weitere Endpunkte

ModulMethodePfadBeschreibung
ChatPOST/api/chatKI-Chat Nachricht senden
ChatGET/api/chat/suggestionsVorschläge laden
ChatGET/api/chat/logsChat-Logs einsehen
RankingGET/api/rankingsGlobales Ranking
RankingGET/api/rankings/athlete/{pid}Athlet-Detail-Ranking
FederationsGET/api/federationsVerbandsliste
FederationsGET/api/federations/treeVerbandshierarchie
ConsentPOST/api/consentDSGVO-Zustimmung protokollieren
CheckinGET/api/competitions/{id}/checkinCheck-in-Liste
CheckinPOST/api/competitions/{id}/checkin/{bid}Check-in durchführen
MailGET/api/mail-designGlobales Mail-Design
MailGET/api/mail-templatesAlle Mail-Templates
ScreenGET/api/screen/{id}Hauptscreen-Daten
DeadlineGET/api/deadline?competition_id={id}Anmeldefrist prüfen
AthleteGET/api/athlete/meEigenes Athlet-Profil
UploadPOST/api/schools/{id}/logoSchul-Logo hochladen
UploadPOST/api/participants/{id}/photoTN-Foto hochladen
🔊

24. ElevenLabs TTS-Integration

// config.php
$config['elevenlabs']['api_key'] = 'your-api-key';
$config['elevenlabs']['voice_id'] = 'voice-id';

// TtsController — Ablauf
1. Text aufbereiten (Templates + Normalisierungsregeln)
2. MD5-Hash des Texts → Cache-Key
3. Cache prüfen: storage/tts/{hash}.mp3 vorhanden?
   - Ja → MP3 direkt streamen
   - Nein → ElevenLabs API aufrufen → MP3 speichern → streamen
4. Frontend: Audio-API spielt MP3 ab
Caching spart API-Kosten und ermöglicht sofortige Wiederholung von Ansagen. Cache liegt in storage/tts/.
🤖

25. KI-Chat-Integration

Der KI-Assistent verwendet Anthropic Claude mit Tool-Use-Loop. 8 vordefinierte Tools ermöglichen sichere DB-Abfragen.

// ChatController → ChatService
1. System-Prompt mit Turnierkontext aufbauen (buildSystemPrompt)
2. User-Nachricht + Tool-Definitionen an Anthropic API senden
3. Modell kann bis zu N Tool-Use-Runden durchführen
4. Jedes Tool ist eine parametrisierte SQL-Query (competition_id-gebunden)
5. Finale Antwort → User

// Tools (Auswahl)
- search_participants(name, competition_id)
- get_category_stats(category_id)
- list_checked_in(competition_id)
- get_bracket_status(bracket_id)
- list_schools(competition_id)

// Modell: claude-sonnet-4-20250514
// Alternativ: OpenAI (gleiche Tool-Definitionen)
📄

26. TCPDF — Rechungs-PDF

// InvoicePdfService nutzt TCPDF (Vendor-Bibliothek)
// Output: storage/invoices/INV-{id}-{timestamp}.pdf

// Features
- Firmenlogo (base64 eingebettet)
- Gesetzeskonforme Rechnungsstruktur
- Auflistung aller Buchungen als Positionen
- Netto/Brutto/MwSt.-Berechnung
- QR-Code für Zahlungsreferenz (optional)
🐳

27. Docker Dev-Setup

# docker-compose.yml (Kurzversion)
services:
  app:
    image: php:8.1-apache
    volumes: [./:/var/www/html]
    ports: ["8080:80"]
  db:
    image: mysql:8.0
    environment:
      MYSQL_DATABASE: fightreg
      MYSQL_ROOT_PASSWORD: secret
  mailpit:
    image: axllent/mailpit
    ports: ["8025:8025"]   # Mail-UI
  phpmyadmin:
    image: phpmyadmin/phpmyadmin
    ports: ["8081:80"]

# Starten
docker-compose up -d

# Datenbank initialisieren
curl http://localhost:8080/install.php
⚠️
Das Projekt liegt auf OneDrive — Sync-Konflikte möglich. Vor dem Arbeiten sicherstellen, dass alle Dateien synchron sind.
🧪

28. Testing

Ein Sprint gilt als fertig, wenn der Prüfstand grün ist — und test-all läuft vor dem Commit, nicht danach.

.\fr.ps1 test-all              # alle Laeufe, danach ein Bericht
                               # -> tests/berichte/-.json
.\fr.ps1 test-all nur=render   # Teillauf: nur render, der Rest wird uebernommen
make test-all                  # dasselbe unter Linux und macOS

33 Läufe: render, klassen-check, rauchtest, ladeschleife, theme-check, beschriftung-check, semantik-check, sprach-check, skalierung-check, dateien-check, dubletten-check, funktionsnamen-check, objektschluessel-check, eingabefarbe-check, farbpaar-check, dialog-inventur, dialog-oeffnung, seiten-bauen, kontrast, wirkung, ikonen, kontrast-360, wirkung-360, ikonen-360, rbac, routen, pfade, zeiten, schema, sprachen, joins, test-kurz, punkte-beruehrt

Dazu 317 PHPUnit-Tests und 282 Renderfälle. Jede neue Ansicht bekommt einen Fall in tests/render/cases.cjs — eine Ansicht ohne Fall ist für jede Prüfung unsichtbar: nicht rot, sondern abwesend.

ℹ️
Ein Teillauf übernimmt die übrigen Zeilen aus dem letzten Bericht — erlaubt nur, wenn seit dessen Messung keine Datei aus dem Messbereich jenes Laufs angefasst wurde. Die Bereiche stehen in tests/laeufe.json, und eine Datei in keinem Bereich gilt als von jedem Lauf gemessen.
🚀

29. Deployment

# Deployment-Prozess
1. Geänderte Dateien in ZIP mit fightReg/ Unterordner paketieren
2. Per FTP auf fightreg.org hochladen
3. ZIP entpacken (überschreibt alte Dateien)
4. Migrations in phpMyAdmin ausführen (falls vorhanden)
5. Service Worker Version in sw.js prüfen/erhöhen

# prepare_claude_project.py
# Master-Script: FTP Download → Filter → Upload
# 88% File-Reduktion (642 → 77 Dateien) via 22 Exclusion Patterns

# Wichtig: config.php enthält Produktions-Secrets
# → NIEMALS in ZIP/Git einschließen!

30. Wichtige Patterns & Gotchas

_saveEvent() / rd() / rb() Pattern

// ✅ Richtig: rd() fällt auf SD.comp zurück wenn DOM-Element fehlt
const name = rd('competition-name', SD.comp.name_de);
const active = rb('competition-active', SD.comp.is_active);

// ❌ Falsch: überschreibt DB-Wert mit null wenn Element nicht im DOM
const name = document.getElementById('competition-name')?.value || null;

showToast triggert render()

// ✅ State VOR showToast setzen
SD.comp.name_de = newName;
showToast('✓ Gespeichert');    // render() läuft hier — State muss aktuell sein

// ❌ State NACH showToast setzen → wird von render() überschrieben
showToast('✓ Gespeichert');
SD.comp.name_de = newName;    // zu spät!

JSON.parse() Absicherung

// ✅ Immer absichern
const langs = JSON.parse(App.comp.languages || '[]');
const arr = Array.isArray(langs) ? langs : [];

// ❌ Direktes .filter() kann crashen
JSON.parse(App.comp.languages).filter(l => l.active)

Route-Reihenfolge in api.php

// ✅ Literal vor Wildcard
$router->get('/api/rankings/global-points', [...]);  // zuerst!
$router->get('/api/rankings/athlete/{pid}', [...]);
$router->get('/api/rankings', [...]);

// ❌ Wildcard zuerst → verschluckt alle nachfolgenden Routen
$router->get('/api/rankings/{id}', [...]);          // zu früh!

description_de vor GROUP BY bereinigen

// PHP: Ordnungszahl-Präfixe entfernen vor Aggregation
$name = preg_replace('/^\d+\.\s*Kategorie\s*/i', '', $description_de);
// "1. Kategorie Kata Einzel" → "Kata Einzel"
🔌

31. MCP Server & OAuth 2.0 / PKCE

FightReg exponiert einen vollständigen Model Context Protocol (MCP) Server unter /mcp. Claude Desktop und andere MCP-kompatible Clients können damit direkt auf Live-Turnierdaten zugreifen.

Endpunkte

MethodeURLBeschreibung
GET / POST/mcpMCP JSON-RPC Endpunkt (SSE + HTTP)
GET/authorizeOAuth 2.0 Authorization Endpoint (PKCE)
POST/tokenOAuth 2.0 Token Exchange
GET/api/mcp/keysAPI-Keys verwalten (Super-Admin)

OAuth 2.0 / PKCE Flow

1. Client generiert code_verifier (zufällig, 43–128 Zeichen)
   code_challenge = BASE64URL(SHA256(code_verifier))

2. GET /authorize?
     client_id=<uuid>
     &redirect_uri=<url>
     &code_challenge=<challenge>
     &code_challenge_method=S256
     &scope=read            (oder: read write)
     &state=<random>

3. Benutzer loggt sich ein → Bestätigung
   → Redirect zu redirect_uri?code=<auth_code>&state=<state>

4. POST /token
     code=<auth_code>
     &code_verifier=<verifier>
     → { access_token, token_type: "Bearer", scope }

5. MCP-Requests mit Header:
     Authorization: Bearer <access_token>
🔒
Auth-Codes sind 5 Minuten gültig. Access-Tokens haben kein serverseitiges Ablaufdatum, können aber über System → MCP-Keys widerrufen werden.

Verfügbare MCP-Tools

ToolScopeBeschreibung
list_competitionsreadAlle aktiven Wettkämpfe auflisten (Einstiegspunkt)
search_participantsreadTeilnehmer nach Name suchen
get_categoriesreadKategorien eines Wettkampfs abrufen
get_schoolsreadAngemeldete Schulen abrufen
get_competition_statsreadStatistiken: TN-Zahlen, Check-in, Kategorien
get_bracket_statusreadBracket-Status und Ergebnisse
get_area_statusreadWettkampfflächen-Status mit laufenden Kämpfen
get_bookingsreadBuchungen mit Filtern abrufen
get_financial_summaryreadFinanzübersicht: Umsatz, offene Rechnungen
suggest_categoriesreadPassende Kategorien für einen TN vorschlagen
book_participantwriteTeilnehmer in Kategorie buchen (Batch)
list_my_competitionsreadEigene Wettkämpfe (school-scoped)
search_my_participantsreadEigene TN suchen (school-scoped)
get_my_bookingsreadEigene Buchungen
get_eligible_categoriesreadPassende Kategorien für eigene TN
get_my_areasreadArenen-Zuweisung eigener TN
get_my_bracketsreadBracket-Ergebnisse eigener TN
get_my_fightorderreadKampfreihenfolge eigener TN

API-Keys verwalten

// DB-Tabelle: mcp_api_keys
// Felder: id, name, key_hash, key_prefix, user_id, scopes (JSON), is_active, expires_at

// Key anlegen via API
POST /api/mcp/keys
{ "name": "Claude Desktop", "scopes": ["read"] }
→ { "key": "fr_live_...", "key_prefix": "fr_live_xxx", "scopes": ["read"] }

// Key widerrufen
PATCH /api/mcp/keys/{id}  { "is_active": false }
DELETE /api/mcp/keys/{id}
⚠️
Der vollständige API-Key wird nur einmalig bei der Erstellung zurückgegeben. Er wird als bcrypt-Hash gespeichert und ist danach nicht mehr lesbar.

MCP Discovery (401 Flow)

// Unauthentifizierter GET /mcp → 401 mit Header:
WWW-Authenticate: Bearer realm="FightReg MCP",
  authorization_uri="https://fightreg.org/authorize",
  token_uri="https://fightreg.org/token"

// Kompatible Clients (Claude Desktop etc.) erkennen
// diesen Header und starten automatisch den OAuth-Flow.
🎓

32. TCPDF — Urkunden & Zertifikate

Urkunden werden via CertificateService + TCPDF generiert. Templates werden in certificate_templates (JSON-Felddefinitionen) gespeichert.

// CertificateService::generate(int $participantId, int $competitionId, string $type)
// $type: 'placement' | 'participation'

// Ablauf
1. Template laden (Kategorie-Assignment → Wettkampf-Assignment → Default)
2. Teilnehmerdaten: Name, Kategorie, Platzierung, Datum
3. TCPDF-Instanz: Seitenformat aus Template (A4/A5, landscape/portrait)
4. Felder iterieren: text | image | qr-code
   - text:  SetFont + Cell mit Platzhalterwerten
   - image: AddImage mit hinterlegtem Pfad
   - qr-code: /api/qr → PNG → eingebettet
5. QR-Code-URL: /verify/{uuid} (UUID in certificate_log gespeichert)
6. Output: PDF-Binary → stream oder Bulk-ZIP

// DB-Tabellen
certificate_templates          – Template-Definitionen (name, type, format, fields JSON)
certificate_template_assignments – Zuweisung Template ↔ Wettkampf/Kategorie
certificate_log                – Generierte Urkunden mit UUID für Verifikation

// Verifikations-Endpunkt (kein Login)
GET /verify/{uuid}   → public_verify.php → Zertifikat-Details anzeigen
💡
QR-Codes werden über /api/qr?data=<url> (endroid/qr-code) generiert — kein externer Service, DSGVO-konform.