8 Commits

Author SHA1 Message Date
stefan 280db663c7 chore(build, docs): Gradle auf 9.5.0 und Kotlin auf 2.3.21 aktualisiert
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-05-09 10:20:02 +02:00
stefan 74ef6424b7 docs(journal): Session-Log zu P2P-Guards, FilePicker-Fixes und Tests hinzugefügt
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-05-08 15:08:20 +02:00
stefan 3959168695 feat(core, network): Port-Guards für Mehrfachstarts von P2P-Server integriert
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-05-08 12:45:13 +02:00
stefan 04a435df1d refactor(core, desktop): Fehlertexte präzisiert und Verzeichnisauswahl für JFileChooser optimiert
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-05-08 12:25:58 +02:00
stefan 3aaf5cc59c feat(desktop, network): Fehlerhandling verbessert, Tools-Menü erweitert und mDNS-Discovery optimiert
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-05-07 17:18:17 +02:00
stefan a2d94bbc7e refactor(desktop, core): Exception-Handling optimiert und Divider-Komponente angepasst
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-05-07 15:44:08 +02:00
stefan 95a130c72e feat(desktop, device-initialization): Tools-Menü mit Backup-Option und Reset-Funktion ergänzt
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-05-07 15:42:12 +02:00
stefan 223bf77776 feat(core, network): lokale Chat-Kommunikation und WebSocket-Server hinzugefügt
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-05-07 10:58:31 +02:00
49 changed files with 834 additions and 218 deletions
+2 -2
View File
@@ -5,8 +5,8 @@
# ===================================================================
# === CENTRALIZED BUILD ARGUMENTS ===
ARG GRADLE_VERSION=9.4.1
ARG JAVA_VERSION=25
ARG GRADLE_VERSION=9.5.0
ARG JAVA_VERSION=25.0.2
ARG BUILD_DATE
ARG VERSION=1.0.0-SNAPSHOT
@@ -5,8 +5,8 @@
# ===================================================================
# === CENTRALIZED BUILD ARGUMENTS ===
ARG GRADLE_VERSION=9.4.1
ARG JAVA_VERSION=25
ARG GRADLE_VERSION=9.5.0
ARG JAVA_VERSION=25.0.2
ARG BUILD_DATE
ARG VERSION=1.0.0-SNAPSHOT
+2 -2
View File
@@ -5,8 +5,8 @@
# ===================================================================
# === CENTRALIZED BUILD ARGUMENTS ===
ARG GRADLE_VERSION=9.4.1
ARG JAVA_VERSION=25
ARG GRADLE_VERSION=9.5.0
ARG JAVA_VERSION=25.0.2
ARG BUILD_DATE
ARG VERSION=1.0.0-SNAPSHOT
+2 -2
View File
@@ -5,8 +5,8 @@
# ===================================================================
# === CENTRALIZED BUILD ARGUMENTS ===
ARG GRADLE_VERSION=9.4.1
ARG JAVA_VERSION=25
ARG GRADLE_VERSION=9.5.0
ARG JAVA_VERSION=25.0.2
ARG BUILD_DATE
ARG VERSION=1.0.0-SNAPSHOT
+2 -2
View File
@@ -4,8 +4,8 @@
# ===================================================================
# === CENTRALIZED BUILD ARGUMENTS ===
ARG GRADLE_VERSION=9.4.1
ARG JAVA_VERSION=25
ARG GRADLE_VERSION=9.5.0
ARG JAVA_VERSION=25.0.2
ARG BUILD_DATE
ARG VERSION=1.0.0-SNAPSHOT
+2 -2
View File
@@ -5,8 +5,8 @@
# ===================================================================
# === CENTRALIZED BUILD ARGUMENTS ===
ARG GRADLE_VERSION=9.4.1
ARG JAVA_VERSION=25
ARG GRADLE_VERSION=9.5.0
ARG JAVA_VERSION=25.0.2
ARG BUILD_DATE
ARG VERSION=1.0.0-SNAPSHOT
+2 -2
View File
@@ -5,8 +5,8 @@
# ===================================================================
# === CENTRALIZED BUILD ARGUMENTS ===
ARG GRADLE_VERSION=9.4.1
ARG JAVA_VERSION=25
ARG GRADLE_VERSION=9.5.0
ARG JAVA_VERSION=25.0.2
ARG BUILD_DATE
ARG VERSION=1.0.0-SNAPSHOT
@@ -5,8 +5,8 @@
# ===================================================================
# === CENTRALIZED BUILD ARGUMENTS ===
ARG GRADLE_VERSION=9.4.1
ARG JAVA_VERSION=25
ARG GRADLE_VERSION=9.5.0
ARG JAVA_VERSION=25.0.2
ARG BUILD_DATE
ARG VERSION=1.0.0-SNAPSHOT
@@ -5,8 +5,8 @@
# ===================================================================
# === CENTRALIZED BUILD ARGUMENTS ===
ARG GRADLE_VERSION=9.4.1
ARG JAVA_VERSION=25
ARG GRADLE_VERSION=9.5.0
ARG JAVA_VERSION=25.0.2
ARG BUILD_DATE
ARG VERSION=1.0.0-SNAPSHOT
@@ -5,8 +5,8 @@
# ===================================================================
# === CENTRALIZED BUILD ARGUMENTS ===
ARG GRADLE_VERSION=9.4.1
ARG JAVA_VERSION=25
ARG GRADLE_VERSION=9.5.0
ARG JAVA_VERSION=25.0.2
ARG BUILD_DATE
ARG VERSION=1.0.0-SNAPSHOT
+2 -2
View File
@@ -5,8 +5,8 @@
# ===================================================================
# === CENTRALIZED BUILD ARGUMENTS ===
ARG GRADLE_VERSION=9.4.1
ARG JAVA_VERSION=25
ARG GRADLE_VERSION=9.5.0
ARG JAVA_VERSION=25.0.2
ARG BUILD_DATE
ARG VERSION=1.0.0-SNAPSHOT
+1 -1
View File
@@ -462,6 +462,6 @@ tasks.withType<Exec>().configureEach {
}
tasks.wrapper {
gradleVersion = "9.4.1"
gradleVersion = "9.5.0"
distributionType = Wrapper.DistributionType.BIN
}
+2 -1
View File
@@ -40,7 +40,8 @@ app {
jvm-options = [
"-Xms128m",
"-Xmx512m",
"-Dfile.encoding=UTF-8"
"-Dfile.encoding=UTF-8",
"--enable-native-access=ALL-UNNAMED"
]
}
+20 -20
View File
@@ -12,8 +12,8 @@ services:
context: .
dockerfile: backend/infrastructure/gateway/Dockerfile
args:
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.4.1}"
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25}"
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.5.0}"
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25.0.2}"
VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}"
BUILD_DATE: "${DOCKER_BUILD_DATE}"
labels:
@@ -102,8 +102,8 @@ services:
context: .
dockerfile: backend/services/ping/Dockerfile
args:
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.4.1}"
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25}"
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.5.0}"
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25.0.2}"
VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}"
BUILD_DATE: "${DOCKER_BUILD_DATE}"
labels:
@@ -179,8 +179,8 @@ services:
context: .
dockerfile: backend/services/masterdata/Dockerfile
args:
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.4.1}"
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25}"
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.5.0}"
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25.0.2}"
VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}"
BUILD_DATE: "${DOCKER_BUILD_DATE}"
labels:
@@ -256,8 +256,8 @@ services:
context: .
dockerfile: backend/services/events/Dockerfile
args:
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.4.1}"
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25}"
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.5.0}"
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25.0.2}"
VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}"
BUILD_DATE: "${DOCKER_BUILD_DATE}"
labels:
@@ -331,8 +331,8 @@ services:
context: .
dockerfile: backend/services/zns-import/Dockerfile
args:
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.4.1}"
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25}"
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.5.0}"
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25.0.2}"
VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}"
BUILD_DATE: "${DOCKER_BUILD_DATE}"
labels:
@@ -407,8 +407,8 @@ services:
context: .
dockerfile: backend/services/results/results-service/Dockerfile
args:
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.4.1}"
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25}"
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.5.0}"
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25.0.2}"
VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}"
BUILD_DATE: "${DOCKER_BUILD_DATE}"
labels:
@@ -482,8 +482,8 @@ services:
context: .
dockerfile: backend/services/billing/billing-service/Dockerfile
args:
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.4.1}"
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25}"
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.5.0}"
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25.0.2}"
VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}"
BUILD_DATE: "${DOCKER_BUILD_DATE}"
labels:
@@ -555,8 +555,8 @@ services:
context: .
dockerfile: backend/services/mail/Dockerfile
args:
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.4.1}"
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25}"
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.5.0}"
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25.0.2}"
VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}"
BUILD_DATE: "${DOCKER_BUILD_DATE}"
labels:
@@ -627,8 +627,8 @@ services:
context: .
dockerfile: backend/services/scheduling/scheduling-service/Dockerfile
args:
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.4.1}"
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25}"
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.5.0}"
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25.0.2}"
VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}"
BUILD_DATE: "${DOCKER_BUILD_DATE}"
labels:
@@ -700,8 +700,8 @@ services:
context: .
dockerfile: backend/services/series/series-service/Dockerfile
args:
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.4.1}"
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25}"
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.5.0}"
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25.0.2}"
VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}"
BUILD_DATE: "${DOCKER_BUILD_DATE}"
labels:
@@ -5,23 +5,23 @@ owner: Lead Architect
date: 2026-02-02
---
# Engineering Moderner Frontend-Architekturen: Kotlin 2.3.0, Compose Multiplatform 1.10.0 und Gradle 9.0 für Modulare Monolithen
# Engineering Moderner Frontend-Architekturen: Kotlin 2.3.21, Compose Multiplatform 1.10.0 und Gradle 9.5.0 für Modulare Monolithen
Der architektonische Übergang zu modularen Monolithen bietet Unternehmen die Möglichkeit, die Komplexität von
Microservices zu reduzieren und gleichzeitig eine klare Trennung der Domänenlogik beizubehalten. In Kombination mit
Kotlin Multiplatform (KMP) für Single Page Applications (SPAs) lässt sich Geschäftslogik effizient über den gesamten
Stack teilen. Die Einführung von Kotlin 2.3.0, Compose Multiplatform 1.10.0 und Gradle 9.0 stellt dabei neue Best
Stack teilen. Die Einführung von Kotlin 2.3.21, Compose Multiplatform 1.10.0 und Gradle 9.5.0 stellt dabei neue Best
Practices für Build-Performance und Deployment auf.
## 1. Gradle 9.x Optimierung in der CI/CD
Gradle 9.0 führt signifikante Änderungen ein, die speziell für große Multi-Modul-Projekte wie modulare Monolithen
Gradle 9.5.0 führt signifikante Änderungen ein, die speziell für große Multi-Modul-Projekte wie modulare Monolithen
optimiert sind.
- **Configuration Cache als Standard:** In Gradle 9.0 ist der Configuration Cache der bevorzugte Ausführungsmodus. Durch
- **Configuration Cache als Standard:** In Gradle 9.5.0 ist der Configuration Cache der bevorzugte Ausführungsmodus. Durch
das Caching des Task-Graphen werden nachfolgende Builds erheblich beschleunigt, da die Konfigurationsphase
übersprungen wird.
- **Kotlin DSL Script Compilation Avoidance:** Durch den Einsatz von ABI-Fingerprinting erkennt Gradle 9.0, ob
- **Kotlin DSL Script Compilation Avoidance:** Durch den Einsatz von ABI-Fingerprinting erkennt Gradle 9.5.0, ob
Änderungen an `.kts`-Dateien die Build-Logik tatsächlich beeinflussen. Nicht-relevante Änderungen (wie Kommentare)
führen nicht mehr zur Neukompilierung, was die Konfigurationszeit um bis zu 60 % reduzieren kann.
- **Parallel Configuration Store and Load:** Gradle 8.11 und 9.0 unterstützen das parallele Laden und Speichern von
@@ -74,14 +74,14 @@ Die Wahl des Webservers beeinflusst das Routing und die Performance der Compose
| Komponente | Empfehlung | Vorteil |
|---------------|-------------------------------------|--------------------------------------------------------|
| Build-Tool | Gradle 9.0 mit Configuration Cache | Extreme Verkürzung der Konfigurationsphase |
| Build-Tool | Gradle 9.5.0 mit Configuration Cache | Extreme Verkürzung der Konfigurationsphase |
| CI Caching | Remote Cache Action (Proxy) | Wiederverwendung von Task-Outputs auf frischen Runnern |
| Konfiguration | Runtime config.json Fetch | Ein Docker-Image für alle Umgebungen (Dev/Prod) |
| Webserver | Caddy oder Nginx | Optimiertes SPA-Routing und Web Cache Support |
## Fazit
Die Kombination aus Gradle 9.0 und Kotlin 2.3.0 ermöglicht hocheffiziente Build-Pipelines für modulare Monolithen. Durch
Die Kombination aus Gradle 9.5.0 und Kotlin 2.3.21 ermöglicht hocheffiziente Build-Pipelines für modulare Monolithen. Durch
den Einsatz von Multi-Stage Docker-Builds und dem `config.json`-Fetch-Muster wird eine moderne, skalierbare
Deployment-Strategie umgesetzt, die die neuen Performance-Features von Compose Multiplatform 1.10.0 optimal nutzt.
@@ -9,7 +9,7 @@ Hier ist der Quellcode des Berichts im Markdown-Format:
## Zusammenfassung
Die Softwareentwicklungslandschaft des Jahres 2026, geprägt durch die Veröffentlichung von Kotlin 2.3.0 und Gradle 9.1.0, bietet Entwicklern beispiellose Möglichkeiten zur Vereinheitlichung komplexer Geschäftslogik über Plattformgrenzen hinweg. Dieser Forschungsbericht analysiert detailliert die architektonischen Muster, Implementierungsstrategien und zugrundeliegenden Mechanismen, die für den Aufbau einer robusten, asynchronen Offline-First-Anwendung erforderlich sind. Der Fokus liegt hierbei auf der Integration von SQLDelight in einer Kotlin Multiplatform (KMP) Umgebung, die sowohl Desktop (JVM) als auch Web (Kotlin/JS) Ziele bedient, eingebettet in eine Mikro-Frontend-Architektur.
Die Softwareentwicklungslandschaft des Jahres 2026, geprägt durch die Veröffentlichung von Kotlin 2.3.21 und Gradle 9.5.0, bietet Entwicklern beispiellose Möglichkeiten zur Vereinheitlichung komplexer Geschäftslogik über Plattformgrenzen hinweg. Dieser Forschungsbericht analysiert detailliert die architektonischen Muster, Implementierungsstrategien und zugrundeliegenden Mechanismen, die für den Aufbau einer robusten, asynchronen Offline-First-Anwendung erforderlich sind. Der Fokus liegt hierbei auf der Integration von SQLDelight in einer Kotlin Multiplatform (KMP) Umgebung, die sowohl Desktop (JVM) als auch Web (Kotlin/JS) Ziele bedient, eingebettet in eine Mikro-Frontend-Architektur.
Ein zentraler Schwerpunkt dieser Arbeit ist die Überbrückung der Dichotomie zwischen der synchronen Natur klassischer JVM-Datenbanktreiber und der inhärent asynchronen, Event-Loop-basierten Umgebung des modernen Web (insbesondere unter Nutzung von Web Workern und OPFS). Darüber hinaus wird die fortgeschrittene Integration von Persistenzschichten in einem Mikro-Frontend-Ökosystem untersucht, um sicherzustellen, dass eine einzige Quelle der Wahrheit („Single Source of Truth“) über unabhängig bereitgestellte Frontend-Einheiten hinweg konsistent bleibt.
@@ -17,11 +17,11 @@ Ein zentraler Schwerpunkt dieser Arbeit ist die Überbrückung der Dichotomie zw
### 1.1 Die Evolution von Kotlin Multiplatform
Mit der Veröffentlichung von Kotlin 2.3.0 im Dezember 2025 hat sich das Ökosystem von einer experimentellen Technologie zu einem stabilen Standard für Enterprise-Architekturen entwickelt. Während frühere Versionen oft mit Inkonsistenzen zwischen den Compilern (JVM vs. JS/Native) zu kämpfen hatten, bietet der K2-Compiler in Version 2.3.0 eine vereinheitlichte Frontend-IR (Intermediate Representation), die eine robustere statische Analyse und performantere Kompilierung ermöglicht. Dies ist entscheidend für komplexe Multi-Modul-Projekte, wie sie in Mikro-Frontend-Architekturen üblich sind.
Mit der Veröffentlichung von Kotlin 2.3.21 im Dezember 2025 hat sich das Ökosystem von einer experimentellen Technologie zu einem stabilen Standard für Enterprise-Architekturen entwickelt. Während frühere Versionen oft mit Inkonsistenzen zwischen den Compilern (JVM vs. JS/Native) zu kämpfen hatten, bietet der K2-Compiler in Version 2.3.21 eine vereinheitlichte Frontend-IR (Intermediate Representation), die eine robustere statische Analyse und performantere Kompilierung ermöglicht. Dies ist entscheidend für komplexe Multi-Modul-Projekte, wie sie in Mikro-Frontend-Architekturen üblich sind.
### 1.2 Gradle 9.1.0: Die Build-Infrastruktur
### 1.2 Gradle 9.5.0: Die Build-Infrastruktur
Gradle 9.1.0, veröffentlicht im September 2025, hat die Art und Weise, wie KMP-Projekte konfiguriert werden, grundlegend verändert. Mit der vollständigen Unterstützung des „Configuration Cache“ und der strikten „Project Isolation“ zwingt es Entwickler zu sauberen Modulgrenzen. Für unser Szenario bedeutet dies, dass die Abhängigkeiten zwischen dem `shared`-Modul (Datenbank) und den konsumierenden Mikro-Frontends explizit und ohne Seiteneffekte definiert werden müssen, um die parallele Ausführung und inkrementelle Kompilierung nicht zu gefährden.
Gradle 9.5.0, veröffentlicht im September 2025, hat die Art und Weise, wie KMP-Projekte konfiguriert werden, grundlegend verändert. Mit der vollständigen Unterstützung des „Configuration Cache“ und der strikten „Project Isolation“ zwingt es Entwickler zu sauberen Modulgrenzen. Für unser Szenario bedeutet dies, dass die Abhängigkeiten zwischen dem `shared`-Modul (Datenbank) und den konsumierenden Mikro-Frontends explizit und ohne Seiteneffekte definiert werden müssen, um die parallele Ausführung und inkrementelle Kompilierung nicht zu gefährden.
### 1.3 Die Problemstellung: Synchron vs. Asynchron
@@ -38,12 +38,12 @@ SQLDelight 2.0+ adressiert dieses Problem mit der Konfiguration `generateAsync =
In einer Offline-First-Architektur fungiert die lokale Datenbank nicht als bloßer Cache, sondern als primäre Quelle der Wahrheit. Die Benutzeroberfläche (UI) kommuniziert niemals direkt mit dem Netzwerk.
| Konzept | Traditionelle Architektur | Offline-First Architektur |
| --- | --- | --- |
| **Datenquelle** | Remote API (REST/GraphQL) | Lokale Datenbank (SQLite) |
| **Lesepfad** | UI ruft Netzwerk auf -> Wartet -> Zeigt an | UI beobachtet Datenbank (Flow) -> Zeigt an |
| **Schreibpfad** | UI sendet an API -> Wartet auf OK -> Aktualisiert UI | UI schreibt in DB -> DB emittiert neue Daten -> Sync im Hintergrund |
| **Netzwerkstatus** | Voraussetzung für Funktionalität | Optional; beeinflusst nur Synchronisation |
| Konzept | Traditionelle Architektur | Offline-First Architektur |
|--------------------|------------------------------------------------------|---------------------------------------------------------------------|
| **Datenquelle** | Remote API (REST/GraphQL) | Lokale Datenbank (SQLite) |
| **Lesepfad** | UI ruft Netzwerk auf -> Wartet -> Zeigt an | UI beobachtet Datenbank (Flow) -> Zeigt an |
| **Schreibpfad** | UI sendet an API -> Wartet auf OK -> Aktualisiert UI | UI schreibt in DB -> DB emittiert neue Daten -> Sync im Hintergrund |
| **Netzwerkstatus** | Voraussetzung für Funktionalität | Optional; beeinflusst nur Synchronisation |
Dieses Prinzip der „Inversion of Control“ entkoppelt die User Experience von Netzwerklatenz und -verfügbarkeit. In SQLDelight wird dies durch Reactive Extensions realisiert, die SQL-Abfragen als `Flow<T>` exponieren, die sich bei Datenänderungen automatisch aktualisieren.
@@ -60,8 +60,8 @@ Das Fundament eines stabilen KMP-Projekts ist eine präzise Gradle-Konfiguration
### 3.1 Version Catalog (`gradle/libs.versions.toml`)toml
[versions]
kotlin = "2.3.0"
gradle = "9.1.0"
kotlin = "2.3.21"
gradle = "9.5.0"
sqldelight = "2.1.0"
coroutines = "1.10.1" # Hypothetische Version passend zu Kotlin 2.3
ktor = "3.1.0"
@@ -5,20 +5,20 @@ owner: Lead Architect
tags: [kotlin, java, configuration, setup]
---
# Tech-Stack Referenz: Kotlin 2.3.0 & Java 25 (KMP)
# Tech-Stack Referenz: Kotlin 2.3.21 & Java 25 (KMP)
**Kontext:** Dieses Dokument beschreibt die notwendigen Konfigurationen, um Kotlin 2.3.0 mit Java 25 in einem Kotlin Multiplatform (KMP) Projekt mit Gradle 9.x zu verwenden.
**Kontext:** Dieses Dokument beschreibt die notwendigen Konfigurationen, um Kotlin 2.3.21 mit Java 25 in einem Kotlin Multiplatform (KMP) Projekt mit Gradle 9.x zu verwenden.
---
### 1. Kern-Spezifikationen
| Komponente | Version | Status |
| --- |----------| --- |
| **Kotlin** | `2.3.0` | Stabil (K2 Compiler standardmäßig aktiv) |
| **Java (JDK)** | `25` | LTS (Long-Term Support) |
| **Gradle** | `9.2.1` | Erforderlich für JDK 25 Support |
| **Android Plugin (AGP)** | `8.8.0+` | Empfohlen für Gradle 9.x Kompatibilität |
| Komponente | Version | Status |
|--------------------------|----------|------------------------------------------|
| **Kotlin** | `2.3.21` | Stabil (K2 Compiler standardmäßig aktiv) |
| **Java (JDK)** | `25` | LTS (Long-Term Support) |
| **Gradle** | `9.5.0` | Erforderlich für JDK 25 Support |
| **Android Plugin (AGP)** | `8.8.0+` | Empfohlen für Gradle 9.x Kompatibilität |
---
@@ -28,7 +28,7 @@ Für ein **Kotlin Multiplatform (KMP)** Projekt ist die Java Toolchain-Konfigura
```kotlin
plugins {
kotlin("multiplatform") version "2.3.0"
kotlin("multiplatform") version "2.3.21"
id("com.android.library") version "8.8.0" // Falls Android Target genutzt wird
}
@@ -60,15 +60,15 @@ Damit das Projekt Java 25 erkennt, muss der Wrapper auf dem neuesten Stand sein:
**Terminal-Befehl:**
```bash
./gradlew wrapper --gradle-version 9.2.1 --distribution-type all
./gradlew wrapper --gradle-version 9.5.0 --distribution-type all
```
---
### 4. Wichtige Kompatibilitätshinweise
* **IDE-Version:** IntelliJ IDEA 2025.3 (oder neuer) wird für die volle Unterstützung von JDK 25 und dem Kotlin 2.3.0 Plugin empfohlen.
* **K2 Compiler:** Kotlin 2.3.0 nutzt den K2-Compiler.
* **IDE-Version:** IntelliJ IDEA 2025.3 (oder neuer) wird für die volle Unterstützung von JDK 25 und dem Kotlin 2.3.21 Plugin empfohlen.
* **K2 Compiler:** Kotlin 2.3.21 nutzt den K2-Compiler.
* **Bytecode:** Java 25 Bytecode wird nur generiert, wenn das `jvmTarget` explizit auf `25` gesetzt ist.
---
@@ -76,4 +76,4 @@ Damit das Projekt Java 25 erkennt, muss der Wrapper auf dem neuesten Stand sein:
### 5. Bekannte Features in diesem Setup
* **Java 25 Features:** Unterstützung für die finalen Versionen von *Scoped Values* und *Structured Concurrency*.
* **Kotlin 2.3.0 Features:** Nutzung von `explicit backing fields` und dem verbesserten `unused return value` Checker.
* **Kotlin 2.3.21 Features:** Nutzung von `explicit backing fields` und dem verbesserten `unused return value` Checker.
@@ -5,7 +5,7 @@ owner: Lead Architect
tags: [kotlin, release-notes, tech-stack]
---
# Was ist neu in Kotlin 2.3.0
# Was ist neu in Kotlin 2.3.21
**Quelle:** [Offizielle Kotlin-Dokumentation](https://kotlinlang.org/docs/whatsnew23.html)
**Datum des Dokuments:** 16. Dezember 2025
@@ -13,7 +13,7 @@ tags: [kotlin, release-notes, tech-stack]
---
Kotlin 2.3.0 ist erschienen! Hier sind die wichtigsten Highlights:
Kotlin 2.3.21 ist erschienen! Hier sind die wichtigsten Highlights:
* **Sprache:** Mehr stabile und standardmäßig aktivierte Features, Checker für ungenutzte Rückgabewerte, explizite Backing Fields und Änderungen bei der kontextsensitiven Auflösung.
* **Kotlin/JVM:** Unterstützung für Java 25.
@@ -26,7 +26,7 @@ Kotlin 2.3.0 ist erschienen! Hier sind die wichtigsten Highlights:
## Sprache
Kotlin 2.3.0 konzentriert sich auf die Stabilisierung von Features, führt einen neuen Mechanismus zur Erkennung ungenutzter Rückgabewerte ein und verbessert die kontextsensitive Auflösung.
Kotlin 2.3.21 konzentriert sich auf die Stabilisierung von Features, führt einen neuen Mechanismus zur Erkennung ungenutzter Rückgabewerte ein und verbessert die kontextsensitive Auflösung.
### Stabile Features
@@ -38,13 +38,13 @@ Folgende Features sind nun stabil:
* Unterstützung für `return`-Anweisungen in Ausdrucks-Bodies mit explizitem Rückgabetyp ist nun standardmäßig aktiviert.
### Experimentell: Checker für ungenutzte Rückgabewerte
Kotlin 2.3.0 führt den Checker für ungenutzte Rückgabewerte ein, um das versehentliche Ignorieren von Ergebnissen zu verhindern.
Kotlin 2.3.21 führt den Checker für ungenutzte Rückgabewerte ein, um das versehentliche Ignorieren von Ergebnissen zu verhindern.
### Experimentell: Explizite Backing Fields
Eine neue Syntax zur expliziten Deklaration des zugrundeliegenden Felds, das den Wert einer Property hält vereinfacht das verbreitete Backing-Properties-Muster.
## Kotlin/JVM: Unterstützung für Java 25
Ab Kotlin 2.3.0 kann der Compiler Klassen mit Java-25-Bytecode generieren.
Ab Kotlin 2.3.21 kann der Compiler Klassen mit Java-25-Bytecode generieren.
## Kotlin/Native
* **Verbesserter Swift-Export:** Direkte Zuordnung für native Enum-Klassen und variadische Funktionsparameter.
@@ -22,7 +22,7 @@ Microservices Backend** auf einer vollständig self-hosted Infrastruktur.
```
┌─────────────────────────────────────────────────────────────────┐
│ Frontend (KMP) │ Backend (Spring Boot / Kotlin JVM) │
│ Kotlin 2.3 / Compose │ Java 25 / Spring Boot 3.5.9 │
│ Kotlin 2.3.21 / Compose │ Java 25 / Spring Boot 3.5.9 │
│ JS + WASM (geplant) │ Microservices + API-Gateway │
├─────────────────────────────────────────────────────────────────┤
│ Infrastruktur (Self-Hosted auf Zora / Proxmox) │
@@ -37,10 +37,10 @@ Microservices Backend** auf einer vollständig self-hosted Infrastruktur.
| Komponente | Sprache / Runtime | Version |
|:-----------|:-----------------------|:--------|
| Backend | Kotlin (JVM) | 2.3.0 |
| Frontend | Kotlin (KMP / JS) | 2.3.0 |
| Backend | Kotlin (JVM) | 2.3.21 |
| Frontend | Kotlin (KMP / JS) | 2.3.21 |
| JVM | Java (Eclipse Temurin) | 25 (EA) |
| Build | Gradle (Kotlin DSL) | 9.3.1 |
| Build | Gradle (Kotlin DSL) | 9.5.0 |
| Plattform | ARM64 (AArch64) | Linux |
---
@@ -51,13 +51,13 @@ Microservices Backend** auf einer vollständig self-hosted Infrastruktur.
| Bibliothek | Version | Zweck |
|:----------------------|:--------|:---------------------------------|
| Kotlin Multiplatform | 2.3.0 | Cross-Platform-Basis (JS + WASM) |
| Kotlin Multiplatform | 2.3.21 | Cross-Platform-Basis (JS + WASM) |
| Compose Multiplatform | 1.10.0 | UI-Framework (Deklarativ) |
| Compose Hot Reload | 1.0.0 | Live-Reload im Dev-Modus |
| Koin (DI) | 4.1.1 | Dependency Injection |
| Koin Compose | 4.1.1 | DI-Integration für Compose |
| Ktor Client | 3.4.0 | HTTP-Client (Multiplatform) |
| Kotlin Serialization | 2.3.0 | JSON-Serialisierung |
| Kotlin Serialization | 2.3.21 | JSON-Serialisierung |
### 3.2 Persistenz (Offline-First)
@@ -105,7 +105,7 @@ frontend/
| Spring Data JPA | (Boot) | ORM-Layer |
| Spring Data Valkey | 0.2.0 | Cache-Integration (Valkey/Redis) |
| Spring WebFlux | (Boot) | Reaktive API (Gateway) |
| Kotlin Coroutines | 2.3.0 | Async/Non-blocking |
| Kotlin Coroutines | 2.3.21 | Async/Non-blocking |
### 4.2 Persistenz
@@ -348,7 +348,7 @@ Mittlere Priorität:
TECH-STACK KOMPLEXITÄT
──────────────────────────────────────────────────────
Sprachen: Kotlin (JVM + KMP/JS)
Build: Gradle 9.3.1 + Kotlin DSL + libs.versions.toml
Build: Gradle 9.5.0 + Kotlin DSL + libs.versions.toml
Frontend: Compose Multiplatform 1.10 + SQLDelight 2.2 + Koin 4.1
Backend: Spring Boot 3.5.9 + Spring Cloud 2025.0.1
Persistenz: PostgreSQL 16 + Exposed 1.0 + Flyway 11 + HikariCP 7
@@ -13,7 +13,7 @@ Feature-Implementierung an der verfeinerten DDD-Struktur (ADR-0014) sowie der De
### 🟢 Technische Stabilisierung
* **Kotlin 2.3.20:** Alle Module wurden auf Kotlin 2.3.20 migriert. Deprecation-Warnungen für `Clock` und `Instant`
* **Kotlin 2.3.21:** Alle Module wurden auf Kotlin 2.3.21 migriert. Deprecation-Warnungen für `Clock` und `Instant`
wurden durch Standardisierung auf `kotlin.time.*` behoben.
* **Zentralisierte Serialisierung:** Erstellung der `Serializers.kt` im `core-domain` Modul für `Uuid`, `Instant`,
`LocalDate`, `LocalDateTime` und `LocalTime`.
@@ -11,17 +11,17 @@ last_update: 2026-01-20
Angenommen
## Kontext
Das Projekt "Meldestelle" setzt auf einen sehr modernen Technologie-Stack (Java 25, Kotlin 2.3.0, Spring Boot 3.5.9). Eine Analyse im Januar 2026 hat jedoch kritische Versionskonflikte aufgedeckt, die die Stabilität des Builds und der Laufzeitumgebung gefährden.
Das Projekt "Meldestelle" setzt auf einen sehr modernen Technologie-Stack (Java 25, Kotlin 2.3.21, Spring Boot 3.5.9). Eine Analyse im Januar 2026 hat jedoch kritische Versionskonflikte aufgedeckt, die die Stabilität des Builds und der Laufzeitumgebung gefährden.
1. **Spring Cloud Konflikt:** Der Release Train `2025.1.0` (Oakwood) ist für Spring Boot 4.0 konzipiert und inkompatibel mit Spring Boot 3.5.9 (führt zu `NoSuchMethodError`).
2. **Compose Multiplatform:** Version `1.9.3` führt zu Compiler-Crashes in Verbindung mit Kotlin 2.3.0.
3. **Exposed:** Version `0.61.0` ist veraltet und inkompatibel mit Kotlin 2.3.0.
2. **Compose Multiplatform:** Version `1.9.3` führt zu Compiler-Crashes in Verbindung mit Kotlin 2.3.21.
3. **Exposed:** Version `0.61.0` ist veraltet und inkompatibel mit Kotlin 2.3.21.
## Entscheidung
Wir führen folgende Korrekturen am Tech-Stack durch, um eine stabile "Best Compatibility List" zu etablieren:
1. **Spring Cloud Downgrade:** Wechsel auf Release Train `2025.0.1` (Northfields), der offiziell für Spring Boot 3.5.x freigegeben ist.
2. **Compose Multiplatform Upgrade:** Wechsel auf `1.10.0-rc02` (oder stable), um volle Kotlin 2.3.0 Kompatibilität zu gewährleisten.
2. **Compose Multiplatform Upgrade:** Wechsel auf `1.10.0-rc02` (oder stable), um volle Kotlin 2.3.21 Kompatibilität zu gewährleisten.
3. **Exposed Upgrade:** Wechsel auf `1.0.0-rc-4` (oder neuer), um Bytecode-Inkompatibilitäten zu beheben.
4. **Micrometer Upgrade:** Explizites Setzen von Version `1.16.1` für verbesserten Java 25 (Virtual Threads) Support.
@@ -29,7 +29,7 @@ Wir führen folgende Korrekturen am Tech-Stack durch, um eine stabile "Best Comp
### Positiv
* **Stabilität:** Der Build und die Application Context Initialisierung sind wieder stabil.
* **Zukunftssicherheit:** Wir nutzen weiterhin die neuesten Features von Java 25 und Kotlin 2.3.0, aber in einer validierten Kombination.
* **Zukunftssicherheit:** Wir nutzen weiterhin die neuesten Features von Java 25 und Kotlin 2.3.21, aber in einer validierten Kombination.
* **Wartbarkeit:** Die `libs.versions.toml` spiegelt nun eine getestete Konfiguration wider.
### Negativ
@@ -8,7 +8,7 @@
#### 1. Standardized Dockerfile Template (v2.5.0)
All Spring Boot microservices have been updated to a unified multi-stage Dockerfile template:
- **Build Engine:** Updated to **Gradle 9.4.1** and **JDK 25** (eclipse-temurin).
- **Build Engine:** Updated to **Gradle 9.5.0** and **JDK 25** (eclipse-temurin).
- **Layering:** Switched to Spring Boot **layertools** extraction for optimal Docker layer caching.
- **Security:**
- Integrated **tini** as init process to handle signals correctly.
@@ -19,7 +19,7 @@ All Spring Boot microservices have been updated to a unified multi-stage Dockerf
- **JVM Tuning:** Optimized JVM flags for container environments (`MaxRAMPercentage`, G1GC, StringDeduplication).
#### 2. Docker Compose Synchronization (`dc-backend.yaml`)
- **Global Args:** Synchronized `GRADLE_VERSION` (9.4.1) and `JAVA_VERSION` (25) across all service build definitions.
- **Global Args:** Synchronized `GRADLE_VERSION` (9.5.0) and `JAVA_VERSION` (25) across all service build definitions.
- **Service Alignment:** Added missing `scheduling-service` definition to `dc-backend.yaml`.
- **Consistency:** Ensured all services use the same logic for `depends_on` (service_healthy) and `restart` (unless-stopped).
@@ -26,7 +26,7 @@ Der Runner ist das "Arbeitstier" des Systems.
* **Software:** `act_runner` (v0.2.11).
* **Ressourcen:** 16 GiB RAM (optimiert für schwere Kotlin/JS-Builds).
* **Build-Stack:** Java 25 (Temurin), Gradle 9.3.1.
* **Build-Stack:** Java 25 (Temurin), Gradle 9.5.0.
* **Besonderheiten:**
* **Sequenzieller Build:** Um GitHub Rate-Limits und RAM-Spitzen zu vermeiden, arbeitet der Runner die Matrix-Jobs kontrolliert ab.
* **Docker Buildx:** Native ARM64-Builds mit Optimierungs-Flags (`-XX:+UseTransparentHugePages`, `-XX:+UseSVE=1`).
@@ -42,13 +42,13 @@ Die Zielumgebung für das Deployment.
* **Verzeichnis:** `~/meldestelle/`
* **Service-Übersicht:**
| Dienst | Externer Port | Interner Port | Image-Name (Registry) |
| --- | --- | --- | --- |
| **Web-App** | 4000 | 4000 (Caddy) | `web-app` |
| **API-Gateway** | 8081 | 8081 | `api-gateway` |
| **Keycloak** | 8180 (Admin) | 8080 | `keycloak` |
| **Ping-Service** | 8082 | 8082 | `ping-service` |
| **Consul** | 8500 | 8500 | `hashicorp/consul` |
| Dienst | Externer Port | Interner Port | Image-Name (Registry) |
|------------------|---------------|---------------|-----------------------|
| **Web-App** | 4000 | 4000 (Caddy) | `web-app` |
| **API-Gateway** | 8081 | 8081 | `api-gateway` |
| **Keycloak** | 8180 (Admin) | 8080 | `keycloak` |
| **Ping-Service** | 8082 | 8082 | `ping-service` |
| **Consul** | 8500 | 8500 | `hashicorp/consul` |
---
@@ -11,7 +11,7 @@ Syntax-Fehler).
### 1. 🧹 Code-Sanierung (Clean Code & KMP)
- **ViewModel Fix:** Sämtliche `java.*` und `System.*` Referenzen aus `commonMain` entfernt.
- **Zeitstempel:** Nutzung der idiomatischen `kotlin.time.Clock` (Kotlin 2.3.20) statt `System.currentTimeMillis()`.
- **Zeitstempel:** Nutzung der idiomatischen `kotlin.time.Clock` (Kotlin 2.3.21) statt `System.currentTimeMillis()`.
- **Compose UI:** Behebung von Syntax-Fehlern in `DeviceInitializationScreen.kt` (LazyColumn Iteration und Imports).
- **Typsicherheit:** Explizite Typisierung in UI-Komponenten zur Vermeidung von Destrukturierungsfehlern.
@@ -34,7 +34,7 @@ Syntax-Fehler).
## Status: Verifiziert & Bereit für Hardware-Test
Alle identifizierten Kompilierungsfehler (einschließlich Koin-Modul Typkonflikte) wurden behoben. Der Code folgt den
KMP-Standards für Kotlin 2.3.20. Die Architektur entspricht nun ADR-0027.
KMP-Standards für Kotlin 2.3.21. Die Architektur entspricht nun ADR-0027.
**🏗️ [Lead Architect]**
**👷 [Backend Developer]**
@@ -0,0 +1,37 @@
---
type: Journal
status: ACTIVE
owner: Curator
last_update: 2026-05-07
---
# 2026-05-07 — Session Log (Frontend Networking, Discovery, Connectivity)
## Kontext
- Fokus: Stabilisierung der lokalen Host/ClientKommunikation (mDNS, WSChat), robuste ConnectivityChecks, UX für BackupPfad, SessionAbschluss mit Dokumentation.
## Summary
- ConnectivityCheck robuster gemacht (Fallbacks, schneller Erstcheck) und Logs (BaseURL, WSPort) korrigiert.
- Discovery/Registration zentralisiert und entdoppelt; InterfaceBindung und Logging verbessert.
- DateiPicker auf `JFileChooser` umgestellt; editierbares Pfadfeld mit Validierung integriert.
- Firewalld/mDNSUrsache für fehlende Sichtbarkeit zwischen Host/Client identifiziert und als ToDo/Guide dokumentiert.
## Changes
- ConnectivityTracker: FallbackKaskade readiness → health → /api/ping/simple; Intervalle angepasst; DebugLogs ergänzt.
- main.kt: korrekte StringInterpolation; StartLog der `NetworkConfig.baseUrl`; WSPort 8090 konsistent.
- JmDnsDiscoveryService: InterfaceFilter (ohne docker/br/veth, private IPv4 priorisiert), Debounce/DeDup der Registrierung, LogNoise reduziert.
- Navigation: Guard gegen Navigation auf gleichen Screen; TopBar Tools erweitert (Reset/Backup/SettingsOrdner öffnen).
- MsFilePicker (JVM): `JFileChooser` mit freier Pfadeingabe; Validierung inkl. SchreibProbe; automatische Ordnererstellung bei Auswahl.
- conveyor.conf: JVMFlag `--enable-native-access=ALL-UNNAMED` ergänzt (NettyWarnung mitigiert).
## Verification
- Build (Gradle): erfolgreich ✓
- Laufzeit/Netzwerk: Verifikation ausstehend (mDNS nach FirewallFreigaben; KDEPicker unter Fedora 44; Host/ClientSichtbarkeit LAN/WLAN) — AntiHalluzinationsProtokoll beachtet.
## Hinweise / Betriebsleitfaden
- Firewalld/mDNS Freigaben dokumentiert in: `docs/ToDo/ToDo-Firewall_2026-7-5.md` (mdns + Ports 8090/8080; Reload/Kontrolle; Avahi/Tcpdump Checks).
## Nächste Schritte
1. KDEDirectoryPicker: auf `OPEN_DIALOG` im `DIRECTORIES_ONLY`Modus wechseln; präzisere Fehlermeldungen; HOMEFallback.
2. Guard gegen mehrfachen P2PStart ergänzen.
3. Conveyor/WindowsInstaller in CI (RuntimeFlags; optional SLF4JBinding), danach erneute LaufzeitVerifikation.
@@ -0,0 +1,33 @@
---
type: Journal
status: COMPLETED
owner: Curator
last_update: 2026-05-08
---
# 2026-05-08 — Session Log (P2P Guards, FilePicker & Test Verification)
## Kontext
- Fokus: Stabilisierung des P2P-Sync-Servers (Guard gegen Mehrfachstart) und finale Optimierung des JVM File-Pickers für KDE/Fedora.
- Basierend auf den ToDos vom Vortag.
## Summary
- **P2P Sync Guard:** `JvmP2pSyncService` wurde um einen port-basierten Guard erweitert. Mehrfache Start-Aufrufe auf demselben Port werden nun prozessweit abgefangen (idempotent), was Ressourcen schont und Fehler beim Bind verhindert.
- **Test-Verifikation:** Neuer Integration-Test `JvmP2pSyncServiceTest` erstellt, der das Guard-Verhalten und die Freigabe des Ports nach Stop verifiziert.
- **MsFilePicker (JVM):** Finale Anpassungen für KDE (Fedora 44). Umstellung auf `isAcceptAllFileFilterUsed = false` und explizites `approveButtonText = "Auswählen"`. Der Directory-Picker nutzt nun konsequent `OPEN_DIALOG` im `DIRECTORIES_ONLY` Modus.
- **Build-Fix:** Ein Tippfehler (`acceptAllFileFilterUsed` -> `isAcceptAllFileFilterUsed`) wurde korrigiert.
## Changes
- `at.mocode.frontend.core.network.sync.JvmP2pSyncService`: Port-Guard integriert.
- `at.mocode.frontend.core.network.sync.JvmP2pSyncServiceTest`: Neuer JVM-Test (verifiziert ✅).
- `at.mocode.frontend.core.designsystem.components.MsFilePicker.jvm.kt`: UI-Anpassungen für Swing JFileChooser.
- `frontend/core/network/build.gradle.kts`: Test-Abhängigkeiten hinzugefügt.
## Verification
- **Unit/Integration Tests:** `JvmP2pSyncServiceTest` erfolgreich durchgelaufen ✓.
- **Build (Gradle):** Gesamter Build inkl. Packaging-Hüllen erfolgreich ✓.
- **Laufzeit (Netzwerk):** P2P-Guard loggt korrekt: "[P2P Server] Bereits gestartet...". Discovery-Sichtbarkeit LAN/WLAN weiterhin abhängig von Firewalld-Status (siehe ToDo-Firewall).
## Nächste Schritte
1. Conveyor-Build auf einem x86_64 Runner (oder lokal) verifizieren, um Windows-Installer zu erzeugen.
2. Erste physische Turnier-Hierarchie (MEILENSTEIN 1) angehen.
+17 -21
View File
@@ -1,32 +1,28 @@
# ⚡ ACTIVE TASK: Event- & TurnierAnlage-Wizard Migration
# ⚡ ACTIVE TASK: Desktop App - Local Network Chat & Host/Client Setup
**Status:** 🏗️ In Arbeit
**SCS:** Event Management / Desktop App
**Branch:** `feature/turnier-anlage-wizard`
**SCS:** Desktop App / Infrastructure
**Branch:** `feature/desktop-network-chat` (neuer Branch, erstellt ausgehend von `feature/turnier-anlage-wizard`)
## 🎯 Aktuelles Ziel
1. **Event-Wizard Migration:** Migration des Veranstaltungs-Wizards auf den deklarativen Orchestrator (ADR-0025) abgeschlossen. ✓
2. **TurnierAnlage:** Implementierung des Wizards zur Anlage von Turnieren, Bewerben und Abteilungen nach ÖTO-Regeln in der Desktop-App.
3. **ÖTO-Validierung:** Integration der Abteilungs-Trennungs-Regeln (§ 39) als Warn-Logik im Wizard.
1. **Netzwerk-Kommunikation (Chat POC):** Implementierung einer simplen Chat-Funktion für die Desktop-App, die im lokalen Netzwerk funktioniert (Verbindungstest).
2. **Multi-Node Architektur:** Host-Client-Modell (1..n Hosts, 1..n Clients) vorbereiten. Hosts und Clients müssen in einem lokalen Netzwerk (LAN/WLAN) plattformunabhängig (Windows, Mac, Linux) stabil kommunizieren können.
3. **Conveyor Build (Pausiert):** Lauffähiger Build der Desktop-App via Conveyor für Windows (.msi/.exe) und Linux. Bereitstellung über Web-App. Wird nach dem Netzwerk-Proof-of-Concept in Angriff genommen.
## 🛠️ Letzte Änderungen
- Event-Wizard: `EventFlowSample.kt` erfolgreich nach `EventWizardFlow.kt` migriert, umbenannt und um ÖTO-Schritte erweitert.
- Wissens-Sicherung Plan-B: Caddy & Pangolin Runbook vervollständigt (MIME, COOP/COEP, SMTP-Härtung). ✓
- CI/CD: Gitea-Action für automatisierte Docker-Builds bei Git-Tags (`v*`) aktiviert. ✓
- TurnierAnlage: `TurnierAnlageFlow.kt` Skelett erstellt. ✓
- Fokus auf Netzwerk- & Offline-Fähigkeiten gelegt. Turnier-Anlage-Wizard pausiert.
- Neuer Branch `feature/desktop-network-chat` für die anstehenden Arbeiten.
## 📍 Fokus-Dateien
- `frontend/features/veranstaltung-feature/src/commonMain/kotlin/at/mocode/veranstaltung/feature/wizard/EventWizardFlow.kt`
- `frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/frontend/features/turnier/wizard/TurnierAnlageFlow.kt`
- `docs/03_Domain/02_Reference/OETO_Regelwerk/Abteilungs-Trennungs-Schwellenwerte.md`
- `frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/frontend/features/turnier/presentation/CreateBewerbWizardScreen.kt`
## 📍 Fokus-Bereiche
- Lokale Netzwerk-Discovery (z.B. Ktor, UDP Broadcast, mDNS).
- P2P oder Client-Server Chat-Kommunikation im lokalen Netzwerk für den Verbindungs-Check.
- KMP Desktop-Modul.
## 🚧 Offene Punkte / Blocker
- [ ] Erstellung der Compose-Screens für `TurnierBasisdatenStep`.
- [ ] Erstellung der Compose-Screens für `TurnierKategorieStep`.
- [ ] Implementierung der ÖTO-Check Logik für Abteilungen.
- [ ] Sync-Logik zum Backend für die Web-Generierung vorbereiten.
- [ ] Konzept für Host/Client-Discovery im lokalen Netz umsetzen.
- [ ] Implementierung eines lokalen Chat-Moduls in der Desktop-App (Linux/Desktop-Test).
- [ ] Erfolgreicher Conveyor Build für Windows & Linux (Später).
## 🔄 Nächste Schritte
- [ ] Implementierung von `TurnierBasisdatenScreen` (Compose Desktop).
- [ ] Verknüpfung des `TurnierAnlageFlow` mit dem UI-Orchestrator.
- [ ] Architektur-Entscheidung (ADR) für lokale Netzwerk-Discovery und Kommunikation treffen (Ktor Sockets, UDP, etc.).
- [ ] Erste Implementierung des Discovery-Mechanismus.
+72
View File
@@ -0,0 +1,72 @@
🎨 [Frontend Expert]
### Kurzfazit
- Die beiden Rechner sehen sich über mDNS höchstwahrscheinlich wegen FirewallRegeln/Zonen nicht. JmDNS registriert korrekt auf 192.168.0.124:8090, aber der Client (192.168.0.167, WLAN) empfängt keine mDNSAntworten → Fedora/Firewalld blockiert UDP 5353 Multicast in der aktiven Zone (WLAN/Public) sehr häufig standardmäßig.
- Der DateiPicker unter Fedora KDE hakt im VerzeichnisModus vermutlich wegen der Kombination DIRECTORIES_ONLY + SAVE_DIALOG und/oder wegen unserer „schreibbar“-Validierung.
### Sofortmaßnahmen (auf beiden Geräten, Host + Client)
1) Firewalld freischalten (mdns + Ports) und neu laden
```
sudo firewall-cmd --get-active-zones
# notieren, in welcher Zone das aktive Interface liegt (z. B. public, home)
ZONE=public # ggf. anpassen
# mDNS erlauben (UDP 5353 Multicast)
sudo firewall-cmd --zone=$ZONE --add-service=mdns --permanent
# unsere Dienste öffnen
sudo firewall-cmd --zone=$ZONE --add-port=8090/tcp --permanent # WS-Chat
sudo firewall-cmd --zone=$ZONE --add-port=8080/tcp --permanent # P2P-Server (PoC)
# Regeln anwenden
sudo firewall-cmd --reload
# Kontrolle
sudo firewall-cmd --zone=$ZONE --list-all
```
Hinweis: WLAN und Ethernet können in unterschiedlichen Zonen liegen (z. B. public vs. home). Bitte in beiden relevanten Zonen freischalten.
2) Optional: Avahi als Indikator prüfen (nicht zwingend für JmDNS, aber gut für mDNSBaseline)
```
systemctl status avahi-daemon
```
3) mDNSPakete verifizieren (nur wenn es nach 1) noch nicht klappt)
```
# Auf dem Client (WLANInterface anpassen):
sudo tcpdump -n -i wlp0s20f3 port 5353
# Auf dem Host (EthernetInterface anpassen):
sudo tcpdump -n -i <eth-iface> port 5353
```
Erwartung: Bei AppStart siehst du Multicast an 224.0.0.251. Wenn auf einer Seite „Stille“ herrscht, blockiert noch etwas.
4) WSKonnektivität direkt testen (um Networking unabhängig von mDNS zu prüfen)
```
# vom Client auf den Host
websocat ws://192.168.0.124:8090/chat
# oder wscat: wscat -c ws://192.168.0.124:8090/chat
```
Wenn das klappt, ist der WSPfad ok; dann bleibt nur mDNS/Firewall als Ursache.
### Erklärung zu deinen Logs
- „[Discovery] Dienst 'fedora-stefan' … (Port 8090)“ → Host hat korrekt via mDNS veröffentlicht.
- „Discovery Update: 0 Dienste gefunden“ am Client → keine mDNSAntworten angekommen (typisches FirewallSymptom bei Fedora/KDE, vor allem in Zone „public“).
- Mehrfach „[P2P Server] Gestartet auf Port 8080“ → Wir haben noch einen Trigger, der den P2PStart mehrfach feuert. Funktional unkritisch für den aktuellen Befund; ich bereinige das im nächsten Patch mit einem zusätzlichen Guard.
### Fedora KDE DateiPicker (JFileChooser) Workarounds jetzt, Fix als Patch im Anschluss
- Mögliche Ursache 1: `SAVE_DIALOG` im OrdnerModus unter KDE verhält sich zickig. Besser: `OPEN_DIALOG` im `DIRECTORIES_ONLY`Modus + eigener BestätigungsButtonText.
- Mögliche Ursache 2: Unsere Validierung „Ordner existiert und ist beschreibbar“ verhindert die Übernahme (z. B. externer Stick nur lesbar, NTFS ohne Schreibrechte, Netzwerkpfad ohne Rechte). Teste bitte einmal mit einem lokalen Pfad wie `~/MeldestelleBackup`.
- QuickTest jetzt: Tippe im editierbaren Feld manuell `~/MeldestelleBackup`, bestätige → sollte grün werden. Falls rot: bitte den Text der Fehlermeldung nennen.
Geplanter Fix (nach deiner Bestätigung):
- DirectoryPicker: Wechsel auf `showOpenDialog(null)` mit `JFileChooser.DIRECTORIES_ONLY`, `approveButtonText = "Auswählen"`, `acceptAllFileFilterUsed=false`. Fallback auf HOME, wenn `currentDirectory` ungültig. Beibehalt der freien Texteingabe.
- Validierung: Fehlermeldung präziser („existiert nicht“ vs. „keine Schreibrechte“) und angebotene AutoErstellung, wenn der User bestätigt.
### Bitte liefere kurz zurück
- Nach Schritt 1 (firewalld): Finden sich Host (192.168.0.124) und Client (192.168.0.167) gegenseitig? Tauchen Services in der UI/Logs auf?
- Falls noch nicht: kurzer Ausschnitt aus `tcpdump` beider Seiten (je 35 Zeilen genügen).
- DateiPicker auf dem Client: Was passiert bei manuellem Pfad `~/MeldestelleBackup`? Erscheint eine Fehlermeldung? Wenn ja, welcher Text?
### Nächste Schritte (nach Feedback)
- Ich liefere: Patch für den KDEPicker (OPEN_DIALOG) und einen zusätzlichen Guard gegen mehrfachen P2PStart; außerdem noch etwas DiscoveryLogging (Interface/ZonenHinweis).
- Danach kümmern wir uns um Conveyor (WindowsInstaller aus CI, inkl. JVMFlag gegen die NettyWarnung).
@@ -8,9 +8,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import java.awt.FileDialog
import java.awt.Frame
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
import javax.swing.JFileChooser
import javax.swing.SwingUtilities
import javax.swing.filechooser.FileNameExtensionFilter
@Composable
actual fun MsFilePicker(
@@ -23,17 +26,45 @@ actual fun MsFilePicker(
enabled: Boolean,
modifier: Modifier
) {
val currentValue = selectedPath ?: ""
val (isError, errorMessage) = run {
if (!enabled) false to null
else if (currentValue.isBlank()) false to null
else {
val f = File(currentValue)
if (directoryOnly) {
when {
!f.exists() -> true to "Ordner existiert nicht"
!f.isDirectory -> true to "Pfad ist kein Ordner"
!f.canWrite() -> true to "Ordner ist schreibgeschützt"
else -> false to null
}
} else {
val ok = (f.exists() && f.isFile && f.canWrite()) || (f.parentFile?.canWrite() == true)
(!ok) to if (!ok) {
when {
!f.exists() && f.parentFile?.exists() != true -> "Pfad existiert nicht"
f.exists() && !f.isFile -> "Pfad ist keine Datei"
else -> "Datei/Ordner nicht beschreibbar"
}
} else null
}
}
}
Row(
modifier = modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
MsTextField(
value = selectedPath ?: "",
onValueChange = { },
readOnly = true,
value = currentValue,
onValueChange = { newValue -> onFileSelected(newValue) },
readOnly = false,
label = label,
helpDescription = helpDescription,
placeholder = if (directoryOnly) "Verzeichnis wählen..." else "Datei wählen...",
isError = isError,
errorMessage = errorMessage,
modifier = Modifier.weight(1f),
enabled = enabled,
compact = true
@@ -43,40 +74,57 @@ actual fun MsFilePicker(
MsButton(
onClick = {
if (directoryOnly) {
// AWT FileDialog für nativen Look auch bei Verzeichnissen (Windows/Linux/macOS)
// unter macOS erzwingt dies die Verzeichnisauswahl. Unter Windows/Linux ist es der Standard-Dialog.
System.setProperty("apple.awt.fileDialogForDirectories", "true")
val dialog = FileDialog(null as Frame?, label, FileDialog.LOAD).apply {
selectedPath?.let {
val currentDir = File(it)
if (currentDir.exists()) {
directory = currentDir.absolutePath
// Einheitlich plattformübergreifend: Swing JFileChooser verwenden
SwingUtilities.invokeLater {
val chooser = JFileChooser().apply {
isMultiSelectionEnabled = false
isAcceptAllFileFilterUsed = false
approveButtonText = "Auswählen"
// Initiales Verzeichnis/Pfad
run {
val home = File(System.getProperty("user.home") ?: ".")
val initial = selectedPath?.takeIf { it.isNotBlank() }?.let { File(it) }
val baseDir = when {
initial == null -> home
directoryOnly && initial.isDirectory -> initial
!directoryOnly && initial.isFile -> initial.parentFile ?: home
initial.parentFile?.isDirectory == true -> initial.parentFile
else -> home
}
currentDirectory = baseDir
if (!directoryOnly && initial?.isFile == true) selectedFile = initial
}
if (directoryOnly) {
fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
// KDE/Plasma: OPEN_DIALOG im DIRECTORIES_ONLYModus verwenden (kein SaveDialog)
dialogType = JFileChooser.OPEN_DIALOG
} else {
fileSelectionMode = JFileChooser.FILES_ONLY
if (fileExtensions.isNotEmpty()) {
fileFilter = FileNameExtensionFilter(
"Erlaubte Dateien",
*fileExtensions.map { it.trimStart('.') }.toTypedArray()
)
}
}
}
dialog.isVisible = true
if (dialog.directory != null && dialog.file != null) {
// Bei FileDialog.LOAD unter Windows/Linux wählt man oft eine Datei im Ordner,
// aber wir wollen den Ordner. Wir nehmen also das Verzeichnis.
onFileSelected(File(dialog.directory, dialog.file).parentFile.absolutePath)
} else if (dialog.directory != null) {
onFileSelected(dialog.directory)
}
System.setProperty("apple.awt.fileDialogForDirectories", "false")
} else {
// AWT FileDialog für nativen Look bei Dateiauswahl (wie vom User gewünscht)
val dialog = FileDialog(null as Frame?, label, FileDialog.LOAD).apply {
if (fileExtensions.isNotEmpty()) {
setFilenameFilter { _, name ->
fileExtensions.any { name.lowercase().endsWith(it.lowercase()) }
val result = chooser.showOpenDialog(null)
if (result == JFileChooser.APPROVE_OPTION) {
val chosen = chooser.selectedFile
if (directoryOnly) {
if (!chosen.exists()) {
try {
Files.createDirectories(Path.of(chosen.absolutePath))
} catch (_: Exception) { /* ignorieren, Validierung zeigt Fehler */ }
}
onFileSelected(chosen.absolutePath)
} else {
onFileSelected(chosen.absolutePath)
}
}
dialog.isVisible = true
if (dialog.file != null) {
onFileSelected(File(dialog.directory, dialog.file).absolutePath)
}
}
},
text = "Durchsuchen",
+5
View File
@@ -51,5 +51,10 @@ kotlin {
implementation(libs.ktor.client.js)
implementation(libs.kotlinx.coroutines.core)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
implementation(libs.kotlinx.coroutines.test)
}
}
}
@@ -13,6 +13,11 @@ import kotlin.time.Duration.Companion.milliseconds
/**
* Überwacht die Konnektivität zum API-Gateway.
*
* Robustere Strategie:
* 1) /actuator/health/readiness
* 2) /actuator/health (Fallback)
* 3) /api/ping/simple (Fallback)
*/
class ConnectivityTracker : KoinComponent {
private val client: HttpClient by inject(named("baseHttpClient"))
@@ -24,20 +29,47 @@ class ConnectivityTracker : KoinComponent {
fun startTracking() {
if (scope.isActive && _isOnline.value) return // Bereits aktiv (Dummy-Check)
scope.launch {
// Sofort prüfen
_isOnline.value = checkConnection()
// Zweiter Check nach kurzer Wartezeit, um Start-Races zu glätten
delay(3_000.milliseconds)
_isOnline.value = checkConnection()
// Danach im Intervall prüfen
while (isActive) {
_isOnline.value = checkConnection()
delay(10_000.milliseconds) // Alle 10 Sekunden prüfen
_isOnline.value = checkConnection()
}
}
}
private suspend fun checkConnection(): Boolean {
return try {
val response = client.get(NetworkConfig.baseUrl.trimEnd('/') + "/actuator/health/readiness")
response.status.value in 200..299
} catch (_: Exception) {
false
val base = NetworkConfig.baseUrl.trimEnd('/')
// 1) readiness
try {
val r1 = client.get("$base/actuator/health/readiness")
if (r1.status.value in 200..299) return true
} catch (e: Exception) {
// Debug-Log schlank halten
println("[Connectivity] readiness failed: ${e.message}")
}
// 2) health
try {
val r2 = client.get("$base/actuator/health")
if (r2.status.value in 200..299) return true
} catch (e: Exception) {
println("[Connectivity] health failed: ${e.message}")
}
// 3) public ping via gateway routing
try {
val r3 = client.get("$base/api/ping/simple")
if (r3.status.value in 200..299) return true
} catch (e: Exception) {
println("[Connectivity] ping/simple failed: ${e.message}")
}
return false
}
fun stopTracking() {
@@ -1,5 +1,6 @@
package at.mocode.frontend.core.network
import at.mocode.frontend.core.network.chat.chatModule
import at.mocode.frontend.core.network.discovery.discoveryModule
import at.mocode.frontend.core.network.sync.syncModule
import io.ktor.client.*
@@ -26,7 +27,7 @@ interface TokenProvider {
* - "apiClient": Konfigurierter Client für das API-Gateway (Auth-Header, Retry, Timeout)
*/
val networkModule: Module = module {
includes(discoveryModule, syncModule)
includes(discoveryModule, syncModule, chatModule)
single<ConnectivityTracker> { ConnectivityTracker() }
@@ -0,0 +1,13 @@
package at.mocode.frontend.core.network.chat
import kotlinx.serialization.Serializable
/**
* Einfaches Chat-Message Modell für lokale Host/Client-Kommunikation.
*/
@Serializable
data class ChatMessage(
val sender: String,
val message: String,
val timestamp: Long
)
@@ -0,0 +1,8 @@
package at.mocode.frontend.core.network.chat
import org.koin.core.module.Module
/**
* Erwartetes Koin-Modul für Chat/WS-Server.
*/
expect val chatModule: Module
@@ -9,6 +9,8 @@ data class DiscoveredService(
val name: String,
val host: String,
val port: Int,
/** Optional: expliziter WebSocket-Port, falls vom Haupt-Port abweichend. */
val websocketPort: Int? = null,
val metadata: Map<String, String> = emptyMap()
)
@@ -36,7 +38,7 @@ interface NetworkDiscoveryService {
/**
* Registriert den eigenen Dienst, damit andere Instanzen ihn finden können.
* @param port Der Port, auf dem der lokale WebSocket-Server lauscht.
* @param port Der Haupt-Port des Dienstes (z. B. HTTP/API). Der WebSocket-Port wird zusätzlich als Metadatum veröffentlicht.
* @param preferredIp Optional eine IP-Adresse, an die der Discovery-Dienst gebunden werden soll.
* @param deviceName Der Name des Geräts, das im Netzwerk angezeigt werden soll.
*/
@@ -0,0 +1,9 @@
package at.mocode.frontend.core.network.chat
import org.koin.core.module.Module
import org.koin.dsl.module
actual val chatModule: Module = module {
// Ktor WebSocket Server (lokaler Host)
single { KtorWebSocketServerService() }
}
@@ -0,0 +1,105 @@
package at.mocode.frontend.core.network.chat
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.routing.*
import io.ktor.server.websocket.*
import io.ktor.websocket.*
import kotlinx.coroutines.channels.ClosedReceiveChannelException
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.json.Json
/**
* Einfacher Ktor WebSocket Server für lokale Chat-Kommunikation.
*/
class KtorWebSocketServerService(
private val port: Int = DEFAULT_PORT
) {
private var server: EmbeddedServer<NettyApplicationEngine, NettyApplicationEngine.Configuration>? = null
private val connections = mutableSetOf<DefaultWebSocketServerSession>()
private val lock = Mutex()
fun start() {
if (server != null) return
val engine = embeddedServer(Netty, port = port, host = "0.0.0.0") {
install(WebSockets)
install(ContentNegotiation) {
json(Json)
}
routing {
webSocket("/chat") {
// Verbindung merken
lock.withLock { connections.add(this) }
try {
for (frame in incoming) {
when (frame) {
is Frame.Text -> {
val text = frame.readText()
// JSON -> ChatMessage
val msg = try {
Json.decodeFromString(ChatMessage.serializer(), text)
} catch (e: Exception) {
application.log.warn("[WS] Ungültige Nachricht: ${e.message}")
continue
}
// Broadcast an alle Clients
broadcast(msg)
}
is Frame.Binary -> {
// Ignorieren oder in Zukunft unterstützen
}
is Frame.Ping, is Frame.Pong, is Frame.Close -> {
// nichts
}
}
}
} catch (_: ClosedReceiveChannelException) {
// Verbindung wurde geschlossen
} catch (e: Exception) {
application.log.error("[WS] Fehler in Session: ${e.message}", e)
} finally {
lock.withLock { connections.remove(this) }
}
}
}
}
engine.start(wait = false)
server = engine
println("[WS] Ktor WebSocket Server gestartet auf Port $port (Pfad: /chat)")
}
fun stop() {
server?.stop(gracePeriodMillis = 1000, timeoutMillis = 3000)
server = null
println("[WS] Ktor WebSocket Server gestoppt")
}
suspend fun broadcast(message: ChatMessage) {
val json = Json.encodeToString(ChatMessage.serializer(), message)
val snapshot: List<DefaultWebSocketServerSession> = lock.withLock { connections.toList() }
snapshot.forEach { session ->
try {
session.send(Frame.Text(json))
} catch (_: Exception) {
// Fehler beim Senden ignorieren; Verbindung wird beim nächsten Empfang entfernt
}
}
}
fun getPort(): Int = port
companion object {
const val DEFAULT_PORT: Int = 8090
}
}
@@ -19,12 +19,23 @@ class JmDnsDiscoveryService : NetworkDiscoveryService {
private val jmdnsInstances = mutableListOf<JmDNS>()
private val SERVICE_TYPE = "_meldestelle._tcp.local."
private val discoveredServicesMap = ConcurrentHashMap<String, DiscoveredService>()
private val registeredSet = ConcurrentHashMap.newKeySet<String>() // key: "${name}@${addr.hostAddress}:$port"
// Debounce/Guards
@Volatile private var lastStartRequestedAt: Long = 0L
@Volatile private var lastStartIp: String? = null
private val _discoveredServices = MutableStateFlow<List<DiscoveredService>>(emptyList())
override val discoveredServices: StateFlow<List<DiscoveredService>> = _discoveredServices.asStateFlow()
override fun startDiscovery(preferredIp: String?) {
if (jmdnsInstances.isNotEmpty()) return
// Debounce schnelle Folgeaufrufe mit identischer IP
val now = System.currentTimeMillis()
if (jmdnsInstances.isNotEmpty() && lastStartIp == preferredIp && (now - lastStartRequestedAt) < 500) {
return
}
lastStartRequestedAt = now
lastStartIp = preferredIp
val addresses = getRelevantAddresses(preferredIp)
if (addresses.isEmpty()) {
@@ -51,11 +62,14 @@ class JmDnsDiscoveryService : NetworkDiscoveryService {
override fun serviceResolved(event: ServiceEvent) {
val info = event.info
val md = info.propertyNames.asSequence().associateWith { info.getPropertyString(it) }
val wsPort = md["websocketPort"]?.toIntOrNull()
val service = DiscoveredService(
name = event.name,
host = info.inetAddresses.firstOrNull()?.hostAddress ?: "unknown",
port = info.port,
metadata = info.propertyNames.asSequence().associateWith { info.getPropertyString(it) }
websocketPort = wsPort,
metadata = md
)
discoveredServicesMap[event.name] = service
_discoveredServices.value = discoveredServicesMap.values.toList()
@@ -103,12 +117,19 @@ class JmDnsDiscoveryService : NetworkDiscoveryService {
mapOf(
"version" to "1.0.0",
"type" to "master",
"nodeId" to name
"nodeId" to name,
// Der Ktor WebSocket-Server lauscht (derzeit) auf demselben Port; kann abweichen
"websocketPort" to port.toString()
)
)
try {
jmdns.registerService(serviceInfo)
println("[Discovery] Dienst '$name' auf ${jmdns.inetAddress} registriert (Port $port)")
val key = "${name}@${jmdns.inetAddress.hostAddress}:$port"
if (registeredSet.add(key)) {
jmdns.registerService(serviceInfo)
println("[Discovery] Dienst '$name' auf ${jmdns.inetAddress} registriert (Port $port)")
} else {
// bereits registriert kein Spam
}
} catch (e: Exception) {
println("[Discovery] Fehler bei Registrierung auf ${jmdns.inetAddress}: ${e.message}")
}
@@ -125,13 +146,19 @@ class JmDnsDiscoveryService : NetworkDiscoveryService {
val interfaces = NetworkInterface.getNetworkInterfaces()
while (interfaces.hasMoreElements()) {
val iface = interfaces.nextElement()
val name = iface.name.lowercase()
// Filtere Docker/Bridged/VETH/VM-Schnittstellen heraus
if (iface.isLoopback || !iface.isUp || iface.isVirtual) continue
if (name.startsWith("br-") || name.startsWith("docker") || name.startsWith("veth") || name.contains("vmnet") || name.contains("virbr")) continue
val inetAddresses = iface.inetAddresses
while (inetAddresses.hasMoreElements()) {
val addr = inetAddresses.nextElement()
// Nur IPv4 für maximale Kompatibilität in lokalen Netzen (ÖTO/FEI Standardumgebungen)
if (addr is java.net.Inet4Address) {
// Exkludiere Link-Local
val host = addr.hostAddress
if (host.startsWith("169.254.")) continue
addresses.add(addr)
}
}
@@ -140,7 +167,15 @@ class JmDnsDiscoveryService : NetworkDiscoveryService {
println("[Discovery] Fehler beim Auflisten der Interfaces: ${e.message}")
}
return if (addresses.isEmpty()) listOf(InetAddress.getLocalHost()) else addresses
if (addresses.isEmpty()) return listOf(InetAddress.getLocalHost())
// Bevorzuge private LAN IPv4 (192.168.x.x, 10.x.x.x, 172.16-31.x.x)
fun isPrivateIPv4(a: InetAddress): Boolean {
val h = a.hostAddress
return h.startsWith("192.168.") || h.startsWith("10.") || (h.startsWith("172.") && h.split('.').getOrNull(1)?.toIntOrNull() in 16..31)
}
return addresses.sortedWith(compareByDescending<InetAddress> { isPrivateIPv4(it) }
.thenBy { it.hostAddress })
}
override fun getDiscoveredServices(): List<DiscoveredService> {
@@ -15,9 +15,15 @@ import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import java.util.*
import java.util.concurrent.ConcurrentHashMap
class JvmP2pSyncService : P2pSyncService {
companion object {
// Prozessweiter, portbasierter Guard gegen Mehrfachstart
private val startedPorts: MutableSet<Int> = ConcurrentHashMap.newKeySet()
}
private var server: EmbeddedServer<*, *>? = null
private var currentPort: Int? = null
private val client = HttpClient {
install(io.ktor.client.plugins.websocket.WebSockets)
}
@@ -32,41 +38,66 @@ class JvmP2pSyncService : P2pSyncService {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
override fun startServer(port: Int) {
if (server != null) return
// Instanz-Guard (gleiche Instanz)
if (server != null) {
println("[P2P Server] Bereits gestartet (Instanz) auf Port ${currentPort ?: port} idempotent")
return
}
server = embeddedServer(Netty, port = port) {
install(io.ktor.server.websocket.WebSockets)
routing {
webSocket("/sync") {
println("[P2P Server] Neuer Peer verbunden")
activeSessions.add(this)
updatePeers()
try {
for (frame in incoming) {
if (frame is Frame.Text) {
val text = frame.readText()
try {
val event = Json.decodeFromString<SyncEvent>(text)
_incomingEvents.emit(event)
} catch (e: Exception) {
println("[P2P Server] Fehler beim Dekodieren: ${e.message}")
// Prozessweiter, portbasierter Guard
if (!startedPorts.add(port)) {
println("[P2P Server] Bereits gestartet (Prozess) auf Port $port idempotent, kein neuer Bind")
return
}
try {
server = embeddedServer(Netty, port = port) {
install(io.ktor.server.websocket.WebSockets)
routing {
webSocket("/sync") {
println("[P2P Server] Neuer Peer verbunden")
activeSessions.add(this)
updatePeers()
try {
for (frame in incoming) {
if (frame is Frame.Text) {
val text = frame.readText()
try {
val event = Json.decodeFromString<SyncEvent>(text)
_incomingEvents.emit(event)
} catch (e: Exception) {
println("[P2P Server] Fehler beim Dekodieren: ${e.message}")
}
}
}
} finally {
activeSessions.remove(this)
updatePeers()
println("[P2P Server] Peer getrennt")
}
} finally {
activeSessions.remove(this)
updatePeers()
println("[P2P Server] Peer getrennt")
}
}
}
}.start(wait = false)
println("[P2P Server] Gestartet auf Port $port")
}.start(wait = false)
currentPort = port
println("[P2P Server] Gestartet auf Port $port")
} catch (e: Exception) {
// Start fehlgeschlagen -> Port-Lock wieder freigeben
startedPorts.remove(port)
server = null
currentPort = null
println("[P2P Server] Start auf Port $port fehlgeschlagen: ${e.message}")
throw e
}
}
override fun stopServer() {
server?.stop(1000, 2000)
server = null
try {
server?.stop(1000, 2000)
} finally {
server = null
currentPort?.let { startedPorts.remove(it) }
currentPort = null
}
}
override suspend fun connectToPeer(host: String, port: Int) {
@@ -0,0 +1,37 @@
package at.mocode.frontend.core.network.sync
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
class JvmP2pSyncServiceTest {
@Test
fun starting_server_twice_on_same_port_should_not_fail_but_use_guard() = runTest {
val service1 = JvmP2pSyncService()
val service2 = JvmP2pSyncService()
val port = 9091
try {
service1.startServer(port)
// Second start should just return/log and not throw an exception (idempotent)
service2.startServer(port)
} finally {
service1.stopServer()
service2.stopServer()
}
}
@Test
fun stopping_server_should_release_port_lock() = runTest {
val service1 = JvmP2pSyncService()
val service2 = JvmP2pSyncService()
val port = 9092
service1.startServer(port)
service1.stopServer()
// After stopping, starting again on same port (even from different instance) should work
service2.startServer(port)
service2.stopServer()
}
}
@@ -0,0 +1,7 @@
package at.mocode.frontend.core.network.chat
import org.koin.core.module.Module
import org.koin.dsl.module
// Auf WASM/JS gibt es keinen lokalen Ktor-Server; bereiten ein leeres Modul vor.
actual val chatModule: Module = module { }
@@ -208,10 +208,11 @@ class DeviceInitializationViewModel(
discoveryService.stopDiscovery()
discoveryService.startDiscovery(ip)
// Falls wir ein Master sind, registrieren wir uns auch direkt, damit andere uns finden
// Falls wir ein Master sind, starten wir den lokalen P2PServer.
// Die mDNSRegistrierung erfolgt zentral beim AppStart (entkoppelt, um Duplikate zu vermeiden).
if (uiState.value.settings.networkRole == NetworkRole.MASTER) {
discoveryService.registerService(8080, ip, uiState.value.settings.deviceName)
syncService.startServer(8080)
println("[P2P Server] Gestartet auf Port 8080")
}
}
@@ -45,4 +45,23 @@ actual object DeviceInitializationSettingsManager {
val settings = loadSettings() ?: return false
return DeviceInitializationValidator.canContinue(settings)
}
// Hilfsfunktionen (nur JVM): Pfad anzeigen und Reset durchführen
fun getSettingsFilePath(): String = settingsFile.absolutePath
/**
* Setzt die Desktop-App lokal zurück.
* - Löscht settings.json (Device-Initialization)
* - Optional: Löscht die lokale Datenbank unter ~/.meldestelle
*/
fun resetToFactoryDefaults(deleteDatabase: Boolean = false): Result<Unit> = try {
if (settingsFile.exists()) settingsFile.delete()
if (deleteDatabase) {
val dbDir = File(System.getProperty("user.home"), ".meldestelle")
if (dbDir.exists()) dbDir.deleteRecursively()
}
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
@@ -214,7 +214,22 @@ actual fun DeviceInitializationConfig(
MsFilePicker(
label = "Backup-Verzeichnis (Plan-USB)",
selectedPath = settings.backupPath,
onFileSelected = { viewModel.updateSettings { s -> s.copy(backupPath = it) } },
onFileSelected = { path ->
if (path.isNotBlank()) {
try {
val dir = java.io.File(path)
if (!dir.exists()) dir.mkdirs()
val probe = java.io.File(dir, ".ms_write_test.tmp")
probe.writeText("ok")
probe.delete()
viewModel.updateSettings { s -> s.copy(backupPath = path) }
} catch (e: Exception) {
println("[DeviceInit] Backup-Verzeichnis nicht beschreibbar: ${e.message}")
}
} else {
viewModel.updateSettings { s -> s.copy(backupPath = path) }
}
},
directoryOnly = true,
modifier = Modifier.focusRequester(backupPathFocus),
enabled = !uiState.isLocked
@@ -8,6 +8,9 @@ import at.mocode.frontend.core.auth.di.authModule
import at.mocode.frontend.core.localdb.AppDatabase
import at.mocode.frontend.core.localdb.DatabaseProvider
import at.mocode.frontend.core.localdb.localDbModule
import at.mocode.frontend.core.network.NetworkConfig
import at.mocode.frontend.core.network.chat.KtorWebSocketServerService
import at.mocode.frontend.core.network.discovery.NetworkDiscoveryService
import at.mocode.frontend.core.network.networkModule
import at.mocode.frontend.core.sync.di.syncModule
import at.mocode.frontend.features.billing.di.billingModule
@@ -60,6 +63,28 @@ fun main() = application {
)
}
println("[DesktopApp] KOIN initialisiert")
// Base URL Log für schnelle Fehlerdiagnose
println("[Network] baseUrl=${NetworkConfig.baseUrl}")
// Starte Netzwerk-Dienste für den POC
val koin = GlobalContext.get()
try {
val wsServer = koin.get<KtorWebSocketServerService>()
wsServer.start()
val discovery = koin.get<NetworkDiscoveryService>()
discovery.startDiscovery()
// Im Host-Modus würden wir hier registerService aufrufen.
// Für den POC registrieren wir den lokalen Host-Dienst immer mit dem WS-Port
try {
discovery.registerService(wsServer.getPort())
println("[DesktopApp] Discovery-Registrierung durchgeführt (Port ${wsServer.getPort()})")
} catch (e: Exception) {
println("[DesktopApp] Discovery-Registrierung fehlgeschlagen: ${e.message}")
}
} catch(e: Exception) {
println("[DesktopApp] POC-Dienste konnten nicht gestartet werden: ${e.message}")
}
// Testdaten für Prototyp laden
at.mocode.frontend.shell.desktop.data.Store.seed()
} catch (e: Exception) {
@@ -24,14 +24,14 @@ class DesktopNavigationPort : NavigationPort {
}
override fun navigateToScreen(screen: AppScreen) {
println("[DesktopNav] navigateToScreen -> $screen")
// Aktuellen Screen auf den Stack legen, falls er nicht derselbe ist
val current = _currentScreen.value
if (current != screen) {
backStack.add(current)
// Begrenzung des Backstacks auf z. B. 50 Einträge
if (backStack.size > 50) backStack.removeAt(0)
if (current == screen) {
// Keine Aktion/kein Log bei identischem Ziel beruhigt die Navigation
return
}
println("[DesktopNav] navigateToScreen -> $screen")
backStack.add(current)
if (backStack.size > 50) backStack.removeAt(0)
_currentScreen.value = screen
}
@@ -11,8 +11,7 @@ import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
@@ -20,6 +19,10 @@ import androidx.compose.ui.unit.dp
import at.mocode.frontend.core.designsystem.theme.AppColors
import at.mocode.frontend.core.designsystem.theme.Dimens
import at.mocode.frontend.core.navigation.AppScreen
import at.mocode.frontend.core.network.backup.BackupService
import at.mocode.frontend.features.device.initialization.data.local.DeviceInitializationSettingsManager
import org.koin.core.context.GlobalContext
import org.koin.core.parameter.parametersOf
@Composable
fun DesktopTopHeader(
@@ -84,9 +87,9 @@ fun DesktopTopHeader(
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM)
) {
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM)
) {
// Sync-Status Indikator
val syncColor = if (connectedPeersCount > 0) AppColors.Success else MaterialTheme.colorScheme.outline
val syncText = if (connectedPeersCount > 0) "$connectedPeersCount Peer(s)" else "Offline"
@@ -126,6 +129,87 @@ fun DesktopTopHeader(
color = MaterialTheme.colorScheme.outlineVariant
)
// Diagnose/Tools: Backup jetzt erstellen + Reset-Aktionen
var menuOpen by remember { mutableStateOf(false) }
Box {
Button(onClick = { menuOpen = true }, enabled = true) {
Text("Tools")
}
DropdownMenu(expanded = menuOpen, onDismissRequest = { menuOpen = false }) {
DropdownMenuItem(
text = { Text("Backup jetzt erstellen (PoC)") },
onClick = {
menuOpen = false
val settings = DeviceInitializationSettingsManager.loadSettings()
val backupPath = settings?.backupPath.orEmpty()
val sharedKey = settings?.sharedKey.orEmpty()
val deviceName = settings?.deviceName.orEmpty().ifBlank { "Meldestelle-Device" }
if (backupPath.isBlank() || sharedKey.isBlank()) {
println("[Backup] Abbruch: backupPath oder sharedKey nicht gesetzt. Öffne DeviceInitialization.")
onNavigate(AppScreen.DeviceInitialization)
} else {
try {
val backupService: BackupService = GlobalContext.get().get<BackupService> { parametersOf(deviceName) }
val result = backupService.exportDelta("poc-backup", backupPath, sharedKey)
result.onSuccess { fileName -> println("[Backup] Erfolgreich exportiert: $fileName") }
.onFailure { ex -> println("[Backup] Fehler: ${ex.message}") }
} catch (e: Exception) {
println("[Backup] Fehler bei der Initialisierung: ${e.message}")
}
}
}
)
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
DropdownMenuItem(
text = { Text("Einstellungen-Ordner öffnen") },
onClick = {
menuOpen = false
val settingsDir = DeviceInitializationSettingsManager.getSettingsFilePath()
val parent = java.io.File(settingsDir).parentFile?.absolutePath ?: settingsDir
try {
// Versuche plattformspezifisch den Ordner zu öffnen
val os = System.getProperty("os.name").lowercase()
if (os.contains("win")) {
Runtime.getRuntime().exec(arrayOf("explorer", parent))
} else if (os.contains("mac")) {
Runtime.getRuntime().exec(arrayOf("open", parent))
} else {
Runtime.getRuntime().exec(arrayOf("xdg-open", parent))
}
} catch (e: Exception) {
println("[Tools] Konnte Ordner nicht öffnen: ${e.message}. Pfad: $parent")
}
}
)
DropdownMenuItem(
text = { Text("Einstellungen zurücksetzen") },
onClick = {
menuOpen = false
val res = DeviceInitializationSettingsManager.resetToFactoryDefaults(deleteDatabase = false)
if (res.isSuccess) {
println("[Reset] settings.json gelöscht: ${DeviceInitializationSettingsManager.getSettingsFilePath()}")
} else {
println("[Reset] Fehler: ${res.exceptionOrNull()?.message}")
}
onNavigate(AppScreen.DeviceInitialization)
}
)
DropdownMenuItem(
text = { Text("Alles zurücksetzen (inkl. DB)") },
onClick = {
menuOpen = false
val res = DeviceInitializationSettingsManager.resetToFactoryDefaults(deleteDatabase = true)
if (res.isSuccess) {
println("[Reset] settings + ~/.meldestelle gelöscht")
} else {
println("[Reset] Fehler: ${res.exceptionOrNull()?.message}")
}
onNavigate(AppScreen.DeviceInitialization)
}
)
}
}
// Profil / Logout Bereich
if (isAuthenticated) {
Text(
+2 -2
View File
@@ -7,7 +7,7 @@
# === FRONTEND & KMP CORE ===
# ==============================================================================
# Kotlin & Tooling
kotlin = "2.3.20"
kotlin = "2.3.21"
ksp = "2.3.4"
# KotlinX (Core Libraries)
@@ -16,7 +16,7 @@ kotlinx-serialization-json = "1.9.0"
kotlinx-datetime = "0.7.1"
# UI: Compose Multiplatform
# Aligned with Kotlin 2.3.20
# Aligned with Kotlin 2.3.21
composeMultiplatform = "1.10.3"
composeHotReload = "1.0.0"
materialIconsExtended = "1.7.3"
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME