From 11c597f1479a333d21b903b0882d7347e15029de Mon Sep 17 00:00:00 2001 From: Stefan Mogeritsch Date: Mon, 2 Feb 2026 16:19:20 +0100 Subject: [PATCH] feat: add runtime configuration for Caddy-based SPA containerization Introduced `config.json` runtime configuration fetch mechanism to support the "Build Once, Deploy Everywhere" pattern. Replaced NGINX with Caddy for SPA deployment, enabling SPA routing, security headers, and static asset management. Updated Gradle and Kotlin/JS build configurations to align with the new runtime environment. Enhanced Dockerfile and health checks for optimized CI/CD workflows and improved SPA delivery. --- config/docker/caddy/web-app/Caddyfile | 41 ++++++++ config/docker/caddy/web-app/Dockerfile | 94 +++++++++++++++++++ config/docker/caddy/web-app/config.json | 3 + core/core-domain/build.gradle.kts | 18 +--- dc-gui.yaml | 11 ++- ...Moderner_Frontend-Architekturen_01-2026.md | 88 +++++++++++++++++ frontend/core/auth/build.gradle.kts | 12 ++- frontend/core/design-system/build.gradle.kts | 33 ++----- frontend/core/domain/build.gradle.kts | 14 +-- frontend/core/local-db/build.gradle.kts | 27 +----- frontend/core/navigation/build.gradle.kts | 18 +--- frontend/core/network/build.gradle.kts | 27 +----- frontend/core/sync/build.gradle.kts | 9 +- .../features/ping-feature/build.gradle.kts | 55 ++--------- .../at/mocode/ping/feature/test/Fakes.kt | 4 +- .../src/jsMain/kotlin/Config.kt | 26 +++++ .../src/jsMain/kotlin/main.kt | 40 +++++--- 17 files changed, 327 insertions(+), 193 deletions(-) create mode 100644 config/docker/caddy/web-app/Caddyfile create mode 100644 config/docker/caddy/web-app/Dockerfile create mode 100644 config/docker/caddy/web-app/config.json create mode 100644 docs/01_Architecture/Reference/Engineering_Moderner_Frontend-Architekturen_01-2026.md create mode 100644 frontend/shells/meldestelle-portal/src/jsMain/kotlin/Config.kt 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}") } } }