FightReg — Developer Guide
Architecture, API reference, database schema, frontend system and deployment guide for developers.
1. Tech Stack
Backend
| Language | PHP 8.5 (container and server; composer.json requires ≥ 8.2) |
| Framework | custom MVC, no Laravel or Symfony |
| Database | MariaDB 10.11, PDO — 103 tables |
| Auth | JWT (HS256, fr_token), one-time passcode as second factor |
| API | 611 routes across 474 paths · 78 Controller · 53 services |
| TCPDF — invoices, receipts, certificates, cards, lists | |
| SMTP from configuration, templates per competition | |
| Test bench | PHPUnit — 317 tests, plus 282 render cases |
Frontend
| JS | custom SPA, no React and no Vue |
| DOM | custom builder h() |
| Icons | Tabler icons through ic() |
| CSS | custom design system: four themes, spacing tokens, font scaling |
| Font | DM Sans, self-hosted |
| Languages | eleven; printouts follow the event language |
| Files | 46 JS files, 40 of them pages |
PWA and operations
| Service Worker | versioned cache, currently v561 |
| Offline | cache-first for JS, CSS and assets |
| Push | VAPID web push — in production, with a log per delivery |
| Deployment | GitHub Actions, dry run first, then live; migrations released by hand |
| Development | Docker, one stack for all worktrees, a separate schema per worktree |
| External services | ElevenLabs, Anthropic Claude — configurable per competition and switchable |
2. Architecture
Backend — MVC Layers
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 Layers
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. Folder Structure
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'; // ← raise on every JS or CSS change 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 Structure
{
"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 across 20 modules, spread over twelve roles).
// src/Config/Permissions.php
class Permissions {
const BRACKETS_EDIT = 'brackets.edit';
const BRACKETS_SCORE = 'brackets.score';
const BILLING_VIEW = 'billing.view';
// ... 67 slugs in total
}
// 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. Controllers (Overview)
78 controllers. The list is generated from src/Controllers — maintained by hand it would be incomplete after two sprints.
| Class | Responsibility |
|---|---|
AiController | AI keys and model selection |
AnalyticsController | Analytics and usage figures |
AnnouncementQueueController | Venue announcement queue |
ApiDocsController | Serves the OpenAPI description |
AreaController | Mats: status, assignment, results |
AthleteBookingController | Athlete self-service booking |
AthleteController | Athlete account and own data |
AuthController | Login, registration, tokens, OAuth |
BackupController | Backups |
BaseChatController | shared base of the chat endpoints |
BibController | Competitor number stock and assignment |
BookingController | Bookings per competition |
BookingTeamController | Team bookings |
BracketController | Create and manage brackets |
BracketFightorderController | Fight order |
BracketMatchController | Individual fights and results |
BracketSettingsController | Per-bracket settings |
BracketStandingsController | Round robin standings |
CardController | Participant card |
CardManifestController | Wallet manifest for the card |
CategoryController | Categories, merging, history |
CategoryImportController | Category import from file |
CertificateController | Generate certificates |
CertificateTemplateController | Certificate templates |
ChatController | AI assistant for organisers |
CheckinController | Check-in, weigh-in, documents |
CoachController | Coaches and their clubs |
CompetitionAdminController | Competition admins |
CompetitionController | Competitions, figures, dashboard |
CompetitionEnrollmentController | Club and athlete entry |
ConsentController | Consents and their log |
EligibilityController | Eligibility per category |
EventDataInviteController | Invitation to maintain event data |
FederationController | Federations and assignments |
FightCallController | Fight calls |
FightScoreController | Fight scoring, rounds, flags |
FileController | Files, photos, documents |
FormsController | Forms: sessions and scoring |
ImportController | Import of participants and lists |
InvoiceController | Invoices |
InvoiceDesignController | Invoice design per issuing entity |
JudgeController | Judge view |
ListPdfController | Lists and notices as PDF |
MailTemplateController | Mail templates and branding |
McpController | MCP interface |
MonitoringController | System status |
NotificationController | Notifications and subscriptions |
OrganizerController | Organiser master data |
OtpController | One-time passcode |
ParticipantCardController | Card via public token |
ParticipantController | Participants, athlete ID, withdrawal |
PaymentController | Payments and receipts |
PayoutRecipientController | Payout recipients |
PayoutReportController | Payout report |
PlanController | Plan and quotas |
PricingRuleController | Pricing rules |
QrController | QR codes |
RankingController | Rankings |
RankingProfileController | Ranking point profiles |
RefereeController | Referee management and registration |
RingController | Rings and assignment |
RoleController | Roles and permissions |
SchoolController | Clubs and schools |
ScreenController | Screens, queue, tokens |
SettingsController | Per-competition settings |
SponsorController | Sponsors, placements, records |
StaffController | Staff and individual rights |
StartlistController | Start lists and their release |
SurchargeDiscountController | Surcharges and discounts |
TrainerChatController | Chat for trainers |
TransferController | Club transfers |
TranslationPageController | Translation page |
TtsAnnouncementController | Announcement texts |
TtsController | Speech synthesis and quota |
TtsSettingsController | Voices, templates, breaks |
UploadController | Uploaded files |
UserController | User administration |
VenueMapController | Venue map and objects |
9. Services
53 services, generated from src/Services. A service holds domain rules that more than one controller needs.
| Class | Responsibility |
|---|---|
AnsageReihenfolge | Order within the announcement queue |
ApiKeyService | Which key is currently in effect |
AthleteIdService | Assigns athlete IDs |
AuditService | Log of security-relevant changes |
AuthService | Tokens, passwords, registration |
Benachrichtigung | Bundles notifications |
BibService | Competitor number stock and states |
BookingTeamService | Teams and seeding |
BracketService | Bracket generation, KO and round robin |
CacheService | Cache |
CardPdfService | Participant card as PDF |
CategoryImportService | Reads and matches imported categories |
CertificateService | Lays out certificates |
ChatService | Tools for the AI assistant |
CircuitBreakerService | Circuit breaker for external services |
CoachService | Coaches and their club binding |
DeadlineService | Deadlines |
DemoMailSperre | No real mail from demo events |
EligibilityService | Checks age, weight and grade |
Empfaengerkreis | Determines who receives a message |
FederationMembershipService | Federation membership |
FederationService | Federation structure |
FightCallService | Triggers fight calls |
FightScoreService | Scoring, rounds, flags |
FormsService | Forms sessions |
FrPdf | Shared PDF base |
InvoiceAttachmentService | Invoice attachments |
InvoiceDesignService | Resolves the invoice design |
InvoicePdfService | Invoice as PDF |
Listenfreigabe | Release of start lists |
ListPdfService | Lists as PDF |
LogService | Logging |
MailService | Mail sending and templates |
MergeService | Merging categories |
OtpService | One-time passcodes |
PdfLayoutTrait | Recurring PDF building blocks |
PermissionService | Resolves role to permissions |
PlanService | Plan and limits |
PricingService | Price calculation |
PrintLanguageService | Language of a printout |
PushService | Web push and delivery |
RankingService | Ranking points |
RbacAuditService | Audits permissions against the database |
ReceiptPdfService | Receipt as PDF |
RedisService | Redis, where available |
RegistrationPolicy | Answers whether registration is open |
RoleCatalogService | Role catalogue |
SponsorFlaechen | Booking per sponsor placement |
TeilnehmerFoto | Participant photos |
TrainerChatService | Chat for trainers |
TranslationHelper | Field in the right language |
UebersetzungsStand | State of the translations |
UiTranslationService | Interface texts from the database |
10. Repository Layer
Four repositories plus an interface: ParticipantRepository, CategoryRepository, SchoolRepository, BookingRepository.
Database::getInstance(). That is known and recorded as an open issue — not an oversight but an order of work.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']);
// Important: Literal paths BEFORE wildcard routes!
$router->get('/api/rankings/global-points', [RankingController::class, 'getGlobalPoints']); // ✅ zuerst
$router->get('/api/rankings/athlete/{pid}', [RankingController::class, 'athleteDetail']); // ✅ dann
// Reading Request Body
$body = Router::getBody(); // json_decode(file_get_contents('php://input'), true)
12. Database Schema — Overview
FightReg nutzt 59 Tables in MySQL 8. Alle FK-Spalten nutzen INT UNSIGNED. Soft-Delete via Status-Felder (kein physisches Delete außer purge).
Core Entities and Relationships
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
Every schema change is a numbered SQL file under migrations/, written idempotently. Currently at 183. What has been applied is recorded in schema_migrations — not in anyone’s memory.
# apply and inspect — both databases .\fr.ps1 migrate # fightreg .\fr.ps1 migrate-test # fightreg_test .\fr.ps1 migrate-status # which file is missing where? # Every new column also goes into install.php, # otherwise it is missing from every fresh installation.
fk_fs_area collides with another table that chose the same abbreviation — the error is errno 121 and sounds like a duplicate row. So the name spells out the table.$row['new'] ?? null): deployed code sometimes runs ahead of its migration, and a notice in the middle of a JSON response breaks it.14. All tables (103)
Every table from install.php, with its column count. Click to expand.
15. API Reference — Overview
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/; 156 existing routes still lack one, and that number may only go down.16. API — Auth Endpoints
| Methode | Pfad | Description | 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 — Competitions
| Methode | Pfad | Description |
|---|---|---|
| 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 — Participants & Schools
| Methode | Pfad | Description |
|---|---|---|
| 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 | Description |
|---|---|---|
| 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 & Invoices
| Methode | Pfad | Description |
|---|---|---|
| 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 & Announcements
| Methode | Pfad | Description |
|---|---|---|
| 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 | Description |
|---|---|---|
| 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 — Other Endpoints
| Modul | Methode | Pfad | Description |
|---|---|---|---|
| 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. AI 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 — Invoice 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
A sprint counts as finished when the test bench is green — and test-all runs before the commit, not after it.
.\fr.ps1 test-all # all runs, then a report
# -> tests/berichte/-.json
.\fr.ps1 test-all nur=render # partial run: only render, the rest is carried over
make test-all # the same on Linux and macOS
33 runs: 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
Plus 317 PHPUnit tests and 282 render cases. Every new view gets a case in tests/render/cases.cjs — a view without a case is invisible to every check: not red, but absent.
tests/laeufe.json, and a file in no scope counts as measured by every run.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. Key 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"