umbau zu comose Multiplatform

This commit is contained in:
2025-09-10 20:32:30 +02:00
parent f43ece082c
commit f236ed0de6
45 changed files with 562 additions and 3271 deletions
+1
View File
@@ -17,3 +17,4 @@ captures
!*.xcworkspace/contents.xcworkspacedata !*.xcworkspace/contents.xcworkspacedata
**/xcshareddata/WorkspaceSettings.xcsettings **/xcshareddata/WorkspaceSettings.xcsettings
/.env /.env
/kotlin-js-store/
+128 -44
View File
@@ -1,10 +1,10 @@
# Web-App-Richtlinie (Compose for Web) # Client-App-Richtlinie (Compose Multiplatform)
## 1. Einleitung ## 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 ## 2. Grundprinzipien
@@ -26,49 +26,95 @@ Der UI-Zustand (State) wird explizit verwaltet.
### Styling ### 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. #### Gemeinsame Styling-Prinzipien (commonMain)
- **Scoped Styles**: Organisieren Sie Stile logisch, z. B. pro Komponente oder pro Bildschirm. - **Compose Material Design**: Nutzen Sie Material3-Komponenten und Theming für konsistente UI.
- **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 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 ```kotlin
// Beispiel für ein StyleSheet // Beispiel für gemeinsames Theming in commonMain
object AppStylesheet : StyleSheet() { @Composable
val container by style { fun AppTheme(content: @Composable () -> Unit) {
padding(24.px) MaterialTheme(
border(1.px, LineStyle.Solid, Color.black) colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme(),
} typography = AppTypography,
content = content
)
} }
``` ```
### Navigation ### 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. Die Navigation wird plattformunabhängig in `commonMain` implementiert:
- Ein `StateFlow` oder eine `mutableState`-Variable im ViewModel kann die aktuell anzuzeigende Route/Screen repräsentieren. - **ViewModel-basierte Navigation**: Ein `StateFlow` oder `mutableState` im ViewModel repräsentiert die aktuelle Route/Screen.
- Ein zentraler -Composable kann auf Änderungen dieses Zustands reagieren und den entsprechenden Screen rendern. `Router` - **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 ## 3. Projekt- und Code-Struktur
Die Codebasis ist klar zwischen plattformunabhängiger Logik (`commonMain`) und plattformspezifischer UI (`jsMain`) getrennt. Die Codebasis ist klar zwischen plattformunabhängiger Logik (`commonMain`) und plattformspezifischer Implementation (`jvmMain`, `wasmJsMain`) getrennt.
- **`core/commonMain`** (oder ein äquivalentes `shared`-Modul):
- **Business-Logik**: Die gesamte Geschäftslogik. ### Source Sets
- **ViewModels/Presenters**: Klassen, die den UI-Zustand verwalten und auf Benutzerinteraktionen reagieren.
- **`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. - **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`**: - **`client/src/jvmMain`** (Desktop-Plattform):
- **UI-Code**: Ausschließlich `@Composable`-Funktionen. - **`main.kt`**: Der Einstiegspunkt der Desktop-Anwendung.
- **`main.kt`**: Der Einstiegspunkt der Web-Anwendung. - **Desktop-spezifische Code**: Plattformspezifische Implementierungen und Integrationen.
- **`StyleSheet.kt`**: Die Kotlin-CSS-DSL-Definitionen. - **Desktop Dependencies**: `compose.desktop.currentOs`, Coroutines für Swing.
- **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/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. - **`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 ## 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. ### Desktop-Entwicklung
Um die Web-App im Entwicklungsmodus zu starten (wie in beschrieben): `README-DOCKER.md` 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 ```shell script
# Startet die Web-App mit Hot-Reload # 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. 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` ### Produktions-Builds
## 5. Dos and Don'ts
- **DO**: Die gesamte Logik (State-Management, Datenabruf, Validierung) in `commonMain` implementieren. #### Desktop-Distribution
- **DO**: Kleine, wiederverwendbare und zustandslose Composables erstellen. ```shell script
- **DO**: Styling ausschließlich über die Kotlin-CSS-DSL (`StyleSheet`) realisieren. # Erstellt native Distributionen für alle konfigurierten Plattformen
- **DO**: Events von der UI über Lambda-Funktionen an die ViewModels in `commonMain` weiterleiten. ./gradlew :client:packageDistributionForCurrentOS
- **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. #### Web-Distribution
--- ```shell script
_Letzte Aktualisierung: 2025-09-10_ # Erstellt optimierte WebAssembly-Artefakte für die Produktion
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. ./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.
+81
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
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"
}
}
}
-357
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*
-73
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
}
@@ -1,6 +0,0 @@
package at.mocode.client.data.service
import kotlinx.serialization.Serializable
@Serializable
data class PingResponse(val status: String)
@@ -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()
}
}
}
}
@@ -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")
}
}
}
@@ -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()
}
}
@@ -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"))
}
}
@@ -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}")
}
}
}
@@ -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}")
}
}
}
@@ -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*
-89
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"))
}
}
}
}
@@ -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"))
}
}
@@ -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")
}
}
@@ -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>
@@ -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)
)
}
}
}
}
}
@@ -0,0 +1,9 @@
package at.mocode
class Greeting {
private val platform = getPlatform()
fun greet(): String {
return "Hello, ${platform.name}!"
}
}
@@ -0,0 +1,7 @@
package at.mocode
interface Platform {
val name: String
}
expect fun getPlatform(): Platform
@@ -0,0 +1,12 @@
package at.mocode
import kotlin.test.Test
import kotlin.test.assertEquals
class ComposeAppCommonTest {
@Test
fun example() {
assertEquals(3, 1 + 2)
}
}
@@ -0,0 +1,7 @@
package at.mocode
class JVMPlatform: Platform {
override val name: String = "Java ${System.getProperty("java.version")}"
}
actual fun getPlatform(): Platform = JVMPlatform()
@@ -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()
}
}
@@ -0,0 +1,7 @@
package at.mocode
class WasmPlatform: Platform {
override val name: String = "Web with Kotlin/Wasm"
}
actual fun getPlatform(): Platform = WasmPlatform()
@@ -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()
}
}
@@ -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>
@@ -0,0 +1,7 @@
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
-303
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
-62
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"))
}
}
}
}
@@ -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()
}
@@ -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>
@@ -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;
}
-5
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';";
@@ -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';";
@@ -1,3 +0,0 @@
// Development server configuration
config.devServer = config.devServer || {};
config.devServer.historyApiFallback = true;
@@ -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;
}
+3 -3
View File
@@ -16,8 +16,8 @@ services:
context: . context: .
dockerfile: dockerfiles/clients/web-app/Dockerfile dockerfile: dockerfiles/clients/web-app/Dockerfile
args: args:
CLIENT_PATH: client/web-app CLIENT_PATH: client
CLIENT_MODULE: client:web-app CLIENT_MODULE: client
CLIENT_NAME: meldestelle-web-app CLIENT_NAME: meldestelle-web-app
container_name: meldestelle-web-app container_name: meldestelle-web-app
environment: environment:
@@ -59,7 +59,7 @@ services:
environment: environment:
SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE:-dev} SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE:-dev}
SERVER_PORT: ${AUTH_SERVICE_PORT:-8087} SERVER_PORT: ${AUTH_SERVICE_PORT:-8087}
KEYCLOAK_SERVER_URL: http://keycloak:8080 KEYCLOAK_SERVER_URL: http://keycloak:8081
KEYCLOAK_REALM: meldestelle KEYCLOAK_REALM: meldestelle
KEYCLOAK_CLIENT_ID: meldestelle-auth-service KEYCLOAK_CLIENT_ID: meldestelle-auth-service
KEYCLOAK_CLIENT_SECRET: ${KEYCLOAK_CLIENT_SECRET:-auth-service-secret} KEYCLOAK_CLIENT_SECRET: ${KEYCLOAK_CLIENT_SECRET:-auth-service-secret}
+1 -1
View File
@@ -57,7 +57,7 @@ services:
# Authentifizierung # Authentifizierung
# =================================================================== # ===================================================================
keycloak: keycloak:
image: quay.io/keycloak/keycloak:23.0 image: quay.io/keycloak/keycloak:25.0.6
container_name: meldestelle-keycloak container_name: meldestelle-keycloak
environment: environment:
KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN:-admin} KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN:-admin}
+14 -20
View File
@@ -5,23 +5,22 @@
# Arguments (can be overridden during build) # Arguments (can be overridden during build)
# =================================================================== # ===================================================================
ARG JVM_VERSION=21 ARG JVM_VERSION=21
ARG GRADLE_VERSION=8.10 ARG GRADLE_VERSION=9.0
ARG NODE_VERSION=18
ARG NGINX_VERSION=1.25-alpine ARG NGINX_VERSION=1.25-alpine
# =================================================================== # ===================================================================
# Build Arguments for Client Configuration # Build Arguments for Client Configuration
# =================================================================== # ===================================================================
ARG CLIENT_PATH=client/web-app ARG CLIENT_PATH=client
ARG CLIENT_MODULE=client:web-app ARG CLIENT_MODULE=client
# =================================================================== # ===================================================================
# Build Stage - Kotlin/JS (Compose for Web) Compilation # Build Stage - Kotlin/JS (Compose for Web) Compilation
# =================================================================== # ===================================================================
FROM gradle:${GRADLE_VERSION}-jdk${JVM_VERSION} AS builder FROM gradle:${GRADLE_VERSION}-jdk${JVM_VERSION} AS builder
ARG CLIENT_PATH=client/web-app ARG CLIENT_PATH=client
ARG CLIENT_MODULE=client:web-app ARG CLIENT_MODULE=client
# Set working directory # Set working directory
WORKDIR /build WORKDIR /build
@@ -53,39 +52,34 @@ COPY client/ client/
COPY temp/ temp/ COPY temp/ temp/
COPY docs/ docs/ 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 # Make Gradle wrapper executable
RUN chmod +x gradlew RUN chmod +x gradlew
# Build client application # 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..." && \ 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 # Production Stage - Nginx Static File Server
# =================================================================== # ===================================================================
FROM nginx:${NGINX_VERSION} AS production FROM nginx:${NGINX_VERSION} AS production
ARG CLIENT_PATH=client/web-app ARG CLIENT_PATH=client
# Set production labels # Set production labels
LABEL service="web-app" \ LABEL service="web-app" \
environment="production" \ environment="production" \
description="Meldestelle Compose for Web Application" description="Meldestelle Compose for Web Application"
# Create nginx user if not exists and set permissions # Install curl for health checks and create nginx user
RUN addgroup -g 1001 -S nginx-group && \ 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 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 built distribution files from builder stage (WASM build output)
COPY --from=builder /build/${CLIENT_PATH}/build/dist/js/productionExecutable/ /usr/share/nginx/html/ COPY --from=builder /build/${CLIENT_PATH}/build/dist/wasmJs/productionExecutable/ /usr/share/nginx/html/
COPY --from=builder /build/${CLIENT_PATH}/src/jsMain/resources/ /usr/share/nginx/html/ COPY --from=builder /build/${CLIENT_PATH}/src/wasmJsMain/resources/ /usr/share/nginx/html/
# Copy custom nginx configuration # Copy custom nginx configuration
COPY dockerfiles/clients/web-app/nginx.conf /etc/nginx/nginx.conf COPY dockerfiles/clients/web-app/nginx.conf /etc/nginx/nginx.conf
+2
View File
@@ -43,6 +43,8 @@ http {
add_header X-Content-Type-Options "nosniff" always; add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always; add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" 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 { server {
listen 3000; listen 3000;
-10
View File
@@ -21,8 +21,6 @@ ktor = "3.2.3"
# --- Compose UI --- # --- Compose UI ---
composeMultiplatform = "1.8.2" composeMultiplatform = "1.8.2"
# --- Kobweb ---
kobweb = "0.23.2"
# --- Database & Persistence --- # --- Database & Persistence ---
exposed = "0.61.0" 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-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" } 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 --- # --- Testing ---
junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junitJupiter" } 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" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
spring-boot = { id = "org.springframework.boot", version.ref = "springBoot" } spring-boot = { id = "org.springframework.boot", version.ref = "springBoot" }
spring-dependencyManagement = { id = "io.spring.dependency-management", version.ref = "springDependencyManagement" } 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" }
-67
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');
+75 -901
View File
File diff suppressed because it is too large Load Diff
+5 -9
View File
@@ -17,7 +17,7 @@ pluginManagement {
} }
} }
plugins { plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
} }
dependencyResolutionManagement { dependencyResolutionManagement {
@@ -58,17 +58,13 @@ include(":infrastructure:monitoring:monitoring-server")
include(":temp:ping-service") include(":temp:ping-service")
// Client modules // Client modules
include(":client:common-ui") include(":client")
// 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")
// Documentation module // Documentation module
include(":docs") include(":docs")
/* /*
// Temporär deaktivierte Fach-Module // Business modules (temporarily disabled - require multiplatform configuration updates)
// Members modules // Members modules
include(":members:members-domain") include(":members:members-domain")
include(":members:members-application") include(":members:members-application")
@@ -97,5 +93,5 @@ include(":masterdata:masterdata-infrastructure")
include(":masterdata:masterdata-api") include(":masterdata:masterdata-api")
include(":masterdata:masterdata-service") 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
*/ */