- 1. Einführung und Ziele
- 2. Randbedingungen
- 3. Kontextabgrenzung
- 4. Lösungsstrategie
- 5. Bausteinsicht
- 5.1. Whitebox Gesamtsystem (Level 0)
- 5.2. Bausteinsicht Level 1 - Backend-Module
- 5.3. Level 2 - Modul "Anmeldung" (Beispiel)
- 5.4. Level 2 - Modul "Bewertung" (Beispiel)
- 5.5. Level 2 - Frontend-Struktur
- 5.6. Modul-Übergreifende Konzepte
- 5.7. Cloud Deployment
- Überblick
- Technologie-Stack für Cloud Deployment
- Deployment-Region (Initial)
- Container-Strategie
- Datenbank-Migration: SQLite → Turso
- Umgebungsvariablen und Secrets
- Deployment-Workflow
- Makefile für Deployment
- 8.4.11 Backup und Disaster Recovery
- Kosten-Kalkulation
- Sicherheitsaspekte
- DB/Schema Migrationen
- Rollback-Strategie
- Entwicklungs-Workflow
1. Einführung und Ziele
1.1. Aufgabenstellung
AQUARIUS ist ein webbasiertes Bewertungssystem für regionale Kunstschwimmen-Wettkämpfe in der Kinderliga. Das System unterstützt den gesamten Wettkampfzyklus von der Saisonplanung über die Anmeldung bis zur Live-Bewertung und Ergebnisauswertung.
Wesentliche Features:
-
Saisonplanung und Wettkampforganisation (Büro/Desktop)
-
Anmeldungsverwaltung für Kinder und Teams
-
Live-Bewertung durch Kampf- und Punktrichter (Schwimmbad/Mobile)
-
Automatische Punkteberechnung nach Liga-Regeln
-
Erstellung von Ranglisten und Preisvergabe nach Altersgruppen
Fokus: Figurenschwimmen (Synchronschwimmen nur erwähnt für Teamwertung)
1.2. Qualitätsziele
| Priorität | Qualitätsziel | Motivation |
|---|---|---|
1 |
Benutzbarkeit |
Ehrenamtliche Helfer ohne IT-Affinität müssen System intuitiv bedienen können |
2 |
Verfügbarkeit |
Wettkampf darf nicht durch technische Probleme gestört werden (Offline-Fähigkeit) |
3 |
Korrektheit |
Bewertungsberechnung muss fehlerfrei nach Liga-Regeln erfolgen |
4 |
Performance |
Schnelle Reaktionszeiten bei Live-Bewertung unter Wettkampfbedingungen |
5 |
Wartbarkeit |
Figuren, Regeln und Altersgruppen ändern sich - System muss flexibel anpassbar sein |
1.3. Stakeholder
| Rolle | Erwartungshaltung |
|---|---|
Präsident |
Einfache Saisonplanung, Überblick über alle Wettkämpfe |
Verein |
Unkomplizierte Anmeldung von Kindern |
Offizieller |
Schnelle Wettkampfplanung, minimaler Administrationsaufwand |
Punktrichter |
Fokus auf Wettkampfablauf, System übernimmt Berechnungen |
Kampfrichter |
Einfache, schnelle Punkteeingabe (auch unter Stress) |
Kind |
Transparente Ergebnisse, faire Bewertung |
2. Randbedingungen
2.1. Technische Randbedingungen
| Randbedingung | Erläuterung |
|---|---|
Web-Anwendung |
Keine Installation, Zugriff über Browser |
Mobile-First |
Primär für Tablets im Schwimmbad optimiert |
Progressive Web App |
Installierbar, Offline-fähig, App-ähnliches Erlebnis |
Cloud-Datenbank |
Turso (libSQL) mit automatischer Synchronisation |
Moderne Browser |
Chrome, Firefox, Safari, Edge (jeweils aktuelle Versionen) |
2.2. Organisatorische Randbedingungen
| Randbedingung | Erläuterung |
|---|---|
Zwei Betriebsmodi |
Planung (Büro, Online) und Durchführung (Schwimmbad, Offline) |
Zentral gehostet |
Cloud-Hosting, keine lokale Serverinfrastruktur erforderlich |
Ehrenamtliche Betreiber |
Kein IT-Personal, einfache Wartung |
Open Source |
Transparenz, Anpassbarkeit, keine Lizenzkosten |
2.3. Konventionen
| Konvention | Erläuterung |
|---|---|
Fachsprache |
Deutsche Domänenbegriffe im Code (Verein, Kind, Wettkampf) |
REST API |
Standardisierte HTTP-Methoden und Status-Codes |
Responsive Design |
Mobile-First mit TailwindCSS |
Git Workflow |
Feature-Branches, Pull Requests, Code Reviews |
arc42-Template |
Version 8.2 für Architekturdokumentation |
3. Kontextabgrenzung
3.1. Fachlicher Kontext
@startuml
!include <C4/C4_Context>
skinparam backgroundColor transparent
Person(praesident, "Präsident", "Plant Saison und Wettkämpfe", $tags="CLEO")
AddElementTag("CLEO", $bgColor="#FDE68A", $fontColor="#92400E")
Person(punktrichter, "Punktrichter", "Verwaltet Wettkampf \nvor Ort")
Person(kampfrichter, "Kampfrichter", "Bewertet Starts")
Person(kind, "Kind", "Nimmt am Wettkampf teil")
Person(eltern, "Eltern", "melden und \nversichern Kinder")
System(aquarius, "Aquarius", "Bewertungssystem für Kunds- und Figurenschwimmen")
System_Ext(schwimmverband, "Schwimmverband", "Figuren und Regeln")
Rel(praesident, aquarius, "Plant Saison, verwaltet Stammdaten")
Rel_U(eltern, aquarius, "Melden Kinder an")
Rel(punktrichter, aquarius, "Führt Wettkampf durch", "offline")
Rel(kampfrichter, aquarius, "Erfasst Punkte", "offline")
Rel_R(kind, aquarius, "Sieht Ergebnisse")
Rel_R(kind, aquarius, "meldet sich an")
Rel_L(schwimmverband, aquarius, "Figurenkatalog, Regeln", "Manuell")
@enduml
Kommunikationsbeziehungen:
| Partner | Eingabe | Ausgabe |
|---|---|---|
Präsident (CLEO, Chief League and Executive Officer) |
Saisonplan, Wettkämpfe, Figurenkatalog |
Auswertungen, Statistiken |
Schwimmverband |
Figurenkatalog-Updates, Regeländerungen |
- |
Kind (Teilnehmende) |
Wettkampfanmeldung, Figurenauswahl |
Startnummer, Ergebnisse |
Punktrichter |
Stationszuweisung, Wettkampfstart |
Berechnete Endpunkte, Ranglisten |
Kampfrichter |
Vorläufige Punktzahlen |
Übersicht aller Bewertungen |
4. Lösungsstrategie
4.1. Architekturansatz
Progressive Web App (PWA) mit klarer Trennung von Planungs- und Durchführungskontext
| Entscheidung | Begründung |
|---|---|
Monorepo mit zwei Frontend-Modulen |
Gemeinsame Datenbasis, unterschiedliche UIs für Büro und Schwimmbad |
React SPA |
Moderne, komponentenbasierte UI, große Community, gute Mobile-Unterstützung |
FastAPI Backend |
Schnell, typsicher (Pydantic), automatische API-Dokumentation |
Turso (libSQL) |
SQLite-kompatibel, Cloud-Sync, hybride Online/Offline-Nutzung |
Service Worker |
Offline-Fähigkeit, App-ähnliches Erlebnis, schnelle Ladezeiten |
4.2. Technologie-Stack
Frontend
Siehe ADRs für Details:
-
React 18 + TypeScript - Typsicherheit, moderne Hooks
-
Vite - Schneller Build, optimiertes Bundling
-
TailwindCSS - Utility-First CSS, responsive Design
-
React Router - Client-seitiges Routing
-
TanStack Query - Server State Management, Caching
-
Zustand - Lokales State Management
-
Workbox - Service Worker für PWA
Backend
Siehe ADRs für Details:
-
Python 3.11+ + FastAPI - Moderne Sprache, Async-fähig
-
Pydantic - Datenvalidierung und Serialisierung
-
SQLAlchemy 2.0 - ORM mit Typsicherheit
-
Alembic - Datenbank-Migrationen
Datenbank & Infrastruktur
-
Turso (libSQL) - Edge-Database mit Sync
-
SQLite - Lokal für Entwicklung und Offline-Betrieb
-
Docker - Containerisierung für Deployment
-
Nginx - Reverse Proxy, Static File Serving
4.3. Zentrale Architekturentscheidungen
Trennung Planung vs. Durchführung
Kontext: Unterschiedliche Nutzungskontexte (Büro vs. Schwimmbad)
Entscheidung: Zwei separate Frontend-Module mit gemeinsamer Datenbasis
Begründung:
-
Büro: Desktop-optimiert, komplexe Formulare, viele Daten
-
Schwimmbad: Touch-optimiert, große Buttons, fokussierte Workflows
-
Code-Sharing für gemeinsame Komponenten (Buttons, Forms)
-
Unterschiedliche Routing-Strukturen
-
Getrennte Service Worker Strategien
Siehe auch: ADR-008: Monorepo-Struktur
Progressive Web App statt Native App
Kontext: Mobile Nutzung im Schwimmbad erforderlich
Entscheidung: PWA mit Service Worker
Begründung:
-
✅ Kein App-Store-Prozess
-
✅ Plattform-unabhängig (iOS, Android, Desktop)
-
✅ Automatische Updates
-
✅ Offline-Fähigkeit über Service Worker
-
✅ Keine separate Codebasis für Mobile
-
❌ Eingeschränkte iOS-Unterstützung (akzeptabel)
Siehe auch: ADR-016: PWA-Architektur
Domain-Driven Design
Kontext: Komplexe fachliche Domäne mit unterschiedlichen Kontexten
Entscheidung: 6 Bounded Contexts
3-Schichten-Architektur
Entscheidung: Router → Service → Repository
Begründung:
-
Klare Verantwortlichkeiten
-
Testbarkeit durch Dependency Injection
-
Wiederverwendbare Business-Logic
Siehe auch: 3-Schichten-Architektur
5. Bausteinsicht
5.1. Whitebox Gesamtsystem (Level 0)
Das Aquarius-System besteht aus zwei Hauptanwendungen, die auf einem gemeinsamen Backend operieren:
@startuml
!include <C4/C4_Container>
LAYOUT_WITH_LEGEND()
Person(praesident, "Präsident", "Plant Saison und Wettkämpfe")
Person(verein, "Verein", "Meldet Kinder an")
Person(punktrichter, "Punktrichter", "Verwaltet Wettkampf vor Ort")
Person(kampfrichter, "Kampfrichter", "Bewertet Starts")
System_Boundary(aquarius, "Aquarius") {
Container(planning_app, "Planungs-App", "React SPA", "Desktop-optimierte Oberfläche für Verwaltung und Planung")
Container(execution_app, "Durchführungs-App", "React PWA", "Touch-optimierte Oberfläche für Live-Bewertung")
Container(backend, "Backend API", "FastAPI", "REST API, Business-Logik, Datenzugriff")
ContainerDb(database, "Datenbank", "Turso (libSQL)", "Persistente Speicherung mit Cloud-Sync")
}
Rel(praesident, planning_app, "Plant Saison, verwaltet Stammdaten", "HTTPS")
Rel(verein, planning_app, "Meldet Kinder an", "HTTPS")
Rel(punktrichter, execution_app, "Führt Wettkampf durch", "HTTPS/Offline")
Rel(kampfrichter, execution_app, "Erfasst Punkte", "HTTPS/Offline")
Rel(planning_app, backend, "API Calls", "JSON/HTTPS")
Rel(execution_app, backend, "API Calls + Sync", "JSON/HTTPS")
Rel(backend, database, "SQL Queries", "libSQL Protocol")
@enduml
Begründung:
-
Zwei Frontend-Anwendungen für unterschiedliche Nutzungskontexte (Büro vs. Schwimmbad)
-
Ein Backend für zentrale Business-Logik und Datenkonsistenz
-
Eine Datenbank mit Cloud-Sync für hybride Online/Offline-Nutzung
5.2. Bausteinsicht Level 1 - Backend-Module
Das Backend ist in 6 fachliche Module (Bounded Contexts) strukturiert:
@startuml
!include <C4/C4_Component>
LAYOUT_TOP_DOWN()
Container_Boundary(backend, "Backend API") {
Component(stammdaten, "Stammdaten", "Modul", "Vereine, Teams, Kinder, Offizielle")
Component(saisonplanung, "Saisonplanung", "Modul", "Saison, Figuren, Wettkämpfe, Schwimmbäder")
Component(anmeldung, "Anmeldung", "Modul", "Wettkampfanmeldungen, Startnummernvergabe")
Component(wettkampf, "Wettkampf", "Modul", "Stationen, Gruppen, Durchgänge")
Component(bewertung, "Bewertung", "Modul", "Punkteerfassung, Bewertungsberechnung")
Component(auswertung, "Auswertung", "Modul", "Ranglisten, Preisvergabe, Export")
}
Rel(anmeldung, stammdaten, "Liest Kinder, Teams", "API")
Rel(anmeldung, saisonplanung, "Liest Wettkämpfe, Figuren", "API")
Rel(wettkampf, anmeldung, "Liest Anmeldungen", "API")
Rel(wettkampf, stammdaten, "Liest Offizielle", "API")
Rel(bewertung, wettkampf, "Liest Durchgänge, Gruppen", "API")
Rel(bewertung, saisonplanung, "Liest Schwierigkeitsfaktoren", "API")
Rel(auswertung, bewertung, "Liest Bewertungen", "API")
Rel(auswertung, wettkampf, "Liest Wettkampfstruktur", "API")
@enduml
Übersicht der Module
| Modul | Verantwortlichkeit | Zentrale Entitäten | Abhängigkeiten |
|---|---|---|---|
Stammdaten |
Verwaltung von Basisentitäten |
Verein, Team, Kind, Offizieller |
- (keine) |
Saisonplanung |
Planung von Saison und Wettkämpfen |
Saison, Figur, Wettkampf, Schwimmbad |
- (keine) |
Anmeldung |
Wettkampfanmeldung und Startnummernvergabe |
Anmeldung |
Stammdaten, Saisonplanung |
Wettkampf |
Wettkampfvorbereitung und -struktur |
Station, Gruppe, Durchgang |
Anmeldung, Stammdaten |
Bewertung |
Live-Punkteerfassung und Berechnung |
Start, Bewertung |
Wettkampf, Saisonplanung |
Auswertung |
Ergebnisse und Ranglisten |
Rangliste, Preis |
Bewertung, Wettkampf |
5.3. Level 2 - Modul "Anmeldung" (Beispiel)
Detaillierte Struktur des Anmeldungs-Moduls:
@startuml
allowmixing
skinparam packageStyle rectangle
package "Anmeldung Modul" {
rectangle "AnmeldungRouter" <<REST>> as AnmeldungRouter
rectangle "AnmeldungService" <<Business Logic>> as AnmeldungService
rectangle "AnmeldungRepository" <<Data Access>> as AnmeldungRepository
package "Domain" {
class Anmeldung {
+id: int
+kind_id: int
+wettkampf_id: int
+startnummer: int
+figuren: List[int]
+status: AnmeldungStatus
+erstellt_am: datetime
}
enum AnmeldungStatus {
VORLAEUFIG
BESTAETIGT
STORNIERT
}
}
package "Schemas" {
class AnmeldungCreate <<DTO>>
class AnmeldungResponse <<DTO>>
}
}
' Externe Abhängigkeiten
package "Stammdaten Modul" {
rectangle "KindService" as KindService
}
package "Saisonplanung Modul" {
rectangle "WettkampfService" as WettkampfService
rectangle "FigurService" as FigurService
}
' Beziehungen
AnmeldungRouter --> AnmeldungService : verwendet
AnmeldungService --> AnmeldungRepository : verwendet
AnmeldungService --> KindService : validiert Kind
AnmeldungService --> WettkampfService : prüft Verfügbarkeit
AnmeldungService --> FigurService : validiert Figuren
AnmeldungRepository --> Anmeldung : CRUD
AnmeldungRouter ..> AnmeldungCreate : empfängt
AnmeldungRouter ..> AnmeldungResponse : sendet
@enduml
Schnittstellen des Anmeldungs-Moduls
Bereitgestellte Schnittstellen (API):
| Endpoint | Methode | Beschreibung |
|---|---|---|
|
POST |
Neue Anmeldung erstellen |
|
GET |
Anmeldung abrufen |
|
GET |
Anmeldungen filtern (nach Kind, Wettkampf) |
|
PUT |
Anmeldung ändern (Figuren) |
|
DELETE |
Anmeldung stornieren |
|
POST |
Startnummer vergeben |
Benötigte Schnittstellen:
| Modul | Service | Methode | Zweck |
|---|---|---|---|
Stammdaten |
KindService |
|
Kind-Validierung |
Stammdaten |
KindService |
|
Berechtigung prüfen |
Saisonplanung |
WettkampfService |
|
Wettkampf-Validierung |
Saisonplanung |
WettkampfService |
|
Kapazität prüfen |
Saisonplanung |
FigurService |
|
Figuren-Validierung |
Wichtige Algorithmen
Startnummernvergabe:
def vergebe_startnummer(self, anmeldung_id: int) -> int:
"""
Vergibt eine eindeutige Startnummer für einen Wettkampf.
Algorithmus:
1. Finde höchste vergebene Startnummer für Wettkampf
2. Nächste freie Nummer = höchste + 1
3. Optimistic Lock: Bei Konflikt Retry
"""
anmeldung = self.repo.find_by_id(anmeldung_id)
wettkampf_id = anmeldung.wettkampf_id
max_nummer = self.repo.get_max_startnummer(wettkampf_id)
neue_nummer = (max_nummer or 0) + 1
anmeldung.startnummer = neue_nummer
anmeldung.status = AnmeldungStatus.BESTAETIGT
try:
self.repo.save(anmeldung)
except StaleDataError:
# Retry bei Konflikt
return self.vergebe_startnummer(anmeldung_id)
return neue_nummer
5.4. Level 2 - Modul "Bewertung" (Beispiel)
@startuml
allowmixing
skinparam packageStyle rectangle
package "Bewertung Modul" {
rectangle "BewertungRouter" <<REST>> as BewertungRouter
rectangle "BewertungService" <<Business Logic>> as BewertungService
rectangle "BewertungRepository" <<Data Access>> as BewertungRepository
rectangle "StartRepository" <<Data Access>> as StartRepository
package "Domain" {
class Start {
+id: int
+durchgang_id: int
+kind_id: int
+reihenfolge: int
+status: StartStatus
}
class Bewertung {
+id: int
+start_id: int
+kampfrichter_id: int
+vorlaeufige_punkte: Decimal
+timestamp: datetime
}
class Endpunkte {
+start_id: int
+endpunkte: Decimal
+gestrichene_werte: List[Decimal]
+durchschnitt: Decimal
+schwierigkeitsfaktor: Decimal
}
enum StartStatus {
WARTEND
AUFGERUFEN
ABGESCHLOSSEN
}
}
}
' Externe Abhängigkeiten
package "Wettkampf Modul" {
rectangle "DurchgangService" as DurchgangService
}
package "Saisonplanung Modul" {
rectangle "FigurService" as FigurService
}
' Beziehungen
BewertungRouter --> BewertungService
BewertungService --> BewertungRepository
BewertungService --> StartRepository
BewertungService --> DurchgangService : liest Durchgang
BewertungService --> FigurService : liest Schwierigkeitsfaktor
BewertungRepository --> Bewertung
StartRepository --> Start
@enduml
Kernalgorithmus: Endpunkteberechnung
def berechne_endpunkte(self, start_id: int) -> Endpunkte:
"""
Berechnet Endpunkte nach Liga-Regeln:
1. Höchste und niedrigste Bewertung streichen
2. Durchschnitt der verbleibenden Bewertungen
3. Multiplikation mit Schwierigkeitsfaktor
Beispiel:
Bewertungen: [7.5, 8.0, 7.0, 8.5, 7.5]
Gestrichen: 8.5 (höchste), 7.0 (niedrigste)
Durchschnitt: (7.5 + 8.0 + 7.5) / 3 = 7.67
Schwierigkeitsfaktor: 2.3
Endpunkte: 7.67 × 2.3 = 17.64
"""
bewertungen = self.bewertung_repo.find_by_start(start_id)
if len(bewertungen) < 3:
raise ValidationError("Mindestens 3 Bewertungen erforderlich")
punkte = [b.vorlaeufige_punkte for b in bewertungen]
punkte_sortiert = sorted(punkte)
# Höchste und niedrigste streichen
gestrichene = [punkte_sortiert[0], punkte_sortiert[-1]]
verbleibende = punkte_sortiert[1:-1]
# Durchschnitt
durchschnitt = sum(verbleibende) / len(verbleibende)
# Schwierigkeitsfaktor holen
start = self.start_repo.find_by_id(start_id)
durchgang = self.durchgang_service.get(start.durchgang_id)
figur = self.figur_service.get(durchgang.figur_id)
# Endpunkte
endpunkte_wert = durchschnitt * figur.schwierigkeitsfaktor
return Endpunkte(
start_id=start_id,
endpunkte=round(endpunkte_wert, 2),
gestrichene_werte=gestrichene,
durchschnitt=durchschnitt,
schwierigkeitsfaktor=figur.schwierigkeitsfaktor
)
5.5. Level 2 - Frontend-Struktur
@startuml
package "Frontend" {
package "Planungs-App" {
[Saisonplanung-Pages]
[Stammdaten-Pages]
[Anmeldung-Pages]
[Reporting-Pages]
}
package "Durchführungs-App" {
[Wettkampf-Setup-Pages]
[Bewertung-Pages]
[Auswertung-Pages]
}
package "Shared Components" {
[UI-Components] <<buttons, forms, tables>>
[Domain-Components] <<KindCard, FigurSelect>>
}
package "Services" {
[API-Client] <<TanStack Query>>
[Auth-Service]
[Sync-Service] <<PWA>>
}
package "State Management" {
[Zustand-Stores] <<lokaler State>>
}
}
' Beziehungen
[Saisonplanung-Pages] --> [UI-Components]
[Saisonplanung-Pages] --> [Domain-Components]
[Saisonplanung-Pages] --> [API-Client]
[Bewertung-Pages] --> [UI-Components]
[Bewertung-Pages] --> [API-Client]
[Bewertung-Pages] --> [Sync-Service]
[API-Client] --> [Auth-Service]
@enduml
Frontend-Module
| Modul | Verantwortlichkeit | Technologie |
|---|---|---|
Planungs-App |
Desktop-optimierte Verwaltungs-UI |
React Router, komplexe Formulare |
Durchführungs-App |
Touch-optimierte Wettkampf-UI |
React Router, PWA, große Buttons |
Shared Components |
Wiederverwendbare UI-Elemente |
Storybook-dokumentiert |
API-Client |
Backend-Kommunikation |
TanStack Query (Caching, Sync) |
Sync-Service |
Offline-Fähigkeit |
Service Worker, IndexedDB |
5.6. Modul-Übergreifende Konzepte
Inter-Modul-Kommunikation
Regel: Module kommunizieren nur über Service-Schnittstellen, niemals direkt über Repositories.
# FALSCH: Direkter Repository-Zugriff
class AnmeldungService:
def __init__(self, db: Session):
self.kind_repo = KindRepository(db) # Direkter Zugriff auf fremdes Modul
# RICHTIG: Über Service-Schnittstelle
class AnmeldungService:
def __init__(
self,
anmeldung_repo: AnmeldungRepository,
kind_service: KindService # Über Service-Interface
):
self.anmeldung_repo = anmeldung_repo
self.kind_service = kind_service
5.7. Cloud Deployment
Überblick
Das Aquarius-System wird als Cloud-Native-Anwendung auf fly.io bereitgestellt. Dies ermöglicht eine kostengünstige, skalierbare und wartungsarme Infrastruktur für Kunstschwimm-Wettkämpfe.
Deployment-Zielarchitektur:
┌─────────────────────────────────────────────────────────┐
│ aquarius.arc42.org │
│ (Custom Domain) │
└────────────────────┬────────────────────────────────────┘
│ HTTPS
▼
┌─────────────────────────────────────────────────────────┐
│ fly.io Platform │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────┐ │
│ │ Aquarius App Container │ │
│ │ (Frontend + Backend + Static Files) │ │
│ │ │ │
│ │ Port 8080: │ │
│ │ • FastAPI Backend (/) │ │
│ │ • React Frontend (static build) │ │
│ │ • Static Assets (/static) │ │
│ └──────────┬─────────────────────────────┘ │
│ │ │
│ │ libSQL Protocol │
│ │ (over HTTP/WebSocket) │
│ ▼ │
│ ┌────────────────────────────────────────┐ │
│ │ Turso Database │ │
│ │ (libSQL Cloud) │ │
│ │ │ │
│ │ • Managed SQLite Database │ │
│ │ • Edge Replication (optional) │ │
│ │ • Automatic Backups │ │
│ └────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
Technologie-Stack für Cloud Deployment
| Komponente | Technologie | Begründung |
|---|---|---|
Container-Platform |
fly.io |
• Einfaches Deployment mit |
Datenbank |
Turso (libSQL) |
• Managed SQLite-as-a-Service • SQLAlchemy-kompatibel über libSQL-Treiber • Edge-Replication für niedrige Latenz • Automatische Backups • Kostenlose Tier (500 MB, 1 Mrd. Row Reads/Monat) |
Application Server |
uvicorn |
• ASGI-Server für FastAPI • Hohe Performance • WebSocket-Support |
Static File Serving |
FastAPI StaticFiles |
• Direkt aus Container • Alternative: fly.io Volumes für größere Mengen |
Domain & SSL |
Custom Domain + fly.io SSL |
• aquarius.arc42.org • Automatische TLS-Zertifikate (Let’s Encrypt) |
Deployment-Region (Initial)
Frankfurt (fra) oder Amsterdam (ams) für niedrige Latenz in Deutschland/Europa
┌──────────────────────────────────────────┐ │ fly.io Region: fra │ ├──────────────────────────────────────────┤ │ │ │ ┌────────────────────────────┐ │ │ │ Aquarius Instance │ │ │ │ • 1 VM (shared-cpu-1x) │ │ │ │ • 256 MB RAM │ │ │ │ • 1 GB Disk │ │ │ └────────────────────────────┘ │ │ │ │ │ │ libSQL/HTTP │ │ ▼ │ │ ┌────────────────────────────┐ │ │ │ Turso Database │ │ │ │ • Primary Location: fra │ │ │ │ • 500 MB Storage │ │ │ └────────────────────────────┘ │ │ │ └──────────────────────────────────────────┘
|
Skalierung für spätere Phasen:
|
Container-Strategie
Dockerfile-Architektur
Multi-Stage Build für optimale Image-Größe:
Static File Serving
Frontend wird als statische Build-Artefakte in den Container integriert:
# backend/app/main.py (Cloud-Konfiguration)
import os
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from pathlib import Path
app = FastAPI()
# Serve React Frontend (Cloud)
if os.getenv("FLY_APP_NAME"): # Production auf fly.io
frontend_dist = Path(__file__).parent.parent.parent / "frontend" / "dist"
# API routes unter /api
app.include_router(router, prefix="/api")
# Static assets (JS, CSS, Images)
app.mount("/assets", StaticFiles(directory=str(frontend_dist / "assets")), name="assets")
# Figuren-Bilder
app.mount("/static", StaticFiles(directory=str(frontend_dist / "static")), name="static")
# SPA fallback: Alle anderen Routes → index.html
@app.get("/{full_path:path}")
async def serve_spa(full_path: str):
return FileResponse(frontend_dist / "index.html")
Datenbank-Migration: SQLite → Turso
Turso Connection String
# backend/app/database.py
import os
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
# Cloud: Turso libSQL
# Local: SQLite File
DATABASE_URL = os.getenv(
"DATABASE_URL",
"sqlite:///./database/aquarius.db" # Fallback für lokale Entwicklung
)
# Turso Format: libsql://[db-name]-[org].turso.io?authToken=...
engine = create_engine(
DATABASE_URL,
connect_args={"check_same_thread": False} if "sqlite" in DATABASE_URL else {},
echo=False
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Umgebungsvariablen und Secrets
fly.io Secrets Management
Secrets werden mit flyctl secrets verwaltet, nicht in fly.toml:
# Turso Database URL mit Auth-Token
flyctl secrets set DATABASE_URL="libsql://aquarius-abc123.turso.io?authToken=eyJ..."
# Weitere Secrets (optional)
flyctl secrets set SECRET_KEY="$(openssl rand -base64 32)"
flyctl secrets set ADMIN_PASSWORD="..."
Umgebungsvariablen in fly.toml
# fly.toml
app = "aquarius-production"
primary_region = "fra"
[build]
dockerfile = "Dockerfile"
[env]
# Öffentliche Konfiguration (keine Secrets!)
ENVIRONMENT = "production"
LOG_LEVEL = "info"
PORT = "8080"
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 1
[[vm]]
cpu_kind = "shared"
cpus = 1
memory_mb = 256
|
Niemals Secrets in Verwende immer |
Deployment-Workflow
Automatisches Deployment (CI/CD)
GitHub Actions Workflow:
name: Deploy to fly.io
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup flyctl
uses: superfly/flyctl-actions/setup-flyctl@master
- name: Deploy to fly.io
run: flyctl deploy --remote-only
env:
FLY_API_TOKEN: $
Manuelles Deployment
# 1. Login bei fly.io
flyctl auth login
# 2. App erstellen (einmalig)
flyctl launch --name aquarius-production --region fra
# 3. Secrets konfigurieren (einmalig)
flyctl secrets set DATABASE_URL="libsql://..."
# 4. Deploy durchführen
make deploy-to-fly
# 5. Status prüfen
flyctl status
flyctl logs
Makefile für Deployment
.PHONY: deploy-to-fly
deploy-to-fly: ## Deploy application to fly.io
@echo "🚀 Deploying to fly.io..."
@echo ""
@echo "Prerequisites:"
@echo " - flyctl installed (brew install flyctl)"
@echo " - Logged in (flyctl auth login)"
@echo " - Secrets configured (flyctl secrets list)"
@echo ""
@read -p "Continue with deployment? [y/N] " -n 1 -r; \
echo; \
if [[ $$REPLY =~ ^[Yy]$$ ]]; then \
flyctl deploy --remote-only; \
else \
echo "❌ Deployment cancelled"; \
exit 1; \
fi
.PHONY: deploy-status
deploy-status: ## Check fly.io deployment status
@flyctl status
@echo ""
@flyctl logs --lines 50
.PHONY: deploy-logs
deploy-logs: ## Show fly.io application logs
flyctl logs
.PHONY: deploy-ssh
deploy-ssh: ## SSH into fly.io container
flyctl ssh console
Turso Database Monitoring
# Turso CLI installieren
brew install tursodatabase/tap/turso
# Database Status
turso db show aquarius-production
# Query Statistics
turso db stats aquarius-production
8.4.11 Backup und Disaster Recovery
Turso Automatic Backups
-
Point-in-Time Recovery: Automatisch für letzte 24-48h
-
Manual Snapshots: Vor größeren Änderungen
# Manual Backup erstellen
turso db dump aquarius-production --output backup.sql
# Restore aus Backup
turso db restore aquarius-production --from backup.sql
Container Volumes (für Figuren-Bilder)
Optional: Persistentes Volume für Upload-Bilder:
# fly.toml
[mounts]
source = "aquarius_data"
destination = "/app/data"
# Volume erstellen
flyctl volumes create aquarius_data --region fra --size 1
# Backup von Volume
flyctl ssh sftp get -r /app/data ./local-backup/
Kosten-Kalkulation
fly.io Pricing (Stand Dezember 2025)
| Ressource | Menge | Kosten/Monat |
|---|---|---|
Shared CPU VM |
1x (256 MB RAM) |
~$3-5 |
Persistentes Volume |
1 GB |
~$0.15 |
Outbound Traffic |
~10 GB |
Inkludiert |
Custom Domain + SSL |
1 Domain |
Kostenlos |
Geschätzte Gesamtkosten: $5-10 / Monat für kleine bis mittlere Nutzung
GitHub Secrets Setup
1. API Tokens erstellen
fly.io API Token:
# Interaktiv
flyctl auth token
# Output: FlyV1 fm2_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Turso Auth Tokens (getrennt für Staging & Production):
# Turso Login
turso auth login
# Staging Database Token
turso db tokens create aquarius-staging --expiration none
# Output: eyJhbGc...
# Production Database Token
turso db tokens create aquarius-production --expiration none
# Output: eyJhbGc...
Turso Database Setup
Datenbanken erstellen
# Staging Database
turso db create aquarius-staging --location fra
turso db show aquarius-staging
# Connection URL kopieren
# Beispiel: libsql://aquarius-staging-abc123.turso.io
# Production Database
turso db create aquarius-production --location fra
turso db show aquarius-production
# Connection URL kopieren
# Beispiel: libsql://aquarius-production-xyz789.turso.io
Secrets in fly.io setzen
# Staging
flyctl secrets set \
DATABASE_URL="libsql://aquarius-staging-abc123.turso.io?authToken=eyJhbGc..." \
--app aquarius-staging
# Production
flyctl secrets set \
DATABASE_URL="libsql://aquarius-production-xyz789.turso.io?authToken=eyJhbGc..." \
--app aquarius-production
Daten von Staging → Production kopieren (optional)
# Staging Dump erstellen
turso db dump aquarius-staging --output staging-backup.sql
# In Production importieren (VORSICHT!)
turso db restore aquarius-production --from staging-backup.sql
fly.io Zertifikate
# Staging
flyctl certs create staging.aquarius.arc42.org --app aquarius-staging
# Production
flyctl certs create aquarius.arc42.org --app aquarius-production
# Status prüfen
flyctl certs list --app aquarius-staging
flyctl certs list --app aquarius-production
Sicherheitsaspekte
DB/Schema Migrationen
Technologie: Alembic
# Lokale Migration testen
cd backend
alembic upgrade head
# Cloud Migration durchführen (manuell)
flyctl ssh console
cd /app/backend
alembic upgrade head
|
Zero-Downtime Migrations:
|
Rollback-Strategie
fly.io Release Management
# Releases anzeigen
flyctl releases
# Rollback zur vorherigen Version
flyctl releases rollback
# Rollback zu spezifischer Version
flyctl releases rollback --version v123
Database Rollback
# Alembic Downgrade (lokal testen!)
alembic downgrade -1
# Im Notfall: Turso Point-in-Time Restore
turso db restore aquarius-production --timestamp "2024-01-15T10:00:00Z"
Entwicklungs-Workflow
Lokale Entwicklung mit Turso
# Turso CLI installieren
brew install tursodatabase/tap/turso
# Turso Auth Token holen
turso db tokens create aquarius-dev
# In .env setzen
DATABASE_URL="libsql://aquarius-dev.turso.io?authToken=..."
# App starten
make dev
-
fly.io Docs: https://fly.io/docs/
-
Turso Docs: https://docs.turso.tech/
-
libSQL Python Client: https://github.com/libsql/libsql-client-py
=== CAP-Theorem: Unique Device Binding vs. Offline Operation
==== Context and Problem The system requirement states: "A mobile device shall be used for rating by exactly one user, and no user shall have a second rating device."
In a distributed system with offline capabilities (Partition tolerance), enforcing a strict global uniqueness constraint (Consistency) presents a classic CAP theorem challenge.
-
P (Partition Tolerance): The system must function during network outages (Wettkampf hall has poor WiFi).
-
A (Availability): Judges must be able to rate figures without interruption.
-
C (Consistency): The mapping
User <→ Devicemust be unique globally to prevent fraud or data corruption.
==== CAP Analysis In the event of a network partition (offline mode), the system cannot simultaneously guarantee Consistency (checking if the user is already logged in elsewhere) and Availability (allowing the user to log in and work).
-
CP Approach (Consistency Priority): To guarantee uniqueness, the device MUST contact the server to "claim" the user session.
-
Consequence: If offline, the judge cannot log in. Availability is sacrificed.
-
-
AP Approach (Availability Priority): The device allows login based on cached credentials/tokens.
-
Consequence: Two devices could theoretically be logged in as the same user (Split-Brain). Consistency is sacrificed.
-
==== Solution Strategies
Given the domain constraints (competitions cannot pause for internet issues), we lean towards Availability, but can mitigate Consistency risks through architectural patterns:
===== 1. Optimistic Locking with Conflict Detection (AP)
We accept the risk of dual logins during the partition.
* Mechanism: Each device generates a unique SessionID (UUID) upon login. Ratings are tagged with (UserID, DeviceID, SessionID).
* Reconciliation: When syncing, the server checks for multiple active SessionIDs for the same UserID in the same timeframe.
* Handling: If duplicates are found, they are flagged for the Chief Recorder (human arbitration).
* Pros: Maximum uptime.
* Cons: Post-event cleanup required if discipline fails.
===== 2. The "Token Check-out" Pattern (Hybrid) We shift the Consistency check to a time before the Partition occurs (Preparation Phase). * Mechanism: 1. Online Phase: A device must "download" a cryptographic lease (Token) for a specific user. The server marks the user as "Checked Out". 2. Offline Phase: The app only functions if a valid lease exists locally. 3. Return: To switch devices, the token must be "returned" (synced) or expire. * Pros: Guarantees uniqueness without needing live internet during the event. * Cons: If a device breaks mid-competition, a "Force Release" admin override is needed on the backend to allow a new device to log in.
===== 3. Local Authority (Edge Computing) Reduce the scope of the Partition. * Mechanism: Do not sync against the Cloud, but sync against a local "Wettkampf Server" (e.g., the Laptop of the protocol officer) via a local LAN/Hotspot. * Pros: The Laptop acts as the single source of truth (CP system within the local network). * Cons: Higher infrastructure complexity on-site.
==== Decision Recommendation For Aquarius, we recommend Strategy 2 (Token Check-out) combined with an Admin Override. This aligns with the "Stammdaten" workflow where judges are assigned to competitions beforehand. The "Check-out" happens when the device creates the offline cache.
== Persistenzkonzept: Verteilte Datenhaltung mit Turso
=== Übersicht
Das Persistenzkonzept von Aquarius basiert auf Turso (libSQL), einer verteilten Datenbanktechnologie, die auf SQLite aufbaut. Dies ermöglicht eine hybride Architektur, die sowohl die Vorteile zentraler Cloud-Datenbanken als auch lokaler Offline-Fähigkeit vereint.
=== Architektur
Das System unterscheidet zwischen zwei primären Laufzeitumgebungen:
-
Backend / API (Cloud):
-
Verbindet sich als Client (
libsql://) mit der zentralen Turso-Datenbank. -
Nutzt Edge-Replikation für niedrige Latenz.
-
Dient als "Source of Truth" für Stammdaten.
-
-
Mobile App (Tablet/Offline):
-
Nutzt eine Embedded Replica (lokale SQLite-Datei).
-
Funktioniert vollständig offline (Lesen & Schreiben).
-
Synchronisiert sich automatisch mit der Cloud, sobald eine Verbindung besteht.
-
cloud "Turso Cloud" {
[Primary DB] as Primary
[Edge Replica] as Edge
}
node "Backend Server" {
[FastAPI App] --> Edge : "libsql:// (HTTP)"
}
node "Tablet (Schwimmbad)" {
database "Local DB\n(Embedded)" as LocalDB
[Mobile App] --> LocalDB : "file:// (Direct)"
LocalDB <..> Edge : "Sync Protocol"
}
Primary -right-> Edge : "Replication"
=== Betriebsmodi
==== 1. Lokale Entwicklung (Dev)
Für die lokale Entwicklung wird keine Cloud-Infrastruktur benötigt.
* Treiber: Standard Python SQLite (sqlite3) oder libsql-experimental.
* URL: sqlite:///./aquarius.db
* Vorteil: Einfaches Setup, kein Netzwerk nötig, File-basiert.
==== 2. Produktion (Cloud)
In der Produktionsumgebung verbindet sich die Anwendung mit dem Turso-Cluster.
* Treiber: libsql-client / sqlalchemy-libsql.
* URL: libsql://aquarius-db.turso.io
* Auth: Token-basiert.
=== CLI-Befehle (Cheat Sheet)
Die Verwaltung der Datenbank erfolgt primär über die turso CLI.
turso db create aquarius --location fra
# URL anzeigen
turso db show aquarius --url
# Auth-Token erstellen (gültig für immer)
turso db tokens create aquarius --expiration none
turso db shell aquarius
# Größe und Statistiken
turso db show aquarius
=== Umsetzung in der Anwendung
Die Umschaltung erfolgt transparent über Umgebungsvariablen in app/database.py:
# Automatische Erkennung des Modus anhand der URL
if DATABASE_URL.startswith("libsql://"):
# Cloud Mode (Turso)
engine = create_engine(DATABASE_URL, connect_args={"auth_token": TOKEN})
else:
# Local Mode (File)
engine = create_engine(DATABASE_URL)
== Querschnittliche Konzepte
Dieses Kapitel beschreibt übergreifende Konzepte, die in mehreren Bausteinen zum Einsatz kommen.
=== 8.1 Domänenmodell und Fachliche Architektur
==== 8.1.1 Ubiquitous Language
Das System verwendet konsequent die Fachbegriffe der Domäne:
-
Verein, Team, Kind (nicht: Club, Group, Child)
-
Wettkampf, Durchgang, Start (nicht: Competition, Round, Attempt)
-
Kampfrichter, Punktrichter, Offizieller (nicht: Judge, Referee, Official)
-
Figur, Schwierigkeitsfaktor (nicht: Figure, Difficulty)
Diese Begriffe werden durchgängig verwendet: in der Datenbank, API, UI und Dokumentation.
==== 8.1.2 Fachliche Module (Bounded Contexts)
┌─────────────────────────────────────────────────────────┐ │ Aquarius System │ ├─────────────────────────────────────────────────────────┤ │ │ │ ┌────────────────┐ ┌────────────────┐ │ │ │ Stammdaten │ │ Saisonplanung │ │ │ │ │ │ │ │ │ │ • Verein │ │ • Saison │ │ │ │ • Team │ │ • Figuren │ │ │ │ • Kind │ │ • Wettkämpfe │ │ │ │ • Offizieller │ │ • Schwimmbäder │ │ │ └────────────────┘ └────────────────┘ │ │ │ │ ┌────────────────┐ ┌────────────────┐ │ │ │ Anmeldung │ │ Wettkampf │ │ │ │ │ │ │ │ │ │ • Registrierung│ │ • Stationen │ │ │ │ • Startnummern │ │ • Gruppen │ │ │ │ • Validierung │ │ • Durchgänge │ │ │ └────────────────┘ └────────────────┘ │ │ │ │ ┌────────────────┐ ┌────────────────┐ │ │ │ Bewertung │ │ Auswertung │ │ │ │ │ │ │ │ │ │ • Punkteingabe │ │ • Ranglisten │ │ │ │ • Berechnung │ │ • Preisvergabe │ │ │ │ • Validierung │ │ • Export │ │ │ └────────────────┘ └────────────────┘ │ └─────────────────────────────────────────────────────────┘
Abhängigkeiten zwischen Modulen:
-
Anmeldung → Stammdaten, Saisonplanung
-
Wettkampf → Anmeldung, Stammdaten
-
Bewertung → Wettkampf
-
Auswertung → Bewertung, Wettkampf
==== Wiederkehrender Aufbau fachlicher Module
Jedes fachliche Modul folgt einer einheitlichen 3-Schichten-Architektur:
┌──────────────────────────────────────┐
│ Router (API Layer) │ ← REST-Endpunkte, HTTP-Handling
│ - Request-Validierung (Pydantic) │
│ - Response-Serialisierung │
│ - HTTP-Status-Codes │
└─────────────┬────────────────────────┘
│
│ DTOs (Data Transfer Objects)
▼
┌──────────────────────────────────────┐
│ Service (Business Layer) │ ← Geschäftslogik, Orchestrierung
│ - Business-Rules │
│ - Workflow-Koordination │
│ - Inter-Modul-Kommunikation │
│ - Transaktionssteuerung │
└─────────────┬────────────────────────┘
│
│ Domain-Objekte
▼
┌──────────────────────────────────────┐
│ Repository (Data Layer) │ ← Datenbankzugriff
│ - CRUD-Operationen │
│ - Queries │
│ - ORM-Mapping │
└──────────────────────────────────────┘
== Wave-Animation Konzept
=== Überblick
Der Aquarius Splash-Header verwendet einen anspruchsvollen Mehrschicht-Wasserfluss-Animationseffekt unter Verwendung von SVG-Turbulenzfiltern, CSS-Masken und geschichteten Pseudo-Elementen.
=== Technische Architektur
==== Drei-Schichten-System
@startuml
rectangle "Layer 1 (::before)\nOberste 33%" as L1 {
note right: Sanfte Turbulenz\n12s Animation\nMaske: Fade 60% -> 0%
}
rectangle "Layer 2 (::after)\nMittlere 33%" as L2 {
note right: Mittlere Turbulenz\n15s Animation (-4s Delay)\nMaske: Fade In/Out
}
rectangle "Layer 3 (wrapper)\nUntere 33%" as L3 {
note right: Starke Turbulenz\n18s Animation (-8s Delay)\nMaske: Fade 0% -> 60%
}
L1 -[hidden]-> L2
L2 -[hidden]-> L3
@enduml
==== SVG Turbulenz-Filter
Drei verschiedene Filter erzeugen den fließenden Wassereffekt:
-
turbulence-gentle (Oberste Ebene)
-
baseFrequency: 0.008-0.016 (animiert) -
numOctaves: 3 -
displacement: 15-20px -
Effekt: Subtile Wellenbewegung
-
-
turbulence-medium (Mittlere Ebene)
-
baseFrequency: 0.01-0.02 (animiert) -
numOctaves: 4 -
displacement: 25-35px -
Effekt: Mittlere Verzerrung + Seed-Animation
-
-
turbulence-strong (Untere Ebene)
-
baseFrequency: 0.015-0.025 (animiert) -
numOctaves: 5 -
displacement: 35-50px -
Effekt: Starke Wellenbewegung + Farbverschiebung (Blautönung)
-
==== CSS Masken (Horizontale Zonen)
Jede Ebene ist durch CSS Linear Gradients auf eine bestimmte vertikale Zone beschränkt:
/* Oberes Drittel */
mask: linear-gradient(to bottom,
rgba(0,0,0,0.6) 0%, /* Start sichtbar */
rgba(0,0,0,0.4) 33%, /* Mitte verblassen */
rgba(0,0,0,0) 35% /* Unten unsichtbar */
);
/* Mittleres Drittel */
mask: linear-gradient(to bottom,
rgba(0,0,0,0) 30%, /* Oben unsichtbar */
rgba(0,0,0,0.5) 35%, /* Einblenden */
rgba(0,0,0,0.5) 65%, /* Sichtbar bleiben */
rgba(0,0,0,0) 70% /* Ausblenden */
);
/* Unteres Drittel */
mask: linear-gradient(to bottom,
rgba(0,0,0,0) 65%, /* Oben unsichtbar */
rgba(0,0,0,0.4) 70%, /* Einblenden */
rgba(0,0,0,0.6) 100% /* Voll sichtbar */
);
=== Animations-Timing
Unterschiedliche Animationsgeschwindigkeiten erzeugen eine organische, sich nicht wiederholende Bewegung:
| Ebene | Dauer | Verzögerung | Transformation | Effekt |
|---|---|---|---|---|
Layer 1 |
12s |
0s |
translateX(±20px), scale(1.05-1.08) |
Sanftes horizontales Wiegen |
Layer 2 |
15s |
-4s |
translateX(±15px), translateY(±10px), scale(1.06-1.08) |
Diagonale Welle |
Layer 3 |
18s |
-8s |
translateX(±25px), scale(1.07-1.10) |
Starker horizontaler Fluss |
LCM (Kleinstes gemeinsames Vielfaches): 180s = Voller Zyklus bis zur Wiederholung
=== Performance Optimierungen
/* GPU Beschleunigung */
will-change: transform, filter;
backface-visibility: hidden;
perspective: 1000px;
/* Reduced Motion Support */
@media (prefers-reduced-motion: reduce) {
animation: none;
transform: none;
}
=== Dateistruktur
docs/
├── assets/css/
│ └── wave-animation.scss # Haupt-CSS (kompiliert durch Jekyll)
├── _includes/
│ ├── wave-filters.html # SVG Filter-Definitionen
│ ├── head/custom.html # Injeziert CSS in <head>
│ └── footer/custom.html # Injeziert SVG vor </body>
└── index.md # Nutzt .page__hero--overlay
=== Funktionsweise
-
Jekyll Build kompiliert
wave-animation.scss→wave-animation.css -
Head Include lädt das CSS Stylesheet
-
Footer Include injeziert SVG-Filter in das DOM
-
Pseudo-Elemente (::before, ::after) duplizieren das Hintergrundbild
-
SVG-Filter verzerren jede Ebene unterschiedlich
-
CSS Masken begrenzen jede Ebene auf ihre horizontale Zone
-
Animationen erzeugen die fließende Bewegung mit unterschiedlichen Geschwindigkeiten
=== Browser-Support
-
✅ Chrome/Edge 88+
-
✅ Firefox 90+
-
✅ Safari 14.1+
-
⚠️ iOS Safari (teilweise Performance-Probleme mit SVG-Filtern auf älteren Geräten)
Fallback: Ohne SVG-Filter-Support animieren die Ebenen trotzdem mittels Transformationen (Graceful Degradation).
=== Anpassung
==== Animationsgeschwindigkeit
In wave-animation.scss:
animation: wave-flow-1 12s ease-in-out infinite; // Langsamer: 20s
==== Turbulenz-Intensität
In wave-filters.html:
<feDisplacementMap in="SourceGraphic" scale="15"> <!-- Höher für mehr Verzerrung -->
==== Zonen-Grenzen
In wave-animation.scss:
mask: linear-gradient(to bottom,
rgba(0,0,0,0.6) 0%,
rgba(0,0,0,0.4) 50%, /* Änderung des Split-Points */
rgba(0,0,0,0) 55%
);
=== Tests & Probleme
Lokal testen:
cd docs
docker compose up
# Öffne http://localhost:4000
Bekannte Probleme: * Mobile Safari: Ruckeln auf iPhone 11 und älter möglich. * Firefox: Leichter Performance-Abfall bei schwachen GPUs. * Edge: Gelegentliche Masken-Rendering-Fehler (Refresh behebt es).
=== Credits
Inspiriert von: * SVG Turbulence: MDN Web Docs * CSS Masks: CSS-Tricks
== 9 Querschnittliche Konzepte: App-Benutzer-Authentifizierung und Autorisierung
=== 9.1 Übersicht
Das Aquarius-System unterscheidet zwischen zwei Benutzerklassen:
-
Admin-Benutzer (Rolle: ROOT)
-
Verwaltung von Systemkonfiguration, Benutzern, Monitoring
-
Zugriff nur über spezielle Admin-Endpunkte (
/admin) -
Benötigt 2FA/TOTP-Authentifizierung
-
Existiert bereits seit Projektbeginn
-
-
App-Benutzer (Rolle: OFFIZIELLER, PLANER)
-
Zugriff auf Wettkampf-Verwaltungsanwendung
-
Granulare Berechtigungen: Lesen vs. Schreiben
-
Umgebungsabhängiges Login-Verhalten
-
NEU in Phase 1 der Implementierung
-
=== 9.2 Umgebungsgestütztes Authentifizierungssystem
Das System bietet zwei Modi, gesteuert durch die Umgebungsvariable ENABLE_APP_AUTH:
==== Entwicklungsmodus (ENABLE_APP_AUTH=false)
Browser Request → GET /api/kind
↓
Kein Token vorhanden
↓
Backend: ENABLE_APP_AUTH=false?
↓ ja
Automatischer Login mit DEFAULT_APP_USER
↓
Default-User hat READ + WRITE Zugriff
↓
Response: 200 OK mit Daten
Ziel: Entwickler können die Anwendung lokal ohne komplexe Auth-Konfiguration nutzen.
Konfiguration:
# .env.local
ENABLE_APP_AUTH=false
DEFAULT_APP_USER=testuser
Verhalten:
- Beim ersten Startup wird automatisch ein Standardbenutzer erstellt
- Benutzername: testuser, Passwort: dev-password (hardcodiert)
- Dieser Benutzer hat is_app_user=true, can_read_all=true, can_write_all=true
- Frontend benötigt keinen Token in localStorage
==== Produktionsmodus (ENABLE_APP_AUTH=true)
Browser Request → GET /api/kind (kein Token)
↓
Kein Token vorhanden
↓
Backend: ENABLE_APP_AUTH=true
↓ ja
HTTPException 401: Authentication required
↓
Frontend: Redirect to /app/login
↓
Benutzer gibt Credentials ein
↓
POST /api/auth/token
↓
Backend: Validate credentials + permissions
↓
Response: JWT Token
↓
Frontend: Store in localStorage, retry request
↓
Request: GET /api/kind (mit Token)
↓
Validation erfolgreich
↓
Response: 200 OK mit Daten
Ziel: Sichere Authentifizierung für Public Cloud-Deployments.
Konfiguration:
# .env (Produktion auf fly.io)
ENABLE_APP_AUTH=true
Verhalten:
- Alle Anfragen ohne gültigen JWT-Token werden mit 401 abgelehnt
- Admin erstellt App-Benutzer über Admin-Panel
- Benutzer loggen sich über /app/login ein
=== 9.3 Datenmodell: Benutzerberechtigungen
==== User-Modell-Erweiterungen
class User(Base):
# ... existierende Felder ...
role: str # ROOT, PLANER, OFFIZIELLER
# NEU in Phase 1:
is_app_user: bool # Kann diese Benutzer auf App zugreifen?
can_read_all: bool # Hat Lesezugriff auf App-Daten?
can_write_all: bool # Hat Schreib-/Änderungszugriff?
==== Berechtigungslogik
┌─────────────────────────────────────────────────────┐
│ Authentifizierungs-Entscheidungsbaum │
└─────────────────────────────────────────────────────┘
Benutzer stellt Anfrage:
│
├─→ Hat gültigen Token? (oder ENABLE_APP_AUTH=false)
│ │
│ ├─→ NEIN: 401 Unauthorized
│ │
│ └─→ JA: Weiter
│
├─→ ist_app_user=true ODER role=ROOT?
│ │
│ ├─→ NEIN: 403 Forbidden ("Not an app user")
│ │
│ └─→ JA: Weiter
│
├─→ Lese-Operation?
│ │
│ ├─→ role=ROOT: ✅ Erlaubt (Admin umgeht alle Checks)
│ │
│ └─→ can_read_all=true: ✅ Erlaubt, sonst 403
│
└─→ Schreib-Operation (POST/PUT/DELETE)?
│
├─→ role=ROOT: ✅ Erlaubt (Admin umgeht alle Checks)
│
└─→ can_write_all=true: ✅ Erlaubt, sonst 403
=== 9.4 Backend-Implementierung
==== Authentifizierungs-Dependencies
Das System implementiert drei Stufen der Dependency Injection (FastAPI):
# Stufe 1: Intelligenter Benutzer-Resolver
async def get_current_app_user(
db: Session = Depends(get_db),
credentials: Optional[HTTPAuthorizationCredentials] = Depends(optional_http_bearer),
) -> models.User:
"""
- ENABLE_APP_AUTH=false: Gibt DEFAULT_APP_USER zurück
- ENABLE_APP_AUTH=true: Validiert JWT Token
"""
# Stufe 2: Lesezugriff-Gatekeeper
async def require_app_read_permission(
current_user: models.User = Depends(get_current_app_user),
) -> models.User:
"""
- Admin (ROOT): ✅ Immer erlaubt
- App-Benutzer: ✅ Wenn can_read_all=true
"""
# Stufe 3: Schreibzugriff-Gatekeeper
async def require_app_write_permission(
current_user: models.User = Depends(get_current_app_user),
) -> models.User:
"""
- Admin (ROOT): ✅ Immer erlaubt
- App-Benutzer: ✅ Wenn can_write_all=true
"""
==== Endpoint-Schutz
Jeder Domain-Router schützt seine Endpunkte:
# Router: kind/router.py
@router.get("/kind", response_model=List[KindDTO])
def list_kind(
# ...
current_user: models.User = Depends(auth.require_app_read_permission),
):
"""GET benötigt Lesezugriff"""
# ...
@router.post("/kind", response_model=KindDTO, status_code=201)
def create_kind(
# ...
current_user: models.User = Depends(auth.require_app_write_permission),
):
"""POST benötigt Schreibzugriff"""
# ...
@router.put("/kind/{kind_id}", response_model=KindDTO)
def update_kind(
# ...
current_user: models.User = Depends(auth.require_app_write_permission),
):
"""PUT benötigt Schreibzugriff"""
# ...
@router.delete("/kind/{kind_id}", status_code=204)
def delete_kind(
# ...
current_user: models.User = Depends(auth.require_app_write_permission),
):
"""DELETE benötigt Schreibzugriff"""
# ...
=== 9.5 Frontend-Implementierung (Phase 2)
==== Auth-Context
// src/context/AuthContext.tsx
interface AuthContextType {
user: User | null;
token: string | null;
isLoading: boolean;
canRead: boolean; // Abgeleitet aus user
canWrite: boolean; // Abgeleitet aus user
login: (username: string, password: string) => Promise<void>;
logout: () => void;
}
export const useAuth = () => useContext(AuthContext);
==== Routen-Schutz
// src/components/AppLoginGuard.tsx
const AppLoginGuard: React.FC = () => {
const auth = useAuth();
if (auth.isLoading) return <LoadingSpinner />;
if (!auth.token) return <Navigate to="/app/login" replace />;
return <Outlet />;
};
// In App.tsx:
<Route path="/" element={<AppLoginGuard />}>
{/* Alle App-Routen hier */}
</Route>
==== Permission-Basierte UI
// In Pages und Components:
const KindForm: React.FC = () => {
const auth = useAuth();
return (
<>
{/* Felder immer sichtbar */}
<Input value={kind.name} disabled={!auth.canWrite} />
{/* Buttons basierend auf Berechtigung */}
{auth.canWrite ? (
<Button onClick={handleSave}>Speichern</Button>
) : (
<Alert>Nur-Lese-Zugriff</Alert>
)}
</>
);
};
=== 9.6 Bootstrap-Verfahren
==== Neue Installation (Produktion)
-
Deployment auf fly.io mit
ENABLE_APP_AUTH=true -
Admin loggt sich über
/admin/loginein (existiert bereits) -
Admin navigiert zu Admin-Panel → "App-Benutzer verwalten"
-
Admin erstellt ersten App-Benutzer
-
App-Benutzer loggt sich über
/app/loginein
Zukünftige Erweiterung: Self-Service-Registrierung mit Einladungslinks
==== Entwickler-Workflow
# 1. Repository klonen
git clone ...
cd web
# 2. Development-Server starten
make dev
# 3. Frontend automatisch unter http://localhost:5173 aufrufbar
# 4. Kein Login erforderlich, Default-Benutzer wird automatisch verwendet
# 5. Voller Lese-/Schreibzugriff auf alle App-Daten
# 6. Optional: Admin-Panel unter http://localhost:8000/admin
# Admin-User: admin_test, Passwort: admin_test_password (aus conftest.py)
=== 9.7 Sicherheitsüberlegungen
==== Passwort-Handling
-
Alle Passwörter werden mit bcrypt gehasht (passlib-Library)
-
Niemals Passwörter in Logdatei oder Code speichern
-
Development-Passwort
dev-passwordfür testuser ist hardcodiert (nur in Dev-Modus)
==== Token-Management
-
JWT-Tokens haben 24 Stunden Gültigkeitsdauer
-
Frontend speichert Token in localStorage (nicht sicher für sensitive Daten, aber ausreichend für interne App)
-
Bei 401-Fehler: Frontend leitet zu
/app/loginum -
Bei 403-Fehler: Benutzer hat unzureichende Berechtigungen (UI zeigt Fehlermeldung)
==== Datenschutz
-
Keine persönlichen Daten von Benutzern werden exponiert
-
Permission-Checks auf Server durchgeführt (nicht auf Client vertraut)
-
Frontend-Checks dienen nur UX-Verbesserung, nicht Sicherheit
=== 9.8 Erweiterungspunkte
Zukünftige Verbesserungen:
-
Rollenbasierte Zugriffskontrolle (RBAC)
-
Granularere Rollen als nur true/false für read/write
-
z.B.: Nur "eigene Wettkämpfe" bearbeiten
-
-
Token-Refresh-Mechanismus
-
Refresh-Tokens für langlebige Sessions
-
Automatische Token-Erneuerung
-
-
2FA für App-Benutzer
-
Optional TOTP für sensible Daten
-
Backup-Codes wie bei Admin-Benutzern
-
-
Audit-Logging
-
Alle Änderungen tracken (Wer? Wann? Was?)
-
Für Compliance und Debugging
-
-
Self-Service-Registrierung
-
Einladungslinks für neue App-Benutzer
-
Email-Verifizierung
-
=== 9.9 Implementierungsplan
| Phase | Komponenten | Status |
|---|---|---|
1 |
Backend-Infrastruktur |
✅ DONE - User-Modell erweitern - Auth-Dependencies - Kind-Router als Beispiel - Alle Tests grün |
2 |
Frontend-Foundation |
🔄 IN PROGRESS - AuthContext - AppLoginGuard - AppLogin-Seite - UserMenu |
3 |
UI Permission Gates |
⏳ PLANNED - Read-Only Formulare - Buttons ausblenden - Visuelle Indikatoren |
4 |
Verbleibende Router |
⏳ PLANNED - Alle Domain-Router schützen - Wettkampf, Anmeldung, Grunddaten, etc. |
5 |
Production Ready |
⏳ GEPLANT - E2E-Tests für Auth-Flows - Performance-Tests - Deployment-Checkliste |
=== 9.10 Referenzen
== Architekturentscheidungen
Die wesentlichen Architekturentscheidungen sind in separaten Architecture Decision Records (ADRs) dokumentiert.
Die befinden sich im GitHub Repo unter /documentation/adr, und auf der [Aquarius Website]
# ADR-020: Benutzerverwaltung und Admin-Anwendung
Status: Proposed Datum: 2025-12-20 Entscheider: Entwicklungsteam Bezieht sich auf: [ADR-014 FastAPI Backend](ADR-014-python-fastapi-backend.md), [ADR-013 React Frontend](ADR-013-react-typescript-frontend.md)
## Kontext
Bisher ist die Aquarius-Anwendung komplett ungeschützt. Jeder mit Netzwerkzugriff kann Daten lesen, ändern und löschen. Für den produktiven Betrieb benötigen wir: 1. Schutz sensibler Funktionen (Löschen von Saisons, Ändern von Stammdaten). 2. Unterschiedliche Rollen (Admin, Planer, Kampfrichter). 3. Dedizierte Verwaltungsoberfläche, die sich optisch klar abhebt, um versehentliche Änderungen ("Fat Finger"-Fehler) zu vermeiden. 4. Eine Möglichkeit, Kampfrichter-Accounts für die spätere Bewertungs-App vorzubereiten.
Anforderung des Product Owners: Die Admin-Oberfläche soll "Root-Zugriff"-Charakter haben: Rötlicher Hintergrund, Signalwirkung, aber gut lesbar.
## Entscheidung
# 1. Datenmodell & Sicherheit
Wir erweitern das Datenmodell um eine User-Entität und implementieren Authentifizierung via JWT (JSON Web Tokens).
Rollen-Konzept (RBAC):
* ROOT (System-Admin): Darf alles, insbesondere User verwalten.
* PLANER (Organisation): Darf Saisons, Wettkämpfe, Stammdaten verwalten. Darf keine User anlegen.
* OFFIZIELLER (Kampfrichter/Punktrichter): Darf sich später an der Mobile-App anmelden und bewerten. Zugriff auf Admin-UI ist gesperrt.
# 2. Eigenständige Admin-Anwendung
Wir entwickeln eine separate React-Anwendung (frontend/apps/admin), die getrennt von der Hauptanwendung gebaut und deployt wird.
-
URL:
admin.aquarius.arc42.org(Subdomain) -
Technologie: React, TailwindCSS (wie Haupt-App, Shared Components möglich).
-
Design-Philosophie: "Danger Zone"
-
Hintergrund: Helles Rötlich (
bg-red-50) -
Akzentfarbe: Rot (
red-700) -
Permanenter Header: "SYSTEM ADMINISTRATION - CAUTION"
## Detailliertes Konzept
# 1. Datenbank-Schema (users Tabelle)
Wir fügen dem Backend-Modell folgende Tabelle hinzu:
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True, nullable=False)
full_name = Column(String, nullable=True)
hashed_password = Column(String, nullable=False)
role = Column(String, nullable=False, default="OFFIZIELLER") # ROOT, PLANER, OFFIZIELLER
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
Sicherheit:
* Passwörter werden niemals im Klartext gespeichert.
* Hashing-Algorithmus: bcrypt (via passlib).
* Auth-Flow: OAuth2 mit Password Flow (User sendet User/Pass, erhält JWT Access Token).
# 2. Visuelles Design (Admin UI)
Um den "Root"-Charakter zu unterstreichen und die kognitive Trennung zur normalen Anwendung zu erzwingen:
-
Global Theme:
-
Body Background:
#FEF2F2(Tailwindbg-red-50) -
Card Background:
#FFFFFF(White) mit rotem Border-Top (border-t-4 border-red-600) -
Text: Dunkelgrau für Lesbarkeit, aber Überschriften in Dunkelrot.
-
Warnhinweise: Kritische Aktionen (Löschen, User anlegen) erfordern explizite Bestätigung mit rotem Button.
-
Layout:
-
Sidebar dunkelrot (
bg-red-900) statt dem Blau der Hauptanwendung. -
Favicon in Rot.
Mockup-Beschreibung: > Der Bildschirm hat einen warmen, leicht rötlichen Ton. Oben prangt eine Leiste: "ADMINSTRATION MODE". Die Navigation links ist dunkelrot. Die Tabelle der User ist weiß, aber wenn man über eine Zeile fährt, wird sie zartrot hervorgehoben. Der "Löschen"-Button ist ein ausgefülltes, leuchtendes Rot.
# 3. Funktionaler Umfang der Admin-App
-
Dashboard:
-
Status des Systems (DB-Größe, Anzahl User, Letztes Backup).
-
System-Logs (aus der
.crush/logsoder Backend-Logs).
-
-
User Management (Nur Rolle ROOT):
-
Liste aller User.
-
User anlegen (Username, Initialpasswort, Rolle).
-
Passwort zurücksetzen.
-
User deaktivieren (statt löschen, um Referenzen zu erhalten).
-
-
System-Reset (Nur Rolle ROOT + Extra Bestätigung):
-
"Not-Aus": Datenbank zurücksetzen (für Testphasen).
-
# 4. API Endpoints
Das Backend wird erweitert um:
-
POST /auth/token: Login (Gibt JWT zurück). -
GET /users/me: Profil des aktuellen Users. -
GET /users: Liste aller User (Nur Admin). -
POST /users: User anlegen (Nur Admin). -
PUT /users/{id}: User ändern/sperren.
# 5. Deployment Strategie
Da wir GitHub Pages nutzen, ist das Hosting von zwei Apps trivial:
-
Struktur im Repo:
-
frontend/main/→ Baut nachdist/main -
frontend/admin/→ Baut nachdist/admin -
URL Mapping:
-
aquarius.arc42.org→ Lädtindex.htmlausdist/main -
admin.aquarius.arc42.org→ Lädtindex.htmlausdist/admin(oderaquarius.arc42.org/admin)
## Implementierungs-Plan
-
Backend:
-
pip install python-jose passlib[bcrypt] -
SQLAlchemy Model
Usererstellen. -
Alembic Migration erstellen.
-
auth.pyModul für JWT Handling. -
Seed-Script anpassen: Erstellen eines Default
admin / changemeUsers.
-
-
Frontend:
-
Refactoring des
frontend-Ordners zu einem Monorepo (Workspace) oder Anlage eines parallelen Ordners. -
Setup des "Red Theme".
-
Login-Screen implementieren.
-
## Konsequenzen
-
Positiv:
-
Klare Trennung von Konfiguration und operativem Geschäft.
-
Hohe Sicherheit durch physisch getrennte UIs (User kann sich nicht "versehentlich" im falschen Modus befinden).
-
Erfüllt die Sicherheitsanforderungen für die öffentliche Erreichbarkeit.
-
Negativ:
-
Initialer Mehraufwand (Auth-Implementierung, zweite Frontend-App).
-
User müssen sich Passwörter merken.