docs: massive restructuring of documentation, development guides and agent playbooks

This commit is contained in:
2026-06-15 12:54:38 +02:00
parent e4988b4397
commit ce63303b2c
686 changed files with 45423 additions and 319 deletions
@@ -0,0 +1,64 @@
---
type: Guide
status: ACTIVE
owner: Lead Architect
tags: [coding-style, kdoc, documentation]
last_update: 2026-03-15
---
# KDoc-Styleguide (Kurzfassung)
Dieser Styleguide definiert die wichtigsten Regeln für KDoc-Kommentare in Kotlin-Projekten der Meldestelle. Ziel:
Verständliche, konsistente API-Dokumentation via Dokka (GFM/HTML).
## Grundregeln
- Sprache: Deutsch für Fließtexte; Code/Bezeichner bleiben Englisch.
- Jeder public class, interface, object, enum, public function und public property erhält einen KDoc-Block.
- KDoc beginnt mit einem vollständigen, aussagekräftigen Satz in der dritten Person.
- Beispiele und wichtige Hinweise als kurze Absätze oder Listen, keine Romane.
## Struktur eines KDoc-Blocks
```kotlin
/**
* Beschreibt prägnant, was das Element macht und warum es existiert.
*
* Details: Optionale Erläuterung von Parametern, Nebenwirkungen, Fehlerfällen.
*
* @param id Eindeutige Kennung des Members
* @return Das gefundene Objekt oder null, wenn nicht vorhanden
* @throws IllegalArgumentException Falls Parameter ungültig sind
*/
fun findMember(id: MemberId): Member?
```
## Tags
- @param: Für jeden Parameter bei public Funktionen
- @return: Wenn Rückgabewert semantisch relevant ist
- @throws: Relevante Exceptions dokumentieren
- @since, @see: Sparsam verwenden, wenn es wirklichen Mehrwert bringt
## Stil & Sprache
- Klar, knapp, aktiv. Keine Redundanz.
- Domänenbegriffe verwenden (BCs: members, horses, events, masterdata, infrastructure).
- Keine Interna oder Secrets dokumentieren.
## Beispielschnipsel
```kotlin
/** Erstellt einen neuen Event und persistiert ihn transaktional. */
fun createEvent(cmd: CreateEventCommand): EventId
```
## Dokka-Hinweise
- Dokka erzeugt GFM (Markdown) unter build/dokka/gfm und HTML unter build/dokka/html.
- Source-Link führt auf GitHub (main-Branch). Prüfe Links in der CI.
## Review
- PR-Checklist: "KDoc vollständig?" anhaken, wenn neue public APIs hinzugekommen sind.
- Vale/markdownlint gelten nur für .md; KDoc wird redaktionell in Code-Reviews geprüft.
@@ -0,0 +1,92 @@
# 🛠️ Guide: Conveyor Installation
Dieses Dokument beschreibt die Installation von **Hydraulic Conveyor** auf verschiedenen Linux-Distributionen (Ubuntu
26.04 und Fedora 44).
---
## 1. Ubuntu 26.04 (Debian-basiert)
Der am einfachsten Weg für Ubuntu ist der direkte Download des `.deb`-Pakets. Dieses konfiguriert bei der Installation
automatisch das APT-Repository für zukünftige Updates.
### Installation via .deb (Empfohlen)
```bash
# Aktuelles Paket herunterladen (Beispiel v12.0 - bitte Version ggf. anpassen)
VERSION="12.0"
curl -L https://downloads.hydraulic.dev/conveyor/conveyor_${VERSION}_amd64.deb -o conveyor.deb
# Installieren (konfiguriert auch das Repo automatisch)
sudo apt update
sudo apt install ./conveyor.deb
```
---
## 2. Fedora 44 (RPM-basiert)
Für Fedora wird die Installation via Tarball empfohlen, da Conveyor als autarkes Binary geliefert wird.
### Installation via Tarball (Systemweit)
Dies ist der zuverlässigste Weg für Fedora:
```bash
# Version definieren (Beispiel v12.0, bitte aktuelle Version prüfen)
VERSION="12.0"
curl -L https://downloads.hydraulic.dev/conveyor/conveyor-${VERSION}-linux-amd64.tar.gz -o conveyor.tar.gz
# Entpacken nach /opt
sudo tar -xzf conveyor.tar.gz -C /opt/
sudo ln -s /opt/conveyor-${VERSION}/bin/conveyor /usr/local/bin/conveyor
# Test
conveyor --version
```
### Installation via RPM (Falls verfügbar)
Prüfen Sie auf der Hydraulic Website, ob mittlerweile ein natives RPM-Repository existiert. Falls ja:
```bash
sudo dnf config-manager --add-repo https://conveyor.hydraulic.dev/rpm/conveyor.repo
sudo dnf install conveyor
```
---
## 3. Post-Installation & Verifikation
Nach der Installation sollten Sie den Pfad und die Version prüfen:
```bash
conveyor --version
```
### Root-Key Initialisierung
Beim ersten Ausführen von `conveyor` wird ein Root-Key generiert. **Sichern Sie diesen unbedingt!**
```bash
conveyor make site
```
*Folgen Sie den Anweisungen im Terminal zur Sicherung des Root-Keys.*
---
## 4. Troubleshooting
### Fehlende Bibliotheken (Fedora)
Falls Conveyor native Hilfe benötigt (z.B. für Icons oder Kompression):
```bash
sudo dnf install libX11 libXext libXrender
```
### Berechtigungen
Stellen Sie sicher, dass Ihr Benutzer in der Gruppe `docker` ist, falls Sie Conveyor innerhalb von Containern nutzen
oder Docker-basierte Inputs verwenden (für dieses Projekt primär lokal relevant).
@@ -0,0 +1,112 @@
# 📦 Guide: Desktop App Packaging (Conveyor & Gradle)
Dieses Dokument beschreibt den professionellen Packaging-Prozess für die Meldestelle Desktop App. Wir nutzen **Conveyor** als primäres Werkzeug für das Cross-Platform Packaging (Windows, Linux, macOS), da es stabile Installer inklusive signierter Updates und gebündelter JREs erzeugt.
---
## 1. Strategie: Conveyor vs. Gradle
| Feature | Conveyor (Empfohlen) | Gradle (Compose Plugin) |
| :--- | :--- | :--- |
| **Zielgruppe** | Endanwender (Produktion) | Entwickler (Lokaler Test) |
| **Plattformen** | Windows (.msix), Linux (.deb), macOS | Nur Host-OS (Linux auf Linux) |
| **Updates** | Automatisch integriert | Manuell |
| **JRE** | Amazon Corretto (isoliert) | System JRE oder Toolchain |
---
## 2. Cross-Packaging mit Conveyor
Conveyor ist so konfiguriert, dass es von Linux aus Pakete für alle Zielsysteme schnüren kann.
### Voraussetzungen
1. **JAR-Dateien:** Die App muss kompiliert sein:
```bash
./gradlew :frontend:shells:meldestelle-desktop:jvmJar
```
2. **Icons:** Das System sucht nach `icon.png` in `frontend/shells/meldestelle-desktop/src/jvmMain/resources/`.
### Pakete bauen
Führen Sie Conveyor im Projekt-Root aus:
```bash
# Komplette Release-Site (Windows & Linux)
conveyor make site
# Nur ein spezifisches Paket (schneller für Tests)
conveyor make debian-package # Linux .deb
conveyor make windows-msix # Windows .msix
```
Die Ergebnisse liegen im Ordner `output/`.
---
## 3. Konfiguration (`conveyor.conf`)
Wichtige Parameter der aktuellen Konfiguration (v1.0.1):
* **JDK:** Nutzt `Amazon Corretto 21` für maximale Cross-Platform Stabilität.
* **Heap-Size:** Erhöht auf `-Xmx1024m`, um auch große Stammdaten-Importe zu bewältigen.
* **Linux-Deps:** Automatische Installation von `libasound2`, `libgl1-mesa-glx` und `libx11-6`.
* **Native Access:** `--enable-native-access=ALL-UNNAMED` ist für Netty/SQLite aktiviert.
---
## 4. Netzwerk & Sicherheit (WICHTIG)
Damit die P2P-Funktionen (Chat, Discovery, Sync) nach der Installation funktionieren, müssen folgende Ports auf dem Host-System offen sein:
| Port | Protokoll | Funktion |
| :--- | :--- | :--- |
| **8080** | TCP | P2P Sync & Datenabgleich |
| **8090** | TCP | Veranstaltungs-Chat (WebSocket) |
| **5353** | UDP | mDNS Discovery (Geräte finden) |
### Firewall-Einrichtung (Linux)
Nutzen Sie das optimierte Setup-Script:
```bash
sudo ./setup-firewall-linux.sh
```
### Windows-Besonderheit
Beim ersten Start der `.msix` App wird Windows fragen, ob der Netzwerkzugriff erlaubt werden soll. **Wichtig:** Sowohl "Private" als auch "Öffentliche" Netzwerke anhaken, falls auf Turnieren oft Gast-WLANs oder Hotspots genutzt werden.
---
## 5. Troubleshooting
### Problem: "No main class specified"
**Lösung:** Stellen Sie sicher, dass in der `Main.kt` eine saubere Top-Level `fun main()` existiert und in der `conveyor.conf` auf `at.mocode.frontend.shell.desktop.MainKt` verwiesen wird.
### Problem: SQLite / Native Libs laden nicht
**Lösung:** Prüfen Sie, ob `extract-native-libraries.conf` in der `conveyor.conf` inkludiert ist.
### Problem: JmDNS findet keine Teilnehmer
**Lösung:** Prüfen Sie die Ports via `ss -tulpn`. Auf Linux blockieren oft Docker-Interfaces (`br-*`) den Broadcast. Die App filtert diese nun automatisch, aber ein aktives `setup-firewall-linux.sh` ist zwingend erforderlich.
## 6. Performance-Optimierung (Gradle)
Der Build-Prozess kann bei aktivierter Web-Kompilierung (WASM/JS) sehr lange dauern. Für die reine Desktop-Entwicklung
wurde WASM standardmäßig deaktiviert.
* **WASM aktivieren (z.B. für CI/Portal):** `./gradlew -PenableWasm=true ...`
* **WASM deaktivieren (Default):** `./gradlew ...` (Spart bis zu 70% Build-Zeit).
## 7. Gradle Deep-Optimierung
Neben dem Deaktivieren von WASM wurden folgende systemweite Optimierungen in der `gradle.properties` vorgenommen:
* **Configuration Cache:** Aktiviert. Gradle merkt sich die Projektstruktur, was den Start jedes Befehls um Sekunden bis
Minuten verkürzt.
* **JVM G1GC & 12GB Heap:** Optimiert für große Multi-Modul-Projekte auf Systemen mit viel RAM (ab 16GB).
* **Parallel Workers:** Erhöht auf 12, um die 16 logischen Kerne Ihres Rechners besser auszulasten.
### Optionale Analysen
Statische Analysen sind nun standardmäßig **deaktiviert**, um den täglichen Workflow nicht zu bremsen.
* **Analyse laufen lassen:** `./gradlew staticAnalysis -PrunStaticAnalysis=true`
* **Dokka Dokumentation bauen:** `./gradlew dokkaAll -PrunDokka=true`
Stellen Sie in der `gradle.properties` sicher, dass `enableWasm=false` gesetzt ist, wenn Sie primär an der Desktop-App
arbeiten.
@@ -0,0 +1,82 @@
---
type: Guide
status: ACTIVE
owner: 🧹 Curator & 🏗️ Lead Architect
last_update: 2026-04-02
sources:
- docs/03_Domain/01_Glossary/Ubiquitous_Language.md
- Domain Workshop 2026-03-17
---
# EventFirstWorkflow (MVP)
Ziel: Ein neuer VeranstaltungsDurchlauf wird konsequent „EventFirst“ aufgebaut. Dabei folgt der Bedienfluss strikt der DomänenHierarchie:
Veranstaltung → Turnier → Bewerbe → Abteilungen → Startliste
---
## 1. Vorbedingungen
- Verein (→ Begriff: Veranstalter) ist im System vorhanden.
- Grundlegende Stammdaten synchron (Reiter, Pferde, Vereine) optional für die Planung, erforderlich für Startlisten.
Querverweis: → Ubiquitous Language, Abschnitt „Hierarchie der VeranstaltungsStruktur“ und Begriffe „Veranstaltung“, „Turnier“, „Bewerb“, „Abteilung“, „Startliste“.
---
## 2. SchrittfürSchritt
1) Veranstaltung anlegen
- Eingaben: Titel, Datum(e), Ort, Typ (Turnier, Reitertreffen, …), Veranstalter (Vereinsnummer), interne EventID (System vergibt).
- Output: Veranstaltung existiert, → VeranstaltungsKassa und → TeilnehmerKontoContainer werden vorbereitet.
2) Turnier anlegen (innerhalb der Veranstaltung; mehrfach möglich)
- Eingaben: Turniernummer (offiziell, wenn vorhanden), Sparte(n) (z. B. CDN, CSN), Kategorie (CNEU, C, …), geplanter Zeitraum.
- Output: Turnier angelegt, → Turnierkassa eröffnet; AusschreibungsMetadaten vorbereiten.
3) Bewerbe anlegen (pro Turnier)
- Eingaben: fortlaufende Bewerbsnummer, Bezeichnung, Klasse/Höhe, Richtverfahren, Startberechtigungen.
- Output: Bewerbe als Container für Abteilungen vorhanden.
4) Abteilungen planen/anlegen (pro Bewerb, mindestens eine)
- Eingaben: Abteilungsnummer (fortlaufend), AbteilungsTyp: `SEPARATE_SIEGEREHRUNG` oder `ORGANISATORISCH`, optional TeilnehmerkreisFilter (Lizenz, Altersklasse …).
- Systemhinweis: Bei Überschreiten von ÖTOSchwellenwerten zeigt das System WARNUNG + Option „Override“ (→ TBA hat letztes Wort).
- Output: Abteilungen stehen für Nennungen/Startlisten bereit.
5) Startliste erzeugen (pro Abteilung)
- Eingaben: Nennungen (Paar Reiter+Pferd), ReihenfolgenLogik (z. B. Zufall, Startwunsch), Kollisionen prüfen.
- Output: Fixierte Startliste je Abteilung; Grundlage für Ergebniserfassung und Abrechnung (Sportförderbeitrag, TierwohlEuro pro Start).
---
## 3. Rollen & Verantwortungen
- Meldestelle: Erfassung/Prüfung der Daten, Startlistenpflege, Kassenabwicklung.
- TBA (Turnierbeauftragter): Genehmigung von AbteilungsOverrides und RegelAbweichungen (OverrideEvent wird protokolliert).
- Veranstalter: Finanzielle Verantwortung, KassenSchluss, Freigabe der Ausschreibung.
---
## 4. Artefakte & Systemobjekte
- Veranstaltung (Root) → VeranstaltungsKassa, TeilnehmerKontoContainer, MultiTurnierVerrechnung.
- Turnier → Turnierkassa, Ausschreibung.
- Bewerb → Liste von Abteilungen.
- Abteilung → kleinste ausführbare Einheit (Nennungen, Startliste, Ergebnis, Siegerehrung nach Typ).
---
## 5. Akzeptanzkriterien (MVP)
- Erstellung in exakt der Reihenfolge möglich; Zwischenspeichern und spätere Fortsetzung unterstützt.
- Abteilungen mindestens 1 pro Bewerb; Typ wählbar; Warnungen bei ÖTOSchwellenwert‑Überschreitung.
- Startliste pro Abteilung generierbar; Kollisionen und Startwünsche werden berücksichtigt.
- Abrechnung: Gebühren pro Start (Sportförderbeitrag, TierwohlEuro) werden korrekt ausgewiesen; Zahlvorgänge können turnierübergreifend auf TeilnehmerKonto verbucht werden (EventEbene).
---
## 6. Querverweise
- Domänenbegriffe: `docs/03_Domain/01_Glossary/Ubiquitous_Language.md`
- ÖTOSchwellenwerte: `docs/03_Domain/02_Reference/OETO_Regelwerk/Abteilungs-Trennungs-Schwellenwerte.md`
@@ -0,0 +1,58 @@
---
type: Guide
status: ACTIVE
owner: Lead Architect
last_update: 2026-04-28
---
# Git Branching & Deployment Strategy (Meldestelle)
Um parallele Weiterentwicklung und stabile Feld-Tests zu ermöglichen, nutzen wir einen vereinfachten **GitHub Flow** mit Release-Tags. Da wir ein kleines Team (bzw. Solo-Entwickler mit KI-Agents) sind, verzichten wir auf übermäßig komplexe Git-Flow-Modelle (wie `develop`, `release/*`, `hotfix/*`), stellen aber Stabilität für Deployments sicher.
## 1. Branching-Struktur
### `main` (Source of Truth / Production)
* **Zweck:** Enthält *immer* den aktuellen, stabilen und im Feld getesteten/auslieferbaren Code.
* **Regel:** Direkte Commits auf `main` sind tabu (außer Notfall-Hotfixes).
* **Deployment:** Ein Push/Merge auf `main` bedeutet **nicht** zwingend ein sofortiges Deployment auf Zora, aber der Code ist *bereit* dafür.
### Feature Branches (`feature/*` oder `fix/*`)
* **Zweck:** Hier findet die eigentliche Entwicklung statt (z.B. neue Bounded Contexts, Wizards).
* **Namenskonvention:** `feature/event-wizard-neu`, `fix/zns-import-bug`
* **Lebensdauer:** So kurz wie möglich. Sobald ein Feature/Fix *in sich geschlossen* und lokal getestet ist, wird ein Pull Request (PR) auf `main` erstellt.
### Release Tags (`v1.x.x`)
* **Zweck:** Markiert einen spezifischen, stabilen Punkt auf dem `main`-Branch, der tatsächlich für ein Turnier (Feld-Test) deployed wurde.
* **Szenario:** Du hast Version `v1.2.0` (Plan-B) für ein Turnier deployed. Du entwickelst weiter auf `feature/*` und mergest in `main`. Das nächste Turnier bekommt dann Tag `v1.3.0`.
## 2. Der Workflow im Alltag
1. **Start:** `git checkout main` -> `git pull` -> `git checkout -b feature/mein-neues-feature`
2. **Entwicklung:** Arbeiten, KI-Agents nutzen, Commits machen.
3. **Abschluss:** Feature ist fertig.
4. **Merge:** Pull Request in Gitea erstellen (oder lokal: `git checkout main`, `git merge feature/mein-neues-feature`, `git push`).
5. **Aufräumen:** `git branch -d feature/mein-neues-feature`
## 3. Strategie für Feld-Tests (Turnier-Einsatz)
Wenn ein Turnier ansteht und ein stabiler Stand eingefroren werden muss:
1. Stelle sicher, dass `main` den gewünschten Zustand hat.
2. Setze einen Tag in Git: `git tag -a v1.2.0 -m "Release für Turnier in Neumarkt"`
3. Pushe den Tag: `git push origin v1.2.0`
4. **Deployment:** Das Deployment-Skript zieht sich *diesen* Tag auf Zora (oder baut den Docker-Container aus diesem Tag).
### Was passiert, wenn während des Turniers ein Bug auftritt (Hotfix)?
*Szenario: Das Turnier läuft auf `v1.2.0`. Auf `main` gibt es schon neuere Features (unfertig).*
1. Checkout des stabilen Tags: `git checkout -b hotfix/turnier-fix v1.2.0`
2. Bug fixen, committen.
3. Neuen Tag für das Deployment setzen: `git tag -a v1.2.1 -m "Hotfix ZNS Import"`
4. `git push origin v1.2.1` -> Fix wird auf Zora deployed.
5. **WICHTIG (Backport):** Damit der Fix nicht verloren geht, den Hotfix-Branch danach in `main` mergen: `git checkout main`, `git merge hotfix/turnier-fix`.
## 4. Gitea Actions (CI/CD)
* **Pushes auf `feature/*`:** Führen Code-Checks/Tests aus.
* **Pushes auf `main`:** Führen erweiterte Tests aus und bauen Docker-Images mit dem Tag `latest` sowie dem Git-SHA in die interne Registry (`10.0.0.22:3000`).
* **Erstellung eines Tags (`v*`):** Triggert automatisch den Build und Push von Docker-Images in die interne Registry. Das Image erhält den Namen des Tags (z.B. `:v1.2.0`). Dies ist die Basis für stabile Deployments auf Zora.
@@ -0,0 +1,455 @@
---
type: Guide
status: ACTIVE
owner: Frontend Expert
last_update: 2026-03-15
---
# SQLDelight-Integration in Compose Multiplatform
Diese Anleitung zeigt, wie SQLDelight in einem Compose Multiplatform-Projekt mit Koin Dependency Injection integriert wird.
## Schritt 1: Abhängigkeiten hinzufügen
Folgende Abhängigkeiten in `gradle/libs.versions.toml` eintragen:
```toml
[versions]
sqldelight = "2.0.1"
koin = "3.5.3"
[libraries]
sqldelight-driver-sqlite = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "sqldelight" }
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
sqldelight-driver-native = { module = "app.cash.sqldelight:native-driver", version.ref = "sqldelight" }
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" }
[plugins]
sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
```
In `build.gradle.kts` (Projektebene):
```kotlin
plugins {
alias(libs.plugins.sqldelight) apply false
}
```
In `shared/build.gradle.kts`:
```kotlin
plugins {
alias(libs.plugins.sqldelight)
}
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.koin.core)
implementation(libs.sqldelight.driver.sqlite)
}
androidMain.dependencies {
implementation(libs.koin.android)
implementation(libs.sqldelight.driver.android)
}
iosMain.dependencies {
implementation(libs.sqldelight.driver.native)
}
desktopMain.dependencies {
implementation(libs.sqldelight.driver.sqlite)
}
}
}
sqldelight {
databases {
create("AppDatabase") {
packageName.set("com.example.database")
}
}
}
```
## Schritt 2: SQL-Schema erstellen
**Verzeichnisstruktur anlegen:**
`shared/src/commonMain/sqldelight/com/example/database/`
Datei `User.sq` erstellen:
```sql
CREATE TABLE User
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
imageUrl TEXT
);
-- Neuen Benutzer einfügen
insertUser
::
INSERT INTO User(name, imageUrl)
VALUES (?, ?);
-- Alle Benutzer abrufen
getAllUsers
::
SELECT *
FROM User;
-- Benutzer nach ID abrufen
getUserById
::
SELECT *
FROM User
WHERE id = ?;
-- Benutzer aktualisieren
updateUser
::
UPDATE User
SET name = ?,
imageUrl = ?
WHERE id = ?;
-- Benutzer löschen
deleteUser
::
DELETE
FROM User
WHERE id = ?;
-- Alle Benutzer löschen
deleteAllUsers
::
DELETE
FROM User;
```
## Schritt 3: Datenbank-Treiber-Interface erstellen
In `shared/src/commonMain/kotlin/database/DatabaseDriverFactory.kt`:
```kotlin
expect class DatabaseDriverFactory {
fun createDriver(): SqlDriver
}
```
## Schritt 4: Plattformspezifische Implementierungen
### Android —
`shared/src/androidMain/kotlin/database/DatabaseDriverFactory.android.kt`:
```kotlin
actual class DatabaseDriverFactory(private val context: Context) {
actual fun createDriver(): SqlDriver {
return AndroidSqliteDriver(
schema = AppDatabase.Schema,
context = context,
name = "app.db"
)
}
}
```
### iOS —
`shared/src/iosMain/kotlin/database/DatabaseDriverFactory.ios.kt`:
```kotlin
actual class DatabaseDriverFactory {
actual fun createDriver(): SqlDriver {
return NativeSqliteDriver(
schema = AppDatabase.Schema,
name = "app.db"
)
}
}
```
### Desktop —
`shared/src/desktopMain/kotlin/database/DatabaseDriverFactory.desktop.kt`:
```kotlin
actual class DatabaseDriverFactory {
actual fun createDriver(): SqlDriver {
val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
AppDatabase.Schema.create(driver)
return driver
}
}
```
## Schritt 5: Repository erstellen
In `shared/src/commonMain/kotlin/repository/UserRepository.kt`:
```kotlin
class UserRepository(database: AppDatabase) {
private val queries = database.userQueries
suspend fun insertUser(name: String, imageUrl: String?) = withContext(Dispatchers.IO) {
queries.insertUser(name, imageUrl)
}
suspend fun getAllUsers(): List<User> = withContext(Dispatchers.IO) {
queries.getAllUsers().executeAsList()
}
suspend fun getUserById(id: Long): User? = withContext(Dispatchers.IO) {
queries.getUserById(id).executeAsOneOrNull()
}
suspend fun updateUser(id: Long, name: String, imageUrl: String?) = withContext(Dispatchers.IO) {
queries.updateUser(name, imageUrl, id)
}
suspend fun deleteUser(id: Long) = withContext(Dispatchers.IO) {
queries.deleteUser(id)
}
suspend fun deleteAllUsers() = withContext(Dispatchers.IO) {
queries.deleteAllUsers()
}
}
```
## Schritt 6: Koin-Module konfigurieren
In `shared/src/commonMain/kotlin/di/DatabaseModule.kt`:
```kotlin
val databaseModule = module {
single { DatabaseDriverFactory() }
single { AppDatabase(get<DatabaseDriverFactory>().createDriver()) }
single { UserRepository(get()) }
}
```
### Plattformspezifische Module
### Android —
`shared/src/androidMain/kotlin/di/PlatformModule.android.kt`:
```kotlin
actual val platformModule = module {
single { DatabaseDriverFactory(androidContext()) }
}
```
### iOS —
`shared/src/iosMain/kotlin/di/PlatformModule.ios.kt`:
```kotlin
actual val platformModule = module {
single { DatabaseDriverFactory() }
}
```
### Desktop —
`shared/src/desktopMain/kotlin/di/PlatformModule.desktop.kt`:
```kotlin
actual val platformModule = module {
single { DatabaseDriverFactory() }
}
```
### Gemeinsame Modul-Deklaration —
`shared/src/commonMain/kotlin/di/PlatformModule.kt`:
```kotlin
expect val platformModule: Module
```
## Schritt 7: Koin initialisieren
In `shared/src/commonMain/kotlin/di/KoinInit.kt`:
```kotlin
fun initKoin(appDeclaration: KoinAppDeclaration = {}) = startKoin {
appDeclaration()
modules(
platformModule,
databaseModule
)
}
```
## Schritt 8: Plattform-Initialisierung
### Android —
In `MainActivity.kt`:
```kotlin
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initKoin {
androidContext(this@MainActivity)
}
setContent {
App()
}
}
}
```
### iOS —
In `iosApp/iosApp/iOSApp.swift`:
```swift
@main
struct iOSApp : App {
init() {
KoinInitKt.doInitKoin()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
```
### Desktop —
In `desktopApp/src/jvmMain/kotlin/main.kt`:
```kotlin
fun main() {
initKoin()
application {
Window(onCloseRequest = ::exitApplication) {
App()
}
}
}
```
## Schritt 9: In Compose verwenden
### ViewModel erstellen —
In `shared/src/commonMain/kotlin/viewmodel/UserViewModel.kt`:
```kotlin
class UserViewModel(private val userRepository: UserRepository) : ViewModel() {
var users by mutableStateOf<List<User>>(emptyList())
private set
var isLoading by mutableStateOf(false)
private set
init {
loadUsers()
}
fun loadUsers() {
viewModelScope.launch {
isLoading = true
users = userRepository.getAllUsers()
isLoading = false
}
}
fun addUser(name: String, imageUrl: String?) {
viewModelScope.launch {
userRepository.insertUser(name, imageUrl)
loadUsers()
}
}
fun deleteUser(id: Long) {
viewModelScope.launch {
userRepository.deleteUser(id)
loadUsers()
}
}
}
```
Im Compose-Screen verwenden:
```kotlin
@Composable
fun UserScreen() {
val userViewModel: UserViewModel = koinInject()
LazyColumn {
items(userViewModel.users) { user ->
UserItem(
user = user,
onDelete = { userViewModel.deleteUser(user.id) }
)
}
}
}
@Composable
fun UserItem(user: User, onDelete: () -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = user.name,
modifier = Modifier.weight(1f)
)
Button(onClick = onDelete) {
Text("Löschen")
}
}
}
```
### Fertig!
SQLDelight ist nun vollständig in das Compose Multiplatform-Projekt integriert mit:
- Datenbankbetrieb auf Android, iOS und Desktop
- Koin Dependency Injection konfiguriert
- Repository-Pattern für Clean Architecture
- Einsatzbereite User-Tabelle mit CRUD-Operationen
Die Datenbank verwaltet automatisch die plattformspezifischen Implementierungen, während dieselbe Geschäftslogik auf allen Plattformen geteilt wird.
@@ -0,0 +1,165 @@
---
type: Guide
status: ACTIVE
owner: Frontend Expert
last_update: 2026-03-15
---
# Architekturstrategien für Asynchrone Persistenz in Kotlin Multiplatform: Eine umfassende Analyse zur Integration von SQLDelight in Web-Umgebungen
## 1. Einleitung und Problemstellung
Die Entwicklung plattformübergreifender Anwendungen mittels Kotlin Multiplatform (KMP) hat in den letzten Jahren einen paradigmatischen Wandel vollzogen. Ein zentraler Bestandteil dieser Architektur ist die Datenpersistenz, für die sich SQLDelight als Industriestandard etabliert hat.
Die Integration der Web-Plattform stellt jedoch eine signifikante architektonische Herausforderung dar. Wie in der Problemstellung korrekt identifiziert, existiert eine fundamentale Diskrepanz zwischen den synchronen I/O-Operationen nativer Plattformen (Android, iOS) und der zwingend asynchronen Natur des Webs. Während native SQLite-Treiber (`AndroidSqliteDriver`, `NativeSqliteDriver`) Datenbankoperationen blockierend ausführen können, erfordert der Browser die Nutzung eines `WebWorkerDriver` und asynchrone Initialisierungsmuster.
Dieser Bericht liefert eine Lösungsarchitektur basierend auf dem "Lazy Async Wrapper"-Muster und Koin.
---
## 2. Theoretisches Fundament: Die Asynchronitäts-Lücke
### 2.1 Native vs. Web-Laufzeitumgebungen
Auf nativen Systemen kann der `SqlDriver` synchron instanziiert werden. Im Browser hingegen nutzt SQLDelight `sql.js` oder `sqlite-wasm` in einem Web Worker. Die Kommunikation erfolgt über Message Passing, was `suspend`-Funktionen für die Initialisierung erzwingt.
### 2.2 Der Paradigmenwechsel mit SQLDelight 2.0
Mit Version 2.0 wurde die Konfiguration `generateAsync` eingeführt:kotlin sqldelight { databases { create("AppDatabase") { packageName.set("com.example.db") generateAsync.set(true) } } }
Setzt man dieses Flag auf `true`, werden alle Datenbankoperationen als `suspend`-Funktionen generiert.[1, 4] Dies ist der erste Schritt zur Vereinheitlichung: Auch native Plattformen nutzen nun (formal) asynchrone Schnittstellen, was den gemeinsamen Code homogenisiert.
---
## 3. Die Lösungsarchitektur: Das "Lazy Async Wrapper"-Muster
Anstatt die Datenbank direkt beim App-Start zu initialisieren (was im Web blockieren oder fehlschlagen würde, wenn der Worker noch nicht bereit ist), kapseln wir den Treiber in einer Wrapper-Klasse.[5, 2]
### 3.1 Definition der Factory
**Datei:** `shared/src/commonMain/kotlin/.../DatabaseDriverFactory.kt`
```kotlin
interface DatabaseDriverFactory {
suspend fun createDriver(): SqlDriver
}
```
### 3.2 Der Database Wrapper
Diese Komponente löst das Problem des Nutzers, indem sie die Initialisierung bis zum ersten Zugriff verzögert und mittels `Mutex` absichert.
**Datei:** `shared/src/commonMain/kotlin/.../DatabaseWrapper.kt`
```kotlin
class DatabaseWrapper(private val driverFactory: DatabaseDriverFactory) {
private var _database: AppDatabase? = null
private val mutex = Mutex()
suspend fun get(): AppDatabase {
_database?.let { return it }
return mutex.withLock {
_database?: AppDatabase(driverFactory.createDriver()).also { _database = it }
}
}
// Helper für Repositories
suspend operator fun <R> invoke(block: suspend (AppDatabase) -> R): R {
return block(get())
}
}
```
---
## 4. Implementierung der Plattform-Treiber
### 4.1 Web (Kotlin/Wasm & JS)
Hier liegt der Kern der Lösung: Wir warten explizit auf die Schema-Erstellung (`awaitCreate`), bevor wir den Treiber zurückgeben.
**Datei:** `shared/src/jsMain/kotlin/.../WebDatabaseDriverFactory.kt`
```kotlin
class WebDatabaseDriverFactory : DatabaseDriverFactory {
override suspend fun createDriver(): SqlDriver {
val worker = Worker(
js("""new URL("@cashapp/sqldelight-sqljs-worker/sqljs.worker.js", import.meta.url)""")
)
val driver = WebWorkerDriver(worker)
// WICHTIG: Hier wird asynchron gewartet!
AppDatabase.Schema.create(driver).await()
return driver
}
}
```
**Webpack Konfiguration:**
Damit dies funktioniert, muss die `sql-wasm.wasm` Datei korrekt kopiert werden.
```javascript
// webpack.config.d/sqljs.js
const CopyWebpackPlugin = require('copy-webpack-plugin');
config.plugins.push(
new CopyWebpackPlugin({
patterns: [
'../../node_modules/sql.js/dist/sql-wasm.wasm'
]
})
);
```
### 4.2 Android (Synchron)
Für Android geben wir den synchronen Treiber einfach in der `suspend`-Funktion zurück.
```kotlin
class AndroidDatabaseDriverFactory(private val context: Context) : DatabaseDriverFactory {
override suspend fun createDriver(): SqlDriver {
return AndroidSqliteDriver(AppDatabase.Schema, context, "app.db")
}
}
```
---
## 5. Integration mit Koin
Da der `DatabaseWrapper` selbst leichtgewichtig ist (er erstellt die DB noch nicht im Konstruktor), kann er problemlos als `single` in Koin registriert werden.
```kotlin
val appModule = module {
single { DatabaseWrapper(get()) }
single { MyRepository(get()) }
}
```
Das Repository nutzt dann den Wrapper:
```kotlin
class MyRepository(private val dbWrapper: DatabaseWrapper) {
suspend fun getItems() = dbWrapper { db ->
db.itemQueries.selectAll().executeAsList()
}
}
```
## 6. Zusammenfassung
Diese Architektur löst den Konflikt zwischen synchronen und asynchronen Welten durch:
1. **`generateAsync = true`**: Erzwingt `suspend` überall.
2. **Wrapper Pattern**: Kapselt die asynchrone Initialisierung (`await()`) im Web.
3. **Koin Singleton**: Der Wrapper kann sofort injiziert werden, die DB wird erst beim ersten `invoke` geladen.
@@ -0,0 +1,54 @@
---
type: Guide
status: ACTIVE
owner: DevOps Engineer
tags: [git, workflow, pr, branching]
---
# Branchschutz & Pull-Request Workflow
Diese Anleitung beschreibt einen einfachen, robusten Flow für `main` mit kurzen Feature-Branches und klaren
Qualitätschecks.
## 1) Branch-Naming
- Feature: `feature/<kurz-beschreibung>`
- Bugfix: `fix/<kurz-beschreibung>`
- Docs: `docs/<kurz-beschreibung>`
Optional: Issue-Key voranstellen, z. B. `feature/MP-7-doku-konsolidieren`.
## 2) Pull Request (PR)
- PR-Titel nach Conventional Commits (kurz): `docs(api): FrontMatter vereinheitlicht (MP-7)`
- Beschreibung kurz mit Bulletpoints; DoD-Checkliste abhaken (Template vorhanden)
- CI muss grün sein (Backend + Docs)
## 3) Branchschutz (GitHub Einstellungen → Branches → main)
- Require a pull request before merging
- Require status checks to pass before merging
- aktivieren: `CI Docs`, `CI` (Backend falls vorhanden)
- Require linear history
- Require approvals: mindestens 1 (bei Solo-Projekt optional, aber empfohlen)
- Allow squash merging only
- Disallow force pushes, Disallow deletions
## 4) Commits & YouTrack
- Commit-Message enthält Issue-Key (z. B. `MP-7`) → erleichtert Nachverfolgung
- In Doku-FrontMatter `yt_epic`/`yt_issues` pflegen
- Optional: GitHub Secrets `YT_URL`, `YT_TOKEN` setzen → CI validiert verlinkte Issues, und `youtrack-sync.yml`
kommentiert beim Merge automatisch ins Issue
## 5) Definition of Done (Auszug)
- Doku aktuell (README/ADR/C4/API)
- FrontMatter valide (`modul`, `status`, `summary`, optional `last_reviewed`, `review_cycle`, `yt_*`)
- Links funktionieren (CI link-check grün)
- Tests grün
## 6) Lokale Tipps
- Vor dem Push: `markdownlint`, `vale` lokal laufen lassen (optional via pre-commit hooks)
- kleine, häufige PRs statt großer Monster-PRs
+89
View File
@@ -0,0 +1,89 @@
---
type: Guide
status: ACTIVE
owner: DevOps Engineer
tags: [setup, local, docker, gradle]
---
# Start Local (Lokales Setup)
Kurzanleitung, um das Projekt lokal in wenigen Minuten zu starten DesktopFirst mit optionalem DockerBackend.
## Voraussetzungen (exakte Versionen)
- Git ≥ 2.40
- JDK 25 (Temurin/Eclipse Adoptium empfohlen) Projekttoolchain lädt bei Bedarf automatisch
- Gradle Wrapper 9.4.0 (wird über `./gradlew` automatisch verwendet)
- Docker Engine ≥ 24, Docker Compose v2 ≥ 2.24
Hinweise:
- Die JavaToolchain wird per Gradle automatisch heruntergeladen (`org.gradle.java.installations.auto-download=true`). Ein lokal installiertes JDK 25 wird dennoch empfohlen für IDERuns.
- Auf AppleSilicon (arm64) sind die DockerImages optimiert; Keycloak nutzt `start --optimized`.
## Schnellstart (nur Backend in Docker)
```bash
# 1) Repository klonen
git clone https://github.com/StefanMoCoAt/meldestelle.git
cd meldestelle
# 2) Runtime-Environment vorbereiten
# Kopiere die Vorlage.
cp .env.example .env
# 3) Infrastruktur starten (Postgres, Valkey, Keycloak, Tracing, Service Discovery)
docker compose --profile infra up -d
# 4) Backend starten (Gateway + Ping Service)
docker compose --profile backend up -d
```
Sobald die Infrastruktur läuft, erreichst du unter anderem:
- Gateway (API): http://localhost:8081
- Keycloak (IAM): http://localhost:8180
- Zipkin (Tracing): http://localhost:9411
- Consul (Service Discovery): http://localhost:8500
- Optional WebApp (falls `--profile gui` gebaut/gestartet): http://localhost:4000
## DesktopApp starten (Compose Desktop)
Die DesktopApp ist der primäre EntwicklungsEntryPoint.
```bash
# 1) Abhängigkeiten bauen (optional; Gradle lädt automatisch beim ersten Run)
./gradlew :frontend:shells:meldestelle-desktop:build
# 2) DesktopApp starten
./gradlew :frontend:shells:meldestelle-desktop:run
```
Voraussetzung: Für Features, die BackendKonnektivität benötigen (z. B. Login, Stammdaten), muss das DockerBackend (infra + backend) laufen. Für rein lokale/offline Flows kann die App auch ohne Docker gestartet werden.
## Tests ausführen
```bash
# Führt alle Tests aus
./gradlew test
# Spezifisches Backend-Modul testen
./gradlew :backend:services:ping:ping-service:test
```
## Troubleshooting
- Dienste starten nicht? Ports belegt oder Logs prüfen:
```bash
docker ps
docker logs <container-name>
```
- Infrastruktur neu starten:
```bash
docker compose down -v
docker compose --profile infra up -d
```
- EnvironmentVariablen: werden aus der `.env`Datei im RootVerzeichnis geladen.
- Gradle/Java Probleme? Stelle sicher, dass JDK 25 aktiv ist bzw. lasse die GradleToolchain das passende JDK laden.
- ARM64 (Apple Silicon): Falls Images nicht starten, lösche alte Images und starte neu: `docker compose down -v && docker system prune -af && docker compose --profile infra up -d`.
## Weiterführende Hinweise
- README QuickStart (DesktopFirst): `README.md`
- ArchitekturKurzkonzept (OfflineFirst Desktop & Backend): `docs/01_Architecture/konzept-offline-first-desktop-backend-de.md`
- ADRs: `docs/01_Architecture/adr/`
- Aktuelle Reports: `docs/90_Reports/`