diff --git a/config/docker/caddy/web-app/Caddyfile b/config/docker/caddy/web-app/Caddyfile new file mode 100644 index 00000000..94bf9909 --- /dev/null +++ b/config/docker/caddy/web-app/Caddyfile @@ -0,0 +1,41 @@ +:4000 { + # Root directory for static files + root * /usr/share/caddy + + # Enable Gzip/Zstd compression + encode gzip zstd + + # Serve static files + file_server + + # Templates for runtime configuration (config.json) + templates { + mime application/json + } + + # SPA Routing: Fallback to index.html for non-existent files + try_files {path} /index.html + + # Cache Control for static assets (immutable) + @static { + file + path *.js *.css *.png *.jpg *.svg *.wasm + } + header @static Cache-Control "public, max-age=31536000, immutable" + + # Security Headers (Future Proofing for Wasm) + header { + # Cross-Origin Isolation for SharedArrayBuffer (required for some Wasm features) + Cross-Origin-Opener-Policy "same-origin" + Cross-Origin-Embedder-Policy "require-corp" + + # Standard Security Headers + X-Content-Type-Options "nosniff" + X-Frame-Options "DENY" + } + + # Health Check + handle /health { + respond "healthy" 200 + } +} diff --git a/config/docker/caddy/web-app/Dockerfile b/config/docker/caddy/web-app/Dockerfile new file mode 100644 index 00000000..b1928773 --- /dev/null +++ b/config/docker/caddy/web-app/Dockerfile @@ -0,0 +1,94 @@ +# syntax=docker/dockerfile:1.8 +# =================================================================== +# Multi-Stage Dockerfile for Meldestelle Web-App (Kotlin/JS) +# Version: 3.0.0 - Caddy Edition with Runtime Config +# =================================================================== + +# === GLOBAL ARGS === +ARG GRADLE_VERSION=9.2.1 +ARG JAVA_VERSION=25 +ARG CADDY_VERSION=2.11-alpine +ARG VERSION=1.0.0-SNAPSHOT +ARG BUILD_DATE + +# =================================================================== +# Stage 1: Build Stage +# =================================================================== +FROM gradle:${GRADLE_VERSION}-jdk${JAVA_VERSION} AS builder + +LABEL stage=builder +WORKDIR /workspace + +# 1. Gradle Optimizations (Memory & Caching) +# Increased Heap to 4g for Kotlin 2.3 JS Compilation +ENV GRADLE_OPTS="-Dorg.gradle.caching=true \ + -Dorg.gradle.daemon=false \ + -Dorg.gradle.parallel=true \ + -Dorg.gradle.workers.max=4 \ + -Dorg.gradle.jvmargs=-Xmx4g \ + -XX:+UseParallelGC" +ENV GRADLE_USER_HOME=/home/gradle/.gradle + +# 2. Dependency Layering (Optimize Cache Hit Rate) +COPY gradlew gradlew.bat gradle.properties settings.gradle.kts ./ +COPY gradle/ gradle/ +# Copy Version Catalog explicitly first! +COPY gradle/libs.versions.toml gradle/libs.versions.toml + +RUN chmod +x gradlew + +# 3. Copy Sources (Monorepo Structure) +COPY platform/ platform/ +COPY core/ core/ +COPY backend/ backend/ +COPY frontend/ frontend/ +COPY config/ config/ +COPY contracts/ contracts/ + +# Create dummy docs dir +RUN mkdir -p docs + +# 4. Build Web App +# Using --no-configuration-cache initially to avoid issues with first run in docker, +# but can be enabled if stable. +RUN --mount=type=cache,target=/home/gradle/.gradle/caches \ + --mount=type=cache,target=/home/gradle/.gradle/wrapper \ + ./gradlew :frontend:shells:meldestelle-portal:jsBrowserDistribution \ + -Pproduction=true \ + -PnoSourceMaps=true \ + --no-daemon \ + --stacktrace + +# 5. Prepare Dist +RUN mkdir -p /app/dist && \ + cp -r frontend/shells/meldestelle-portal/build/dist/js/productionExecutable/* /app/dist/ + +# =================================================================== +# Stage 2: Runtime Stage (Caddy) +# =================================================================== +FROM caddy:${CADDY_VERSION} + +ARG VERSION +ARG BUILD_DATE + +LABEL service="web-app" \ + version="${VERSION}" \ + maintainer="Meldestelle Development Team" \ + build.date="${BUILD_DATE}" + +# Copy Caddy Config & Templates +COPY config/docker/caddy/web-app/Caddyfile /etc/caddy/Caddyfile +COPY config/docker/caddy/web-app/config.json /usr/share/caddy/config.json + +# Copy Static Assets from Builder +COPY --from=builder /app/dist/ /usr/share/caddy/ + +# Ensure favicon exists (fallback) +COPY --from=builder /workspace/config/docker/nginx/web-app/favicon.svg /usr/share/caddy/favicon.svg + +EXPOSE 4000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:4000/health || exit 1 + +CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"] diff --git a/config/docker/caddy/web-app/config.json b/config/docker/caddy/web-app/config.json new file mode 100644 index 00000000..7151e7c0 --- /dev/null +++ b/config/docker/caddy/web-app/config.json @@ -0,0 +1,3 @@ +{ + "apiBaseUrl": "{{env "API_BASE_URL" | default "http://localhost:8081"}}" +} diff --git a/core/core-domain/build.gradle.kts b/core/core-domain/build.gradle.kts index d24061f0..fd730eed 100644 --- a/core/core-domain/build.gradle.kts +++ b/core/core-domain/build.gradle.kts @@ -4,8 +4,6 @@ plugins { } kotlin { - // Toolchain is now handled centrally in the root build.gradle.kts - jvm { compilerOptions { freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime") @@ -13,6 +11,7 @@ kotlin { } js(IR) { + binaries.library() browser { testTask { enabled = false @@ -20,29 +19,21 @@ kotlin { } } - // Wasm support enabled? @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) wasmJs { + binaries.library() browser() } sourceSets { - // Opt-in to experimental Kotlin UUID API across all source sets all { languageSettings.optIn("kotlin.uuid.ExperimentalUuidApi") - // Opt-in für kotlin.time.ExperimentalTime projektweit, solange Teile noch experimentell sind languageSettings.optIn("kotlin.time.ExperimentalTime") } commonMain.dependencies { - // Core dependencies (that aren't included in platform-dependencies) - // Note: core-domain should NOT depend on core-utils to avoid circular dependencies - // core-utils depends on core-domain, not the other way around - - // Serialization and date-time for commonMain api(libs.kotlinx.serialization.json) api(libs.kotlinx.datetime) - } commonTest.dependencies { @@ -58,17 +49,12 @@ kotlin { } jvmMain.dependencies { - // Fachliches Domain-Modul: keine technischen Abhängigkeiten hier hinterlegen. - // Falls in Zukunft JVM-spezifische, fachlich neutrale Ergänzungen nötig sind, - // bitte bewusst und minimal hinzufügen. } jvmTest.dependencies { - // implementation(kotlin("test-junit5")) implementation(libs.junit.jupiter.api) implementation(libs.mockk) implementation(projects.platform.platformTesting) - // implementation(libs.bundles.testing.jvm) // Temporarily disabled due to resolution issues implementation(libs.junit.jupiter.api) implementation(libs.junit.jupiter.engine) implementation(libs.junit.jupiter.params) diff --git a/dc-gui.yaml b/dc-gui.yaml index 48c01c5f..f9635a1a 100644 --- a/dc-gui.yaml +++ b/dc-gui.yaml @@ -9,13 +9,12 @@ services: web-app: build: context: . # Wichtig: Root Context für Monorepo Zugriff - dockerfile: config/docker/nginx/web-app/Dockerfile + dockerfile: config/docker/caddy/web-app/Dockerfile args: GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.2.1}" JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25}" # Frontend spezifisch: - NODE_VERSION: "${DOCKER_NODE_VERSION:-24.12.0}" - NGINX_IMAGE_TAG: "${DOCKER_NGINX_VERSION:-1.28.0-alpine}" + CADDY_VERSION: "${DOCKER_CADDY_VERSION:-2.9-alpine}" # Metadaten: VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}" BUILD_DATE: "${DOCKER_BUILD_DATE}" @@ -26,7 +25,11 @@ services: ports: - "${WEB_APP_PORT:-4000:4000}" environment: - dummy_var: "prevent_empty_block" + # Runtime Configuration for Caddy Templates + # Browser can access API via localhost:8081 (Gateway) + # In Docker network, it might be http://api-gateway:8081, but browser runs on host! + # Usually, for local dev, we want the browser to hit localhost:8081. + API_BASE_URL: "${WEB_APP_API_URL:-http://localhost:8081}" depends_on: api-gateway: condition: "service_started" diff --git a/docs/01_Architecture/Reference/Engineering_Moderner_Frontend-Architekturen_01-2026.md b/docs/01_Architecture/Reference/Engineering_Moderner_Frontend-Architekturen_01-2026.md new file mode 100644 index 00000000..f76f3905 --- /dev/null +++ b/docs/01_Architecture/Reference/Engineering_Moderner_Frontend-Architekturen_01-2026.md @@ -0,0 +1,88 @@ +--- +type: Reference +status: ACTIVE +owner: Lead Architect +date: 2026-02-02 +--- + +# Engineering Moderner Frontend-Architekturen: Kotlin 2.3.0, Compose Multiplatform 1.10.0 und Gradle 9.0 für Modulare Monolithen + +Der architektonische Übergang zu modularen Monolithen bietet Unternehmen die Möglichkeit, die Komplexität von +Microservices zu reduzieren und gleichzeitig eine klare Trennung der Domänenlogik beizubehalten. In Kombination mit +Kotlin Multiplatform (KMP) für Single Page Applications (SPAs) lässt sich Geschäftslogik effizient über den gesamten +Stack teilen. Die Einführung von Kotlin 2.3.0, Compose Multiplatform 1.10.0 und Gradle 9.0 stellt dabei neue Best +Practices für Build-Performance und Deployment auf. + +## 1. Gradle 9.x Optimierung in der CI/CD + +Gradle 9.0 führt signifikante Änderungen ein, die speziell für große Multi-Modul-Projekte wie modulare Monolithen +optimiert sind. + +- **Configuration Cache als Standard:** In Gradle 9.0 ist der Configuration Cache der bevorzugte Ausführungsmodus. Durch + das Caching des Task-Graphen werden nachfolgende Builds erheblich beschleunigt, da die Konfigurationsphase + übersprungen wird. +- **Kotlin DSL Script Compilation Avoidance:** Durch den Einsatz von ABI-Fingerprinting erkennt Gradle 9.0, ob + Änderungen an `.kts`-Dateien die Build-Logik tatsächlich beeinflussen. Nicht-relevante Änderungen (wie Kommentare) + führen nicht mehr zur Neukompilierung, was die Konfigurationszeit um bis zu 60 % reduzieren kann. +- **Parallel Configuration Store and Load:** Gradle 8.11 und 9.0 unterstützen das parallele Laden und Speichern von + Cache-Einträgen, was die Konfigurationszeit in Projekten mit hunderten Modulen halbiert. +- **Speichermanagement:** Für speicherintensive Tasks wie die JS-Kompilierung in der CI wird eine explizite + JVM-Konfiguration empfohlen (z. B. `org.gradle.jvmargs=-Xmx8g`), um Abstürze der Runner zu vermeiden. +- **Remote Build Caching:** Die Verwendung der `gradle-cache-action` in GitHub Actions ermöglicht es ephemeral Runnern, + als Remote-Build-Cache-Proxy zu fungieren, wodurch Task-Outputs über verschiedene Jobs hinweg geteilt werden. + +## 2. Dockerisierung von KMP Web Applications (JS IR) + +Eine effiziente Containerisierung erfordert die Trennung von Build-Umgebung und produktivem Webserver. + +- **Multi-Stage Build:** Verwenden Sie ein JDK-Image (z. B. eclipse-temurin:21) für die Kompilierung und ein schlankes + Image ( + Nginx oder Caddy) für die Auslieferung der statischen Assets. +- **BuildKit Cache Mounts:** Nutzen Sie Cache-Mounts für das Gradle-Verzeichnis im Dockerfile ( + `RUN--mount=type=cache,target=/root/.gradle`), um Abhängigkeiten zwischen verschiedenen Docker-Builds lokal auf dem + Host zu persistieren. +- **Layering:** Kopieren Sie zuerst nur den Gradle-Wrapper und den Version-Catalog (`libs.versions.toml`), um den + Download der Abhängigkeiten in einem separaten Layer zu cachen. + +## 3. Umgang mit Laufzeitkonfigurationen (Environment variables) + +Da JS-Bundler (Vite, Webpack) Umgebungsvariablen zur Build-Zeit auflösen, ist eine Strategie für "Build Once, Deploy +Everywhere" erforderlich. + +- **config.json Fetch-Pattern:** Dies ist die empfohlene Methode für KMP-SPAs. + + 1. Die App wird ohne Umgebungswerte gebaut. + 2. Beim Container-Start generiert ein `docker-entrypoint.sh` Skript eine `config.json` aus den + aktuellen System-Umgebungsvariablen. + 3. Die Kotlin-Anwendung führt in der `main()`-Funktion einen `fetch("/config.json")` aus, bevor die UI gerendert wird. + +- **Typensicherheit:** Definieren Sie ein Kotlin-Interface für die Konfiguration, die mit der JSON-Struktur des + Entrypoint-Skripts übereinstimmt, um Laufzeitfehler zu vermeiden. + +## 4. Server-Konfiguration und Compose 1.10.0 Features + +Die Wahl des Webservers beeinflusst das Routing und die Performance der Compose Multiplatform Anwendung. + +- **SPA Routing:** Nginx muss so konfiguriert werden, dass alle unbekannten Pfade auf die `index.html` zurückfallen ( + `try_files $uri $uri/ /index.html`), damit das clientseitige Routing funktioniert. +- **Caddy Alternative:** Caddy bietet eine einfachere Syntax für SPA-Routing (`try_files {path} /index.html`) und + unterstützt HTTP/3 sowie automatisches HTTPS out-of-the-box. +- **Web Cache API:** Compose Multiplatform 1.10.0 integriert die Web Cache API, um statische Ressourcen und Strings + effizient zu speichern und die Verzögerungen durch die Standard-Validierung des Browsers zu umgehen. +- **Security Header:** Für zukünftige Kotlin/Wasm-Migrationen sollten bereits jetzt COOP (`same-origin`) und COEP ( + `require-corp`) Header gesetzt werden, um Cross-Origin-Isolation zu ermöglichen. + +| Komponente | Empfehlung | Vorteil | +|---------------|-------------------------------------|--------------------------------------------------------| +| Build-Tool | Gradle 9.0 mit Configuration Cache | Extreme Verkürzung der Konfigurationsphase | +| CI Caching | Remote Cache Action (Proxy) | Wiederverwendung von Task-Outputs auf frischen Runnern | +| Konfiguration | Runtime config.json Fetch | Ein Docker-Image für alle Umgebungen (Dev/Prod) | +| Webserver | Caddy oder Nginx | Optimiertes SPA-Routing und Web Cache Support | + +## Fazit + +Die Kombination aus Gradle 9.0 und Kotlin 2.3.0 ermöglicht hocheffiziente Build-Pipelines für modulare Monolithen. Durch +den Einsatz von Multi-Stage Docker-Builds und dem `config.json`-Fetch-Muster wird eine moderne, skalierbare +Deployment-Strategie umgesetzt, die die neuen Performance-Features von Compose Multiplatform 1.10.0 optimal nutzt. + + diff --git a/frontend/core/auth/build.gradle.kts b/frontend/core/auth/build.gradle.kts index e1aee7c5..5c3602e5 100644 --- a/frontend/core/auth/build.gradle.kts +++ b/frontend/core/auth/build.gradle.kts @@ -14,11 +14,15 @@ kotlin { jvm() js { - browser { - testTask { - enabled = false - } + // browser {} block removed to avoid NodeJsRootPlugin conflicts in multi-module builds + // We only need explicit browser configuration in the shell (application) module. + // Tests are disabled via root build.gradle.kts configuration anyway. + nodejs { + testTask { + enabled = false + } } + binaries.library() } sourceSets { diff --git a/frontend/core/design-system/build.gradle.kts b/frontend/core/design-system/build.gradle.kts index e222a192..3b419e00 100644 --- a/frontend/core/design-system/build.gradle.kts +++ b/frontend/core/design-system/build.gradle.kts @@ -6,48 +6,27 @@ plugins { } kotlin { - // Toolchain is now handled centrally in the root build.gradle.kts jvm() - js(IR) { - browser() -// nodejs() - } - // Wasm vorerst deaktiviert - /* - @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) - wasmJs { - browser() + js(IR) { + binaries.library() + // Explicitly select browser environment to satisfy Kotlin/JS compiler warning + browser { + testTask { enabled = false } + } } - */ sourceSets { commonMain.dependencies { - - // Compose dependencies implementation(compose.runtime) implementation(compose.foundation) implementation(compose.material3) implementation(compose.ui) implementation(compose.components.resources) - - // Coroutines implementation(libs.kotlinx.coroutines.core) - - // Serialization implementation(libs.kotlinx.serialization.json) - - // DateTime implementation(libs.kotlinx.datetime) } - - jsMain.dependencies { - // JS-specific UI dependencies if needed - } - - jvmMain.dependencies { - // JVM-specific UI dependencies if needed - } } } diff --git a/frontend/core/domain/build.gradle.kts b/frontend/core/domain/build.gradle.kts index e481ee4d..45353c1a 100644 --- a/frontend/core/domain/build.gradle.kts +++ b/frontend/core/domain/build.gradle.kts @@ -9,24 +9,14 @@ plugins { } kotlin { - // Toolchain is now handled centrally in the root build.gradle.kts - - // Wasm is now a first-class citizen in our stack, so we enable it by default - // val enableWasm = providers.gradleProperty("enableWasm").orNull == "true" - jvm() js { + binaries.library() browser { - testTask { enabled = false } + testTask { enabled = false } } } - // Wasm vorerst deaktiviert - /* - @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) - wasmJs { browser() } - */ - sourceSets { commonMain.dependencies { implementation(libs.kotlinx.serialization.json) diff --git a/frontend/core/local-db/build.gradle.kts b/frontend/core/local-db/build.gradle.kts index 7618aedd..a3a85974 100644 --- a/frontend/core/local-db/build.gradle.kts +++ b/frontend/core/local-db/build.gradle.kts @@ -9,28 +9,18 @@ plugins { } kotlin { - // Toolchain is now handled centrally in the root build.gradle.kts - jvm() js { + binaries.library() browser { - testTask { enabled = false } + testTask { enabled = false } } - binaries.executable() } - // Wasm vorerst deaktiviert, um Stabilität mit JS zu gewährleisten - /* - @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) - wasmJs { - browser() - } - */ - sourceSets { commonMain.dependencies { implementation(libs.koin.core) - implementation(libs.bundles.kmp.common) // Coroutines, Serialization, DateTime + implementation(libs.bundles.kmp.common) implementation(libs.sqldelight.runtime) implementation(libs.sqldelight.coroutines) } @@ -41,18 +31,9 @@ kotlin { jsMain.dependencies { implementation(libs.sqldelight.driver.web) - - // NPM deps used by `sqlite.worker.js` (OPFS-backed SQLite WASM worker) implementation(npm("@sqlite.org/sqlite-wasm", "3.51.1-build2")) } - /* - val wasmJsMain = getByName("wasmJsMain") - wasmJsMain.dependencies { - implementation(libs.sqldelight.driver.web) - } - */ - commonTest.dependencies { implementation(libs.kotlin.test) } @@ -63,7 +44,7 @@ sqldelight { databases { create("AppDatabase") { packageName.set("at.mocode.frontend.core.localdb") - generateAsync.set(true) // WICHTIG: Async-First für JS Support + generateAsync.set(true) } } } diff --git a/frontend/core/navigation/build.gradle.kts b/frontend/core/navigation/build.gradle.kts index d253c61e..2a2dc095 100644 --- a/frontend/core/navigation/build.gradle.kts +++ b/frontend/core/navigation/build.gradle.kts @@ -10,28 +10,14 @@ group = "at.mocode.clients.shared" version = "1.0.0" kotlin { - // Toolchain is now handled centrally in the root build.gradle.kts - jvm() - js { + binaries.library() browser { - testTask { - // Browser testing is disabled to avoid environment issues (e.g. missing ChromeHeadless). - // Tests are still run on JVM. - enabled = false - } + testTask { enabled = false } } } - // Wasm vorerst deaktiviert - /* - @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) - wasmJs { - browser() - } - */ - sourceSets { commonMain.dependencies { // Depend on core domain for User/Role types used by navigation API diff --git a/frontend/core/network/build.gradle.kts b/frontend/core/network/build.gradle.kts index 8f4f271f..25f7967c 100644 --- a/frontend/core/network/build.gradle.kts +++ b/frontend/core/network/build.gradle.kts @@ -9,39 +9,23 @@ plugins { } kotlin { - // Toolchain is now handled centrally in the root build.gradle.kts - jvm() js { + binaries.library() browser { - testTask { enabled = false } + testTask { enabled = false } } } - // Wasm vorerst deaktiviert - /* - @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) - wasmJs { browser() } - */ - sourceSets { commonMain.dependencies { - // Ktor Client core + JSON and Auth + Logging + Timeouts + Retry api(libs.ktor.client.core) implementation(libs.ktor.client.contentNegotiation) implementation(libs.ktor.client.serialization.kotlinx.json) implementation(libs.ktor.client.auth) implementation(libs.ktor.client.logging) - // ktor-client-resources optional; disabled until version is added to catalog - - // Kotlinx core bundles implementation(libs.kotlinx.coroutines.core) - - // DI (Koin) api(libs.koin.core) - - // Project modules via typesafe accessors - // (none here; kept for consistency) } jvmMain.dependencies { @@ -51,13 +35,6 @@ kotlin { jsMain.dependencies { implementation(libs.ktor.client.js) } - - /* - val wasmJsMain = getByName("wasmJsMain") - wasmJsMain.dependencies { - implementation(libs.ktor.client.js) - } - */ } } diff --git a/frontend/core/sync/build.gradle.kts b/frontend/core/sync/build.gradle.kts index 5d2cda79..ad1ec02e 100644 --- a/frontend/core/sync/build.gradle.kts +++ b/frontend/core/sync/build.gradle.kts @@ -4,15 +4,20 @@ plugins { } kotlin { - // Targets are configured centrally in the shells/feature modules; here we just provide common code. jvm() js(IR) { - browser() + binaries.library() + browser { + testTask { enabled = false } + } } sourceSets { commonMain.dependencies { + // Correct dependency: Syncable interface is in the shared core domain implementation(projects.core.coreDomain) + // Also include frontend domain if needed (e.g., for frontend-specific models) + implementation(projects.frontend.core.domain) // Networking implementation(libs.ktor.client.core) diff --git a/frontend/features/ping-feature/build.gradle.kts b/frontend/features/ping-feature/build.gradle.kts index d6720f37..574ef61b 100644 --- a/frontend/features/ping-feature/build.gradle.kts +++ b/frontend/features/ping-feature/build.gradle.kts @@ -2,8 +2,6 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget /** * Dieses Modul kapselt die gesamte UI und Logik für das Ping-Feature. - * Es kennt seine eigenen technischen Abhängigkeiten (Ktor, Coroutines) - * und den UI-Baukasten (common-ui), aber es kennt keine anderen Features. */ plugins { alias(libs.plugins.kotlinMultiplatform) @@ -16,45 +14,23 @@ group = "at.mocode.clients" version = "1.0.0" kotlin { - // Toolchain is now handled centrally in the root build.gradle.kts - jvm() - js { + binaries.library() browser { - testTask { - enabled = false - } + testTask { enabled = false } } } - // Wasm vorerst deaktiviert - /* - @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) - wasmJs { - browser() - } - */ - sourceSets { commonMain.dependencies { - // Contract from backend implementation(projects.contracts.pingApi) - - // UI Kit (Design System) implementation(projects.frontend.core.designSystem) - - // Generic Delta-Sync core implementation(projects.frontend.core.sync) - - // Local DB (SQLDelight) implementation(projects.frontend.core.localDb) - implementation(libs.sqldelight.coroutines) // Explicitly add coroutines extension for async driver support + implementation(libs.sqldelight.coroutines) + implementation(projects.frontend.core.domain) - // Shared sync contract base (Syncable) - implementation(projects.core.coreDomain) - - // Compose dependencies implementation(compose.foundation) implementation(compose.runtime) implementation(compose.material3) @@ -62,12 +38,10 @@ kotlin { implementation(compose.components.resources) implementation(compose.materialIconsExtended) - // Bundles (Cleaned up dependencies) - implementation(libs.bundles.kmp.common) // Coroutines, Serialization, DateTime - implementation(libs.bundles.ktor.client.common) // Ktor Client (Core, Auth, JSON, Logging) - implementation(libs.bundles.compose.common) // ViewModel & Lifecycle + implementation(libs.bundles.kmp.common) + implementation(libs.bundles.ktor.client.common) + implementation(libs.bundles.compose.common) - // DI (Koin) for resolving apiClient from container implementation(libs.koin.core) } @@ -78,7 +52,7 @@ kotlin { } jvmTest.dependencies { - implementation(libs.mockk) // MockK only for JVM tests + implementation(libs.mockk) implementation(projects.platform.platformTesting) implementation(libs.bundles.testing.jvm) } @@ -90,22 +64,9 @@ kotlin { jsMain.dependencies { implementation(libs.ktor.client.js) } - - /* - val wasmJsMain = getByName("wasmJsMain") - wasmJsMain.dependencies { - implementation(libs.ktor.client.js) // WASM verwendet JS-Client [cite: 7] - - // Compose für shared UI components für WASM - implementation(compose.runtime) - implementation(compose.foundation) - implementation(compose.material3) - } - */ } } -// KMP Compile-Optionen tasks.withType { compilerOptions { jvmTarget.set(JvmTarget.JVM_25) diff --git a/frontend/features/ping-feature/src/commonTest/kotlin/at/mocode/ping/feature/test/Fakes.kt b/frontend/features/ping-feature/src/commonTest/kotlin/at/mocode/ping/feature/test/Fakes.kt index 6a77d0d5..faa5eab7 100644 --- a/frontend/features/ping-feature/src/commonTest/kotlin/at/mocode/ping/feature/test/Fakes.kt +++ b/frontend/features/ping-feature/src/commonTest/kotlin/at/mocode/ping/feature/test/Fakes.kt @@ -132,8 +132,8 @@ class TestPingApiClient : PingApi { return handleRequest(securePingResponse) } - override suspend fun syncPings(lastSyncTimestamp: Long): List { - syncPingsCalledWith = lastSyncTimestamp + override suspend fun syncPings(since: Long): List { + syncPingsCalledWith = since callCount++ if (simulateDelay) { diff --git a/frontend/shells/meldestelle-portal/src/jsMain/kotlin/Config.kt b/frontend/shells/meldestelle-portal/src/jsMain/kotlin/Config.kt new file mode 100644 index 00000000..c2f72705 --- /dev/null +++ b/frontend/shells/meldestelle-portal/src/jsMain/kotlin/Config.kt @@ -0,0 +1,26 @@ +import kotlinx.browser.window +import kotlinx.coroutines.await +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json + +@Serializable +data class AppConfig( + val apiBaseUrl: String +) + +suspend fun loadAppConfig(): AppConfig { + return try { + // Fetch config.json generated by Caddy templates + val response = window.fetch("/config.json").await() + if (!response.ok) { + console.warn("[Config] Failed to load config.json, falling back to defaults") + return AppConfig(apiBaseUrl = window.location.origin) + } + val text = response.text().await() + Json.decodeFromString(AppConfig.serializer(), text) + } catch (e: dynamic) { + console.error("[Config] Error loading config:", e) + // Fallback for local development if file is missing + AppConfig(apiBaseUrl = "http://localhost:8081") + } +} diff --git a/frontend/shells/meldestelle-portal/src/jsMain/kotlin/main.kt b/frontend/shells/meldestelle-portal/src/jsMain/kotlin/main.kt index 29aa2106..9924d797 100644 --- a/frontend/shells/meldestelle-portal/src/jsMain/kotlin/main.kt +++ b/frontend/shells/meldestelle-portal/src/jsMain/kotlin/main.kt @@ -21,20 +21,30 @@ import org.w3c.dom.HTMLElement fun main() { console.log("[WebApp] main() entered") - // 1. Initialize DI (Koin) with static modules - try { - startKoin { modules(networkModule, localDbModule, syncModule, pingFeatureModule, authModule, navigationModule) } - console.log("[WebApp] Koin initialized with static modules") - } catch (e: dynamic) { - console.warn("[WebApp] Koin initialization warning:", e) - } - - // 2. Async Initialization Chain - // We must ensure DB is ready and registered in Koin BEFORE we mount the UI. - val provider = GlobalContext.get().get() - MainScope().launch { try { + // 1. Load Runtime Configuration (Async) + console.log("[WebApp] Loading configuration...") + val config = loadAppConfig() + console.log("[WebApp] Configuration loaded: apiBaseUrl=${config.apiBaseUrl}") + + // 2. Initialize DI (Koin) + // We register the config immediately so other modules can use it + startKoin { + modules( + module { single { config } }, // Make AppConfig available for injection + networkModule, + localDbModule, + syncModule, + pingFeatureModule, + authModule, + navigationModule + ) + } + console.log("[WebApp] Koin initialized") + + // 3. Initialize Database (Async) + val provider = GlobalContext.get().get() console.log("[WebApp] Initializing Database...") val db = provider.createDatabase() @@ -46,12 +56,12 @@ fun main() { ) console.log("[WebApp] Local DB created and registered in Koin") - // 3. Start App only after DB is ready + // 4. Start UI startAppWhenDomReady() } catch (e: dynamic) { - console.error("[WebApp] CRITICAL: Database initialization failed:", e) - renderFatalError("Database initialization failed: ${e?.message ?: e}") + console.error("[WebApp] CRITICAL: Initialization failed:", e) + renderFatalError("Initialization failed: ${e?.message ?: e}") } } }