FightReg — Entwicklerhandbuch
Architektur, API-Referenz, Datenbankschema, Frontend-System und Deployment-Anleitung für Entwickler.
1. Tech Stack
Backend
| Sprache | PHP 8.5 (Container und Server; composer.json verlangt ≥ 8.2) |
| Framework | eigenes MVC, kein Laravel oder Symfony |
| Datenbank | MariaDB 10.11, PDO — 103 Tabellen |
| Anmeldung | JWT (HS256, fr_token), Einmalkennwort als zweiter Faktor |
| API | 611 Routen auf 474 Pfaden · 78 Controller · 53 Dienste |
| TCPDF — Rechnungen, Quittungen, Urkunden, Karten, Listen | |
| SMTP über die Konfiguration, Vorlagen je Veranstaltung | |
| Prüfstand | PHPUnit — 317 Tests, dazu 282 Renderfälle |
Frontend
| JS | eigene SPA, kein React und kein Vue |
| DOM | eigener Bauer h() |
| Symbole | Tabler-Icons über ic() |
| CSS | eigenes Design-System: vier Themen, Abstands-Token, Schriftskalierung |
| Schrift | DM Sans, selbst ausgeliefert |
| Sprachen | elf, Ausdrucke folgen der Eventsprache |
| Dateien | 46 JS-Dateien, davon 40 Seiten |
PWA und Betrieb
| Service Worker | versionierter Zwischenspeicher, aktuell v561 |
| Offline | Cache-First für JS, CSS und Anhänge |
| Push | VAPID Web-Push — produktiv, mit Protokoll je Zustellung |
| Ausliefern | GitHub Actions, erst Probelauf, dann echt; Migrationen von Hand freigeben |
| Entwicklung | Docker, ein Stack für alle Worktrees, eigenes Schema je Worktree |
| Externe Dienste | ElevenLabs, Anthropic Claude — je Veranstaltung hinterlegbar und abschaltbar |
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
- POST
/api/auth/login→ Server prüft Credentials → gibt JWT zurück - Frontend speichert JWT in
localStorageunter Keyfr_token - Jeder API-Request:
Authorization: Bearer <token> AuthMiddleware::authenticate()dekodiert Token, gibt User-Objekt zurückrequirePermission($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
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', ];
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
}
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.
| Klasse | Zuständigkeit |
|---|---|
AiController | KI-Schlüssel und Modellwahl |
AnalyticsController | Auswertungen und Nutzungszahlen |
AnnouncementQueueController | Warteschlange der Hallenansage |
ApiDocsController | OpenAPI-Beschreibung ausliefern |
AreaController | Flächen, Status, Belegung, Ergebnisse |
AthleteBookingController | Selbstbuchung des Sportlers |
AthleteController | Athletenkonto und eigene Daten |
AuthController | Anmeldung, Registrierung, Token, OAuth |
BackupController | Sicherungen |
BaseChatController | gemeinsamer Unterbau der Chat-Endpunkte |
BibController | Startnummern-Vorrat und Vergabe |
BookingController | Buchungen je Wettkampf |
BookingTeamController | Mannschaftsbuchungen |
BracketController | Bäume erzeugen und verwalten |
BracketFightorderController | Kampfreihenfolge |
BracketMatchController | einzelne Kämpfe und Ergebnisse |
BracketSettingsController | Einstellungen je Baum |
BracketStandingsController | Tabellen im Rundenspiel |
CardController | Teilnehmerkarte |
CardManifestController | Wallet-Manifest der Karte |
CategoryController | Kategorien, Zusammenlegen, Verlauf |
CategoryImportController | Kategorien aus Datei einlesen |
CertificateController | Urkunden erzeugen |
CertificateTemplateController | Urkundenvorlagen |
ChatController | KI-Assistent für Veranstalter |
CheckinController | Check-in, Wiegen, Dokumente |
CoachController | Coaches und ihre Vereine |
CompetitionAdminController | Wettkampf-Admins |
CompetitionController | Wettkämpfe, Kennzahlen, Dashboard |
CompetitionEnrollmentController | Meldung von Verein und Sportler |
ConsentController | Einwilligungen und Protokoll |
EligibilityController | Startberechtigung je Kategorie |
EventDataInviteController | Einladung zur Datenpflege |
FederationController | Verbände und Zuordnungen |
FightCallController | Kampfaufruf |
FightScoreController | Kampfwertung, Runden, Fahnen |
FileController | Dateien, Fotos, Dokumente |
FormsController | Formen: Sitzungen und Wertung |
ImportController | Import von Teilnehmern und Listen |
InvoiceController | Rechnungen |
InvoiceDesignController | Belegbild je Rechnungssteller |
JudgeController | Kampfrichter-Ansicht |
ListPdfController | Listen und Aushänge als PDF |
MailTemplateController | Mailvorlagen und Erscheinungsbild |
McpController | MCP-Schnittstelle |
MonitoringController | Zustand des Systems |
NotificationController | Benachrichtigungen und Abos |
OrganizerController | Veranstalter-Stammdaten |
OtpController | Einmalkennwort |
ParticipantCardController | Karte über öffentlichen Token |
ParticipantController | Teilnehmer, Athleten-ID, Rückzug |
PaymentController | Zahlungen und Quittungen |
PayoutRecipientController | Empfänger von Auszahlungen |
PayoutReportController | Auszahlungsübersicht |
PlanController | Tarif und Kontingente |
PricingRuleController | Preisregeln |
QrController | QR-Codes |
RankingController | Rangliste |
RankingProfileController | Punkteprofile der Rangliste |
RefereeController | Kampfrichterverwaltung und Meldung |
RingController | Ringe und Zuordnung |
RoleController | Rollen und Rechte |
SchoolController | Vereine und Schulen |
ScreenController | Bildschirme, Warteschlange, Token |
SettingsController | Einstellungen je Veranstaltung |
SponsorController | Sponsoren, Flächen, Nachweis |
StaffController | Mitarbeiter und Einzelrechte |
StartlistController | Startlisten und Freigabe |
SurchargeDiscountController | Aufschläge und Rabatte |
TrainerChatController | Chat für Trainer |
TransferController | Vereinswechsel |
TranslationPageController | Übersetzungsseite |
TtsAnnouncementController | Ansagetexte |
TtsController | Sprachausgabe und Kontingent |
TtsSettingsController | Stimmen, Vorlagen, Pausen |
UploadController | Hochgeladene Dateien |
UserController | Benutzerverwaltung |
VenueMapController | Hallenplan und Objekte |
9. Services
53 Dienste, erzeugt aus src/Services. Ein Dienst hält fachliche Regeln, die mehr als ein Controller braucht.
| Klasse | Zuständigkeit |
|---|---|
AnsageReihenfolge | Reihenfolge in der Ansage-Warteschlange |
ApiKeyService | welcher Schlüssel gerade gilt |
AthleteIdService | Athleten-ID vergeben |
AuditService | Protokoll sicherheitsrelevanter Änderungen |
AuthService | Token, Passwörter, Registrierung |
Benachrichtigung | Benachrichtigungen bündeln |
BibService | Startnummern-Vorrat und Zustände |
BookingTeamService | Mannschaften und Seeding |
BracketService | Bäume erzeugen, KO und Rundenspiel |
CacheService | Zwischenspeicher |
CardPdfService | Teilnehmerkarte als PDF |
CategoryImportService | Kategorien einlesen und abgleichen |
CertificateService | Urkunden setzen |
ChatService | Werkzeuge des KI-Assistenten |
CircuitBreakerService | Schutzschalter für externe Dienste |
CoachService | Coaches und Vereinsbindung |
DeadlineService | Fristen |
DemoMailSperre | kein echter Mailversand aus Demos |
EligibilityService | Alter, Gewicht, Graduierung prüfen |
Empfaengerkreis | wer eine Nachricht bekommt |
FederationMembershipService | Mitgliedschaft im Verband |
FederationService | Verbandsstruktur |
FightCallService | Kampfaufruf auslösen |
FightScoreService | Wertung, Runden, Fahnen |
FormsService | Formen-Sitzungen |
FrPdf | gemeinsamer PDF-Unterbau |
InvoiceAttachmentService | Anhänge an Rechnungen |
InvoiceDesignService | Belegbild ermitteln |
InvoicePdfService | Rechnung als PDF |
Listenfreigabe | Freigabe der Startlisten |
ListPdfService | Listen als PDF |
LogService | Protokolle |
MailService | Mailversand und Vorlagen |
MergeService | Kategorien zusammenlegen |
OtpService | Einmalkennwörter |
PdfLayoutTrait | wiederkehrende PDF-Bausteine |
PermissionService | Rolle zu Berechtigungen auflösen |
PlanService | Tarif und Grenzen |
PricingService | Preis berechnen |
PrintLanguageService | Sprache des Ausdrucks |
PushService | Web-Push und Zustellung |
RankingService | Punkte der Rangliste |
RbacAuditService | Rechte gegen die Datenbank prüfen |
ReceiptPdfService | Quittung als PDF |
RedisService | Redis, sofern vorhanden |
RegistrationPolicy | ist die Anmeldung offen |
RoleCatalogService | Rollenkatalog |
SponsorFlaechen | Buchung je Sponsorenfläche |
TeilnehmerFoto | Teilnehmerfotos |
TrainerChatService | Chat für Trainer |
TranslationHelper | Feld in der richtigen Sprache |
UebersetzungsStand | Stand der Übersetzungen |
UiTranslationService | Oberflächentexte aus der Datenbank |
10. Repository-Layer
Vier Repositories plus Schnittstelle: ParticipantRepository, CategoryRepository, SchoolRepository, BookingRepository.
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.
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.$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
src/OpenApi/ mit; 156 Bestandsrouten haben noch keinen, und diese Zahl darf nur fallen.16. API — Auth-Endpunkte
| Methode | Pfad | Beschreibung | Auth |
|---|---|---|---|
| POST | /api/auth/login | Login → JWT | — |
| POST | /api/auth/register | Trainer-Registrierung | — |
| POST | /api/auth/register/athlete | Athlet-Registrierung mit Athlet-ID | — |
| POST | /api/auth/refresh | JWT erneuern | JWT |
| GET | /api/auth/me | Eigenes User-Profil | JWT |
| PUT | /api/auth/profile | Profil aktualisieren | JWT |
| PUT | /api/auth/password | Passwort ändern | JWT |
| POST | /api/auth/forgot-password | Reset-Link per Mail | — |
| POST | /api/auth/reset-password | Passwort mit Reset-Token setzen | — |
| GET | /api/auth/permissions | Eigene Permissions laden | JWT |
| POST | /api/auth/oauth | OAuth Login (extern) | — |
| DELETE | /api/auth/account | Account löschen | JWT |
17. API — Wettkämpfe
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /api/competitions | Alle Wettkämpfe auflisten |
| POST | /api/competitions | Neuer 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}/duplicate | Wettkampf duplizieren |
| GET | /api/competitions/{id}/stats | Statistiken |
| GET | /api/competitions/{id}/dashboard | Dashboard-Daten |
| GET | /api/competitions/{id}/schools | Angemeldete Schulen |
| POST | /api/competitions/{id}/enroll | Schule anmelden |
| PATCH | /api/competitions/{id}/schools/{sid} | Schul-Anmeldung bestätigen/ablehnen |
| GET | /api/competitions/{id}/participants | Angemeldete TN |
| POST | /api/competitions/{id}/participants | TN anmelden |
| DELETE | /api/competitions/{id}/participants/{pid} | TN abmelden |
| GET | /api/competitions/{id}/categories | Kategorien des Wettkampfs |
| GET | /api/competitions/{id}/bookings | Alle Buchungen |
| GET | /api/competitions/{id}/admins | Wettkampf-Admins |
| POST | /api/competitions/{id}/admins | Admin hinzufügen |
| GET | /api/competitions/{id}/rankings | Wettkampf-Ranking |
| GET | /api/competitions/{id}/fightorder | Kampfreihenfolge |
| PATCH | /api/competitions/{id}/fightorder/reorder | Reihenfolge ändern |
| GET | /api/competitions/{id}/surcharges | Aufschläge |
| GET | /api/competitions/{id}/discounts | Rabatte |
18. API — Teilnehmer & Schulen
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /api/participants | Alle TN (Admin) |
| POST | /api/participants | Neuer TN |
| PUT | /api/participants/{id} | TN aktualisieren |
| DELETE | /api/participants/{id} | TN löschen |
| PATCH | /api/participants/{id}/assign-athlete-id | Athlet-ID manuell zuweisen |
| PATCH | /api/participants/{id}/ai-exclude | KI-Ausschluss toggle |
| PATCH | /api/participants/{id}/self-payer | Selbstzahler toggle |
| GET | /api/participants/{id}/eligible-categories | Geeignete Kategorien |
| POST | /api/participants/{id}/invite | Einladungsmail senden |
| GET | /api/participant-card/{token} | Öffentliche TN-Karte (kein Auth) |
| GET | /api/schools | Alle Schulen |
| PUT | /api/schools/{id} | Schule bearbeiten |
| PATCH | /api/schools/{id}/approve | Schule freischalten |
| POST | /api/schools/{id}/bulk-assign-athlete-ids | Bulk Athlet-IDs vergeben |
| POST | /api/import/participants | CSV/Excel-Import |
| GET | /api/import/template | Import-Vorlage herunterladen |
19. API — Brackets & Matches
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /api/competitions/{id}/brackets | Brackets eines Wettkampfs |
| POST | /api/brackets | Bracket erstellen |
| GET | /api/brackets/{id} | Bracket-Details inkl. Matches |
| POST | /api/brackets/{id}/generate | Bracket generieren (Seeding → Matches) |
| PATCH | /api/brackets/{id} | Bracket-Metadaten aktualisieren |
| DELETE | /api/brackets/{id} | Bracket löschen |
| PATCH | /api/brackets/{id}/activate | Bracket aktivieren |
| PATCH | /api/brackets/{id}/arena | Arena/Matte zuweisen |
| PATCH | /api/brackets/{id}/swap | TN im Bracket tauschen |
| PATCH | /api/bracket-matches/{id}/result | Match-Ergebnis eintragen |
| PATCH | /api/bracket-matches/{id}/revert | Match-Ergebnis zurücksetzen |
| GET | /api/brackets/{id}/final-standings | Platzierungen nach Abschluss |
| POST | /api/brackets/{id}/next-round | Nächste Runde starten (RR) |
20. API — Billing & Rechnungen
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /api/competitions/{id}/invoices | Rechnungen des Wettkampfs |
| POST | /api/competitions/{id}/invoices | Rechnung generieren |
| GET | /api/invoices/{id} | Rechnungsdetails |
| GET | /api/invoices/{id}/pdf | PDF herunterladen |
| POST | /api/invoices/{id}/send | Rechnung per Mail senden |
| PATCH | /api/invoices/{id}/status | Rechnungsstatus setzen |
| GET | /api/competitions/{id}/pricing-rules | Preisregeln |
| POST | /api/competitions/{id}/pricing-rules | Preisregel anlegen |
| GET | /api/invoice-issuer | Rechnungssteller |
| PUT | /api/invoice-issuer | Rechnungssteller speichern |
21. API — TTS & Ansagen
| Methode | Pfad | Beschreibung |
|---|---|---|
| POST | /api/tts/generate | Text → MP3 via ElevenLabs |
| GET | /api/tts/settings | TTS-Einstellungen |
| PUT | /api/tts/settings | TTS-Einstellungen speichern |
| GET | /api/tts/templates | Ansage-Templates |
| PUT | /api/tts/templates | Templates speichern |
| GET | /api/tts/history | Ansage-Verlauf |
| GET | /api/competitions/{id}/announcements | Ansagen eines Wettkampfs |
| POST | /api/competitions/{id}/announcements | Neue Ansage |
22. API — Staff & RBAC
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /api/competitions/{id}/staff | Staff-Liste |
| POST | /api/competitions/{id}/staff | Staff hinzufügen |
| PUT | /api/competitions/{id}/staff/{sid} | Staff aktualisieren |
| DELETE | /api/competitions/{id}/staff/{sid} | Staff entfernen |
| GET | /api/competitions/{id}/staff/matrix | Permission-Matrix |
| GET | /api/competitions/{id}/staff/{sid}/permissions | Individual-Permissions |
| PUT | /api/competitions/{id}/staff/{sid}/permissions | Permissions setzen |
| GET | /api/roles | Alle Rollen |
| POST | /api/roles | Rolle erstellen |
| PUT | /api/roles/{id}/permissions | Rollen-Permissions setzen |
| GET | /api/system/rbac-audit | RBAC-Konsistenzprüfung |
| GET | /api/users | Alle Benutzer |
| POST | /api/users/create | Benutzer anlegen (OTP) |
| DELETE | /api/users/{id} | Benutzer löschen |
23. API — Weitere Endpunkte
| Modul | Methode | Pfad | Beschreibung |
|---|---|---|---|
| Chat | POST | /api/chat | KI-Chat Nachricht senden |
| Chat | GET | /api/chat/suggestions | Vorschläge laden |
| Chat | GET | /api/chat/logs | Chat-Logs einsehen |
| Ranking | GET | /api/rankings | Globales Ranking |
| Ranking | GET | /api/rankings/athlete/{pid} | Athlet-Detail-Ranking |
| Federations | GET | /api/federations | Verbandsliste |
| Federations | GET | /api/federations/tree | Verbandshierarchie |
| Consent | POST | /api/consent | DSGVO-Zustimmung protokollieren |
| Checkin | GET | /api/competitions/{id}/checkin | Check-in-Liste |
| Checkin | POST | /api/competitions/{id}/checkin/{bid} | Check-in durchführen |
| GET | /api/mail-design | Globales Mail-Design | |
| GET | /api/mail-templates | Alle Mail-Templates | |
| Screen | GET | /api/screen/{id} | Hauptscreen-Daten |
| Deadline | GET | /api/deadline?competition_id={id} | Anmeldefrist prüfen |
| Athlete | GET | /api/athlete/me | Eigenes Athlet-Profil |
| Upload | POST | /api/schools/{id}/logo | Schul-Logo hochladen |
| Upload | POST | /api/participants/{id}/photo | TN-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
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
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.
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"