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

System-Kontext Aquarius
@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:

Backend

Siehe ADRs für Details:

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

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)

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


5. Bausteinsicht

5.1. Whitebox Gesamtsystem (Level 0)

Das Aquarius-System besteht aus zwei Hauptanwendungen, die auf einem gemeinsamen Backend operieren:

System Overview
@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:

Backend Modules
@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:

Anmeldung Module Structure
@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

/api/anmeldungen

POST

Neue Anmeldung erstellen

/api/anmeldungen/{id}

GET

Anmeldung abrufen

/api/anmeldungen

GET

Anmeldungen filtern (nach Kind, Wettkampf)

/api/anmeldungen/{id}

PUT

Anmeldung ändern (Figuren)

/api/anmeldungen/{id}

DELETE

Anmeldung stornieren

/api/anmeldungen/{id}/startnummer

POST

Startnummer vergeben

Benötigte Schnittstellen:

Modul Service Methode Zweck

Stammdaten

KindService

get_kind(id)

Kind-Validierung

Stammdaten

KindService

ist_startberechtigt(id)

Berechtigung prüfen

Saisonplanung

WettkampfService

get_wettkampf(id)

Wettkampf-Validierung

Saisonplanung

WettkampfService

ist_voll(id)

Kapazität prüfen

Saisonplanung

FigurService

validate_figuren(ids, wettkampf_id)

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)

Bewertung Module Structure
@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

Frontend Structure
@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 flyctl • Automatisches HTTPS/TLS • Integriertes Load Balancing • Globale Edge-Locations • Kostenlose Tier für kleine Apps

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:

  • Multi-Region Deployment: Replicas in mehreren Regionen

  • Database Replication: Turso Edge-Replicas für globale Verfügbarkeit

  • Auto-Scaling: Fly.io Autoscaling basierend auf Traffic

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 fly.toml committen!

Verwende immer flyctl secrets set für sensible Daten.

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:

  1. Backward-Compatible Changes: Neue Spalten als nullable

  2. Blue-Green Deployment: Alte Version läuft während Migration

  3. Rollback-Plan: Jede Migration muss downgrade() haben

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

=== 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 <→ Device must 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).

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

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

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

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

Datenbank erstellen
turso db create aquarius --location fra
Verbindungsdaten abrufen
# URL anzeigen
turso db show aquarius --url

# Auth-Token erstellen (gültig für immer)
turso db tokens create aquarius --expiration none
Lokale Shell öffnen
turso db shell aquarius
Datenbank inspizieren
# 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:

  1. turbulence-gentle (Oberste Ebene)

    • baseFrequency: 0.008-0.016 (animiert)

    • numOctaves: 3

    • displacement: 15-20px

    • Effekt: Subtile Wellenbewegung

  2. turbulence-medium (Mittlere Ebene)

    • baseFrequency: 0.01-0.02 (animiert)

    • numOctaves: 4

    • displacement: 25-35px

    • Effekt: Mittlere Verzerrung + Seed-Animation

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

  1. Jekyll Build kompiliert wave-animation.scsswave-animation.css

  2. Head Include lädt das CSS Stylesheet

  3. Footer Include injeziert SVG-Filter in das DOM

  4. Pseudo-Elemente (::before, ::after) duplizieren das Hintergrundbild

  5. SVG-Filter verzerren jede Ebene unterschiedlich

  6. CSS Masken begrenzen jede Ebene auf ihre horizontale Zone

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

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

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

Automatische Authentifizierung ohne Token-Verwaltung
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)

Vollständige JWT-basierte Authentifizierung
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)

  1. Deployment auf fly.io mit ENABLE_APP_AUTH=true

  2. Admin loggt sich über /admin/login ein (existiert bereits)

  3. Admin navigiert zu Admin-Panel → "App-Benutzer verwalten"

  4. Admin erstellt ersten App-Benutzer

  5. App-Benutzer loggt sich über /app/login ein

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-password fü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/login um

  • 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:

  1. Rollenbasierte Zugriffskontrolle (RBAC)

    • Granularere Rollen als nur true/false für read/write

    • z.B.: Nur "eigene Wettkämpfe" bearbeiten

  2. Token-Refresh-Mechanismus

    • Refresh-Tokens für langlebige Sessions

    • Automatische Token-Erneuerung

  3. 2FA für App-Benutzer

    • Optional TOTP für sensible Daten

    • Backup-Codes wie bei Admin-Benutzern

  4. Audit-Logging

    • Alle Änderungen tracken (Wer? Wann? Was?)

    • Für Compliance und Debugging

  5. 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 (Tailwind bg-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

  1. Dashboard:

    • Status des Systems (DB-Größe, Anzahl User, Letztes Backup).

    • System-Logs (aus der .crush/logs oder Backend-Logs).

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

  3. 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 nach dist/main

  • frontend/admin/ → Baut nach dist/admin

  • URL Mapping:

  • aquarius.arc42.org → Lädt index.html aus dist/main

  • admin.aquarius.arc42.org → Lädt index.html aus dist/admin (oder aquarius.arc42.org/admin)

## Implementierungs-Plan

  1. Backend:

    • pip install python-jose passlib[bcrypt]

    • SQLAlchemy Model User erstellen.

    • Alembic Migration erstellen.

    • auth.py Modul für JWT Handling.

    • Seed-Script anpassen: Erstellen eines Default admin / changeme Users.

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


--- [← Zurück zur Architektur-Übersicht](/architecture/)