umbau zu comose Multiplatform

This commit is contained in:
Stefan Mogeritsch 2025-09-10 20:32:30 +02:00
parent f43ece082c
commit f236ed0de6
45 changed files with 562 additions and 3271 deletions

1
.gitignore vendored
View File

@ -17,3 +17,4 @@ captures
!*.xcworkspace/contents.xcworkspacedata
**/xcshareddata/WorkspaceSettings.xcsettings
/.env
/kotlin-js-store/

View File

@ -1,10 +1,10 @@
# Web-App-Richtlinie (Compose for Web)
# Client-App-Richtlinie (Compose Multiplatform)
## 1. Einleitung
Diese Richtlinie beschreibt die Architektur und die Best Practices für die Entwicklung des Web-Frontends für das "Meldestelle"-Projekt. Das Frontend wird mit **Compose for Web** (Teil von Compose Multiplatform) entwickelt.
Diese Richtlinie beschreibt die Architektur und die Best Practices für die Entwicklung der Client-Anwendungen für das "Meldestelle"-Projekt. Die Client-Anwendungen werden mit **Compose Multiplatform** für Desktop und Web entwickelt.
Das Hauptziel ist die maximale Wiederverwendung von Code zwischen verschiedenen Plattformen (potenziell Web, Android, Desktop) durch die konsequente Nutzung des `commonMain`-Source-Sets von Kotlin Multiplatform (KMP).
Das Hauptziel ist die maximale Wiederverwendung von Code zwischen den Desktop- und Web-Plattformen durch die konsequente Nutzung des `commonMain`-Source-Sets von Kotlin Multiplatform (KMP). Die Anwendung läuft sowohl als native Desktop-Anwendung (JVM) als auch als Web-Anwendung (WebAssembly).
## 2. Grundprinzipien
@ -26,49 +26,95 @@ Der UI-Zustand (State) wird explizit verwaltet.
### Styling
Das Styling erfolgt über eine Kotlin-DSL, die CSS abbildet.
Das Styling erfolgt plattformspezifisch, aber mit gemeinsamen Prinzipien:
- **`StyleSheet`**: Definieren Sie Stile in einem `object`, das von `StyleSheet` erbt. Dies fördert die Wiederverwendbarkeit und Typsicherheit.
- **Scoped Styles**: Organisieren Sie Stile logisch, z. B. pro Komponente oder pro Bildschirm.
- **Kein globales CSS**: Vermeiden Sie die Verwendung globaler, ungebundener CSS-Dateien. Das Styling sollte ausschließlich über die Kotlin-DSL verwaltet werden, um Konflikte zu vermeiden und die Kapselung zu wahren.
#### Gemeinsame Styling-Prinzipien (commonMain)
- **Compose Material Design**: Nutzen Sie Material3-Komponenten und Theming für konsistente UI.
- **Gemeinsame Designsystem**: Definieren Sie gemeinsame Farben, Typografie und Spacing in `commonMain`.
- **Responsive Design**: Berücksichtigen Sie verschiedene Bildschirmgrößen (Desktop-Fenster vs. Browser-Viewports).
#### Web-spezifisches Styling (wasmJsMain)
- **CSS-Integration**: Web-spezifische Styling-Anforderungen können über CSS in den Resources behandelt werden.
- **Browser-Kompatibilität**: Berücksichtigen Sie Web-spezifische Rendering-Unterschiede.
#### Desktop-spezifisches Styling (jvmMain)
- **Native Look & Feel**: Desktop-Anwendungen sollten sich nativ anfühlen.
- **Fenster-Management**: Berücksichtigen Sie Desktop-spezifische UI-Patterns (Menüleisten, etc.).
```kotlin
// Beispiel für ein StyleSheet
object AppStylesheet : StyleSheet() {
val container by style {
padding(24.px)
border(1.px, LineStyle.Solid, Color.black)
}
// Beispiel für gemeinsames Theming in commonMain
@Composable
fun AppTheme(content: @Composable () -> Unit) {
MaterialTheme(
colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme(),
typography = AppTypography,
content = content
)
}
```
### Navigation
Die Navigation zwischen verschiedenen Bildschirmen ("Screens") wird durch Zustandsänderungen gesteuert. Da es noch keine offizielle "Compose Navigation"-Bibliothek für das Web gibt, verwenden wir einen einfachen, zustandsbasierten Ansatz.
- Ein `StateFlow` oder eine `mutableState`-Variable im ViewModel kann die aktuell anzuzeigende Route/Screen repräsentieren.
- Ein zentraler -Composable kann auf Änderungen dieses Zustands reagieren und den entsprechenden Screen rendern. `Router`
Die Navigation wird plattformunabhängig in `commonMain` implementiert:
- **ViewModel-basierte Navigation**: Ein `StateFlow` oder `mutableState` im ViewModel repräsentiert die aktuelle Route/Screen.
- **Gemeinsamer Router**: Ein zentraler `Router`-Composable in `commonMain` reagiert auf Zustandsänderungen und rendert den entsprechenden Screen.
- **Plattformspezifische Einstiegspunkte**: Desktop und Web haben separate `main.kt`-Dateien, aber nutzen denselben gemeinsamen App-Composable.
## 3. Projekt- und Code-Struktur
Die Codebasis ist klar zwischen plattformunabhängiger Logik (`commonMain`) und plattformspezifischer UI (`jsMain`) getrennt.
- **`core/commonMain`** (oder ein äquivalentes `shared`-Modul):
- **Business-Logik**: Die gesamte Geschäftslogik.
- **ViewModels/Presenters**: Klassen, die den UI-Zustand verwalten und auf Benutzerinteraktionen reagieren.
Die Codebasis ist klar zwischen plattformunabhängiger Logik (`commonMain`) und plattformspezifischer Implementation (`jvmMain`, `wasmJsMain`) getrennt.
### Source Sets
- **`client/src/commonMain`**:
- **UI-Code**: Alle `@Composable`-Funktionen, die zwischen Desktop und Web geteilt werden.
- **Business-Logik**: ViewModels/Presenters, die den UI-Zustand verwalten.
- **Data-Klassen**: Modelle, die Daten repräsentieren.
- **Repositories/Services**: Code für den Datenzugriff (z.B. Ktor-HTTP-Clients zum Aufrufen des Backends).
- **Common Dependencies**: Shared Compose-Dependencies (runtime, foundation, material3, ui).
- **`client/jsMain`**:
- **UI-Code**: Ausschließlich `@Composable`-Funktionen.
- **`main.kt`**: Der Einstiegspunkt der Web-Anwendung.
- **`StyleSheet.kt`**: Die Kotlin-CSS-DSL-Definitionen.
- **Abhängigkeit von `commonMain`**: Die UI konsumiert die ViewModels und Datenklassen aus `commonMain`. Die UI ist "dumm" und dient nur zur Darstellung des Zustands.
- **`client/src/jvmMain`** (Desktop-Plattform):
- **`main.kt`**: Der Einstiegspunkt der Desktop-Anwendung.
- **Desktop-spezifische Code**: Plattformspezifische Implementierungen und Integrationen.
- **Desktop Dependencies**: `compose.desktop.currentOs`, Coroutines für Swing.
- **`client/src/jsMain/resources`**:
- **`client/src/wasmJsMain`** (Web-Plattform):
- **`main.kt`**: Der Einstiegspunkt der Web-Anwendung (WebAssembly).
- **Web-spezifische Code**: Browser-spezifische Implementierungen.
- **Platform-spezifische Implementierungen**: Web-APIs und Browser-Integrationen.
- **`client/src/wasmJsMain/resources`**:
- **`index.html`**: Das Host-HTML-Dokument für die Compose-Anwendung.
- **Statische Assets**: Bilder, Schriftarten und andere statische Dateien.
- **Statische Assets**: Bilder, Schriftarten und andere statische Dateien für die Web-Version.
### Shared Module Integration
- **`core/commonMain`** (oder äquivalente `shared`-Module):
- **Repositories/Services**: Code für den Datenzugriff (z.B. Ktor-HTTP-Clients zum Aufrufen des Backends).
- **Business-Logik**: Plattformunabhängige Geschäftslogik, die von allen Client-Plattformen genutzt wird.
## 4. Entwicklung und Ausführung
### Lokale Entwicklung mit Hot-Reload
Für eine schnelle Entwicklungs-Loop mit automatischer Neuladung bei Änderungen wird der Gradle-Task `jsBrowserDevelopmentRun` verwendet. Dies wird durch unser Docker-Setup vereinfacht.
Um die Web-App im Entwicklungsmodus zu starten (wie in beschrieben): `README-DOCKER.md`
### Desktop-Entwicklung
Für die Desktop-Anwendung stehen folgende Gradle-Tasks zur Verfügung:
```shell script
# Desktop-Anwendung direkt ausführen
./gradlew :client:run
# Desktop-Distribution erstellen (DMG, MSI, DEB)
./gradlew :client:createDistributable
./gradlew :client:packageDmg # macOS
./gradlew :client:packageMsi # Windows
./gradlew :client:packageDeb # Linux
```
### Web-Entwicklung mit Hot-Reload
Für die Web-Anwendung mit automatischer Neuladung bei Änderungen:
```shell script
# Web-App mit Hot-Reload starten
./gradlew :client:wasmJsBrowserDevelopmentRun
```
#### Docker-Setup für Web-Entwicklung
Das Docker-Setup ist spezifisch für die Web-Entwicklung konfiguriert (wie in `README-DOCKER.md` beschrieben):
```shell script
# Startet die Web-App mit Hot-Reload
@ -77,17 +123,55 @@ docker-compose -f docker-compose.yml \
```
Der Dienst ist dann unter dem in der `docker-compose.clients.yml` konfigurierten Port (z.B. Port `3000`) erreichbar.
### Produktions-Build
Um eine optimierte JavaScript-Datei für die Produktion zu erstellen, wird der Gradle-Task `jsBrowserDistribution` verwendet. Das Docker-Image für die Produktion ( im `client`-Verzeichnis) sollte diesen Task nutzen, um die finalen Artefakte zu bauen. `Dockerfile`
## 5. Dos and Don'ts
- **DO**: Die gesamte Logik (State-Management, Datenabruf, Validierung) in `commonMain` implementieren.
- **DO**: Kleine, wiederverwendbare und zustandslose Composables erstellen.
- **DO**: Styling ausschließlich über die Kotlin-CSS-DSL (`StyleSheet`) realisieren.
- **DO**: Events von der UI über Lambda-Funktionen an die ViewModels in `commonMain` weiterleiten.
- **DON'T**: Geschäftslogik, API-Aufrufe oder komplexe Zustandsmanipulationen direkt in `@Composable`-Funktionen schreiben.
- **DON'T**: Den DOM direkt manipulieren. Compose for Web verwaltet den DOM. Falls eine Interaktion mit externen JS-Bibliotheken unumgänglich ist, nutzen Sie die `external`- und `@JsModule`-Mechanismen von Kotlin/JS sauber gekapselt.
- **DON'T**: Globale CSS-Dateien verwenden, die mit der von Compose generierten Stil-Logik in Konflikt geraten könnten.
---
_Letzte Aktualisierung: 2025-09-10_
Diese Richtlinie bietet eine solide Grundlage für die Entwicklung Ihrer Webanwendung mit Compose for Web und stellt sicher, dass neue Teammitglieder die Architektur und die erwarteten Konventionen schnell verstehen.
### Produktions-Builds
#### Desktop-Distribution
```shell script
# Erstellt native Distributionen für alle konfigurierten Plattformen
./gradlew :client:packageDistributionForCurrentOS
```
#### Web-Distribution
```shell script
# Erstellt optimierte WebAssembly-Artefakte für die Produktion
./gradlew :client:wasmJsBrowserDistribution
```
Das Docker-Image für die Web-Produktion (`Dockerfile` im `client`-Verzeichnis) sollte den `wasmJsBrowserDistribution`-Task nutzen, um die finalen Artefakte zu bauen.
## 5. Plattformspezifische Besonderheiten
### Desktop (jvmMain)
- **Fenster-Management**: Nutzen Sie Compose Desktop-APIs für Fensteroperationen.
- **System-Integration**: Zugriff auf Desktop-spezifische Features (Dateisystem, Notifications, etc.).
- **Performance**: Desktop-Apps können mehr Ressourcen nutzen als Web-Apps.
### Web (wasmJsMain)
- **Browser-APIs**: Zugriff auf Web-APIs erfolgt über `external`-Deklarationen.
- **Bundle-Size**: Achten Sie auf die Größe der WebAssembly-Bundles für optimale Ladezeiten.
- **SEO und Accessibility**: Berücksichtigen Sie Web-spezifische Anforderungen.
## 6. Dos and Don'ts
### Multiplatform Best Practices
- **DO**: Die gesamte UI-Logik (State-Management, Datenabruf, Validierung) in `commonMain` implementieren.
- **DO**: Kleine, wiederverwendbare und zustandslose Composables in `commonMain` erstellen.
- **DO**: Material3 und gemeinsames Theming für konsistente UI zwischen Plattformen verwenden.
- **DO**: Events von der UI über Lambda-Funktionen an die ViewModels in `commonMain` weiterleiten.
- **DO**: Plattformspezifische Features über `expect`/`actual`-Mechanismus abstrahieren.
### Platform-Specific Guidelines
- **DO** (Desktop): Native Look & Feel und Desktop-UI-Patterns verwenden.
- **DO** (Web): Web-Standards und Accessibility-Guidelines befolgen.
### Don'ts
- **DON'T**: Geschäftslogik, API-Aufrufe oder komplexe Zustandsmanipulationen direkt in `@Composable`-Funktionen schreiben.
- **DON'T**: Plattformspezifische Code direkt in `commonMain` verwenden ohne `expect`/`actual`.
- **DON'T** (Web): Den DOM direkt manipulieren. Compose Multiplatform verwaltet das Rendering. Falls Interaktion mit externen Bibliotheken nötig ist, nutzen Sie `external`-Mechanismen sauber gekapselt.
- **DON'T**: Annahmen über die Zielplattform in `commonMain` machen.
---
_Letzte Aktualisierung: 2025-01-10_
Diese Richtlinie bietet eine solide Grundlage für die Entwicklung Ihrer Desktop- und Web-Anwendungen mit Compose Multiplatform und stellt sicher, dass neue Teammitglieder die Multiplatform-Architektur und die erwarteten Konventionen schnell verstehen.

View File

@ -0,0 +1,81 @@
# Folder Structure Analysis - Meldestelle Project
**Datum:** 10. September 2025
**Frage:** "müssen das 2 Ordner sein? analysieren, korrigieren und optimieren"
## Analyse der aktuellen Ordnerstruktur
### ✅ Korrekt getrennte Ordner (KEINE Duplikate)
#### 1. `docker/` vs `dockerfiles/`
- **docker/**: Runtime-Volumes und Daten (monitoring, services)
- **dockerfiles/**: Dockerfile-Definitionen (clients, infrastructure, services, templates)
- **Bewertung**: ✅ **Korrekte Trennung** - unterschiedliche Zwecke
#### 2. `kotlin-js-store/` vs `client/`
- **kotlin-js-store/**: Build-Artifacts und Yarn-Dependencies für JS/WASM
- **client/**: Quellcode des Compose Multiplatform Clients
- **Bewertung**: ✅ **Funktional notwendig** - Build-Cache vs Source
### ✅ Bereits optimierte Struktur
#### Business Module Ordner (Korrekt deaktiviert)
```
├── members/ # Temporär deaktiviert
├── horses/ # Temporär deaktiviert
├── events/ # Temporär deaktiviert
└── masterdata/ # Temporär deaktiviert
```
- **Status**: Physisch vorhanden, aber in `settings.gradle.kts` auskommentiert
- **Grund**: Benötigen Multiplatform-Konfiguration für KMP/WASM
- **Empfehlung**: ✅ **Korrekt so belassen** bis Migration abgeschlossen
## Antwort auf die Hauptfrage
### "Müssen das 2 Ordner sein?"
**ANTWORT: JA** - Die identifizierten "doppelten" Ordner sind **KEINE Duplikate**, sondern haben unterschiedliche, wichtige Funktionen:
1. **docker/ + dockerfiles/**: Verschiedene Docker-Aspekte (Runtime vs Definitions)
2. **kotlin-js-store/ + client/**: Build-Artifacts vs Source Code
3. **Business Module Ordner**: Temporär deaktiviert, aber für zukünftige Migration notwendig
## Optimierungsempfehlungen
### 🟢 Keine strukturellen Änderungen erforderlich
- Aktuelle Struktur ist **optimal organisiert**
- Alle "doppelten" Ordner haben **legitime, getrennte Zwecke**
- Folgt **Best Practices** für Gradle Multimodule + Docker
### 🔄 Mögliche kleine Verbesserungen
#### 1. kotlin-js-store/ Optimierung
```bash
# Kann in .gitignore aufgenommen werden (falls nicht schon geschehen)
echo "kotlin-js-store/" >> .gitignore
```
- **Begründung**: Build-Artifacts sollten nicht versioniert werden
- **Status**: Prüfung erforderlich
#### 2. Dokumentation verbessern
- README-Dateien in docker/ und dockerfiles/ zur Erklärung der Unterschiede
- Kommentare in settings.gradle.kts erweitern
## Fazit
### ✅ **STRUKTUR IST OPTIMAL**
- **Keine Duplikate** vorhanden
- **Alle Ordner haben klare Zwecke**
- **Folgt modernen Best Practices**
- **Bereits gut optimiert**
### 🎯 **Empfehlung: Keine Änderungen**
Die aktuelle 2-Ordner-Struktur ist **notwendig und korrekt**. Jeder Ordner erfüllt einen spezifischen Zweck in der modernen Kotlin Multiplatform + Docker Architektur.
### 📋 **Nächste Schritte**
1. kotlin-js-store/ in .gitignore prüfen
2. Bei Business Module Migration: Ordner reaktivieren
3. Dokumentation für Docker-Ordner-Unterschiede ergänzen
---
**Status:** ✅ Analyse abgeschlossen - Struktur ist optimal
**Ergebnis:** Aktuelle Ordnerstruktur beibehalten

53
client/build.gradle.kts Normal file
View File

@ -0,0 +1,53 @@
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.compose.multiplatform)
alias(libs.plugins.compose.compiler)
}
kotlin {
jvm()
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser {
commonWebpackConfig {
outputFileName = "composeApp.js"
}
}
binaries.executable()
}
sourceSets {
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.ui)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
}
jvmMain.dependencies {
implementation(compose.desktop.currentOs)
implementation(libs.kotlinx.coroutines.swing)
}
}
}
compose.desktop {
application {
mainClass = "at.mocode.MainKt"
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "at.mocode"
packageVersion = "1.0.0"
}
}
}

View File

@ -1,357 +0,0 @@
# Client Common-UI Modul
## Überblick
Das **common-ui** Modul stellt die geteilten Benutzeroberflächen-Komponenten und Geschäftslogik für die Meldestelle Client-Anwendungen bereit. Dieses Modul implementiert die Kern-"Tracer Bullet" Funktionalität unter Verwendung eines modernen MVVM-Architekturmusters und dient sowohl der Desktop- als auch der Web-Anwendung.
**Hauptfunktionen:**
- 🏗️ **MVVM-Architektur** - ordnungsgemäße Trennung der Belange mit ViewModel-Muster
- 🌐 **Plattformübergreifend** - geteilter Code für Desktop (JVM) und Web (JavaScript) Anwendungen
- 🎯 **Vier UI-Zustände** - vollständige Implementierung gemäß trace-bullet-guideline.md
- 🔧 **Ressourcenverwaltung** - ordnungsgemäßer HttpClient-Lebenszyklus und Speicherverwaltung
- 🧪 **Testabdeckung** - umfassende Testsuite für alle kritischen Funktionen
---
## Architektur
### Modulstruktur
```
client/common-ui/src/
├── commonMain/kotlin/at/mocode/client/
│ ├── data/service/
│ │ ├── PingResponse.kt # Datenmodell für API-Antworten
│ │ └── PingService.kt # HTTP-Service mit Ressourcenverwaltung
│ └── ui/
│ ├── App.kt # Hauptanwendungskomponente
│ └── viewmodel/
│ └── PingViewModel.kt # MVVM-Zustandsverwaltung
└── commonTest/kotlin/at/mocode/client/
├── data/service/
│ ├── PingResponseTest.kt # Datenmodell-Tests
│ └── PingServiceTest.kt # Service-Schicht-Tests
└── ui/viewmodel/
└── PingViewModelTest.kt # ViewModel- und Zustands-Tests
```
### MVVM-Muster Implementierung
**PingUiState (Sealed Class):**
- `Initial` - Neutrale Nachricht, Button aktiv
- `Loading` - Ladeindikator, Button deaktiviert
- `Success` - Positive Antwortanzeige, Button aktiv
- `Error` - Klare Fehlernachricht, Button aktiv
**PingViewModel:**
- Verwaltet UI-Zustandsübergänge
- Behandelt Coroutine-Lebenszyklus
- Ordnungsgemäße Ressourcenentsorgung
**PingService:**
- HTTP-Client-Verwaltung
- Result-Wrapper-Muster
- Ressourcen-Bereinigungsunterstützung
---
## Abhängigkeiten
### Laufzeit-Abhängigkeiten
```kotlin
// Compose Multiplatform UI
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
// Netzwerk & Serialisierung
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.contentNegotiation)
implementation(libs.ktor.client.serialization.kotlinx.json)
implementation(libs.kotlinx.serialization.json)
// Coroutines
implementation(libs.kotlinx.coroutines.core)
```
### Plattformspezifische Abhängigkeiten
```kotlin
// JVM (Desktop)
jvmMain {
implementation(libs.ktor.client.cio)
}
// JS (Web)
jsMain {
implementation(libs.ktor.client.js)
}
```
### Test-Abhängigkeiten
```kotlin
commonTest {
implementation(libs.kotlin.test)
implementation(libs.kotlinx.coroutines.test)
}
```
---
## Verwendung
### Grundlegende Integration
```kotlin
@Composable
fun YourApplication() {
// Verwendet at.mocode.client.ui.App
App(baseUrl = "https://your-api.com")
}
```
### Erweiterte Verwendung mit benutzerdefinierter Konfiguration
```kotlin
// Benutzerdefinierte Service-Konfiguration
// Verwendet at.mocode.client.data.service.PingService
val customService = PingService(
baseUrl = "https://custom-api.com",
httpClient = createCustomHttpClient()
)
// Benutzerdefiniertes ViewModel mit spezifischem Scope
// Verwendet at.mocode.client.ui.viewmodel.PingViewModel
val customViewModel = PingViewModel(
pingService = customService,
coroutineScope = customCoroutineScope
)
```
---
## API-Referenz
### PingService
```kotlin
class PingService(
private val baseUrl: String = "http://localhost:8080",
private val httpClient: HttpClient = createDefaultHttpClient()
) {
suspend fun ping(): Result<PingResponse>
fun close()
companion object {
fun createDefaultHttpClient(): HttpClient
}
}
```
### PingViewModel
```kotlin
class PingViewModel(
private val pingService: PingService,
private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
) {
var uiState: PingUiState by mutableStateOf(PingUiState.Initial)
private set
fun pingBackend()
fun dispose()
}
```
### PingUiState
```kotlin
sealed class PingUiState {
data object Initial : PingUiState()
data object Loading : PingUiState()
data class Success(val response: PingResponse) : PingUiState()
data class Error(val message: String) : PingUiState()
}
```
---
## Entwicklung
### Das Modul erstellen
```bash
# Für alle Plattformen kompilieren
./gradlew :client:common-ui:build
# Nur JVM-Kompilierung
./gradlew :client:common-ui:compileKotlinJvm
# Nur JavaScript-Kompilierung
./gradlew :client:common-ui:compileKotlinJs
```
### Tests ausführen
```bash
# Alle Tests ausführen
./gradlew :client:common-ui:jvmTest
# Spezifische Testklasse ausführen
./gradlew :client:common-ui:jvmTest --tests "PingViewModelTest"
```
### Codequalität
Das Modul hält hohe Codequalitätsstandards aufrecht:
- **Testabdeckung**: 32 umfassende Tests über alle Schichten
- **Architektur-Konformität**: 100% MVVM-Muster-Einhaltung
- **Ressourcenverwaltung**: Ordnungsgemäßer Lebenszyklus und Bereinigung
- **Speichersicherheit**: Keine Speicherlecks durch ordnungsgemäße Entsorgung
---
## Tests
### Testabdeckung Übersicht
| Komponente | Test-Datei | Tests | Abdeckung |
|-----------|-----------|-------|----------|
| PingResponse | PingResponseTest.kt | 7 | Datenmodell, Serialisierung |
| PingService | PingServiceTest.kt | 10 | HTTP-Service, Lebenszyklus |
| PingViewModel | PingViewModelTest.kt | 8 | MVVM, Zustandsverwaltung |
### Spezifische Test-Suites ausführen
```bash
# Datenschicht-Tests
./gradlew :client:common-ui:jvmTest --tests "*PingResponseTest*"
# Service-Schicht-Tests
./gradlew :client:common-ui:jvmTest --tests "*PingServiceTest*"
# ViewModel-Tests
./gradlew :client:common-ui:jvmTest --tests "*PingViewModelTest*"
```
---
## Architektur-Vorteile
### 🏗️ **Moderne MVVM-Implementierung**
- **Testbarkeit**: Ordnungsgemäße Dependency Injection ermöglicht umfassende Unit-Tests
- **Wartbarkeit**: Klare Trennung der Belange und Single-Responsibility-Prinzip
- **Skalierbarkeit**: Architektur unterstützt zukünftige Funktionserweiterungen nahtlos
### 🚀 **Laufzeit-Effizienz**
- **Ressourcenverwaltung**: Ordnungsgemäße HttpClient-Bereinigung verhindert Speicherlecks
- **Leistung**: Eliminierung unnötiger Operationen und Callback-Muster
- **Stabilität**: Verbesserte Fehlerbehandlung und Zustandsverwaltung
### 🔧 **Entwicklererfahrung**
- **Code-Klarheit**: Selbstdokumentierender Code mit Sealed Classes und klarer Benennung
- **Debugging**: Einfache Zustandsverfolgung und Problemidentifikation
- **Integration**: Einfaches Integrationsmuster für abhängige Module
---
## Migrations-Hinweise
### Von der vorherigen Implementierung
Das Modul wurde vollständig von einem komponentenbasierten Ansatz zu MVVM refaktoriert:
**Vorher (Komponentenbasiert):**
- Vermischte Belange in einzelnen Dateien
- Callback-basierte Zustandsverwaltung
- Manuelle Ressourcenverwaltung
- Speicherleck-Potenzial
**Nachher (MVVM):**
- Klare Trennung der Belange
- Compose-Zustandsverwaltung
- Automatische Ressourcenbereinigung
- Speicherleck-Prävention
### Breaking Changes
**Keine** - Das Refactoring behielt vollständige Rückwärtskompatibilität für abhängige Module bei.
---
## Zukünftige Entwicklung
### Empfohlene Verbesserungen
1. **Konfigurationsverwaltung**
- Umgebungsspezifische Einstellungen
- Konfigurationsvalidierung
2. **Fehlerbehandlung**
- Spezifische Fehlertypen
- Wiederholungsmechanismen für Netzwerkausfälle
3. **Monitoring-Integration**
- Metriken-Sammlung
- Leistungsüberwachung
4. **Internationalisierung**
- Mehrsprachige Unterstützung
- Sprachspezifische Formatierung
---
## Mitwirken
### Entwicklungsumgebung einrichten
1. Stellen Sie sicher, dass JDK 21 installiert ist
2. Klonen Sie das Repository
3. Führen Sie `./gradlew :client:common-ui:build` aus, um die Einrichtung zu verifizieren
### Code-Standards
- Befolgen Sie Kotlin-Codierungskonventionen
- Fügen Sie Tests für neue Funktionalität hinzu
- Behalten Sie MVVM-Architekturmuster bei
- Stellen Sie ordnungsgemäße Ressourcenverwaltung sicher
### Test-Anforderungen
- Alle öffentlichen APIs müssen Tests haben
- Mindestens 90% Testabdeckung für neue Features
- Integrationstests für modulübergreifende Funktionalität
---
## Fehlerbehebung
### Häufige Probleme
| Problem | Lösung |
|-------|----------|
| `HttpClient` nicht ordnungsgemäß geschlossen | Stellen Sie sicher, dass `dispose()` im ViewModel aufgerufen wird |
| Zustand wird in UI nicht aktualisiert | Überprüfen Sie die Compose-Zustandsbeobachtung-Einrichtung |
| Netzwerk-Timeouts | Überprüfen Sie `baseUrl`-Konfiguration und Konnektivität |
| Test-Fehler auf JS-Plattform | Verwenden Sie JS-kompatible Test-Muster (keine Reflection) |
### Debug-Informationen
```bash
# Abhängigkeitskonflikte überprüfen
./gradlew :client:common-ui:dependencies
# Ausführliche Test-Ausgabe
./gradlew :client:common-ui:jvmTest --info
# Build-Scan für detaillierte Analyse
./gradlew :client:common-ui:build --scan
```
---
**Modul-Status**: ✅ Produktionsbereit
**Architektur**: ✅ MVVM-konform
**Testabdeckung**: ✅ Umfassend (32 Tests)
**Dokumentation**: ✅ Vollständig
*Zuletzt aktualisiert: 16. August 2025*

View File

@ -1,73 +0,0 @@
plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.kotlin.serialization)
id("org.jetbrains.compose")
id("org.jetbrains.kotlin.plugin.compose")
id("maven-publish")
}
group = "at.mocode.client"
version = "1.0.0-SNAPSHOT"
kotlin {
jvm()
js(IR) { browser() }
sourceSets {
val commonMain by getting {
dependencies {
// Compose Multiplatform
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
// Skiko is provided by Compose Multiplatform - no explicit dependency needed
// Serialization
implementation(libs.kotlinx.serialization.json)
// Ktor Client for API calls
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.contentNegotiation)
implementation(libs.ktor.client.serialization.kotlinx.json)
// Coroutines for background tasks
implementation(libs.kotlinx.coroutines.core)
}
}
val jvmMain by getting {
dependencies {
// Ktor engine for Desktop
implementation(libs.ktor.client.cio)
}
}
val jsMain by getting {
dependencies {
// Ktor engine for Browser
implementation(libs.ktor.client.js)
}
}
val commonTest by getting {
dependencies {
implementation(libs.kotlin.test)
implementation(libs.kotlinx.coroutines.test)
// Note: ktor-client-mock would be ideal but may not be available in libs
// Using core testing dependencies for now
}
}
val jsTest by getting {
// Avoid duplicate Skiko runtime files in test processedResources
resources.exclude("**/skiko.*")
resources.exclude("**/skikod8.mjs")
}
}
}
// Avoid overwrite warnings when syncing JS test executable: keep first occurrence of duplicate resources
// Configure the Kotlin JS incremental sync task directly using fully-qualified types (no imports in the middle of the file)
tasks.named<org.jetbrains.kotlin.gradle.targets.js.ir.DefaultIncrementalSyncTask>("jsTestTestDevelopmentExecutableCompileSync").configure {
// Skip copying duplicates that already exist in destination
duplicatesStrategy = org.gradle.api.file.DuplicatesStrategy.EXCLUDE
}

View File

@ -1,6 +0,0 @@
package at.mocode.client.data.service
import kotlinx.serialization.Serializable
@Serializable
data class PingResponse(val status: String)

View File

@ -1,31 +0,0 @@
package at.mocode.client.data.service
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.serialization.kotlinx.json.*
class PingService(
private val baseUrl: String = "http://localhost:8080",
private val httpClient: HttpClient = createDefaultHttpClient()
) {
suspend fun ping(): Result<PingResponse> = try {
val response = httpClient.get("$baseUrl/api/ping/ping").body<PingResponse>()
Result.success(response)
} catch (e: Exception) {
Result.failure(e)
}
fun close() {
httpClient.close()
}
companion object {
fun createDefaultHttpClient(): HttpClient = HttpClient {
install(ContentNegotiation) {
json()
}
}
}
}

View File

@ -1,98 +0,0 @@
package at.mocode.client.ui
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import at.mocode.client.data.service.PingService
import at.mocode.client.ui.viewmodel.PingViewModel
import at.mocode.client.ui.viewmodel.PingUiState
@Composable
fun App(baseUrl: String = "http://localhost:8080") {
MaterialTheme {
PingScreen(baseUrl)
}
}
@Composable
fun PingScreen(baseUrl: String) {
val pingService = remember { PingService(baseUrl) }
val viewModel = remember { PingViewModel(pingService) }
DisposableEffect(viewModel) {
onDispose {
viewModel.dispose()
}
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "Ping Backend Service",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(bottom = 24.dp)
)
// Status display area with fixed height for consistent layout
Box(
modifier = Modifier
.fillMaxWidth()
.height(100.dp),
contentAlignment = Alignment.Center
) {
when (val state = viewModel.uiState) {
is PingUiState.Initial -> {
Text(
text = "Klicke auf den Button, um das Backend zu testen",
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyLarge
)
}
is PingUiState.Loading -> {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
CircularProgressIndicator()
Text(
text = "Pinge Backend ...",
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(top = 8.dp)
)
}
}
is PingUiState.Success -> {
Text(
text = "Antwort vom Backend: ${state.response.status}",
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.bodyLarge
)
}
is PingUiState.Error -> {
Text(
text = "Fehler: ${state.message}",
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodyLarge
)
}
}
}
Spacer(modifier = Modifier.height(24.dp))
Button(
onClick = { viewModel.pingBackend() },
enabled = viewModel.uiState !is PingUiState.Loading
) {
Text("Ping Backend")
}
}
}

View File

@ -1,54 +0,0 @@
package at.mocode.client.ui.viewmodel
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import at.mocode.client.data.service.PingService
import at.mocode.client.data.service.PingResponse
import kotlinx.coroutines.*
/**
* Represents the four distinct UI states as defined in the trace-bullet-guideline.md
*/
sealed class PingUiState {
/** Initial state: neutral message, button active */
data object Initial : PingUiState()
/** Loading state: loading message, button disabled */
data object Loading : PingUiState()
/** Success state: positive response, button active */
data class Success(val response: PingResponse) : PingUiState()
/** Error state: clear error message, button active */
data class Error(val message: String) : PingUiState()
}
class PingViewModel(
private val pingService: PingService,
private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
) {
var uiState by mutableStateOf<PingUiState>(PingUiState.Initial)
private set
fun pingBackend() {
uiState = PingUiState.Loading
coroutineScope.launch {
pingService.ping()
.onSuccess { response ->
uiState = PingUiState.Success(response)
}
.onFailure { error ->
uiState = PingUiState.Error(
error.message ?: "Unbekannter Fehler beim Verbinden mit dem Backend"
)
}
}
}
fun dispose() {
coroutineScope.cancel()
pingService.close()
}
}

View File

@ -1,108 +0,0 @@
package at.mocode.client.data.service
import kotlinx.serialization.json.Json
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class PingResponseTest {
@Test
fun `should create PingResponse with status`() {
// Given
val status = "pong"
// When
val response = PingResponse(status = status)
// Then
assertEquals(status, response.status)
assertNotNull(response)
}
@Test
fun `should serialize to JSON correctly`() {
// Given
val response = PingResponse(status = "pong")
// When
val json = Json.encodeToString(response)
// Then
assertTrue(json.contains("\"status\":\"pong\""))
assertTrue(json.startsWith("{"))
assertTrue(json.endsWith("}"))
}
@Test
fun `should deserialize from JSON correctly`() {
// Given
val json = """{"status":"pong"}"""
// When
val response = Json.decodeFromString<PingResponse>(json)
// Then
assertEquals("pong", response.status)
}
@Test
fun `should handle different status values`() {
// Given & When & Then
val responses = listOf("pong", "ok", "alive", "healthy")
responses.forEach { status ->
val response = PingResponse(status = status)
assertEquals(status, response.status)
// Test serialization roundtrip
val json = Json.encodeToString(response)
val deserialized = Json.decodeFromString<PingResponse>(json)
assertEquals(status, deserialized.status)
}
}
@Test
fun `should handle empty status`() {
// Given
val emptyStatus = ""
// When
val response = PingResponse(status = emptyStatus)
// Then
assertEquals("", response.status)
// Test serialization works with empty string
val json = Json.encodeToString(response)
val deserialized = Json.decodeFromString<PingResponse>(json)
assertEquals("", deserialized.status)
}
@Test
fun `should be data class with proper equals and hashCode`() {
// Given
val response1 = PingResponse("pong")
val response2 = PingResponse("pong")
val response3 = PingResponse("different")
// Then
assertEquals(response1, response2)
assertEquals(response1.hashCode(), response2.hashCode())
assertTrue(response1 != response3)
}
@Test
fun `should have proper toString representation`() {
// Given
val response = PingResponse("pong")
// When
val toString = response.toString()
// Then
assertTrue(toString.contains("PingResponse"))
assertTrue(toString.contains("pong"))
}
}

View File

@ -1,155 +0,0 @@
package at.mocode.client.data.service
import io.ktor.client.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import kotlin.test.*
class PingServiceTest {
@Test
fun `should create service with default parameters`() {
// When
val service = PingService()
// Then
assertNotNull(service)
}
@Test
fun `should create service with custom baseUrl`() {
// Given
val customUrl = "https://custom-api.example.com"
// When
val service = PingService(baseUrl = customUrl)
// Then
assertNotNull(service)
// Note: baseUrl is private, so we test indirectly through behavior
}
@Test
fun `should create default HttpClient with ContentNegotiation`() {
// When
val client = PingService.createDefaultHttpClient()
// Then
assertNotNull(client)
// Verify the client is properly configured by checking it's not null and can be closed
client.close()
}
@Test
fun `should create service with custom HttpClient`() {
// Given
val customClient = HttpClient {
install(ContentNegotiation) {
json()
}
}
// When
val service = PingService("http://localhost:8080", customClient)
// Then
assertNotNull(service)
// Cleanup
service.close()
}
@Test
fun `should close httpClient when service is closed`() {
// Given
val service = PingService()
// When & Then
// Verify that close() doesn't throw exceptions
assertDoesNotThrow { service.close() }
}
@Test
fun `should handle multiple close calls gracefully`() {
// Given
val service = PingService()
// When & Then
// Multiple close calls should not throw exceptions
assertDoesNotThrow {
service.close()
service.close()
service.close()
}
}
@Test
fun `should create companion object HttpClient`() {
// When
val client1 = PingService.createDefaultHttpClient()
val client2 = PingService.createDefaultHttpClient()
// Then
assertNotNull(client1)
assertNotNull(client2)
// Each call should create a new instance
assertNotSame(client1, client2)
// Cleanup
client1.close()
client2.close()
}
@Test
fun `should handle service creation with different baseUrl formats`() {
// Given & When & Then
val urls = listOf(
"http://localhost:8080",
"https://api.example.com",
"http://192.168.1.100:3000",
"https://secure.api.com:9443"
)
urls.forEach { url ->
val service = PingService(baseUrl = url)
assertNotNull(service, "Service should be created with URL: $url")
service.close()
}
}
@Test
fun `should handle Result wrapper for ping operations`() {
// Given
val service = PingService()
// Note: We can't easily test the actual ping() method without a mock server
// But we can verify the service structure is correct for Result handling
assertNotNull(service)
// The ping() method returns Result<PingResponse> - this is tested indirectly
// through the service structure validation
service.close()
}
@Test
fun `should properly encapsulate HttpClient lifecycle`() {
// Given
val client: HttpClient? = null
// When
val service = PingService()
// We can't access the private httpClient directly, but we can test lifecycle
assertNotNull(service)
// Then - Service should handle cleanup properly
assertDoesNotThrow { service.close() }
}
private fun assertDoesNotThrow(block: () -> Unit) {
try {
block()
} catch (e: Exception) {
fail("Expected no exception, but got: ${e.message}")
}
}
}

View File

@ -1,148 +0,0 @@
package at.mocode.client.ui.viewmodel
import at.mocode.client.data.service.PingResponse
import at.mocode.client.data.service.PingService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlin.test.*
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
class PingViewModelTest {
@Test
fun `should create PingUiState sealed class instances`() {
// When & Then
val initial = PingUiState.Initial
val loading = PingUiState.Loading
val success = PingUiState.Success(PingResponse("pong"))
val error = PingUiState.Error("Test error")
assertNotNull(initial)
assertNotNull(loading)
assertNotNull(success)
assertNotNull(error)
}
@Test
fun `should have correct PingUiState Success data`() {
// Given
val response = PingResponse("pong")
// When
val successState = PingUiState.Success(response)
// Then
assertEquals("pong", successState.response.status)
}
@Test
fun `should have correct PingUiState Error message`() {
// Given
val errorMessage = "Network connection failed"
// When
val errorState = PingUiState.Error(errorMessage)
// Then
assertEquals(errorMessage, errorState.message)
}
@Test
fun `should create ViewModel with initial state`() {
// Given
val pingService = PingService("http://test-server")
val testScope = CoroutineScope(Dispatchers.Default)
// When
val viewModel = PingViewModel(pingService, testScope)
// Then
assertTrue(viewModel.uiState is PingUiState.Initial)
// Cleanup
testScope.cancel()
pingService.close()
}
@Test
fun `should transition to Loading state when pingBackend is called`() {
// Given
val pingService = PingService("http://unreachable-server")
val testScope = CoroutineScope(Dispatchers.Default)
val viewModel = PingViewModel(pingService, testScope)
// When
viewModel.pingBackend()
// Then - Should immediately transition to Loading
assertTrue(viewModel.uiState is PingUiState.Loading)
// Cleanup
testScope.cancel()
pingService.close()
}
@Test
fun `should dispose without throwing exceptions`() {
// Given
val pingService = PingService("http://test")
val testScope = CoroutineScope(Dispatchers.Default)
val viewModel = PingViewModel(pingService, testScope)
// When & Then - Should complete without exceptions
assertDoesNotThrow { viewModel.dispose() }
}
@Test
fun `should preserve uiState immutability`() {
// Given
val pingService = PingService("http://test")
val testScope = CoroutineScope(Dispatchers.Default)
val viewModel = PingViewModel(pingService, testScope)
// When
val initialState = viewModel.uiState
// Then - uiState should be immutable (no setter accessible from outside)
assertTrue(initialState is PingUiState.Initial)
// The uiState property should be read-only from external access
// This is enforced by the private setter in the ViewModel
// Cleanup
testScope.cancel()
pingService.close()
}
@Test
fun `should handle different service configurations`() {
// Given - Different service configurations
val service1 = PingService("http://server1")
val service2 = PingService("https://server2:8443")
val testScope1 = CoroutineScope(Dispatchers.Default)
val testScope2 = CoroutineScope(Dispatchers.Default)
// When
val viewModel1 = PingViewModel(service1, testScope1)
val viewModel2 = PingViewModel(service2, testScope2)
// Then
assertTrue(viewModel1.uiState is PingUiState.Initial)
assertTrue(viewModel2.uiState is PingUiState.Initial)
// Cleanup
testScope1.cancel()
testScope2.cancel()
service1.close()
service2.close()
}
private fun assertDoesNotThrow(block: () -> Unit) {
try {
block()
} catch (e: Exception) {
fail("Expected no exception, but got: ${e.message}")
}
}
}

View File

@ -1,459 +0,0 @@
# Client Desktop-App Modul
## Überblick
Das **desktop-app** Modul stellt eine native Desktop-Anwendung für das Meldestelle-System bereit, die Kotlin Multiplatform und Compose for Desktop verwendet. Dieses Modul dient als plattformübergreifender Desktop-Client, der nahtlos mit dem geteilten common-ui Modul integriert ist, um eine konsistente Benutzererfahrung zu liefern.
**Hauptfunktionen:**
- 🖥️ **Native Desktop-App** - Plattformübergreifende Unterstützung für Windows, macOS und Linux
- 🏗️ **Moderne Architektur** - Integriert mit MVVM common-ui Modul
- 🚀 **Optimierter Build** - Modernisierte Gradle-Konfiguration mit nativer Distribution
- 🧪 **Testabdeckung** - Umfassende Testsuite für Desktop-spezifische Funktionalität
- 📦 **Einfache Distribution** - Eigenständiges Packaging für alle Plattformen
---
## Architektur
### Modulstruktur
```
client/desktop-app/
├── build.gradle.kts # Modernisierte Build-Konfiguration
├── src/
│ ├── jvmMain/kotlin/at/mocode/client/desktop/
│ │ └── Main.kt # Desktop-Anwendung Einstiegspunkt
│ └── jvmTest/kotlin/at/mocode/client/desktop/
│ └── MainTest.kt # Desktop-spezifische Tests
└── README-CLIENT-DESKTOP-APP.md # Diese Dokumentation
```
### Integration mit Common-UI
Die Desktop-App nutzt die geteilte MVVM-Architektur von common-ui:
```kotlin
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "Meldestelle Desktop App",
state = WindowState(
position = WindowPosition(Alignment.Center),
width = 800.dp,
height = 600.dp
)
) {
// Verwendet geteilte App-Komponente mit MVVM-Architektur
App(baseUrl = System.getProperty("meldestelle.api.url", "http://localhost:8080"))
}
}
```
---
## Build-Konfiguration
### Moderne Gradle-Einrichtung
Die desktop-app verwendet eine modernisierte Build-Konfiguration nach Projektstandards:
#### Plugin-Konfiguration
```kotlin
plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.compose.multiplatform)
alias(libs.plugins.compose.compiler)
}
```
#### Abhängigkeiten-Organisation
```kotlin
val jvmMain by getting {
dependencies {
// Projekt-Abhängigkeiten
implementation(project(":client:common-ui"))
// Compose Desktop
implementation(compose.desktop.currentOs)
implementation(compose.material3)
implementation(compose.ui)
implementation(compose.uiTooling)
implementation(compose.runtime)
implementation(compose.foundation)
// Serialisierungsunterstützung
implementation(libs.kotlinx.serialization.json)
// HTTP Client & Coroutines
implementation(libs.ktor.client.cio)
implementation(libs.ktor.client.contentNegotiation)
implementation(libs.ktor.client.serialization.kotlinx.json)
implementation(libs.kotlinx.coroutines.swing)
// Logging
implementation(libs.kotlin.logging.jvm)
}
}
```
#### Test-Konfiguration
```kotlin
val jvmTest by getting {
dependencies {
implementation(libs.bundles.testing.jvm)
}
}
```
#### Native Distribution
```kotlin
nativeDistributions {
targetFormats(TargetFormat.Deb, TargetFormat.Dmg, TargetFormat.Msi)
packageName = "Meldestelle"
packageVersion = "1.0.0"
}
```
---
## Entwicklung
### Voraussetzungen
| Tool | Version | Zweck |
|------|---------|-------|
| JDK | 21 (Temurin) | Desktop-Laufzeit und Gradle-Build |
| Gradle | 8.x (wrapper) | Build-Automatisierung |
### Die Anwendung erstellen
```bash
# Die Desktop-Anwendung kompilieren
./gradlew :client:desktop-app:compileKotlinJvm
# Die Anwendung im Entwicklungsmodus ausführen
./gradlew :client:desktop-app:run
# Vollständige Anwendung erstellen
./gradlew :client:desktop-app:build
```
### Tests ausführen
```bash
# Alle Desktop-Tests ausführen
./gradlew :client:desktop-app:jvmTest
# Spezifischen Test ausführen
./gradlew :client:desktop-app:jvmTest --tests "MainTest"
# Ausführliche Test-Ausgabe
./gradlew :client:desktop-app:jvmTest --info
```
### Packaging für Distribution
```bash
# Verteilbare Pakete für alle Plattformen erstellen
./gradlew :client:desktop-app:createDistributable
# Paket für spezifische Plattform
./gradlew :client:desktop-app:packageDeb # Linux .deb
./gradlew :client:desktop-app:packageDmg # macOS .dmg
./gradlew :client:desktop-app:packageMsi # Windows .msi
```
---
## Konfiguration
### Systemeigenschaften
Die Desktop-Anwendung unterstützt Konfiguration über JVM-Systemeigenschaften:
| Eigenschaft | Standard | Beschreibung |
|----------|---------|-------------|
| `meldestelle.api.url` | `http://localhost:8080` | Backend-API Basis-URL |
#### Verwendungsbeispiele
```bash
# Mit benutzerdefinierter API-URL ausführen
./gradlew :client:desktop-app:run -Dmeldestelle.api.url=https://api.example.com
# Mit Entwicklungseinstellungen ausführen
./gradlew :client:desktop-app:run -Dmeldestelle.api.url=http://localhost:8080
```
### Fenster-Konfiguration
Standard-Fenstereinstellungen können in `Main.kt` angepasst werden:
```kotlin
WindowState(
position = WindowPosition(Alignment.Center),
width = 800.dp, // Anpassbar
height = 600.dp // Anpassbar
)
```
---
## Tests
### Testabdeckung
| Komponente | Test-Datei | Tests | Abdeckung |
|-----------|-----------|-------|----------|
| Hauptanwendung | MainTest.kt | 3 | Bootstrap, Konfiguration, Struktur |
### Test-Implementierung
```kotlin
class MainTest {
@Test
fun `should have valid main class configuration`()
@Test
fun `should have proper package structure`()
@Test
fun `should be able to instantiate system property for base URL`()
}
```
### Test-Suites ausführen
```bash
# Alle Tests
./gradlew :client:desktop-app:jvmTest
# Mit Abdeckungsbericht
./gradlew :client:desktop-app:jvmTest jacocoTestReport
```
---
## Build-Optimierungshistorie
### 14. August 2025 - Build-Modernisierung
**Plugin-Konfiguration-Verbesserungen:**
- Migration zu `alias()` für type-safe Plugin-Referenzen
- Serialisierung und Compose Compiler-Unterstützung hinzugefügt
- TargetFormat-Imports für native Distribution behoben
**Abhängigkeiten-Verbesserungen:**
- Strukturierte Logging-Unterstützung hinzugefügt
- Erweiterte HTTP-Client-Fähigkeiten
- Verbesserte Compose-Komponentenorganisation
- Umfassende Test-Infrastruktur hinzugefügt
**Native Distribution:**
- TargetFormat-Konfiguration behoben
- Plattformübergreifendes Packaging aktiviert (Deb, Dmg, Msi)
- Package-Metadaten optimiert
### 16. August 2025 - Tests & Integration
**Test-Infrastruktur:**
- Umfassende MainTest.kt hinzugefügt
- Mit common-ui MVVM-Architektur integriert
- Anwendungs-Bootstrap und Konfiguration validiert
- Systemeigenschaften-Tests hinzugefügt
**Architektur-Validierung:**
- Nahtlose Integration mit aktualisiertem common-ui bestätigt
- MVVM-Muster-Konformität verifiziert
- Ressourcenverwaltungs-Integration validiert
---
## Leistung & Qualität
### Build-Leistung
- ✅ Schnelle inkrementelle Builds mit moderner Gradle-Konfiguration
- ✅ Effiziente Plugin-Auflösung durch Versions-Katalog
- ✅ Optimierte Abhängigkeitsverwaltung
### Laufzeit-Leistung
- ✅ Native Desktop-Leistung mit JVM-Optimierung
- ✅ Effiziente Ressourcenverwaltung durch common-ui Integration
- ✅ Minimaler Speicher-Footprint mit ordnungsgemäßer Bereinigung
### Code-Qualität
- ✅ 100% Architektur-Konformität mit MVVM-Muster
- ✅ Umfassende Testabdeckung für Desktop-spezifische Funktionalität
- ✅ Konsistente Code-Organisation und Dokumentation
---
## Integrations-Vorteile
### Vom Common-UI Modul
Die Desktop-App profitiert automatisch von allen common-ui Optimierungen:
- **MVVM-Architektur**: Ordnungsgemäße Trennung der Belange durch PingViewModel
- **Ressourcenverwaltung**: Automatische Bereinigung über DisposableEffect in geteilten Komponenten
- **UI-Zustandsverwaltung**: Vier distinkte Zustände gemäß Trace-Bullet-Richtlinien
- **Speicherleck-Prävention**: Eliminierte Callback-Muster zugunsten von Compose-State
### Desktop-spezifische Vorteile
- **Native Leistung**: Direkte JVM-Ausführung ohne Browser-Overhead
- **System-Integration**: Native Dateidialoge, Benachrichtigungen, System-Tray-Unterstützungspotential
- **Offline-Fähigkeit**: Vollständige Funktionalität ohne Netzwerkabhängigkeiten
- **Plattformübergreifend**: Einzige Codebasis läuft auf Windows, macOS und Linux
---
## Deployment
### Entwicklungs-Deployment
```bash
# Schneller Entwicklungslauf
./gradlew :client:desktop-app:run
# Mit benutzerdefinierter Konfiguration ausführen
./gradlew :client:desktop-app:run -Dmeldestelle.api.url=https://staging-api.com
```
### Produktions-Deployment
```bash
# Produktions-Build erstellen
./gradlew :client:desktop-app:build
# Für Distribution packen
./gradlew :client:desktop-app:createDistributable
# Das Distributionspaket wird erstellt in:
# build/compose/binaries/main/app/
```
### Distributions-Formate
| Plattform | Format | Befehl | Ausgabe |
|----------|--------|---------|--------|
| Linux | .deb | `packageDeb` | Debian Package-Installer |
| macOS | .dmg | `packageDmg` | macOS Disk-Image |
| Windows | .msi | `packageMsi` | Windows Installer |
---
## Fehlerbehebung
### Häufige Probleme
| Problem | Symptome | Lösung |
|-------|----------|----------|
| SLF4J-Warnungen | Logging-Warnungen beim Start | Logback-Abhängigkeit hinzufügen (nicht kritisch) |
| Hauptklasse nicht gefunden | Build/Run-Fehler | Main.kt Package-Struktur überprüfen |
| Fenster wird nicht angezeigt | Anwendung startet, aber kein Fenster | Display-Einstellungen und Fensterzustand überprüfen |
| API-Verbindung fehlgeschlagen | Netzwerkfehler | `meldestelle.api.url` Systemeigenschaft überprüfen |
### Debug-Befehle
```bash
# Hauptklassen-Konfiguration überprüfen
./gradlew :client:desktop-app:printMainClassName
# Abhängigkeiten analysieren
./gradlew :client:desktop-app:dependencies
# Ausführliche Build-Ausgabe
./gradlew :client:desktop-app:build --info --stacktrace
```
### Leistungsüberwachung
```bash
# Mit JVM-Profiling ausführen
./gradlew :client:desktop-app:run -Dcom.sun.management.jmxremote
# Speicher-Analyse
./gradlew :client:desktop-app:run -XX:+PrintGCDetails
```
---
## Zukünftige Verbesserungen
### Empfohlene Entwicklung
1. **Desktop-spezifische Features**
- System-Tray-Integration
- Native Benachrichtigungen
- Dateisystem-Dialoge
- Desktop-Verknüpfungen
2. **Erweiterte Protokollierung**
- Logback-Konfiguration hinzufügen
- Strukturierte Protokollierung mit JSON-Ausgabe
- Log-Rotation und Archivierung
3. **Konfigurationsverwaltung**
- Konfigurationsdatei-Unterstützung
- Benutzereinstellungen-Persistierung
- Umgebungsspezifische Konfigurationen
4. **Erweiterte Tests**
- UI-Tests mit Compose-Test-Utilities
- Integrationstests mit Mock-Backend
- Leistungs-Benchmarking
5. **Distributions-Optimierung**
- JVM-Optimierung-Flags
- Anwendungspaket-Größenreduzierung
- Auto-Update-Mechanismen
---
## Mitwirken
### Entwicklungsablauf
1. **Einrichtung**
```bash
# JDK 21 Installation überprüfen
java -version
# Erstellen und testen
./gradlew :client:desktop-app:build
```
2. **Testen**
```bash
# Tests vor Änderungen ausführen
./gradlew :client:desktop-app:jvmTest
# Integration mit common-ui testen
./gradlew :client:common-ui:jvmTest :client:desktop-app:jvmTest
```
3. **Code-Standards**
- Kotlin-Codierungskonventionen befolgen
- Tests für neue Desktop-spezifische Funktionalität hinzufügen
- Integration mit common-ui MVVM-Architektur beibehalten
- Konfigurationsänderungen dokumentieren
### Pull Request-Anforderungen
- [ ] Alle bestehenden Tests bestehen
- [ ] Neue Funktionalität beinhaltet Tests
- [ ] Integration mit common-ui verifiziert
- [ ] Dokumentation aktualisiert
- [ ] Build-Konfigurationsänderungen dokumentiert
---
**Modul-Status**: ✅ Produktionsbereit
**Architektur**: ✅ MVVM-integriert
**Build-System**: ✅ Modernisiert
**Testabdeckung**: ✅ Desktop-spezifische Funktionalität
**Distribution**: ✅ Plattformübergreifend bereit
*Zuletzt aktualisiert: 16. August 2025*

View File

@ -1,89 +0,0 @@
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.compose.multiplatform)
alias(libs.plugins.compose.compiler)
}
kotlin {
jvm {
compilations.all {
// compileTaskProvider.configure{
// compilerOptions {
// freeCompilerArgs.add("-Xjvm-default=all")
// freeCompilerArgs.add("-Xcontext-receivers")
// freeCompilerArgs.add("-Xno-param-assertions")
// freeCompilerArgs.add("-Xno-call-assertions")
// freeCompilerArgs.add("-Xno-receiver-assertions")
// freeCompilerArgs.add("-Xno-optimize")
// freeCompilerArgs.add("-Xno-param-assertions")
// freeCompilerArgs.add("-Xno-receiver-assertions")
// freeCompilerArgs.add("-Xno-optimize")
// freeCompilerArgs.add("-Xno-check-impl")
// freeCompilerArgs.add("-Xno-optimize")
// }
compilerOptions.configure {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21)
}
}
}
sourceSets {
val jvmMain by getting {
dependencies {
// Project dependencies
implementation(project(":client:common-ui"))
// Compose Desktop
implementation(compose.desktop.currentOs)
implementation(compose.material3)
implementation(compose.ui)
implementation(compose.uiTooling)
implementation(compose.runtime)
implementation(compose.foundation)
// Serialization support
implementation(libs.kotlinx.serialization.json)
// HTTP Client & Coroutines
implementation(libs.ktor.client.cio)
implementation(libs.ktor.client.contentNegotiation)
implementation(libs.ktor.client.serialization.kotlinx.json)
implementation(libs.kotlinx.coroutines.swing)
// Logging
implementation(libs.kotlin.logging.jvm)
}
}
val jvmTest by getting {
dependencies {
implementation(libs.bundles.testing.jvm)
}
}
}
}
compose.desktop {
application {
mainClass = "at.mocode.client.desktop.MainKt"
nativeDistributions {
targetFormats(TargetFormat.Deb, TargetFormat.Dmg, TargetFormat.Msi)
packageName = "Meldestelle Desktop"
packageVersion = "1.0.0"
windows {
iconFile.set(project.file("src/jvmMain/resources/icon.ico"))
}
linux {
iconFile.set(project.file("src/jvmMain/resources/icon.png"))
}
macOS {
iconFile.set(project.file("src/jvmMain/resources/icon.icns"))
}
}
}
}

View File

@ -1,25 +0,0 @@
package at.mocode.client.desktop
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.WindowState
import androidx.compose.ui.window.application
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.dp
import at.mocode.client.ui.App
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "Meldestelle Desktop App",
state = WindowState(
position = WindowPosition(Alignment.Center),
width = 800.dp,
height = 600.dp
)
) {
// Use the shared App component from common-ui
// This eliminates code duplication and ensures consistent UI across platforms
App(baseUrl = System.getProperty("meldestelle.api.url", "http://localhost:8081"))
}
}

View File

@ -1,39 +0,0 @@
package at.mocode.client.desktop
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.*
class MainTest {
@Test
fun `should have valid main class configuration`() = runTest {
// Verify that the main class exists and is properly structured
val mainClass = this::class.java.classLoader.loadClass("at.mocode.client.desktop.MainKt")
assertNotNull(mainClass, "Main class should be loadable")
// Verify that the main method exists
val mainMethod = mainClass.getMethod("main")
assertNotNull(mainMethod, "Main method should exist")
}
@Test
fun `should have proper package structure`() {
// Verify the package exists and is accessible
val packageName = "at.mocode.client.desktop"
assertTrue(packageName.isNotBlank(), "Package name should not be blank")
// Verify we can access classes in this package
val currentClass = this::class.java
assertTrue(currentClass.packageName.startsWith("at.mocode.client"),
"Test should be in the correct package hierarchy")
}
@Test
fun `should be able to instantiate system property for base URL`() {
// Test the default configuration used in Main.kt
val defaultUrl = System.getProperty("meldestelle.api.url", "http://localhost:8080")
assertNotNull(defaultUrl, "Default API URL should not be null")
assertTrue(defaultUrl.startsWith("http"), "API URL should be a valid HTTP URL")
}
}

View File

@ -0,0 +1,44 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="450dp"
android:height="450dp"
android:viewportWidth="64"
android:viewportHeight="64">
<path
android:pathData="M56.25,18V46L32,60 7.75,46V18L32,4Z"
android:fillColor="#6075f2"/>
<path
android:pathData="m41.5,26.5v11L32,43V60L56.25,46V18Z"
android:fillColor="#6b57ff"/>
<path
android:pathData="m32,43 l-9.5,-5.5v-11L7.75,18V46L32,60Z">
<aapt:attr name="android:fillColor">
<gradient
android:centerX="23.131"
android:centerY="18.441"
android:gradientRadius="42.132"
android:type="radial">
<item android:offset="0" android:color="#FF5383EC"/>
<item android:offset="0.867" android:color="#FF7F52FF"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M22.5,26.5 L32,21 41.5,26.5 56.25,18 32,4 7.75,18Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="44.172"
android:startY="4.377"
android:endX="17.973"
android:endY="34.035"
android:type="linear">
<item android:offset="0" android:color="#FF33C3FF"/>
<item android:offset="0.878" android:color="#FF5383EC"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="m32,21 l9.526,5.5v11L32,43 22.474,37.5v-11z"
android:fillColor="#000000"/>
</vector>

View File

@ -0,0 +1,69 @@
package at.mocode
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.ui.tooling.preview.Preview
@Composable
@Preview
fun App() {
MaterialTheme {
var showContent by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.safeContentPadding()
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = "Meldestelle",
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(top = 32.dp, bottom = 16.dp)
)
Button(
onClick = { showContent = !showContent },
modifier = Modifier.padding(16.dp)
) {
Text(if (showContent) "Platform-Info ausblenden" else "Platform-Info anzeigen")
}
AnimatedVisibility(showContent) {
val greeting = remember { Greeting().greet() }
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = greeting,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(8.dp)
)
Text(
text = "Willkommen in der Meldestelle-Anwendung!",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 8.dp)
)
}
}
}
}
}

View File

@ -0,0 +1,9 @@
package at.mocode
class Greeting {
private val platform = getPlatform()
fun greet(): String {
return "Hello, ${platform.name}!"
}
}

View File

@ -0,0 +1,7 @@
package at.mocode
interface Platform {
val name: String
}
expect fun getPlatform(): Platform

View File

@ -0,0 +1,12 @@
package at.mocode
import kotlin.test.Test
import kotlin.test.assertEquals
class ComposeAppCommonTest {
@Test
fun example() {
assertEquals(3, 1 + 2)
}
}

View File

@ -0,0 +1,7 @@
package at.mocode
class JVMPlatform: Platform {
override val name: String = "Java ${System.getProperty("java.version")}"
}
actual fun getPlatform(): Platform = JVMPlatform()

View File

@ -0,0 +1,13 @@
package at.mocode
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "Meldestelle",
) {
App()
}
}

View File

@ -0,0 +1,7 @@
package at.mocode
class WasmPlatform: Platform {
override val name: String = "Web with Kotlin/Wasm"
}
actual fun getPlatform(): Platform = WasmPlatform()

View File

@ -0,0 +1,12 @@
package at.mocode
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.ComposeViewport
import kotlinx.browser.document
@OptIn(ExperimentalComposeUiApi::class)
fun main() {
ComposeViewport(document.body!!) {
App()
}
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Meldestelle</title>
<link type="text/css" rel="stylesheet" href="styles.css">
<script type="application/javascript" src="composeApp.js"></script>
</head>
<body>
</body>
</html>

View File

@ -0,0 +1,7 @@
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}

View File

@ -1,303 +0,0 @@
# Meldestelle Web-App
Eine moderne Web-Anwendung basierend auf **Compose for Web** (Compose Multiplatform) für das Meldestelle-Projekt.
## 📋 Überblick
Diese Web-Anwendung implementiert das Frontend für das Meldestelle-System unter Verwendung von Compose for Web. Sie folgt dem Architekturprinzip der maximalen Code-Wiederverwendung durch die Nutzung des `commonMain`-Source-Sets von Kotlin Multiplatform.
### Technologie-Stack
- **Frontend Framework**: Compose for Web (Compose Multiplatform 1.8.2)
- **Programmiersprache**: Kotlin/JS
- **Build-System**: Gradle 8.10
- **HTTP-Client**: Ktor Client
- **UI-Komponenten**: Compose Material 3 (aus commonMain)
- **Bundler**: Webpack (über Kotlin/JS Plugin)
- **Container**: Nginx (Production)
## 🏗️ Architektur
```
client/web-app/
├── src/
│ └── jsMain/
│ ├── kotlin/at/mocode/client/web/
│ │ └── main.kt # Entry Point
│ └── resources/
│ └── index.html # HTML Template
├── build.gradle.kts # Build Konfiguration
└── build/ # Build Artefakte
└── dist/js/productionExecutable/ # Produktionsversion
```
### Design Prinzipien
1. **Code-Wiederverwendung**: Maximale Nutzung des `client:common-ui` Moduls
2. **Compose for Web**: Deklarative UI mit `@Composable` Funktionen
3. **State Management**: Zustandsverwaltung über ViewModels im `commonMain`
4. **Plattform-Trennung**: UI-Code in `jsMain`, Logik in `commonMain`
## 🚀 Schnellstart
### Voraussetzungen
- Java 21+
- Docker und Docker Compose
- Node.js 18+ (wird automatisch im Container installiert)
### Entwicklung starten
#### Mit Docker (Empfohlen)
```bash
# Web-App im Entwicklungsmodus starten
docker compose -f docker-compose.yml -f docker-compose.clients.yml up -d web-app
# Anwendung ist verfügbar unter:
# http://localhost:3000
```
#### Lokale Entwicklung
```bash
# Abhängigkeiten installieren und Entwicklungsserver starten
./gradlew :client:web-app:jsBrowserDevelopmentRun
# Anwendung läuft auf http://localhost:8080
```
### Produktionsbuild
```bash
# Optimierte JavaScript-Bundles erstellen
./gradlew :client:web-app:jsBrowserDistribution
# Artefakte befinden sich in:
# client/web-app/build/dist/js/productionExecutable/
```
## 🔧 Entwicklung
### Projekt-Struktur
```kotlin
// main.kt - Entry Point
fun main() {
renderComposable(rootElementId = "root") {
WebApp()
}
}
@Composable
fun WebApp() {
// Verwendet die gemeinsame App-Komponente aus commonMain
App()
}
```
### Hot-Reload
Die Entwicklungsumgebung unterstützt Hot-Reload:
- Änderungen an Kotlin-Code werden automatisch neu kompiliert
- Browser wird automatisch aktualisiert
- Schnelle Entwicklungszyklen durch Webpack Dev Server
### Build-Konfiguration
Die `build.gradle.kts` konfiguriert:
```kotlin
kotlin {
js(IR) {
browser {
commonWebpackConfig {
outputFileName = "web-app.js"
// Webpack optimization directory
configDirectory = project.projectDir.resolve("webpack.config.d")
}
webpackTask {
mainOutputFileName = "web-app.js"
}
}
binaries.executable()
}
}
```
### Webpack-Optimierungen
Das Projekt verwendet erweiterte Webpack-Optimierungen für bessere Performance:
#### Code Splitting
- **Separate Chunks**: Bundle wird in ~60 kleinere, cacheable Dateien aufgeteilt
- **Vendor Chunks**: Große Libraries (Kotlin stdlib, Compose runtime, Coroutines) werden separat geladen
- **Lazy Loading**: Verbessertes Caching durch getrennte Vendor- und App-Code-Chunks
#### Bundle-Größenoptimierung
- **Tree Shaking**: Entfernt ungenutzten Code
- **Minification**: Aggressive Komprimierung im Produktionsbuild
- **Scope Hoisting**: Optimiert JavaScript-Execution
- **Performance Budget**: Warnt bei zu großen Bundles (500KB pro Asset, 800KB Gesamt)
#### Generierte Chunks (Beispiel)
```
web-app-runtime.js 1.67 KiB (Runtime)
web-app.js 482 bytes (Main App)
web-app-compose-runtime-*.js 274 KiB (Compose Framework)
web-app-kotlin-stdlib.js 165 KiB (Kotlin Standard Library)
web-app-coroutines.js 119 KiB (Kotlinx Coroutines)
web-app-vendors-*.js 1.17 MiB (Weitere Dependencies)
```
```
**Abhängigkeiten:**
- `compose.web.core` - Compose for Web Framework
- `compose.runtime` - Compose Runtime
- `project(":client:common-ui")` - Gemeinsame UI-Komponenten
- `kotlinx-coroutines-core-js` - Coroutines für Web
## 🌐 Deployment
### Docker Container
Die Anwendung wird als Docker-Container deployed:
```dockerfile
# Multi-stage Build
FROM gradle:8.10-jdk21 AS builder
# ... Build Phase
FROM nginx:1.25-alpine AS production
# ... Production Phase
```
**Features:**
- Multi-stage Build für optimale Image-Größe
- Nginx als Static File Server
- Health Checks
- Security Headers
- Gzip Kompression
### Konfiguration
Umgebungsvariablen:
- `NODE_ENV`: Entwicklungs-/Produktionsmodus
- `API_BASE_URL`: Backend API URL
- `APP_TITLE`: Anwendungstitel
- `APP_VERSION`: Versionsnummer
### Health Checks
```bash
# Container Health Check
curl --fail http://localhost:3000/health
# Antwort: {"status":"ok","service":"web-app"}
```
## 🔗 Integration
### Backend-Kommunikation
Die Web-App kommuniziert mit dem Backend über:
- **API Gateway**: `http://api-gateway:8081`
- **REST APIs**: Über Ktor Client
- **WebSocket**: Für Realtime-Updates (geplant)
### Gemeinsame Komponenten
Nutzt Komponenten aus `client:common-ui`:
- **ViewModels**: `PingViewModel` für Backend-Tests
- **UI-Komponenten**: `App`, `PingScreen`
- **Services**: `PingService` für HTTP-Aufrufe
- **Models**: Datenklassen und UI-States
### Beispiel Integration
```kotlin
@Composable
fun WebApp() {
// Verwendet die gemeinsame App-Komponente
// Diese enthält Material 3 Komponenten und ViewModels
App(baseUrl = "http://localhost:8081")
}
```
## 📊 Build-Artefakte
Nach dem Build werden folgende Dateien generiert:
```
build/dist/js/productionExecutable/
├── web-app.js # Hauptanwendung (minifiziert)
├── web-app.js.map # Source Maps
├── 731.js # Code Splitting Chunk
├── index.html # HTML Template
├── skiko.wasm # Compose Runtime (WebAssembly)
└── skiko.js # Compose JavaScript Runtime
```
## 🧪 Testing
### Entwicklungstests
```bash
# Backend-Konnektivität testen
# Öffne http://localhost:3000
# Klicke "Ping Backend" Button
```
### Build-Validierung
```bash
# Build ohne Ausführung testen
./gradlew :client:web-app:jsBrowserDevelopmentRun --dry-run
# Produktionsbuild testen
./gradlew :client:web-app:jsBrowserDistribution
```
## 📚 Weiterführende Dokumentation
- [Web-App Guideline](../../.junie/guidelines/web-app-guideline.md) - Architektur-Richtlinien
- [Docker README](../../README-DOCKER.md) - Container-Dokumentation
- [Compose for Web Docs](https://github.com/JetBrains/compose-multiplatform) - Offizielle Dokumentation
## 🔍 Troubleshooting
### Häufige Probleme
**Problem**: `Cannot connect to backend`
```bash
# Lösung: Backend Services starten
docker-compose -f docker-compose.yml -f docker-compose.services.yml up -d
```
**Problem**: `Module build failed`
```bash
# Lösung: Clean Build
./gradlew :client:web-app:clean :client:web-app:jsBrowserDevelopmentRun
```
**Problem**: `Port 3000 already in use`
```bash
# Lösung: Port in docker-compose.clients.yml ändern
ports:
- "3001:3000" # Externer Port ändern
```
### Logs und Debugging
```bash
# Container Logs anzeigen
docker logs meldestelle-web-app
# Build Logs mit Details
./gradlew :client:web-app:jsBrowserDevelopmentRun --info --stacktrace
```
---
**Letzte Aktualisierung**: 2025-09-10
**Version**: 1.0.0

View File

@ -1,62 +0,0 @@
plugins {
kotlin("multiplatform")
id("org.jetbrains.compose")
id("org.jetbrains.kotlin.plugin.compose")
}
group = "at.mocode.client.web"
version = "1.0.0"
kotlin {
js(IR) {
browser {
commonWebpackConfig {
// devServer = devServer?.copy(
// port = 8080,
// static = mutableListOf("src/jsMain/resources")
// )
devServer = devServer?.copy(
port = 8080,
static = mutableListOf(project.projectDir.resolve("src/jsMain/resources").path)
)
// Webpack optimization settings
configDirectory = project.projectDir.resolve("webpack.config.d")
}
webpackTask {
args.add("--devtool=source-map")
}
runTask {
args.add("--devtool=source-map")
}
// Add npm dependencies for webpack plugins
useCommonJs()
}
binaries.executable()
}
sourceSets {
val jsMain by getting {
dependencies {
// Compose for Web
implementation(compose.html.core)
implementation(compose.runtime)
// Common UI module (contains ViewModels and shared components)
implementation(project(":client:common-ui"))
// Coroutines for web
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.7.3")
}
}
val jsTest by getting {
dependencies {
implementation(kotlin("test-js"))
}
}
}
}

View File

@ -1,22 +0,0 @@
package at.mocode.client.web
import androidx.compose.runtime.Composable
import at.mocode.client.ui.App
import org.jetbrains.compose.web.renderComposable
/**
* Entry point for the Compose for Web application.
* Follows the web-app guideline by using the shared App component from commonMain.
*/
fun main() {
renderComposable(rootElementId = "root") {
WebApp()
}
}
@Composable
fun WebApp() {
// Use the shared App component from commonMain
// This follows the guideline principle of maximum code reuse
App()
}

View File

@ -1,24 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Meldestelle</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
}
#root {
width: 100%;
height: 100vh;
}
</style>
</head>
<body>
<div id="root"></div>
<script src="web-app.js"></script>
</body>
</html>

View File

@ -1,37 +0,0 @@
// Compression and module resolution optimizations
// Enhanced module resolution to reduce bundle size
config.resolve = config.resolve || {};
config.resolve.alias = config.resolve.alias || {};
// Resolve optimizations
config.resolve.modules = ['node_modules'];
config.resolve.extensions = ['.js', '.json', '.wasm'];
// Output optimizations
config.output = config.output || {};
config.output.pathinfo = false; // Disable path info in production for smaller bundles
// Module concatenation for better tree shaking
config.optimization = config.optimization || {};
config.optimization.concatenateModules = true;
// Enable scope hoisting for better performance
config.optimization.moduleIds = 'deterministic';
config.optimization.chunkIds = 'deterministic';
// Webpack production mode optimizations
if (config.mode === 'production') {
// Disable development features
config.devtool = false; // Disable source maps in production for smaller size
// Additional optimization flags
config.optimization.flagIncludedChunks = true;
config.optimization.mergeDuplicateChunks = true;
config.optimization.removeAvailableModules = true;
config.optimization.removeEmptyChunks = true;
// Aggressive dead code elimination
config.optimization.innerGraph = true;
config.optimization.mangleExports = true;
}

View File

@ -1,5 +0,0 @@
// Content Security Policy configuration for development
config.devServer = config.devServer || {};
config.devServer.headers = config.devServer.headers || {};
config.devServer.headers['Content-Security-Policy'] =
"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src *; img-src 'self' data:; font-src 'self' data:; frame-src 'self';";

View File

@ -1,14 +0,0 @@
// Content Security Policy configuration for development
// More relaxed CSP suitable for development environment
config.devServer = config.devServer || {};
config.devServer.headers = config.devServer.headers || {};
config.devServer.headers['Content-Security-Policy'] =
"default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:; " +
"script-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:; " +
"style-src 'self' 'unsafe-inline' data: blob:; " +
"img-src 'self' data: blob: http: https:; " +
"font-src 'self' data: blob: http: https:; " +
"connect-src 'self' ws: wss: http: https:; " +
"frame-src 'self' data: blob:; " +
"object-src 'none'; " +
"base-uri 'self';";

View File

@ -1,3 +0,0 @@
// Development server configuration
config.devServer = config.devServer || {};
config.devServer.historyApiFallback = true;

View File

@ -1,104 +0,0 @@
const path = require('path');
// Webpack optimization configuration for production builds
config.optimization = {
// Enable tree shaking and dead code elimination
usedExports: true,
sideEffects: false,
// Code splitting configuration optimized for Kotlin/JS
splitChunks: {
chunks: 'all',
minSize: 30000,
maxSize: 300000,
maxInitialRequests: 8, // Allow more initial requests for better caching
maxAsyncRequests: 15,
cacheGroups: {
// Kotlin standard library - separate chunk
kotlinStdlib: {
test: /kotlin-kotlin-stdlib/,
name: 'kotlin-stdlib',
chunks: 'all',
enforce: true,
priority: 30,
reuseExistingChunk: true,
},
// Coroutines - separate chunk
coroutines: {
test: /kotlinx-coroutines/,
name: 'coroutines',
chunks: 'all',
enforce: true,
priority: 25,
reuseExistingChunk: true,
},
// Compose runtime - separate chunk
composeRuntime: {
test: /compose.*runtime/,
name: 'compose-runtime',
chunks: 'all',
enforce: true,
priority: 20,
reuseExistingChunk: true,
},
// Large vendor libraries
largeVendors: {
test: /ktor|androidx-collection|kotlinx-serialization/,
name: 'large-vendors',
chunks: 'all',
enforce: true,
priority: 15,
reuseExistingChunk: true,
},
// Common vendors
vendors: {
test: /[\\/]kotlin[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10,
reuseExistingChunk: true,
},
// Application code
default: {
minChunks: 2,
priority: -10,
reuseExistingChunk: true,
},
},
},
// Minimize bundle size
minimize: true,
minimizer: [
// Use default TerserPlugin for JS minification
'...',
],
// Runtime chunk optimization
runtimeChunk: {
name: 'runtime',
},
};
// Performance budget adjusted for Kotlin/JS applications
// Note: Kotlin/JS apps require all dependencies loaded initially, so larger budgets are realistic
config.performance = {
maxAssetSize: 1000000, // 1MB per asset (realistic for large Kotlin/Compose libs)
maxEntrypointSize: 7000000, // 7MB total entry point (realistic for Kotlin/JS + Compose)
hints: 'warning',
assetFilter: function(assetFilename) {
// Only check JS files for performance
return assetFilename.endsWith('.js');
},
};
// Production-specific optimizations
if (config.mode === 'production') {
// Additional compression and optimizations
config.optimization.concatenateModules = true;
config.optimization.providedExports = true;
// More aggressive code splitting for production
config.optimization.splitChunks.maxInitialRequests = 10;
config.optimization.splitChunks.maxAsyncRequests = 10;
}

View File

@ -16,8 +16,8 @@ services:
context: .
dockerfile: dockerfiles/clients/web-app/Dockerfile
args:
CLIENT_PATH: client/web-app
CLIENT_MODULE: client:web-app
CLIENT_PATH: client
CLIENT_MODULE: client
CLIENT_NAME: meldestelle-web-app
container_name: meldestelle-web-app
environment:
@ -59,7 +59,7 @@ services:
environment:
SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE:-dev}
SERVER_PORT: ${AUTH_SERVICE_PORT:-8087}
KEYCLOAK_SERVER_URL: http://keycloak:8080
KEYCLOAK_SERVER_URL: http://keycloak:8081
KEYCLOAK_REALM: meldestelle
KEYCLOAK_CLIENT_ID: meldestelle-auth-service
KEYCLOAK_CLIENT_SECRET: ${KEYCLOAK_CLIENT_SECRET:-auth-service-secret}

View File

@ -57,7 +57,7 @@ services:
# Authentifizierung
# ===================================================================
keycloak:
image: quay.io/keycloak/keycloak:23.0
image: quay.io/keycloak/keycloak:25.0.6
container_name: meldestelle-keycloak
environment:
KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN:-admin}

View File

@ -5,23 +5,22 @@
# Arguments (can be overridden during build)
# ===================================================================
ARG JVM_VERSION=21
ARG GRADLE_VERSION=8.10
ARG NODE_VERSION=18
ARG GRADLE_VERSION=9.0
ARG NGINX_VERSION=1.25-alpine
# ===================================================================
# Build Arguments for Client Configuration
# ===================================================================
ARG CLIENT_PATH=client/web-app
ARG CLIENT_MODULE=client:web-app
ARG CLIENT_PATH=client
ARG CLIENT_MODULE=client
# ===================================================================
# Build Stage - Kotlin/JS (Compose for Web) Compilation
# ===================================================================
FROM gradle:${GRADLE_VERSION}-jdk${JVM_VERSION} AS builder
ARG CLIENT_PATH=client/web-app
ARG CLIENT_MODULE=client:web-app
ARG CLIENT_PATH=client
ARG CLIENT_MODULE=client
# Set working directory
WORKDIR /build
@ -53,39 +52,34 @@ COPY client/ client/
COPY temp/ temp/
COPY docs/ docs/
# Install Node.js for JavaScript toolchain
RUN apt-get update && \
apt-get install -y curl && \
curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - && \
apt-get install -y nodejs
# Make Gradle wrapper executable
RUN chmod +x gradlew
# Build client application
# For Compose for Web projects, jsBrowserDistribution produces static assets
# For Compose Multiplatform Web (WASM), wasmJsBrowserDistribution produces static assets
RUN echo "Building ${CLIENT_MODULE} module..." && \
./gradlew ${CLIENT_MODULE}:jsBrowserDistribution --no-daemon --stacktrace --info
./gradlew ${CLIENT_MODULE}:wasmJsBrowserDistribution --no-daemon --stacktrace --info
# ===================================================================
# Production Stage - Nginx Static File Server
# ===================================================================
FROM nginx:${NGINX_VERSION} AS production
ARG CLIENT_PATH=client/web-app
ARG CLIENT_PATH=client
# Set production labels
LABEL service="web-app" \
environment="production" \
description="Meldestelle Compose for Web Application"
# Create nginx user if not exists and set permissions
RUN addgroup -g 1001 -S nginx-group && \
# Install curl for health checks and create nginx user
RUN apk add --no-cache curl && \
addgroup -g 1001 -S nginx-group && \
adduser -S -D -H -u 1001 -h /var/cache/nginx -s /sbin/nologin -G nginx-group -g nginx nginx-user
# Copy built distribution files from builder stage
COPY --from=builder /build/${CLIENT_PATH}/build/dist/js/productionExecutable/ /usr/share/nginx/html/
COPY --from=builder /build/${CLIENT_PATH}/src/jsMain/resources/ /usr/share/nginx/html/
# Copy built distribution files from builder stage (WASM build output)
COPY --from=builder /build/${CLIENT_PATH}/build/dist/wasmJs/productionExecutable/ /usr/share/nginx/html/
COPY --from=builder /build/${CLIENT_PATH}/src/wasmJsMain/resources/ /usr/share/nginx/html/
# Copy custom nginx configuration
COPY dockerfiles/clients/web-app/nginx.conf /etc/nginx/nginx.conf

View File

@ -43,6 +43,8 @@ http {
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self';" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
server {
listen 3000;

View File

@ -21,8 +21,6 @@ ktor = "3.2.3"
# --- Compose UI ---
composeMultiplatform = "1.8.2"
# --- Kobweb ---
kobweb = "0.23.2"
# --- Database & Persistence ---
exposed = "0.61.0"
@ -181,12 +179,6 @@ compose-html-core-js = { module = "org.jetbrains.compose.html:html-core-js", ver
compose-html-svg-js = { module = "org.jetbrains.compose.html:html-svg-js", version.ref = "composeMultiplatform"}
compose-desktop-currentOs = { module = "org.jetbrains.compose.desktop:desktop", version.ref = "composeMultiplatform" }
# --- Kobweb ---
kobweb-core = { module = "com.varabyte.kobweb:kobweb-core", version.ref = "kobweb" }
kobweb-silk-core = { module = "com.varabyte.kobweb:kobweb-silk", version.ref = "kobweb" }
kobweb-silk-icons-fa = { module = "com.varabyte.kobweb:kobweb-silk-icons-fa", version.ref = "kobweb" }
kobweb-api = { module = "com.varabyte.kobweb:kobweb-api", version.ref = "kobweb" }
kobwebx-markdown = { module = "com.varabyte.kobwebx:kobwebx-markdown", version.ref = "kobweb" }
# --- Testing ---
junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junitJupiter" }
@ -295,5 +287,3 @@ compose-multiplatform = { id = "org.jetbrains.compose", version.ref = "composeMu
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
spring-boot = { id = "org.springframework.boot", version.ref = "springBoot" }
spring-dependencyManagement = { id = "io.spring.dependency-management", version.ref = "springDependencyManagement" }
kobweb-application = { id = "com.varabyte.kobweb.application", version.ref = "kobweb" }
kobwebx-markdown = { id = "com.varabyte.kobwebx.markdown", version.ref = "kobweb" }

View File

@ -1,67 +0,0 @@
// Karma configuration for Chrome browser testing
// This file fixes Chrome/Chromium path issues and permission errors
config.set({
// Use Chrome with custom configuration to avoid snap permission issues
browsers: ['ChromeHeadlessNoSandbox'],
// Custom browser configuration
customLaunchers: {
ChromeHeadlessNoSandbox: {
base: 'ChromeHeadless',
flags: [
'--no-sandbox',
'--disable-web-security',
'--disable-features=VizDisplayCompositor',
'--disable-dev-shm-usage',
'--disable-gpu',
'--remote-debugging-port=9222'
]
},
ChromeHeadlessCI: {
base: 'ChromeHeadless',
flags: [
'--no-sandbox',
'--disable-web-security',
'--disable-features=VizDisplayCompositor',
'--disable-dev-shm-usage',
'--disable-gpu',
'--headless',
'--disable-extensions',
'--disable-plugins',
'--disable-images',
'--disable-javascript',
'--disable-default-apps',
'--disable-translate',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
'--disable-device-discovery-notifications'
]
}
},
// Browser detection and fallback
detectBrowsers: {
enabled: false // Disable auto-detection to use our custom config
},
// Timeout configuration to handle slower CI environments
browserNoActivityTimeout: 60000,
browserDisconnectTimeout: 10000,
browserDisconnectTolerance: 3,
// Process configuration
processKillTimeout: 5000,
captureTimeout: 60000
});
// Try to use system Chrome if snap Chromium fails
if (process.env.CI || process.env.GITHUB_ACTIONS) {
// Use CI-optimized Chrome configuration in CI environments
config.browsers = ['ChromeHeadlessCI'];
} else {
// Use standard no-sandbox configuration for local development
config.browsers = ['ChromeHeadlessNoSandbox'];
}
console.log('Chrome browser configuration applied for testing');

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,7 @@ pluginManagement {
}
}
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}
dependencyResolutionManagement {
@ -58,17 +58,13 @@ include(":infrastructure:monitoring:monitoring-server")
include(":temp:ping-service")
// Client modules
include(":client:common-ui")
// kobweb-app is now an independent build with its own Gradle wrapper (8.14.2). See client/kobweb-app/README.md for details.
//include(":client:kobweb-app")
include(":client:web-app")
include(":client:desktop-app")
include(":client")
// Documentation module
include(":docs")
/*
// Temporär deaktivierte Fach-Module
// Business modules (temporarily disabled - require multiplatform configuration updates)
// Members modules
include(":members:members-domain")
include(":members:members-application")
@ -97,5 +93,5 @@ include(":masterdata:masterdata-infrastructure")
include(":masterdata:masterdata-api")
include(":masterdata:masterdata-service")
// Legacy modules have been removed after successful migration
*/
// Note: These modules need multiplatform configuration updates to work with current KMP/WASM setup
*/