Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 280db663c7 | |||
| 74ef6424b7 | |||
| 3959168695 | |||
| 04a435df1d | |||
| 3aaf5cc59c | |||
| a2d94bbc7e | |||
| 95a130c72e | |||
| 223bf77776 |
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -462,6 +462,6 @@ tasks.withType<Exec>().configureEach {
|
||||
}
|
||||
|
||||
tasks.wrapper {
|
||||
gradleVersion = "9.4.1"
|
||||
gradleVersion = "9.5.0"
|
||||
distributionType = Wrapper.DistributionType.BIN
|
||||
}
|
||||
|
||||
+2
-1
@@ -40,7 +40,8 @@ app {
|
||||
jvm-options = [
|
||||
"-Xms128m",
|
||||
"-Xmx512m",
|
||||
"-Dfile.encoding=UTF-8"
|
||||
"-Dfile.encoding=UTF-8",
|
||||
"--enable-native-access=ALL-UNNAMED"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
+20
-20
@@ -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:
|
||||
|
||||
+7
-7
@@ -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
|
||||
|
||||
+1
-1
@@ -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/Client‑Kommunikation (mDNS, WS‑Chat), robuste Connectivity‑Checks, UX für Backup‑Pfad, Session‑Abschluss mit Dokumentation.
|
||||
|
||||
## Summary
|
||||
- ConnectivityCheck robuster gemacht (Fallbacks, schneller Erstcheck) und Logs (Base‑URL, WS‑Port) korrigiert.
|
||||
- Discovery/Registration zentralisiert und entdoppelt; Interface‑Bindung und Logging verbessert.
|
||||
- Datei‑Picker auf `JFileChooser` umgestellt; editierbares Pfadfeld mit Validierung integriert.
|
||||
- Firewalld/mDNS‑Ursache für fehlende Sichtbarkeit zwischen Host/Client identifiziert und als ToDo/Guide dokumentiert.
|
||||
|
||||
## Changes
|
||||
- ConnectivityTracker: Fallback‑Kaskade readiness → health → /api/ping/simple; Intervalle angepasst; Debug‑Logs ergänzt.
|
||||
- main.kt: korrekte String‑Interpolation; Start‑Log der `NetworkConfig.baseUrl`; WS‑Port 8090 konsistent.
|
||||
- JmDnsDiscoveryService: Interface‑Filter (ohne docker/br/veth, private IPv4 priorisiert), Debounce/De‑Dup der Registrierung, Log‑Noise reduziert.
|
||||
- Navigation: Guard gegen Navigation auf gleichen Screen; Top‑Bar Tools erweitert (Reset/Backup/Settings‑Ordner öffnen).
|
||||
- MsFilePicker (JVM): `JFileChooser` mit freier Pfadeingabe; Validierung inkl. Schreib‑Probe; automatische Ordnererstellung bei Auswahl.
|
||||
- conveyor.conf: JVM‑Flag `--enable-native-access=ALL-UNNAMED` ergänzt (Netty‑Warnung mitigiert).
|
||||
|
||||
## Verification
|
||||
- Build (Gradle): erfolgreich ✓
|
||||
- Laufzeit/Netzwerk: Verifikation ausstehend (mDNS nach Firewall‑Freigaben; KDE‑Picker unter Fedora 44; Host/Client‑Sichtbarkeit LAN/WLAN) — Anti‑Halluzinations‑Protokoll 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. KDE‑Directory‑Picker: auf `OPEN_DIALOG` im `DIRECTORIES_ONLY`‑Modus wechseln; präzisere Fehlermeldungen; HOME‑Fallback.
|
||||
2. Guard gegen mehrfachen P2P‑Start ergänzen.
|
||||
3. Conveyor/Windows‑Installer in CI (Runtime‑Flags; optional SLF4J‑Binding), danach erneute Laufzeit‑Verifikation.
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
🎨 [Frontend Expert]
|
||||
|
||||
### Kurzfazit
|
||||
- Die beiden Rechner sehen sich über mDNS höchstwahrscheinlich wegen Firewall‑Regeln/Zonen nicht. JmDNS registriert korrekt auf 192.168.0.124:8090, aber der Client (192.168.0.167, WLAN) empfängt keine mDNS‑Antworten → Fedora/Firewalld blockiert UDP 5353 Multicast in der aktiven Zone (WLAN/Public) sehr häufig standardmäßig.
|
||||
- Der Datei‑Picker unter Fedora KDE hakt im Verzeichnis‑Modus 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 mDNS‑Baseline)
|
||||
```
|
||||
systemctl status avahi-daemon
|
||||
```
|
||||
|
||||
3) mDNS‑Pakete verifizieren (nur wenn es nach 1) noch nicht klappt)
|
||||
```
|
||||
# Auf dem Client (WLAN‑Interface anpassen):
|
||||
sudo tcpdump -n -i wlp0s20f3 port 5353
|
||||
# Auf dem Host (Ethernet‑Interface anpassen):
|
||||
sudo tcpdump -n -i <eth-iface> port 5353
|
||||
```
|
||||
Erwartung: Bei App‑Start siehst du Multicast an 224.0.0.251. Wenn auf einer Seite „Stille“ herrscht, blockiert noch etwas.
|
||||
|
||||
4) WS‑Konnektivitä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 WS‑Pfad 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 mDNS‑Antworten angekommen (typisches Firewall‑Symptom bei Fedora/KDE, vor allem in Zone „public“).
|
||||
- Mehrfach „[P2P Server] Gestartet auf Port 8080“ → Wir haben noch einen Trigger, der den P2P‑Start mehrfach feuert. Funktional unkritisch für den aktuellen Befund; ich bereinige das im nächsten Patch mit einem zusätzlichen Guard.
|
||||
|
||||
### Fedora KDE Datei‑Picker (JFileChooser) – Workarounds jetzt, Fix als Patch im Anschluss
|
||||
- Mögliche Ursache 1: `SAVE_DIALOG` im Ordner‑Modus unter KDE verhält sich zickig. Besser: `OPEN_DIALOG` im `DIRECTORIES_ONLY`‑Modus + eigener Bestätigungs‑Button‑Text.
|
||||
- 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`.
|
||||
- Quick‑Test 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):
|
||||
- Directory‑Picker: 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 Auto‑Erstellung, 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 3–5 Zeilen genügen).
|
||||
- Datei‑Picker 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 KDE‑Picker (OPEN_DIALOG) und einen zusätzlichen Guard gegen mehrfachen P2P‑Start; außerdem noch etwas Discovery‑Logging (Interface/Zonen‑Hinweis).
|
||||
- Danach kümmern wir uns um Conveyor (Windows‑Installer aus CI, inkl. JVM‑Flag gegen die Netty‑Warnung).
|
||||
+81
-33
@@ -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_ONLY‑Modus verwenden (kein Save‑Dialog)
|
||||
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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+38
-6
@@ -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() {
|
||||
|
||||
+2
-1
@@ -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() }
|
||||
|
||||
|
||||
+13
@@ -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
|
||||
)
|
||||
+8
@@ -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
|
||||
+3
-1
@@ -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.
|
||||
*/
|
||||
|
||||
+9
@@ -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() }
|
||||
}
|
||||
+105
@@ -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
|
||||
}
|
||||
}
|
||||
+41
-6
@@ -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> {
|
||||
|
||||
+57
-26
@@ -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) {
|
||||
|
||||
+37
@@ -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()
|
||||
}
|
||||
}
|
||||
+7
@@ -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 { }
|
||||
+3
-2
@@ -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 P2P‑Server.
|
||||
// Die mDNS‑Registrierung erfolgt zentral beim App‑Start (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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+19
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+16
-1
@@ -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
|
||||
|
||||
+25
@@ -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) {
|
||||
|
||||
+6
-6
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+89
-5
@@ -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(
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user