diff --git a/.env.example b/.env.example index 656949d1..12c035ba 100644 --- a/.env.example +++ b/.env.example @@ -21,12 +21,10 @@ BACKUP_RETENTION_DAYS=7 DOCKER_REGISTRY=git.mo-code.at/mocode-software/meldestelle DOCKER_TAG=latest DOCKER_VERSION=1.0.0-SNAPSHOT -DOCKER_BUILD_DATE=2026-02-02T15:00:00Z +DOCKER_BUILD_DATE=2026-03-15T12:00:00Z DOCKER_GRADLE_VERSION=9.3.1 -# Java 25 = Early Access; für LTS auf 21 setzen DOCKER_JAVA_VERSION=25 DOCKER_NODE_VERSION=24.12.0 -# Caddy Version für den Web-App Container DOCKER_CADDY_VERSION=2.11-alpine # ARM64 spezifische JVM-Optionen (leer lassen auf x86/amd64, z.B. auf Apple Silicon) # Beispiel ARM64: JVM_OPTS_ARM64=-XX:UseSVE=0 @@ -43,7 +41,7 @@ POSTGRES_DB_URL=jdbc:postgresql://postgres:5432/pg-meldestelle-db POSTGRES_SHARED_BUFFERS=256MB POSTGRES_EFFECTIVE_CACHE_SIZE=768MB -# --- VALKEY (formerly Redis) --- +# --- VALKEY --- VALKEY_IMAGE=valkey/valkey:9-alpine VALKEY_PASSWORD= VALKEY_PORT=6379:6379 diff --git a/frontend/core/local-db/src/jsMain/resources/sqlite.worker.js b/frontend/core/local-db/src/jsMain/resources/sqlite.worker.js index 88275dd9..64b5ee3a 100644 --- a/frontend/core/local-db/src/jsMain/resources/sqlite.worker.js +++ b/frontend/core/local-db/src/jsMain/resources/sqlite.worker.js @@ -1,122 +1,123 @@ -// Minimal debug worker -console.log("Worker: sqlite.worker.js loaded. Starting initialization..."); +'use strict'; -try { - // We do NOT import from node_modules anymore to avoid Webpack bundling issues. - // import sqlite3InitModule from '@sqlite.org/sqlite-wasm'; +// --- State --- +let db = null; +let isReady = false; +let initError = null; +const messageQueue = []; - // Message buffer for messages arriving before DB is ready - let messageQueue = []; - let db = null; - let isReady = false; - - // Minimal worker protocol compatible with SQLDelight's `web-worker-driver`. - self.onmessage = (event) => { +// --- Message Handler --- +self.onmessage = (event) => { if (!isReady) { - console.log("Worker: Buffering message (DB not ready)", event.data); - messageQueue.push(event); + if (initError) { + postMessage({id: event.data?.id, error: `Worker not initialized: ${initError}`}); + } else { + messageQueue.push(event); + } return; } processMessage(event); - }; +}; - function processMessage(event) { +self.onerror = (event) => { + console.error('[sqlite.worker] Uncaught error:', event.message, `${event.filename}:${event.lineno}`); +}; + +// --- Message Processor --- +function processMessage(event) { const data = event.data; + if (!data) return; + try { - switch (data && data.action) { + switch (data.action) { case 'exec': { - if (!data.sql) throw new Error('exec: Missing query string'); + if (!data.sql) throw new Error('exec: Missing sql string'); const rows = []; db.exec({ sql: data.sql, bind: data.params ?? [], rowMode: 'array', - callback: (row) => rows.push(row) + callback: (row) => rows.push(row), }); - return postMessage({id: data.id, results: {values: rows}}); + postMessage({id: data.id, results: {values: rows}}); + break; } case 'begin_transaction': db.exec('BEGIN TRANSACTION;'); - return postMessage({id: data.id, results: []}); + postMessage({id: data.id, results: []}); + break; case 'end_transaction': db.exec('END TRANSACTION;'); - return postMessage({id: data.id, results: []}); + postMessage({id: data.id, results: []}); + break; case 'rollback_transaction': db.exec('ROLLBACK TRANSACTION;'); - return postMessage({id: data.id, results: []}); + postMessage({id: data.id, results: []}); + break; default: - throw new Error(`Unsupported action: ${data && data.action}`); + throw new Error(`Unsupported action: ${data.action}`); } } catch (err) { - console.error("Worker: Error processing message", err); - return postMessage({id: data && data.id, error: err?.message ?? String(err)}); + console.error('[sqlite.worker] Error processing message:', err); + postMessage({id: data.id, error: err?.message ?? String(err)}); } - } +} - self.onerror = function (event) { - console.error("Error in Web Worker (onerror):", event.message, event.filename, event.lineno); - // Don't postMessage here as it might confuse the driver if it expects a response to a query - }; - - async function init() { +// --- Initialization --- +async function init() { try { - // 1. Load the sqlite3.js library manually via importScripts. - console.log("Worker: Loading sqlite3.js via importScripts..."); + // Load sqlite3.js via importScripts (avoids Webpack bundling issues) try { importScripts('sqlite3.js'); } catch (e) { - throw new Error("Failed to importScripts('sqlite3.js'). Check if file exists at root. Error: " + e.message); + throw new Error(`importScripts('sqlite3.js') failed – is the file served at root? ${e.message}`); } if (typeof self.sqlite3InitModule !== 'function') { - throw new Error("sqlite3InitModule is not defined after importScripts. Check if sqlite3.js was loaded correctly."); + throw new Error('sqlite3InitModule not found after importScripts – sqlite3.js may be corrupt or wrong version.'); } - console.log("Worker: Fetching sqlite3.wasm manually..."); - const response = await fetch('sqlite3.wasm'); - if (!response.ok) { - throw new Error(`Failed to fetch sqlite3.wasm: ${response.status} ${response.statusText}`); + // Fetch WASM binary manually so we control the URL and can detect failures early + const wasmResponse = await fetch('sqlite3.wasm'); + if (!wasmResponse.ok) { + throw new Error(`Failed to fetch sqlite3.wasm: ${wasmResponse.status} ${wasmResponse.statusText}`); } - const wasmBinary = await response.arrayBuffer(); - console.log("Worker: sqlite3.wasm fetched successfully, size:", wasmBinary.byteLength); + const wasmBinary = await wasmResponse.arrayBuffer(); - console.log("Worker: Calling sqlite3InitModule with wasmBinary..."); const sqlite3 = await self.sqlite3InitModule({ print: console.log, printErr: console.error, - wasmBinary: wasmBinary + wasmBinary, }); - console.log("Worker: sqlite3InitModule resolved successfully"); const opfsAvailable = 'opfs' in sqlite3; - console.log("Worker: OPFS available:", opfsAvailable); - - // Initialize DB const dbName = 'app.db'; + if (opfsAvailable) { - console.log("Initialisiere persistente OPFS Datenbank: " + dbName); + console.log(`[sqlite.worker] Using persistent OPFS database: ${dbName}`); db = new sqlite3.oo1.OpfsDb(dbName); } else { - console.warn("OPFS nicht verfügbar, Fallback auf In-Memory"); + console.warn('[sqlite.worker] OPFS not available – falling back to in-memory database'); db = new sqlite3.oo1.DB(dbName); } - // Mark as ready and process queue + // Flush buffered messages isReady = true; - console.log("Worker: DB Ready. Processing " + messageQueue.length + " buffered messages."); + console.log(`[sqlite.worker] Ready. Flushing ${messageQueue.length} buffered message(s).`); while (messageQueue.length > 0) { processMessage(messageQueue.shift()); } } catch (e) { - console.error("Database initialization error in worker:", e); - // We can't easily communicate this back to the driver during init, - // but console.error should show up. + initError = e?.message ?? String(e); + console.error('[sqlite.worker] Initialization failed:', e); + + // Notify all buffered callers so they don't hang indefinitely + while (messageQueue.length > 0) { + const queued = messageQueue.shift(); + postMessage({id: queued.data?.id, error: `Worker initialization failed: ${initError}`}); + } } - } - - init(); - -} catch (e) { - console.error("Critical Worker Error:", e); } + +init(); diff --git a/frontend/core/navigation/src/commonMain/kotlin/at/mocode/frontend/core/navigation/CurrentUserProvider.kt b/frontend/core/navigation/src/commonMain/kotlin/at/mocode/frontend/core/navigation/CurrentUserProvider.kt index d8b83185..421b123c 100644 --- a/frontend/core/navigation/src/commonMain/kotlin/at/mocode/frontend/core/navigation/CurrentUserProvider.kt +++ b/frontend/core/navigation/src/commonMain/kotlin/at/mocode/frontend/core/navigation/CurrentUserProvider.kt @@ -3,7 +3,7 @@ package at.mocode.frontend.core.navigation import at.mocode.frontend.core.domain.models.User /** - * Abstraction to obtain the current authenticated user (or null if guest). + * Abstraction to get the current authenticated user (or null if guest). * Implementations live in shells/apps and provide access to the actual auth state. */ interface CurrentUserProvider { diff --git a/frontend/core/navigation/src/commonMain/kotlin/at/mocode/frontend/core/navigation/DeepLinkHandler.kt b/frontend/core/navigation/src/commonMain/kotlin/at/mocode/frontend/core/navigation/DeepLinkHandler.kt index c287fd75..2a831150 100644 --- a/frontend/core/navigation/src/commonMain/kotlin/at/mocode/frontend/core/navigation/DeepLinkHandler.kt +++ b/frontend/core/navigation/src/commonMain/kotlin/at/mocode/frontend/core/navigation/DeepLinkHandler.kt @@ -4,7 +4,7 @@ import at.mocode.frontend.core.domain.models.AppRoles /** * Deep link handling with minimal auth-aware guard via CurrentUserProvider. - * This version is self-contained in core:navigation and has no dependency on shared app store. + * This version is self-contained in core:navigation and has no dependency on the shared app store. */ class DeepLinkHandler( private val navigation: NavigationPort, diff --git a/frontend/core/network/src/jsMain/kotlin/at/mocode/frontend/core/network/PlatformConfig.js.kt b/frontend/core/network/src/jsMain/kotlin/at/mocode/frontend/core/network/PlatformConfig.js.kt index bc38db4f..e3f89964 100644 --- a/frontend/core/network/src/jsMain/kotlin/at/mocode/frontend/core/network/PlatformConfig.js.kt +++ b/frontend/core/network/src/jsMain/kotlin/at/mocode/frontend/core/network/PlatformConfig.js.kt @@ -31,7 +31,7 @@ actual object PlatformConfig { return resolvedUrl } - // 3) Fallback to the local gateway directly (e.g. for tests without window) + // 3) Fallback to the local gateway directly (e.g., for tests without a window) val fallbackUrl = "http://localhost:8081/api" console.log("[PlatformConfig] Fallback API_BASE_URL: $fallbackUrl") return fallbackUrl diff --git a/frontend/shells/meldestelle-portal/src/jsMain/kotlin/Config.kt b/frontend/shells/meldestelle-portal/src/jsMain/kotlin/Config.kt index a234cfb0..377d61c5 100644 --- a/frontend/shells/meldestelle-portal/src/jsMain/kotlin/Config.kt +++ b/frontend/shells/meldestelle-portal/src/jsMain/kotlin/Config.kt @@ -3,6 +3,8 @@ import kotlinx.coroutines.await import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json +private val AppJson = Json { ignoreUnknownKeys = true } + @Serializable data class AppConfig( val apiBaseUrl: String, @@ -18,7 +20,7 @@ suspend fun loadAppConfig(): AppConfig { return fallbackFromGlobal() } val text = response.text().await() - Json.decodeFromString(AppConfig.serializer(), text) + AppJson.decodeFromString(AppConfig.serializer(), text) } catch (e: dynamic) { console.error("[Config] Error loading config:", e) // Fallback: Caddy-injizierte Werte aus index.html (globalThis.API_BASE_URL / KEYCLOAK_URL) @@ -28,10 +30,10 @@ suspend fun loadAppConfig(): AppConfig { private fun fallbackFromGlobal(): AppConfig { val apiBase = (js("globalThis.API_BASE_URL") as? String) - ?.takeIf { it.isNotBlank() && !it.startsWith("\${") } + ?.takeIf { it.isNotBlank() && !it.startsWith($$"${") } ?: window.location.origin val kcUrl = (js("globalThis.KEYCLOAK_URL") as? String) - ?.takeIf { it.isNotBlank() && !it.startsWith("\${") } + ?.takeIf { it.isNotBlank() && !it.startsWith($$"${") } console.log("[Config] Fallback: apiBaseUrl=$apiBase, keycloakUrl=$kcUrl") return AppConfig(apiBaseUrl = apiBase, keycloakUrl = kcUrl) } diff --git a/frontend/shells/meldestelle-portal/src/jsMain/kotlin/main.kt b/frontend/shells/meldestelle-portal/src/jsMain/kotlin/main.kt index 1af3896b..00e1204f 100644 --- a/frontend/shells/meldestelle-portal/src/jsMain/kotlin/main.kt +++ b/frontend/shells/meldestelle-portal/src/jsMain/kotlin/main.kt @@ -28,7 +28,7 @@ fun main() { val config = loadAppConfig() console.log("[WebApp] Configuration loaded: apiBaseUrl=${config.apiBaseUrl}, keycloakUrl=${config.keycloakUrl}") - // Inject config into global JS scope for PlatformConfig to find + // Inject config into the global JS scope for PlatformConfig to find val global = js("typeof globalThis !== 'undefined' ? globalThis : window") global.API_BASE_URL = config.apiBaseUrl config.keycloakUrl?.let { global.KEYCLOAK_URL = it } diff --git a/frontend/shells/meldestelle-portal/src/jsMain/resources/sqlite.worker.js b/frontend/shells/meldestelle-portal/src/jsMain/resources/sqlite.worker.js deleted file mode 100644 index e429b4b5..00000000 --- a/frontend/shells/meldestelle-portal/src/jsMain/resources/sqlite.worker.js +++ /dev/null @@ -1,122 +0,0 @@ -// Minimal debug worker -console.log("Worker: sqlite.worker.js loaded. Starting initialization..."); - -try { - // We do NOT import from node_modules anymore to avoid Webpack bundling issues. - // import sqlite3InitModule from '@sqlite.org/sqlite-wasm'; - - // Message buffer for messages arriving before DB is ready - let messageQueue = []; - let db = null; - let isReady = false; - - // Minimal worker protocol compatible with SQLDelight's `web-worker-driver`. - self.onmessage = (event) => { - if (!isReady) { - console.log("Worker: Buffering message (DB not ready)", event.data); - messageQueue.push(event); - return; - } - processMessage(event); - }; - - function processMessage(event) { - const data = event.data; - try { - switch (data && data.action) { - case 'exec': { - if (!data.sql) throw new Error('exec: Missing query string'); - const rows = []; - db.exec({ - sql: data.sql, - bind: data.params ?? [], - rowMode: 'array', - callback: (row) => rows.push(row) - }); - return postMessage({ id: data.id, results: { values: rows } }); - } - case 'begin_transaction': - db.exec('BEGIN TRANSACTION;'); - return postMessage({ id: data.id, results: [] }); - case 'end_transaction': - db.exec('END TRANSACTION;'); - return postMessage({ id: data.id, results: [] }); - case 'rollback_transaction': - db.exec('ROLLBACK TRANSACTION;'); - return postMessage({ id: data.id, results: [] }); - default: - throw new Error(`Unsupported action: ${data && data.action}`); - } - } catch (err) { - console.error("Worker: Error processing message", err); - return postMessage({ id: data && data.id, error: err?.message ?? String(err) }); - } - } - - self.onerror = function(event) { - console.error("Error in Web Worker (onerror):", event.message, event.filename, event.lineno); - // Don't postMessage here as it might confuse the driver if it expects a response to a query - }; - - async function init() { - try { - // 1. Load the sqlite3.js library manually via importScripts. - console.log("Worker: Loading sqlite3.js via importScripts..."); - try { - importScripts('sqlite3.js'); - } catch (e) { - throw new Error("Failed to importScripts('sqlite3.js'). Check if file exists at root. Error: " + e.message); - } - - if (typeof self.sqlite3InitModule !== 'function') { - throw new Error("sqlite3InitModule is not defined after importScripts. Check if sqlite3.js was loaded correctly."); - } - - console.log("Worker: Fetching sqlite3.wasm manually..."); - const response = await fetch('sqlite3.wasm'); - if (!response.ok) { - throw new Error(`Failed to fetch sqlite3.wasm: ${response.status} ${response.statusText}`); - } - const wasmBinary = await response.arrayBuffer(); - console.log("Worker: sqlite3.wasm fetched successfully, size:", wasmBinary.byteLength); - - console.log("Worker: Calling sqlite3InitModule with wasmBinary..."); - const sqlite3 = await self.sqlite3InitModule({ - print: console.log, - printErr: console.error, - wasmBinary: wasmBinary - }); - - console.log("Worker: sqlite3InitModule resolved successfully"); - const opfsAvailable = 'opfs' in sqlite3; - console.log("Worker: OPFS available:", opfsAvailable); - - // Initialize DB - const dbName = 'app.db'; - if (opfsAvailable) { - console.log("Initialisiere persistente OPFS Datenbank: " + dbName); - db = new sqlite3.oo1.OpfsDb(dbName); - } else { - console.warn("OPFS nicht verfügbar, Fallback auf In-Memory"); - db = new sqlite3.oo1.DB(dbName); - } - - // Mark as ready and process queue - isReady = true; - console.log("Worker: DB Ready. Processing " + messageQueue.length + " buffered messages."); - while (messageQueue.length > 0) { - processMessage(messageQueue.shift()); - } - - } catch (e) { - console.error("Database initialization error in worker:", e); - // We can't easily communicate this back to the driver during init, - // but console.error should show up. - } - } - - init(); - -} catch (e) { - console.error("Critical Worker Error:", e); -} diff --git a/frontend/shells/meldestelle-portal/src/jsMain/resources/sw.js b/frontend/shells/meldestelle-portal/src/jsMain/resources/sw.js index 07278855..0f588771 100644 --- a/frontend/shells/meldestelle-portal/src/jsMain/resources/sw.js +++ b/frontend/shells/meldestelle-portal/src/jsMain/resources/sw.js @@ -1,6 +1,6 @@ const IS_DEV = self.location.hostname === 'localhost' || self.location.hostname === '127.0.0.1' || self.location.hostname === '::1'; -const CACHE_NAME = 'meldestelle-cache-v2'; +const CACHE_NAME = 'meldestelle-cache-v3'; const PRECACHE_URLS = [ '/', '/index.html', @@ -10,7 +10,8 @@ const PRECACHE_URLS = [ self.addEventListener('install', (event) => { if (IS_DEV) { // In dev, don't precache. Just activate the SW immediately. - self.skipWaiting(); + self.skipWaiting().then(_ => { + }); return; } event.waitUntil( @@ -74,6 +75,12 @@ self.addEventListener('fetch', (event) => { return; } + // App-Bundle immer vom Netzwerk – niemals aus dem Cache (verhindert veraltete JS-Versionen) + if (url.pathname.endsWith('web-app.js') || url.pathname.endsWith('web-app.js.map')) { + event.respondWith(fetch(req)); + return; + } + // Avoid noisy errors for favicon during dev/prod when missing if (url.pathname === '/favicon.ico') { event.respondWith( diff --git a/frontend/shells/meldestelle-portal/src/jsMain/resources/wasi-dummy.js b/frontend/shells/meldestelle-portal/src/jsMain/resources/wasi-dummy.js deleted file mode 100644 index ec4db8a0..00000000 --- a/frontend/shells/meldestelle-portal/src/jsMain/resources/wasi-dummy.js +++ /dev/null @@ -1,15 +0,0 @@ -// Dummy module to satisfy WASI imports in Webpack -// Used for skiko.wasm and potentially others - -export function abort() { - console.error("WASI abort called"); -} - -// Some WASM modules might look for these -export function emscripten_notify_memory_growth() { -} - -export default { - abort, - emscripten_notify_memory_growth -};