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:
Stefan Mogeritsch 2026-02-02 16:19:20 +01:00
parent 86d8d780f5
commit 11c597f147
17 changed files with 327 additions and 193 deletions

View 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
}
}

View 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"]

View File

@ -0,0 +1,3 @@
{
"apiBaseUrl": "{{env "API_BASE_URL" | default "http://localhost:8081"}}"
}

View File

@ -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)

View File

@ -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"

View File

@ -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.

View File

@ -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 {

View File

@ -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
}
}
}

View File

@ -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)

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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)
}
*/
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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) {

View File

@ -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")
}
}

View File

@ -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}")
}
}
}