umbau zu comose Multiplatform
This commit is contained in:
parent
f43ece082c
commit
f236ed0de6
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -17,3 +17,4 @@ captures
|
|||
!*.xcworkspace/contents.xcworkspacedata
|
||||
**/xcshareddata/WorkspaceSettings.xcsettings
|
||||
/.env
|
||||
/kotlin-js-store/
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
81
FOLDER_STRUCTURE_ANALYSIS.md
Normal file
81
FOLDER_STRUCTURE_ANALYSIS.md
Normal 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
53
client/build.gradle.kts
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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*
|
||||
|
|
@ -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*
|
||||
|
|
@ -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>
|
||||
69
client/src/commonMain/kotlin/at/mocode/App.kt
Normal file
69
client/src/commonMain/kotlin/at/mocode/App.kt
Normal 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
client/src/commonMain/kotlin/at/mocode/Greeting.kt
Normal file
9
client/src/commonMain/kotlin/at/mocode/Greeting.kt
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package at.mocode
|
||||
|
||||
class Greeting {
|
||||
private val platform = getPlatform()
|
||||
|
||||
fun greet(): String {
|
||||
return "Hello, ${platform.name}!"
|
||||
}
|
||||
}
|
||||
7
client/src/commonMain/kotlin/at/mocode/Platform.kt
Normal file
7
client/src/commonMain/kotlin/at/mocode/Platform.kt
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
7
client/src/jvmMain/kotlin/at/mocode/Platform.jvm.kt
Normal file
7
client/src/jvmMain/kotlin/at/mocode/Platform.jvm.kt
Normal 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()
|
||||
13
client/src/jvmMain/kotlin/at/mocode/main.kt
Normal file
13
client/src/jvmMain/kotlin/at/mocode/main.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package at.mocode
|
||||
|
||||
class WasmPlatform: Platform {
|
||||
override val name: String = "Web with Kotlin/Wasm"
|
||||
}
|
||||
|
||||
actual fun getPlatform(): Platform = WasmPlatform()
|
||||
12
client/src/wasmJsMain/kotlin/at/mocode/main.kt
Normal file
12
client/src/wasmJsMain/kotlin/at/mocode/main.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
12
client/src/wasmJsMain/resources/index.html
Normal file
12
client/src/wasmJsMain/resources/index.html
Normal 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>
|
||||
7
client/src/wasmJsMain/resources/styles.css
Normal file
7
client/src/wasmJsMain/resources/styles.css
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user