Docs Technical Documentation
Technical Documentation

FightReg — Developer Guide

Architecture, API reference, database schema, frontend system and deployment guide for developers.

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

1. Tech Stack

Backend

LanguagePHP 8.5 (container and server; composer.json requires ≥ 8.2)
Frameworkcustom MVC, no Laravel or Symfony
DatabaseMariaDB 10.11, PDO — 103 tables
AuthJWT (HS256, fr_token), one-time passcode as second factor
API611 routes across 474 paths · 78 Controller · 53 services
PDFTCPDF — invoices, receipts, certificates, cards, lists
MailSMTP from configuration, templates per competition
Test benchPHPUnit — 317 tests, plus 282 render cases

Frontend

JScustom SPA, no React and no Vue
DOMcustom builder h()
IconsTabler icons through ic()
CSScustom design system: four themes, spacing tokens, font scaling
FontDM Sans, self-hosted
Languageseleven; printouts follow the event language
Files46 JS files, 40 of them pages

PWA and operations

Service Workerversioned cache, currently v561
Offlinecache-first for JS, CSS and assets
PushVAPID web push — in production, with a log per delivery
DeploymentGitHub Actions, dry run first, then live; migrations released by hand
DevelopmentDocker, one stack for all worktrees, a separate schema per worktree
External servicesElevenLabs, Anthropic Claude — configurable per competition and switchable
📐
All figures measured in the repository as of sprint 169 — 183 migrations, 67 permissions, 33 check runs.
🏗

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

  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. 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

🚫
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';   // ← 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',
];
⚠️
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 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
}
🔍
RBAC-Audit: 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.

ClassResponsibility
AiControllerAI keys and model selection
AnalyticsControllerAnalytics and usage figures
AnnouncementQueueControllerVenue announcement queue
ApiDocsControllerServes the OpenAPI description
AreaControllerMats: status, assignment, results
AthleteBookingControllerAthlete self-service booking
AthleteControllerAthlete account and own data
AuthControllerLogin, registration, tokens, OAuth
BackupControllerBackups
BaseChatControllershared base of the chat endpoints
BibControllerCompetitor number stock and assignment
BookingControllerBookings per competition
BookingTeamControllerTeam bookings
BracketControllerCreate and manage brackets
BracketFightorderControllerFight order
BracketMatchControllerIndividual fights and results
BracketSettingsControllerPer-bracket settings
BracketStandingsControllerRound robin standings
CardControllerParticipant card
CardManifestControllerWallet manifest for the card
CategoryControllerCategories, merging, history
CategoryImportControllerCategory import from file
CertificateControllerGenerate certificates
CertificateTemplateControllerCertificate templates
ChatControllerAI assistant for organisers
CheckinControllerCheck-in, weigh-in, documents
CoachControllerCoaches and their clubs
CompetitionAdminControllerCompetition admins
CompetitionControllerCompetitions, figures, dashboard
CompetitionEnrollmentControllerClub and athlete entry
ConsentControllerConsents and their log
EligibilityControllerEligibility per category
EventDataInviteControllerInvitation to maintain event data
FederationControllerFederations and assignments
FightCallControllerFight calls
FightScoreControllerFight scoring, rounds, flags
FileControllerFiles, photos, documents
FormsControllerForms: sessions and scoring
ImportControllerImport of participants and lists
InvoiceControllerInvoices
InvoiceDesignControllerInvoice design per issuing entity
JudgeControllerJudge view
ListPdfControllerLists and notices as PDF
MailTemplateControllerMail templates and branding
McpControllerMCP interface
MonitoringControllerSystem status
NotificationControllerNotifications and subscriptions
OrganizerControllerOrganiser master data
OtpControllerOne-time passcode
ParticipantCardControllerCard via public token
ParticipantControllerParticipants, athlete ID, withdrawal
PaymentControllerPayments and receipts
PayoutRecipientControllerPayout recipients
PayoutReportControllerPayout report
PlanControllerPlan and quotas
PricingRuleControllerPricing rules
QrControllerQR codes
RankingControllerRankings
RankingProfileControllerRanking point profiles
RefereeControllerReferee management and registration
RingControllerRings and assignment
RoleControllerRoles and permissions
SchoolControllerClubs and schools
ScreenControllerScreens, queue, tokens
SettingsControllerPer-competition settings
SponsorControllerSponsors, placements, records
StaffControllerStaff and individual rights
StartlistControllerStart lists and their release
SurchargeDiscountControllerSurcharges and discounts
TrainerChatControllerChat for trainers
TransferControllerClub transfers
TranslationPageControllerTranslation page
TtsAnnouncementControllerAnnouncement texts
TtsControllerSpeech synthesis and quota
TtsSettingsControllerVoices, templates, breaks
UploadControllerUploaded files
UserControllerUser administration
VenueMapControllerVenue map and objects
⚙️

9. Services

53 services, generated from src/Services. A service holds domain rules that more than one controller needs.

ClassResponsibility
AnsageReihenfolgeOrder within the announcement queue
ApiKeyServiceWhich key is currently in effect
AthleteIdServiceAssigns athlete IDs
AuditServiceLog of security-relevant changes
AuthServiceTokens, passwords, registration
BenachrichtigungBundles notifications
BibServiceCompetitor number stock and states
BookingTeamServiceTeams and seeding
BracketServiceBracket generation, KO and round robin
CacheServiceCache
CardPdfServiceParticipant card as PDF
CategoryImportServiceReads and matches imported categories
CertificateServiceLays out certificates
ChatServiceTools for the AI assistant
CircuitBreakerServiceCircuit breaker for external services
CoachServiceCoaches and their club binding
DeadlineServiceDeadlines
DemoMailSperreNo real mail from demo events
EligibilityServiceChecks age, weight and grade
EmpfaengerkreisDetermines who receives a message
FederationMembershipServiceFederation membership
FederationServiceFederation structure
FightCallServiceTriggers fight calls
FightScoreServiceScoring, rounds, flags
FormsServiceForms sessions
FrPdfShared PDF base
InvoiceAttachmentServiceInvoice attachments
InvoiceDesignServiceResolves the invoice design
InvoicePdfServiceInvoice as PDF
ListenfreigabeRelease of start lists
ListPdfServiceLists as PDF
LogServiceLogging
MailServiceMail sending and templates
MergeServiceMerging categories
OtpServiceOne-time passcodes
PdfLayoutTraitRecurring PDF building blocks
PermissionServiceResolves role to permissions
PlanServicePlan and limits
PricingServicePrice calculation
PrintLanguageServiceLanguage of a printout
PushServiceWeb push and delivery
RankingServiceRanking points
RbacAuditServiceAudits permissions against the database
ReceiptPdfServiceReceipt as PDF
RedisServiceRedis, where available
RegistrationPolicyAnswers whether registration is open
RoleCatalogServiceRole catalogue
SponsorFlaechenBooking per sponsor placement
TeilnehmerFotoParticipant photos
TrainerChatServiceChat for trainers
TranslationHelperField in the right language
UebersetzungsStandState of the translations
UiTranslationServiceInterface texts from the database
🗄

10. Repository Layer

Four repositories plus an interface: ParticipantRepository, CategoryRepository, SchoolRepository, BookingRepository.

ℹ️
The remaining controllers still access the database directly through 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.
⚠️
InnoDB keeps foreign key names per schema, not per table. A short name such as 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.
ℹ️
A new column is read defensively in PHP ($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
📐
611 routes across 474 paths — 236 GET, 170 POST, 87 DELETE, 61 PATCH, 57 PUT. Every new route brings its entry in src/OpenApi/; 156 existing routes still lack one, and that number may only go down.
🔑

16. API — Auth Endpoints

MethodePfadDescriptionAuth
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 — Competitions

MethodePfadDescription
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 — Participants & Schools

MethodePfadDescription
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

MethodePfadDescription
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 & Invoices

MethodePfadDescription
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 & Announcements

MethodePfadDescription
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

MethodePfadDescription
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 — Other Endpoints

ModulMethodePfadDescription
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. 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
⚠️
Das Projekt liegt auf OneDrive — Sync-Konflikte möglich. Vor dem Arbeiten sicherstellen, dass alle Dateien synchron sind.
🧪

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.

ℹ️
A partial run carries the remaining lines over from the last report — allowed only if no file within that run’s measurement scope has been touched since. The scopes live in 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"
🔌

31. MCP Server & OAuth 2.0 / PKCE

FightReg exposes a full Model Context Protocol (MCP) server at /mcp. Claude Desktop and other MCP-compatible clients can access live competition data directly.

Endpoints

MethodURLDescription
GET / POST/mcpMCP JSON-RPC endpoint (SSE + HTTP)
GET/authorizeOAuth 2.0 Authorization endpoint (PKCE)
POST/tokenOAuth 2.0 Token exchange
GET/api/mcp/keysManage API keys (super_admin)

OAuth 2.0 / PKCE Flow

1. Client generates code_verifier (random, 43–128 chars)
   code_challenge = BASE64URL(SHA256(code_verifier))

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

3. User logs in → confirms access
   → Redirect to 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 with header:
     Authorization: Bearer <access_token>
🔒
Auth codes expire after 5 minutes. Access tokens have no server-side expiry but can be revoked via System → MCP Keys.

Available MCP Tools

ToolScopeDescription
list_competitionsreadList all active competitions (entry point)
search_participantsreadSearch participants by name
get_categoriesreadFetch categories for a competition
get_schoolsreadList enrolled schools
get_competition_statsreadStats: participant counts, check-in, categories
get_bracket_statusreadBracket status and results
get_area_statusreadCompetition area status with live fights
get_bookingsreadFetch bookings with filters
get_financial_summaryreadFinancial overview: revenue, open invoices
suggest_categoriesreadSuggest eligible categories for a participant
book_participantwriteBook participant into category (batch)
list_my_competitionsreadOwn competitions (school-scoped)
search_my_participantsreadOwn participants (school-scoped)
get_my_bookingsreadOwn bookings
get_eligible_categoriesreadEligible categories for own participants
get_my_areasreadArena assignments for own participants
get_my_bracketsreadBracket results for own participants
get_my_fightorderreadFight order for own participants

Managing API Keys

// DB table: mcp_api_keys
// Fields: id, name, key_hash, key_prefix, user_id, scopes (JSON), is_active, expires_at

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

// Revoke
PATCH /api/mcp/keys/{id}  { "is_active": false }
DELETE /api/mcp/keys/{id}
⚠️
The full API key is only returned once at creation. It is stored as a bcrypt hash and cannot be retrieved afterwards.

MCP Discovery (401 Flow)

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

// Compatible clients (Claude Desktop etc.) detect this header
// and automatically initiate the OAuth flow.
🎓

32. TCPDF — Certificates

Certificates are generated via CertificateService + TCPDF. Templates are stored in certificate_templates as JSON field definitions.

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

// Flow
1. Load template (category assignment → competition assignment → default)
2. Participant data: name, category, placement, date
3. TCPDF instance: page format from template (A4/A5, landscape/portrait)
4. Iterate fields: text | image | qr-code
   - text:  SetFont + Cell with placeholder values
   - image: AddImage with stored path
   - qr-code: /api/qr → PNG → embedded
5. QR code URL: /verify/{uuid} (UUID stored in certificate_log)
6. Output: PDF binary → stream or bulk ZIP

// DB tables
certificate_templates          – Template definitions (name, type, format, fields JSON)
certificate_template_assignments – Template ↔ competition / category assignment
certificate_log                – Generated certificates with UUID for verification

// Verification endpoint (no login required)
GET /verify/{uuid}  → public_verify.php → show certificate details
💡
QR codes are generated via /api/qr?data=<url> (endroid/qr-code) — no external service, GDPR-compliant.