diff --git a/client/build.gradle.kts b/client/build.gradle.kts index 0d713a77..ed92e37d 100644 --- a/client/build.gradle.kts +++ b/client/build.gradle.kts @@ -1,8 +1,8 @@ +@file:OptIn(ExperimentalWasmDsl::class) + +import jdk.jfr.Experimental import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl -import org.gradle.api.tasks.Copy -import org.gradle.api.tasks.Sync -import org.gradle.api.file.DuplicatesStrategy plugins { alias(libs.plugins.kotlin.multiplatform) @@ -50,60 +50,83 @@ kotlin { } js(IR) { - // Disable browser-based tests (Karma/Chrome) to avoid ChromeHeadless issues browser { commonWebpackConfig { outputFileName = "meldestelle-client.js" - // Enable CSS support and optimization cssSupport { enabled.set(true) } + // Webpack performance optimizations for smaller bundles + devServer?.apply { + open = false + port = 8080 + } } testTask { - // Prevent launching ChromeHeadless (snap permission issues on some systems) + // Disable browser tests due to ChromeHeadless permission issues enabled = false } - } - // Run JS tests in Node.js instead (no browser needed) - nodejs { - testTask { - useMocha() + webpackTask { + // Production optimizations + args.add("--mode=production") + args.add("--optimization-minimize") + } + runTask { + // Development optimizations + args.add("--mode=development") } } + + // Use Node.js for testing instead of browser + nodejs { + testTask { + useMocha { + timeout = "10s" + } + } + } + binaries.executable() } - @OptIn(ExperimentalWasmDsl::class) + @OptIn(Experimental::class) wasmJs { - // Disable browser-based tests for WASM as well to avoid Karma/Chrome browser { commonWebpackConfig { outputFileName = "meldestelle-wasm.js" - // Enable CSS support for better bundling cssSupport { enabled.set(true) } + // WASM-specific webpack optimizations handled by webpack.config.d files } testTask { + // Disable WASM browser tests due to environment issues enabled = false } + webpackTask { + // Production optimizations for WASM + args.add("--mode=production") + args.add("--optimization-minimize") + } } - binaries.executable() + // WASM-specific compiler optimizations for smaller bundles compilations.all { compileTaskProvider.configure { compilerOptions { freeCompilerArgs.addAll( - "-Xwasm-use-new-exception-proposal", // Use efficient WASM exception handling - "-Xwasm-debugger-custom-formatters", // Optimize debug info for smaller size - "-Xwasm-enable-array-range-checks", // Optimize array bounds checking - "-Xwasm-generate-wat=false", // Skip WAT generation for smaller output - "-opt-in=kotlin.ExperimentalStdlibApi", // Enable stdlib optimizations - "-opt-in=kotlin.js.ExperimentalJsExport" // Enable JS export optimizations + "-Xwasm-use-new-exception-proposal", + "-Xwasm-debugger-custom-formatters", + "-Xwasm-enable-array-range-checks", + "-Xwasm-generate-wat=false", + "-opt-in=kotlin.ExperimentalStdlibApi", + "-opt-in=kotlin.js.ExperimentalJsExport" ) } } } + + binaries.executable() } sourceSets { diff --git a/client/src/commonMain/composeResources/drawable/compose-multiplatform.xml b/client/src/commonMain/composeResources/drawable/compose-multiplatform.xml index 07752584..fa8fe0fa 100644 --- a/client/src/commonMain/composeResources/drawable/compose-multiplatform.xml +++ b/client/src/commonMain/composeResources/drawable/compose-multiplatform.xml @@ -1,57 +1,36 @@ - - + - + android:pathData="M301.21,418.53C300.97,418.54 300.73,418.56 300.49,418.56C297.09,418.59 293.74,417.72 290.79,416.05L222.6,377.54C220.63,376.43 219,374.82 217.85,372.88C216.7,370.94 216.09,368.73 216.07,366.47L216.07,288.16C216.06,287.32 216.09,286.49 216.17,285.67C216.38,283.54 216.91,281.5 217.71,279.6L199.29,268.27L177.74,256.19C175.72,260.43 174.73,265.23 174.78,270.22L174.79,387.05C174.85,393.89 178.57,400.2 184.53,403.56L286.26,461.02C290.67,463.51 295.66,464.8 300.73,464.76C300.91,464.76 301.09,464.74 301.27,464.74C301.24,449.84 301.22,439.23 301.22,439.23L301.21,418.53Z" + android:fillColor="#041619" + android:fillType="nonZero"/> - + android:pathData="M409.45,242.91L312.64,188.23C303.64,183.15 292.58,183.26 283.68,188.51L187.92,245C183.31,247.73 179.93,251.62 177.75,256.17L177.74,256.19L199.29,268.27L217.71,279.6C217.83,279.32 217.92,279.02 218.05,278.74C218.24,278.36 218.43,277.98 218.64,277.62C219.06,276.88 219.52,276.18 220.04,275.51C221.37,273.8 223.01,272.35 224.87,271.25L289.06,233.39C290.42,232.59 291.87,231.96 293.39,231.51C295.53,230.87 297.77,230.6 300,230.72C302.98,230.88 305.88,231.73 308.47,233.2L373.37,269.85C375.54,271.08 377.49,272.68 379.13,274.57C379.68,275.19 380.18,275.85 380.65,276.53C380.86,276.84 381.05,277.15 381.24,277.47L397.79,266.39L420.34,252.93L420.31,252.88C417.55,248.8 413.77,245.35 409.45,242.91Z" + android:fillColor="#37BF6E" + android:fillType="nonZero"/> - - - - - - - - + android:pathData="M381.24,277.47C381.51,277.92 381.77,278.38 382.01,278.84C382.21,279.24 382.39,279.65 382.57,280.06C382.91,280.88 383.19,281.73 383.41,282.59C383.74,283.88 383.92,285.21 383.93,286.57L383.93,361.1C383.96,363.95 383.35,366.77 382.16,369.36C381.93,369.86 381.69,370.35 381.42,370.83C379.75,373.79 377.32,376.27 374.39,378L310.2,415.87C307.47,417.48 304.38,418.39 301.21,418.53L301.22,439.23C301.22,439.23 301.24,449.84 301.27,464.74C306.1,464.61 310.91,463.3 315.21,460.75L410.98,404.25C419.88,399 425.31,389.37 425.22,379.03L425.22,267.85C425.17,262.48 423.34,257.34 420.34,252.93L397.79,266.39L381.24,277.47Z" + android:fillColor="#3870B2" + android:fillType="nonZero"/> - - - - - - - - + android:pathData="M177.75,256.17C179.93,251.62 183.31,247.73 187.92,245L283.68,188.51C292.58,183.26 303.64,183.15 312.64,188.23L409.45,242.91C413.77,245.35 417.55,248.8 420.31,252.88L420.34,252.93L498.59,206.19C494.03,199.46 487.79,193.78 480.67,189.75L320.86,99.49C306.01,91.1 287.75,91.27 273.07,99.95L114.99,193.2C107.39,197.69 101.81,204.11 98.21,211.63L177.74,256.19L177.75,256.17ZM301.27,464.74C301.09,464.74 300.91,464.76 300.73,464.76C295.66,464.8 290.67,463.51 286.26,461.02L184.53,403.56C178.57,400.2 174.85,393.89 174.79,387.05L174.78,270.22C174.73,265.23 175.72,260.43 177.74,256.19L98.21,211.63C94.86,218.63 93.23,226.58 93.31,234.82L93.31,427.67C93.42,438.97 99.54,449.37 109.4,454.92L277.31,549.77C284.6,553.88 292.84,556.01 301.2,555.94L301.2,555.8C301.39,543.78 301.33,495.26 301.27,464.74Z" + android:strokeWidth="10" + android:fillColor="#00000000" + android:strokeColor="#083042" + android:fillType="nonZero"/> + android:pathData="M498.59,206.19L420.34,252.93C423.34,257.34 425.17,262.48 425.22,267.85L425.22,379.03C425.31,389.37 419.88,399 410.98,404.25L315.21,460.75C310.91,463.3 306.1,464.61 301.27,464.74C301.33,495.26 301.39,543.78 301.2,555.8L301.2,555.94C309.48,555.87 317.74,553.68 325.11,549.32L483.18,456.06C497.87,447.39 506.85,431.49 506.69,414.43L506.69,230.91C506.6,222.02 503.57,213.5 498.59,206.19Z" + android:strokeWidth="10" + android:fillColor="#00000000" + android:strokeColor="#083042" + android:fillType="nonZero"/> + diff --git a/client/src/jsMain/kotlin/at/mocode/main.kt b/client/src/jsMain/kotlin/at/mocode/main.kt new file mode 100644 index 00000000..809bbf01 --- /dev/null +++ b/client/src/jsMain/kotlin/at/mocode/main.kt @@ -0,0 +1,17 @@ +package at.mocode + +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.window.CanvasBasedWindow +import androidx.compose.ui.window.ComposeViewport +import kotlinx.browser.document + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + + ComposeViewport(document.getElementById("ComposeTarget")!!) { + App() + } +// CanvasBasedWindow(canvasElementId = "ComposeTarget") { +// App() +// } +} diff --git a/client/src/jsMain/resources/index.html b/client/src/jsMain/resources/index.html new file mode 100644 index 00000000..9c5ce286 --- /dev/null +++ b/client/src/jsMain/resources/index.html @@ -0,0 +1,13 @@ + + + + + + Meldestelle + + + +
+ + + diff --git a/client/src/jsMain/resources/styles.css b/client/src/jsMain/resources/styles.css new file mode 100644 index 00000000..0549b10f --- /dev/null +++ b/client/src/jsMain/resources/styles.css @@ -0,0 +1,7 @@ +html, body { + width: 100%; + height: 100%; + margin: 0; + padding: 0; + overflow: hidden; +} \ No newline at end of file diff --git a/client/src/jvmMain/kotlin/at/mocode/main.kt b/client/src/jvmMain/kotlin/at/mocode/main.kt index f887178c..1b5f4787 100644 --- a/client/src/jvmMain/kotlin/at/mocode/main.kt +++ b/client/src/jvmMain/kotlin/at/mocode/main.kt @@ -1,5 +1,7 @@ package at.mocode +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.runtime.Composable import androidx.compose.ui.window.Window import androidx.compose.ui.window.application @@ -10,4 +12,10 @@ fun main() = application { ) { App() } -} \ No newline at end of file +} + +@Preview +@Composable +fun AppDesktopPreview() { + App() +} diff --git a/client/src/wasmJsMain/kotlin/at/mocode/main.kt b/client/src/wasmJsMain/kotlin/at/mocode/main.kt index 7d9574c6..6a59244a 100644 --- a/client/src/wasmJsMain/kotlin/at/mocode/main.kt +++ b/client/src/wasmJsMain/kotlin/at/mocode/main.kt @@ -1,12 +1,18 @@ package at.mocode import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.window.CanvasBasedWindow import androidx.compose.ui.window.ComposeViewport import kotlinx.browser.document @OptIn(ExperimentalComposeUiApi::class) fun main() { - ComposeViewport(document.body!!) { + +// ComposeViewport(document.getElementById("ComposeTarget")!!) { +// App() +// } + + CanvasBasedWindow(canvasElementId = "ComposeTarget") { App() } -} \ No newline at end of file +} diff --git a/client/src/wasmJsMain/resources/index.html b/client/src/wasmJsMain/resources/index.html index 71c10791..5f254fb5 100644 --- a/client/src/wasmJsMain/resources/index.html +++ b/client/src/wasmJsMain/resources/index.html @@ -5,8 +5,9 @@ Meldestelle - + + diff --git a/client/webpack.config.d/wasm-optimization.js b/client/webpack.config.d/wasm-optimization.js index 3f43d1de..5fc0b095 100644 --- a/client/webpack.config.d/wasm-optimization.js +++ b/client/webpack.config.d/wasm-optimization.js @@ -65,10 +65,10 @@ config.optimization = { // Performance optimization config.performance = { ...(config.performance || {}), - // Increase hint limits for WASM (which is naturally larger) - maxAssetSize: 2000000, // 2MB for individual assets - maxEntrypointSize: 2000000, // 2MB for entrypoints - hints: 'warning' + // Realistic hint limits for WASM bundles (which are naturally larger) + maxAssetSize: 20000000, // 20MB for individual assets (WASM files can be large) + maxEntrypointSize: 5000000, // 5MB for entrypoints + hints: 'warning' // Show warnings but don't fail the build }; // Resolve optimization for faster builds @@ -95,8 +95,10 @@ if (config.mode === 'production') { // Production-specific optimizations config.output = { ...(config.output || {}), - // Better file names for caching - filename: '[name].[contenthash:8].js', + // Use conditional filename to match HTML template expectations for main chunk only + filename: (chunkData) => { + return chunkData.chunk.name === 'main' ? 'meldestelle-wasm.js' : '[name].[contenthash:8].js'; + }, chunkFilename: '[name].[contenthash:8].chunk.js' }; diff --git a/docker-compose.clients.yml b/docker-compose.clients.yml index 291c2ca0..bf50ba9a 100644 --- a/docker-compose.clients.yml +++ b/docker-compose.clients.yml @@ -54,39 +54,39 @@ services: # =================================================================== # Desktop Application (Kotlin Desktop + VNC) # =================================================================== - desktop-app: - build: - context: . - dockerfile: dockerfiles/clients/desktop-app/Dockerfile - container_name: meldestelle-desktop-app - environment: - # API Configuration - fallback to external gateway if not in same compose network - API_BASE_URL: http://${GATEWAY_HOST:-api-gateway}:${GATEWAY_PORT:-8081} - # VNC Configuration - DISPLAY: ":99" - VNC_PORT: "5901" - NOVNC_PORT: "6080" - # App Information - APP_TITLE: ${APP_NAME:-Meldestelle} - APP_VERSION: ${APP_VERSION:-1.0.0} - ports: - - "6080:6080" # Web-based VNC (noVNC) - - "5901:5901" # VNC direct access - networks: - - meldestelle-network - # depends_on removed for standalone client deployment - # When using multi-file setup, api-gateway dependency is handled externally - healthcheck: - test: [ "CMD", "/opt/health-check.sh" ] - interval: 30s - timeout: 10s - retries: 3 - start_period: 60s - restart: unless-stopped - labels: - - "traefik.enable=true" - - "traefik.http.routers.desktop-app.rule=Host(`localhost`) && PathPrefix(`/desktop`)" - - "traefik.http.services.desktop-app.loadbalancer.server.port=6080" +# desktop-app: +# build: +# context: . +# dockerfile: dockerfiles/clients/desktop-app/Dockerfile +# container_name: meldestelle-desktop-app +# environment: +# # API Configuration - fallback to external gateway if not in same compose network +# API_BASE_URL: http://${GATEWAY_HOST:-api-gateway}:${GATEWAY_PORT:-8081} +# # VNC Configuration +# DISPLAY: ":99" +# VNC_PORT: "5901" +# NOVNC_PORT: "6080" +# # App Information +# APP_TITLE: ${APP_NAME:-Meldestelle} +# APP_VERSION: ${APP_VERSION:-1.0.0} +# ports: +# - "6080:6080" # Web-based VNC (noVNC) +# - "5901:5901" # VNC direct access +# networks: +# - meldestelle-network +# # depends_on removed for standalone client deployment +# # When using multi-file setup, api-gateway dependency is handled externally +# healthcheck: +# test: [ "CMD", "/opt/health-check.sh" ] +# interval: 30s +# timeout: 10s +# retries: 3 +# start_period: 60s +# restart: unless-stopped +# labels: +# - "traefik.enable=true" +# - "traefik.http.routers.desktop-app.rule=Host(`localhost`) && PathPrefix(`/desktop`)" +# - "traefik.http.services.desktop-app.loadbalancer.server.port=6080" # =================================================================== # Auth Server (Custom Keycloak Extension) diff --git a/dockerfiles/clients/web-app/Dockerfile b/dockerfiles/clients/web-app/Dockerfile index 122a5740..6fac1202 100644 --- a/dockerfiles/clients/web-app/Dockerfile +++ b/dockerfiles/clients/web-app/Dockerfile @@ -28,8 +28,8 @@ RUN chmod +x ./gradlew # Dependencies downloaden (für besseres Caching) RUN ./gradlew :client:dependencies --no-configure-on-demand -# Kotlin/JS Web-App kompilieren -RUN ./gradlew :client:jsBrowserDistribution --no-configure-on-demand +# Kotlin/WASM Web-App kompilieren +RUN ./gradlew :client:wasmJsBrowserDistribution --no-configure-on-demand # =================================================================== # Stage 2: Runtime Stage - Nginx für Static Files + API Proxy @@ -40,7 +40,7 @@ FROM nginx:1.25-alpine RUN apk add --no-cache curl # Kopiere kompilierte Web-App von Build-Stage -COPY --from=builder /app/client/build/dist/js/productionExecutable/ /usr/share/nginx/html/ +COPY --from=builder /app/client/build/dist/wasmJs/productionExecutable/ /usr/share/nginx/html/ # Kopiere Nginx-Konfiguration COPY dockerfiles/clients/web-app/nginx.conf /etc/nginx/nginx.conf diff --git a/gradle.properties b/gradle.properties index 4f8f24e9..21360f76 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,9 +1,13 @@ +#Android +android.useAndroidX=true +android.nonTransitiveRClass=true + # Kotlin Configuration kotlin.code.style=official kotlin.daemon.jvmargs=-Xmx3072M -XX:+UseParallelGC -XX:MaxMetaspaceSize=1024M # Gradle Configuration -org.gradle.jvmargs=-Xmx3072M -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxMetaspaceSize=1024M -XX:+HeapDumpOnOutOfMemoryError -Xshare:off -Djava.awt.headless=true +org.gradle.jvmargs=-Xmx3072M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" -XX:+UseParallelGC -XX:MaxMetaspaceSize=1024M -XX:+HeapDumpOnOutOfMemoryError -Xshare:off -Djava.awt.headless=true org.gradle.parallel=true org.gradle.caching=true # org.gradle.configureondemand=true # Deprecated - removed for Gradle 9.0 compatibility @@ -24,6 +28,7 @@ io.ktor.development=true # IDE Configuration kotlin.mpp.androidSourceSetLayoutVersion=2 +kotlin.mpp.enableCInteropCommonization=true org.jetbrains.kotlin.wasm.check.wasm.binary.format=false kotlin.native.ignoreDisabledTargets=true idea.project.settings.delegate.build.run.actions.to.gradle=true @@ -34,6 +39,7 @@ kotlin.build.report.single_file=false # Compose Experimental Features org.jetbrains.compose.experimental.jscanvas.enabled=true +org.jetbrains.compose.experimental.wasm.enabled=true # Java Toolchain: ensure Gradle auto-downloads a full JDK when needed org.gradle.java.installations.auto-download=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 260f2762..72aa0b1f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,9 @@ # Last updated: 2025-07-31 [versions] +# --- Android Ecosystem --- +agp = "8.1.4" + # --- Kotlin Ecosystem --- kotlin = "2.2.10" kotlinx = "1.9.0" @@ -277,6 +280,8 @@ spring-cloud-gateway = [ [plugins] +androidApplication = { id = "com.android.application", version.ref = "agp" } +androidLibrary = { id = "com.android.library", version.ref = "agp" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }