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.
This commit is contained in:
parent
86d8d780f5
commit
11c597f147
41
config/docker/caddy/web-app/Caddyfile
Normal file
41
config/docker/caddy/web-app/Caddyfile
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
94
config/docker/caddy/web-app/Dockerfile
Normal file
94
config/docker/caddy/web-app/Dockerfile
Normal file
|
|
@ -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"]
|
||||
3
config/docker/caddy/web-app/config.json
Normal file
3
config/docker/caddy/web-app/config.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"apiBaseUrl": "{{env "API_BASE_URL" | default "http://localhost:8081"}}"
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
11
dc-gui.yaml
11
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"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_25)
|
||||
|
|
|
|||
|
|
@ -132,8 +132,8 @@ class TestPingApiClient : PingApi {
|
|||
return handleRequest(securePingResponse)
|
||||
}
|
||||
|
||||
override suspend fun syncPings(lastSyncTimestamp: Long): List<PingEvent> {
|
||||
syncPingsCalledWith = lastSyncTimestamp
|
||||
override suspend fun syncPings(since: Long): List<PingEvent> {
|
||||
syncPingsCalledWith = since
|
||||
callCount++
|
||||
|
||||
if (simulateDelay) {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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<DatabaseProvider>()
|
||||
|
||||
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<DatabaseProvider>()
|
||||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user