From 034892e8909747c7f563aa84099526e3e3a76f6f Mon Sep 17 00:00:00 2001 From: StefanMo <61204035+StefanMoCoAt@users.noreply.github.com> Date: Sun, 30 Nov 2025 23:14:00 +0100 Subject: [PATCH] chore(MP-23): network DI client, frontend architecture guards, detekt & ktlint setup, docs, ping DI factory (#21) * chore(MP-21): snapshot pre-refactor state (Epic 1) * chore(MP-22): scaffold new repo structure, relocate Docker Compose, move frontend/backend modules, update Makefile; add docs mapping and env template * MP-22 Epic 2: Erfolgreich umgesetzt und verifiziert * MP-23 Epic 3: Gradle/Build Governance zentralisieren --- ...-koin-compose-4.0.0-commonMain-QYyzSQ.klib | Bin 0 -> 7621 bytes ...ose-viewmodel-4.0.0-commonMain-Lzy-yA.klib | Bin 0 -> 6240 bytes ...oin-koin-core-4.0.0-commonMain-CvvNiA.klib | Bin 0 -> 45233 bytes ...ore-viewmodel-4.0.0-commonMain-H79fAg.klib | Bin 0 -> 8430 bytes ...e-core-bundle-1.0.0-commonMain-cb_PMQ.klib | Bin 0 -> 4062 bytes ....core-core-bundle-1.0.0-jbMain-cb_PMQ.klib | Bin 0 -> 4305 bytes ...model-compose-2.8.0-commonMain-207ecg.klib | Bin 0 -> 4795 bytes ...viewmodel-compose-2.8.0-jbMain-207ecg.klib | Bin 0 -> 3376 bytes Makefile | 30 +- backend/README.md | 7 + .../gateway/build.gradle.kts | 0 .../gateway/docs/index.html | 0 .../gateway}/gateway/CONFIGURATION.md | 0 .../gateway}/gateway/README-INFRA-GATEWAY.md | 0 .../gateway/GatewayApplication.kt | 0 .../gateway/config/GatewayConfig.kt | 0 .../gateway/controller/FallbackController.kt | 0 .../gateway/health/GatewayHealthIndicator.kt | 0 .../gateway/metrics/GatewayMetricsConfig.kt | 0 .../gateway/security/SecurityConfig.kt | 0 .../main/resources/application-keycloak.yml | 0 .../src/main/resources/application.conf | 0 .../src/main/resources/application.yml | 0 .../src/main/resources/logback-spring.xml | 0 .../gateway/src/main/resources/logback.xml | 0 .../main/resources/openapi/documentation.yaml | 0 .../src/main/resources/static/docs/index.html | 0 .../postman/Meldestelle_API_Collection.json | 0 .../gateway/FallbackControllerTests.kt | 0 .../gateway/GatewayApplicationTests.kt | 0 .../gateway/GatewayFiltersTests.kt | 0 .../gateway/GatewayRoutingTests.kt | 0 .../gateway/GatewaySecurityTests.kt | 0 .../gateway/KeycloakGatewayIntegrationTest.kt | 0 .../gateway/config/TestSecurityConfig.kt | 0 .../src/test/resources/application-dev.yml | 0 .../application-keycloak-integration-test.yml | 0 .../src/test/resources/application-test.yml | 0 .../src/test/resources/logback-test.xml | 0 .../resources/test-init-keycloak-schema.sql | 0 .../services}/ping/ping-api/build.gradle.kts | 0 .../kotlin/at/mocode/ping/api/PingApi.kt | 0 .../kotlin/at/mocode/ping/api/PingData.kt | 0 .../ping/ping-service/build.gradle.kts | 2 +- .../at/mocode/ping/service/PingController.kt | 0 .../ping/service/PingServiceApplication.kt | 0 .../ping/service/PingServiceCircuitBreaker.kt | 0 .../service/config/SecurityConfiguration.kt | 0 .../src/main/resources/application.yml | 0 .../src/main/resources/logback-spring.xml | 0 .../service/PingControllerIntegrationTest.kt | 0 .../mocode/ping/service/PingControllerTest.kt | 0 .../service/PingServiceCircuitBreakerTest.kt | 0 .../src/test/resources/application-test.yml | 0 build.gradle.kts | 405 +++++++++++------- clients/auth-feature/build.gradle.kts | 4 +- clients/ping-feature/build.gradle.kts | 9 +- clients/shared/build.gradle.kts | 3 + .../mocode/clients/shared/di/SharedModule.kt | 9 +- compose.hardcoded.yaml | 190 -------- docker/.env.example | 43 ++ docker/docker-compose.clients.yml | 1 + docker/docker-compose.services.yml | 1 + compose.yaml => docker/docker-compose.yml | 21 +- dockerfiles/clients/desktop-app/Dockerfile | 11 +- dockerfiles/clients/web-app/Dockerfile | 15 +- docs/ARCHITECTURE.md | 67 +++ docs/adr/README.md | 13 + ...AntwortenOffenerFragenArchitekturReview.md | 160 +++++++ .../ProjectArchitecture_StructureGuide.md | 155 +++++++ frontend/README.md | 7 + frontend/core/.gitkeep | 0 .../core/design-system}/build.gradle.kts | 0 .../shared/commonui/components/AppFooter.kt | 0 .../shared/commonui/components/AppHeader.kt | 0 .../shared/commonui/components/AppScaffold.kt | 0 .../commonui/components/LoadingIndicator.kt | 0 .../commonui/components/MeldestelleButton.kt | 0 .../components/MeldestelleTextField.kt | 0 .../commonui/components/NotificationCard.kt | 0 .../clients/shared/commonui/theme/AppTheme.kt | 0 .../core}/navigation/build.gradle.kts | 0 .../clients/shared/navigation/AppScreen.kt | 0 frontend/features/.gitkeep | 0 .../meldestelle-portal}/build.gradle.kts | 9 +- .../src/commonMain/kotlin/DevelopmentMode.kt | 0 .../src/commonMain/kotlin/MainApp.kt | 0 .../commonTest/kotlin/ComposeAppCommonTest.kt | 0 .../src/jsMain/kotlin/DevelopmentMode.js.kt | 0 .../src/jsMain/kotlin/main.kt | 30 ++ .../src/jsMain/resources/icons/icon-192.png | Bin .../src/jsMain/resources/icons/icon-512.png | Bin .../src/jsMain/resources/index.html | 18 + .../src/jsMain/resources/manifest.webmanifest | 0 .../src/jsMain/resources/styles.css | 0 .../src/jsMain/resources/sw.js | 0 .../src/jvmMain/kotlin/DevelopmentMode.jvm.kt | 0 .../src/jvmMain/kotlin/main.kt | 9 + .../src/wasmJsMain/kotlin/main.kt | 0 .../webpack.config.d/webpack.config.js | 2 +- settings.gradle.kts | 43 +- 101 files changed, 857 insertions(+), 407 deletions(-) create mode 100644 .kotlin/metadata/kotlinTransformedMetadataLibraries/io.insert-koin-koin-compose-4.0.0-commonMain-QYyzSQ.klib create mode 100644 .kotlin/metadata/kotlinTransformedMetadataLibraries/io.insert-koin-koin-compose-viewmodel-4.0.0-commonMain-Lzy-yA.klib create mode 100644 .kotlin/metadata/kotlinTransformedMetadataLibraries/io.insert-koin-koin-core-4.0.0-commonMain-CvvNiA.klib create mode 100644 .kotlin/metadata/kotlinTransformedMetadataLibraries/io.insert-koin-koin-core-viewmodel-4.0.0-commonMain-H79fAg.klib create mode 100644 .kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.androidx.core-core-bundle-1.0.0-commonMain-cb_PMQ.klib create mode 100644 .kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.androidx.core-core-bundle-1.0.0-jbMain-cb_PMQ.klib create mode 100644 .kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.androidx.lifecycle-lifecycle-viewmodel-compose-2.8.0-commonMain-207ecg.klib create mode 100644 .kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.androidx.lifecycle-lifecycle-viewmodel-compose-2.8.0-jbMain-207ecg.klib create mode 100644 backend/README.md rename {infrastructure => backend}/gateway/build.gradle.kts (100%) rename {infrastructure => backend}/gateway/docs/index.html (100%) rename {infrastructure => backend/gateway}/gateway/CONFIGURATION.md (100%) rename {infrastructure => backend/gateway}/gateway/README-INFRA-GATEWAY.md (100%) rename {infrastructure => backend}/gateway/src/main/kotlin/at/mocode/infrastructure/gateway/GatewayApplication.kt (100%) rename {infrastructure => backend}/gateway/src/main/kotlin/at/mocode/infrastructure/gateway/config/GatewayConfig.kt (100%) rename {infrastructure => backend}/gateway/src/main/kotlin/at/mocode/infrastructure/gateway/controller/FallbackController.kt (100%) rename {infrastructure => backend}/gateway/src/main/kotlin/at/mocode/infrastructure/gateway/health/GatewayHealthIndicator.kt (100%) rename {infrastructure => backend}/gateway/src/main/kotlin/at/mocode/infrastructure/gateway/metrics/GatewayMetricsConfig.kt (100%) rename {infrastructure => backend}/gateway/src/main/kotlin/at/mocode/infrastructure/gateway/security/SecurityConfig.kt (100%) rename {infrastructure => backend}/gateway/src/main/resources/application-keycloak.yml (100%) rename {infrastructure => backend}/gateway/src/main/resources/application.conf (100%) rename {infrastructure => backend}/gateway/src/main/resources/application.yml (100%) rename {infrastructure => backend}/gateway/src/main/resources/logback-spring.xml (100%) rename {infrastructure => backend}/gateway/src/main/resources/logback.xml (100%) rename {infrastructure => backend}/gateway/src/main/resources/openapi/documentation.yaml (100%) rename {infrastructure => backend}/gateway/src/main/resources/static/docs/index.html (100%) rename {infrastructure => backend}/gateway/src/main/resources/static/docs/postman/Meldestelle_API_Collection.json (100%) rename {infrastructure => backend}/gateway/src/test/kotlin/at/mocode/infrastructure/gateway/FallbackControllerTests.kt (100%) rename {infrastructure => backend}/gateway/src/test/kotlin/at/mocode/infrastructure/gateway/GatewayApplicationTests.kt (100%) rename {infrastructure => backend}/gateway/src/test/kotlin/at/mocode/infrastructure/gateway/GatewayFiltersTests.kt (100%) rename {infrastructure => backend}/gateway/src/test/kotlin/at/mocode/infrastructure/gateway/GatewayRoutingTests.kt (100%) rename {infrastructure => backend}/gateway/src/test/kotlin/at/mocode/infrastructure/gateway/GatewaySecurityTests.kt (100%) rename {infrastructure => backend}/gateway/src/test/kotlin/at/mocode/infrastructure/gateway/KeycloakGatewayIntegrationTest.kt (100%) rename {infrastructure => backend}/gateway/src/test/kotlin/at/mocode/infrastructure/gateway/config/TestSecurityConfig.kt (100%) rename {infrastructure => backend}/gateway/src/test/resources/application-dev.yml (100%) rename {infrastructure => backend}/gateway/src/test/resources/application-keycloak-integration-test.yml (100%) rename {infrastructure => backend}/gateway/src/test/resources/application-test.yml (100%) rename {infrastructure => backend}/gateway/src/test/resources/logback-test.xml (100%) rename {infrastructure => backend}/gateway/src/test/resources/test-init-keycloak-schema.sql (100%) rename {services => backend/services}/ping/ping-api/build.gradle.kts (100%) rename {services => backend/services}/ping/ping-api/src/commonMain/kotlin/at/mocode/ping/api/PingApi.kt (100%) rename {services => backend/services}/ping/ping-api/src/commonMain/kotlin/at/mocode/ping/api/PingData.kt (100%) rename {services => backend/services}/ping/ping-service/build.gradle.kts (97%) rename {services => backend/services}/ping/ping-service/src/main/kotlin/at/mocode/ping/service/PingController.kt (100%) rename {services => backend/services}/ping/ping-service/src/main/kotlin/at/mocode/ping/service/PingServiceApplication.kt (100%) rename {services => backend/services}/ping/ping-service/src/main/kotlin/at/mocode/ping/service/PingServiceCircuitBreaker.kt (100%) rename {services => backend/services}/ping/ping-service/src/main/kotlin/at/mocode/ping/service/config/SecurityConfiguration.kt (100%) rename {services => backend/services}/ping/ping-service/src/main/resources/application.yml (100%) rename {services => backend/services}/ping/ping-service/src/main/resources/logback-spring.xml (100%) rename {services => backend/services}/ping/ping-service/src/test/kotlin/at/mocode/ping/service/PingControllerIntegrationTest.kt (100%) rename {services => backend/services}/ping/ping-service/src/test/kotlin/at/mocode/ping/service/PingControllerTest.kt (100%) rename {services => backend/services}/ping/ping-service/src/test/kotlin/at/mocode/ping/service/PingServiceCircuitBreakerTest.kt (100%) rename {services => backend/services}/ping/ping-service/src/test/resources/application-test.yml (100%) delete mode 100644 compose.hardcoded.yaml create mode 100644 docker/.env.example create mode 100644 docker/docker-compose.clients.yml create mode 100644 docker/docker-compose.services.yml rename compose.yaml => docker/docker-compose.yml (94%) create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/adr/README.md create mode 100644 docs/clients/visionen/AntwortenOffenerFragenArchitekturReview.md create mode 100644 docs/clients/visionen/ProjectArchitecture_StructureGuide.md create mode 100644 frontend/README.md create mode 100644 frontend/core/.gitkeep rename {clients/shared/common-ui => frontend/core/design-system}/build.gradle.kts (100%) rename {clients/shared/common-ui => frontend/core/design-system}/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/AppFooter.kt (100%) rename {clients/shared/common-ui => frontend/core/design-system}/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/AppHeader.kt (100%) rename {clients/shared/common-ui => frontend/core/design-system}/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/AppScaffold.kt (100%) rename {clients/shared/common-ui => frontend/core/design-system}/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/LoadingIndicator.kt (100%) rename {clients/shared/common-ui => frontend/core/design-system}/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/MeldestelleButton.kt (100%) rename {clients/shared/common-ui => frontend/core/design-system}/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/MeldestelleTextField.kt (100%) rename {clients/shared/common-ui => frontend/core/design-system}/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/NotificationCard.kt (100%) rename {clients/shared/common-ui => frontend/core/design-system}/src/commonMain/kotlin/at/mocode/clients/shared/commonui/theme/AppTheme.kt (100%) rename {clients/shared => frontend/core}/navigation/build.gradle.kts (100%) rename {clients/shared => frontend/core}/navigation/src/commonMain/kotlin/at/mocode/clients/shared/navigation/AppScreen.kt (100%) create mode 100644 frontend/features/.gitkeep rename {clients/app => frontend/shells/meldestelle-portal}/build.gradle.kts (93%) rename {clients/app => frontend/shells/meldestelle-portal}/src/commonMain/kotlin/DevelopmentMode.kt (100%) rename {clients/app => frontend/shells/meldestelle-portal}/src/commonMain/kotlin/MainApp.kt (100%) rename {clients/app => frontend/shells/meldestelle-portal}/src/commonTest/kotlin/ComposeAppCommonTest.kt (100%) rename {clients/app => frontend/shells/meldestelle-portal}/src/jsMain/kotlin/DevelopmentMode.js.kt (100%) rename {clients/app => frontend/shells/meldestelle-portal}/src/jsMain/kotlin/main.kt (59%) rename {clients/app => frontend/shells/meldestelle-portal}/src/jsMain/resources/icons/icon-192.png (100%) rename {clients/app => frontend/shells/meldestelle-portal}/src/jsMain/resources/icons/icon-512.png (100%) rename {clients/app => frontend/shells/meldestelle-portal}/src/jsMain/resources/index.html (56%) rename {clients/app => frontend/shells/meldestelle-portal}/src/jsMain/resources/manifest.webmanifest (100%) rename {clients/app => frontend/shells/meldestelle-portal}/src/jsMain/resources/styles.css (100%) rename {clients/app => frontend/shells/meldestelle-portal}/src/jsMain/resources/sw.js (100%) rename {clients/app => frontend/shells/meldestelle-portal}/src/jvmMain/kotlin/DevelopmentMode.jvm.kt (100%) rename {clients/app => frontend/shells/meldestelle-portal}/src/jvmMain/kotlin/main.kt (50%) rename {clients/app => frontend/shells/meldestelle-portal}/src/wasmJsMain/kotlin/main.kt (100%) rename {clients/app => frontend/shells/meldestelle-portal}/webpack.config.d/webpack.config.js (99%) diff --git a/.kotlin/metadata/kotlinTransformedMetadataLibraries/io.insert-koin-koin-compose-4.0.0-commonMain-QYyzSQ.klib b/.kotlin/metadata/kotlinTransformedMetadataLibraries/io.insert-koin-koin-compose-4.0.0-commonMain-QYyzSQ.klib new file mode 100644 index 0000000000000000000000000000000000000000..233c148a69eb275ac6b0b7efd34d2a0c57eb97ac GIT binary patch literal 7621 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 KpbGPK0N}sZr>SKC literal 0 HcmV?d00001 diff --git a/.kotlin/metadata/kotlinTransformedMetadataLibraries/io.insert-koin-koin-compose-viewmodel-4.0.0-commonMain-Lzy-yA.klib b/.kotlin/metadata/kotlinTransformedMetadataLibraries/io.insert-koin-koin-compose-viewmodel-4.0.0-commonMain-Lzy-yA.klib new file mode 100644 index 0000000000000000000000000000000000000000..4bf9569dbc5ca23dfe0691495a0f3c968d51827a GIT binary patch literal 6240 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 literal 0 HcmV?d00001 diff --git a/.kotlin/metadata/kotlinTransformedMetadataLibraries/io.insert-koin-koin-core-4.0.0-commonMain-CvvNiA.klib b/.kotlin/metadata/kotlinTransformedMetadataLibraries/io.insert-koin-koin-core-4.0.0-commonMain-CvvNiA.klib new file mode 100644 index 0000000000000000000000000000000000000000..f936634a9f39368d260bfca038094693362484ea GIT binary patch literal 45233 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

McyPmtzT#S}W4?9;`VO9mu)0{>3xk3T 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 l-(P3qyAg5>4p zK_Jj-;Ug+}kOGJl972quL}IqM%Ywk+(PJtQ5Wz_fSPPQ*ypr%$0fd;qE@D_{NN_~t z7a`{q96VlE2{cw~e}qMcTQ^u9D@qB~vxf=x`)=}hL9_C?XPbBA9G-00-JsTV-6-_X z_HO0+XA6)CS5OmZtG+h~)KIDhJMd?DWPFx5;2naV}Tb*Q`$u(MNxvk_51Tr?Jp3&Et?Az%toEM#ZU-z-U zW@vStMb`^bw*SZ~i^-_pZh5!%-Mx-#jkl3{f`CsLI`f4NDu-QD!s zMa`5g{S2O68iq$2JYzf}h{?Sw(8{^D@j;HD;MS>RcU$~Vn>b=g&|e4-P{AlZ zKI9i!6op_rVN$Oy;HNB%ORG+}A{*yXnbd1{dc2$X;TqB=sYX!NO+Ee2)$0g?(18(e{jGn@Uni3mT4(hZZ%i;>MVfYvQ}}^~}hYCvyx64wm39 zcQH*|P2-glk1HExB*up1`Cxrz(z16gOTjnznB`Tj*!*-1uVgrW_3&CAgO1AnP4V4L zKdtrQz4 z?Na43jI9kejxISrpJC{H5$Rl78TtB`PHq)1E0;&=TbWr^b2q|fUnmECZfT6MVFTvY zyJp0%Sqt*he}^U~a*s08Gi5F1Y886Z>^mJygE#>kA|)VAy-z(8iw^R+E-=5b4O^Qu zvMiYz5Vv5cPyOzF(&>Dk!yNs7`!SnjWWWc+(1sP!k8CXOwmlG(h1iU0=|5JhG-D5+ zD7|Ar;O~7me1G*8uTzI=3hFdF4&sZ0qngr2{dRYAO~CGAV-aSYkQFNj34UzN3OF`0~8Tc`GBP2{7V4`r1P^(=~;?6T;- zP*eDG3&O(zFeHJFQD@PLo9$BQ`=HOXw6L9z29`jW^tDh5yp~k|a9}A^4KFwerqeel zmE&W$^?Gvs*v`KPemek{>o*^ug>&mw>dc}`>$02@oG7qYdkEsI@ zL&^e7x!=Kpd9L6bbL$MOZ%|EcbFL0Mc2kTqdj%_TtFw(`%lYE$t9|j@MDFcL8CJ&D zqp_6#YC_UrB!|7egjTN$@kLLPF6R7e^F`DFlZ%%ojjmZ7f4{A~^vn?X=B1H}H_d_w zqUxj^aDQibIll6k2Lf?mv)$j{Kge*>2H7%(Evyl%dhPY4Q0q*Bc0w6_vw{me;mM=? z0kk{YEzLbp_CfZC5(}{^ z)X*+#Jfk}n9EH-#Eg8GFFi^*>#hwql3_<>}RC9E7^(E$#6Zwdw9cqR=EjOH*lEM`w zzbAMBhQ~-j5;tzY6B^o;x#F2Zs~jn|?zYiMYOidWH7XH%$3&Ny2*oY^5A$Vn_+p#- z99>NL$BHg0yBs|V29E3=FS(*oc)h%v^@`DvHv4H*YNY>XMEJ4G)8L!3SpR94$B-Be zfInT42}copxUhGnS^HNJGol4YP@`x;!4b3f43<$9qJ=o;P5~^yxzlR~^%9(>Zx;tk zfO16zAoSGs8O$pw18ke>0-#V)JrTa0l9|b*k~-o@2r#Iq%)wH0B#2Z}4cI-^6@ZbV zs*$Glb*PjS7AHIaB1NSx^qUcPhVWOAQql&Ho$4RJMNy^C#YO@iCDp|l4uC~bjm}+N z3OY*q0J>8t03sAs3}g<6^(n;+K9n>OAF#j}6;=Otn@FWYNkj4RG~JLT{8RbOpTM+K zQm442A&_Y57=fuLsy^X|8HUnlprokykOF3&sLU0=B`PriC8YtWseXYeC#q9o(o$v~ V!ATYhEQSkTgTg!WZ{czf=pQ3g0VV(d literal 0 HcmV?d00001 diff --git a/.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.androidx.core-core-bundle-1.0.0-jbMain-cb_PMQ.klib b/.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.androidx.core-core-bundle-1.0.0-jbMain-cb_PMQ.klib new file mode 100644 index 0000000000000000000000000000000000000000..5172f08c976586fd155e71fd593c460830c1ca4e GIT binary patch literal 4305 zcmbW4c{r47AIArceQTmR!jUCpoly3r$d;WEm1!`RFl3KY9K(@}$dV4;ld>1bn%&V* z$TF4)$r6%nUh5H|-g$b{jIpGCb3NBJf86)?`}^M4ef^%#W2j5Dj}1giOA7*l)Cdo; zXhC!!TcjPr%h^-H%oGfwaP$5Ep#mA|(g15glwVgG?kK=YBH)5RIocsTJiiI)H9?sJ zc^FmOtL%+&^>RjjOFaU~*TzZcbm<*$(S4yE(W*1A*W6rKXq`8# z)vPPr*m^E2Z#X1>QT2F^x?!mwYlJ?dz8jN%CdZdkKPF;-OpXx7de=a_+qL0_uyI1z zBjE^?t-GtEt?$nzl-!fF_CzJ~@x;3kHl(p~E=gYUk`JfVMLMl>elwWey@}|-s zBxGXSHJ9LC%*Ab7-H|^R4!U=JNjPA$W4cpc)18)(s}^Ga5)oIPwRD6K@0VR8ito7v zaceJ>t@BSNmH%NIN%;T$Hbr)c*Y)XBXRFT%b-5<`{6OE;q|~suBEvI|SB#UX>7^&o zi|bnO$~$=NN3gpXfe+Hzvi-9y%OFvU{L#GWTLMxZGd(fI3@ufk2d_Jv@R{a}QPFuu zH;k2~qmj7TZ%~I}^NaCiy;^}@vWV6|oXf^9N14{hHrMc!AI1dTxW@&Vx{|ct-&gAJ z9e0&5-dC+P{Q7idaxa3co~bA2#YfaG2bQ*7U2t?=usA!veiLkCtun-=;gSSKBw5*q zrk2L=E7rkRFd|cd$l#0CDbOjJUyei#N2NKsJ#FZx5nd=1T$AI>f9aUly5{-pa9~8$@hH<#|zF$w;L>_{c7htZXY&i^Xc9kQ^EVnl3IAN5;sjKKTBPy!NupO zV8^pMQt`8auOBH~Ud8e^a#Obl-)R)3ba}(~h`+|IT;nab9j3?K%gB15nf=`-7T|LJ(T@YTwZ{bm7TihZqVwjBpvo>e%d5oP=;L^e&t2r_##XpSvVwtbC~ zB}~DXI-Dupib+!8@<=cCmdeuE8dr>%`5^%m%;VaS_l$70hr6!7K{Rv_a@c_4P4rm! ziOOgXlS8MwQaHR1q{oNy-RNdmlmG8rlS5dVWP)v#`TnL1_Q0m?> zKIB^#gnfv9sVoE^<`2R9JE*8ugcYN`8SwQE6%HzAR>7}xXV>G63hhvGC3BG%WZdODCWZokZYUDS56O{#C~3Gct814MPQ|+UW z()f@Qmtr7y|I;M7C_zJ5@@O#3m}0!$2O1jWoRSn8T?{kE!;Zec-kkNjVn_kAKt@rt zylUiMbxGhTd&85-qZn8RXuO>TYQKNfB9=NfsY{13kb9lhBY{Unt!&>sXCrdlO@Atn zu`T7kHI5#$xNu6-k5RMy)7|9hcb@;5=xnuVKVCUsdAFFpEd_3k<7i+I5)&#*jw>n` zYv`@x{B@MM(d%gVBJZ0F-m1?2z+jOJ^JBF+x$U_bNFg%HnZ7sY9+9% zgPT4!kt18kR~K!H?c+!?pS?}(L_g4V`4ZNjso%nz$2z&ky5+mQEKK6M z?CjO%sXrV{>yDj@Zid>z3kyYiZ=GMMXu(0V%*)K9ujNeWTNXUz@$R=6nR^uw8~}a) zNxdL-EQ*AL_!T}C0Rpk>ht@% z?^?2-HmcV@XVgiTA1BVPWk^R2JEd*p;1+d+zuap)(ssJJL1IX&$?d~pm5=-cqP49x z`|+hednX6}VULG`exPWkT zcIh8NV&snWaP@MxL3-@oGX*6ZfvHH*euLQowBKA?K;KYz^LA322Ed+Jz*ob@_AQuC zE(2`a=mJ1*VvQ00Zpdr}adI6}76!nZSdxTmutkRq!^zcv-5Xs2{7kHp?bLRrX>wsw zv<2vySP+8WmNvHt@4(XJHh}C#{{SZwD{?P3WYkQqP71F8B@=6A@9NtyGr13-yO9Dw zVPd&c?cuP#q}T$+iuDpZLyf#kaRq4HZ&o)fAXCS#K>5u|u9gFycSgeQ$l literal 0 HcmV?d00001 diff --git a/.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.androidx.lifecycle-lifecycle-viewmodel-compose-2.8.0-commonMain-207ecg.klib b/.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.androidx.lifecycle-lifecycle-viewmodel-compose-2.8.0-commonMain-207ecg.klib new file mode 100644 index 0000000000000000000000000000000000000000..b8bb10e884513377f7a99d933997a9753db7a9cd GIT binary patch literal 4795 zcmbtXdpOg39G)?^$lM~K^B|XUS%fJeCD%nX9L3BwTFlBNw=}m-$7SdgrN}M!P739I z%cUdNI6Pu5<&x7)Dx|{MuP3%9zCUjR+L|v{UeaFc?`$#x^G2@P=kaJQ#5=V8 ztPcEE&Xj#lOhmQo&X>+|6GawTh*R%tdB>jVSqjwO*BtdxzGB^I>b5W8tYJ@V)s9RN zC-1F5(@D3WD(=*#qp9!Jo93r$+70jSibZ>&*C$_u84`hr^>)K8Phq6&Lo$S<#JLzUXHrd%mzO`b$!T+uE#6xto@ zLBwNy*QX!$L(s5g7G+m=0&Or(P^VZY-lB($b3 z#UCM8cB7t2j334gw?2h?ev$-+S|(ggtf~B0vHB`l7mNcl7#=R-Y`IJfl;z+l~UFw`>js-uuyF$!e9qS{?;P2Fwk9AAb4Re(^oQwJiy z=1_Fj+odvwNH|IKT<7U9{bgDh>T3hvj405mn%HFly_ZZH9s3JzBL}&tAG@7BQ{L&} z)gx)f3S1th!H}kUK5X}$&=I)TexBc^G~X=NP^weZFqzSbD6>ML_}PKL*#RGy-#Myx z4Lgj=of;;Ec!*YrbVcIjQxYLg>XCu``gpPS@hSZvPjU0gH&NW}@9rCm$vG878~~5| zCH}FG%}%#0$g&`+(@{2MS}MG-W_Ix0Q$0I2Jk+q0O|1Cw%XwG5XWAL+@y1eVLXMdD zesFR#zZ1*dR@jqdzbBS@(&ep-_03_Vzx;DVI}OiEwR^=p%>#J46!%VM%i zpN^;+)jO%08}5=7+B`2ZVypija!4Yk@%#**u;}&_PYBW>ot|yB;)LPydrhh3UF88>8pVXjA?X7DOBcz5 z4!2GSOzGvG4&ckLtpbl}YJUvaoOG>jfo~Fr$ z5)wIsSjl0;Y!t@hdd{f)2DMW{ZQYd3kV@NTmf4f%SYL|XER>1F@^oiSflsJfOKLYr zxIT=HGW1i!)g-8pgQuQD6FcRK#$9zX{be^fO5k#$%xiVH42z0_4(9kL$a-qYrp@29 zEv)~f$m=qf68^fvJ2nD#(fBi=YBb(s?D+mh)r)8uQkq6v;XB_O*)gdIH~w~cl+%Mq zg;$qf`$5kYZYHF`!|UZ)UwjhJz8=BjcvaTwg0p2yO+do4Na6vRL-;)=b>yuE@UuxK zym;>0`)kZKH#*Kt!OU>e=?_3NJ+BIPCuLc2br-+m>K$s8(+%@#bG|v)-#k*`6_H(0 z@n^aLJ|oi?|4xOST@~gB;k^y7%e8rgBo88sIMp8YTnXQZ+yq8)52PF)mL1#Zj*mi{ z8glqV^mUyQ+*8I{w>S5>Z-%42%2VFT=fgTkrOy@F?uWY@`>%F2?lE{!er`P2xw|Q= zq5Q+`j`!JRAu-&Dp*$~AzBm7l(1&j>CqL$Z`Mv_TxNn)O`e;;4bQ&n;xSM*4rFULn z%cqUCh?wbwuHus5ew~Tl8BH(Bw7o?6!gvrLY0RKGZ(xCh*K&AQtn6APMgDu23Hqta z*?Ce+kQXIS8tU+qO#LG*&4bGI9>}raeYA=a0gnAeHWC$D;S{xsHJ`ZKcLkis(CEkK_>2CVG5y|BM1bgzIdpg J5&*9O{SP0V@cRG& literal 0 HcmV?d00001 diff --git a/.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.androidx.lifecycle-lifecycle-viewmodel-compose-2.8.0-jbMain-207ecg.klib b/.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.androidx.lifecycle-lifecycle-viewmodel-compose-2.8.0-jbMain-207ecg.klib new file mode 100644 index 0000000000000000000000000000000000000000..48437ca13382c25321d73a44a0e3abace22b9213 GIT binary patch literal 3376 zcmWIWW@Zs#;Nak3U|?_rVg%q|;ABWiO-n4zDbWuNVP#+}DEltN%n;zs4pPg&1XUS; z(*OY^19B7dGSgCvOW+3i_FD5DG7w<>ZOgr}U_rwn_Xj-Jj#Vyj{U-It^T^x4E|1J~ z6RCMe^ff{^S!L%v^HE&h?=ppX-|UZ0K{ZpS7rkcPz53idH?Itv;>XT~rqgzv+J0DD zeTRmBOMHif*uhz!dMt7l$2`1$x!Lt4Kgh|8Qs*${0G-?g#JHU-h~(s)%)IQB#F9kv zJTH#Tq}=?J(wtOy7(5Qzc=Du=x9>WibDmdqPy6VeKkM&#_M-2(^SUSW{d&)PeeykX zX7lEx4Uas|cx#?I@3V4)P}9aQ53JUv7##ibU|HnK2a~Th-+FrF0n7HooZYg16>mYF zW9LxVnLjTS=+P5+J*$Mxi3N$t*@@|?@rijUMfsU273BHefGDF4;I`;x=j9@TB}pJ@ zLQ=(nqd&U3l@0ZmYSyvS? z)((qeqsu{)wI>$`?QGe&a?0nE{(ff;DlMKqb;gZD*F;5>RsgsdxS7vZu(nvXSROP^lqlXSE95{0H*g=g0ItLk^ZDjIHV>DYJ(am5T z(G%Ff?6HY+gV#xqkDLY(1{DTzkCGlG)E+qcsdHj)w`#OznS&g!eg`y`d=LU zyO4c*F#jtvaQyAvoT7Ie7Yynx6pI;IWEvU##yeb5ZHHb$Ic%(9k5$yh^ zZh@zKK=!5zY7bN& zBLJwfhiS*CozaZ}X@j`}R7WDf7a$X63}%gqZVqzA1gaqszz&!UFw8-#FVRf{>4v!q zRGA^bLu{tuuG-KIMy_W-RT%=<0PVwYCWgT{DmHX$K*qw{52~~fU?(xwV5`E=tw63i zKvfn3$T1V)6C^8eRbc2gf^2|!3REQ_zy{K6#8yF}TZUW-fGQ*ekY^#udq|eys-Dp8 z2H6GkD5#o2fTQHvjaliSTZ&w2f+`sVuwf().configureEach { - useJUnitPlatform { - excludeTags("perf") - } - // Configure CDS in auto-mode to prevent bootstrap classpath warnings - jvmArgs("-Xshare:auto", "-Djdk.instrument.traceUsage=false") - // Increase test JVM memory with a stable configuration - minHeapSize = "512m" - maxHeapSize = "2g" - // Parallel test execution for better performance - maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1) - // Removed byte-buddy-agent configuration to fix Gradle 9.0.0 deprecation warning - // The agent configuration was causing Task.project access at execution time + tasks.withType().configureEach { + useJUnitPlatform { + excludeTags("perf") } + // Configure CDS in auto-mode to prevent bootstrap classpath warnings + jvmArgs("-Xshare:auto", "-Djdk.instrument.traceUsage=false") + // Increase test JVM memory with a stable configuration + minHeapSize = "512m" + maxHeapSize = "2g" + // Parallel test execution for better performance + maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1) + // Removed byte-buddy-agent configuration to fix Gradle 9.0.0 deprecation warning + // The agent configuration was causing Task.project access at execution time + } - // Erzwinge eine stabile Version von kotlinx-serialization-json für alle Konfigurationen, - // um Auflösungsfehler (z.B. 1.10.2, nicht verfügbar auf Maven Central) zu vermeiden - configurations.configureEach { - resolutionStrategy { - force("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") - } + // Erzwinge eine stabile Version von kotlinx-serialization-json für alle Konfigurationen, + // um Auflösungsfehler (z.B. 1.10.2, nicht verfügbar auf Maven Central) zu vermeiden + configurations.configureEach { + resolutionStrategy { + force("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") } + } - // Dedicated performance test task per JVM subproject - plugins.withId("java") { - val javaExt = extensions.getByType() - // Ensure a full JDK toolchain with compiler is available (Gradle will auto-download if missing) - javaExt.toolchain.languageVersion.set(JavaLanguageVersion.of(21)) + // Dedicated performance test task per JVM subproject + plugins.withId("java") { + val javaExt = extensions.getByType() + // Ensure a full JDK toolchain with compiler is available (Gradle will auto-download if missing) + javaExt.toolchain.languageVersion.set(JavaLanguageVersion.of(21)) - tasks.register("perfTest") { - description = "Runs tests tagged with 'perf'" - group = "verification" - // Use the regular test source set outputs - testClassesDirs = javaExt.sourceSets.getByName("test").output.classesDirs - classpath = javaExt.sourceSets.getByName("test").runtimeClasspath - useJUnitPlatform { - includeTags("perf") + tasks.register("perfTest") { + description = "Runs tests tagged with 'perf'" + group = "verification" + // Use the regular test source set outputs + testClassesDirs = javaExt.sourceSets.getByName("test").output.classesDirs + classpath = javaExt.sourceSets.getByName("test").runtimeClasspath + useJUnitPlatform { + includeTags("perf") + } + shouldRunAfter("test") + // Keep the same JVM settings for consistency + jvmArgs("-Xshare:auto", "-Djdk.instrument.traceUsage=false") + maxHeapSize = "2g" + dependsOn("testClasses") + } + } + + // Suppress Node.js deprecation warnings (e.g., DEP0040 punycode) during Kotlin/JS npm/yarn tasks + // Applies to all Exec-based tasks (covers Yarn/NPM invocations used by Kotlin JS plugin) + tasks.withType().configureEach { + // Merge existing NODE_OPTIONS with --no-deprecation + val current = (environment["NODE_OPTIONS"] as String?) ?: System.getenv("NODE_OPTIONS") + val merged = if (current.isNullOrBlank()) "--no-deprecation" else "$current --no-deprecation" + environment("NODE_OPTIONS", merged) + // Also set the legacy switch to silence warnings entirely + environment("NODE_NO_WARNINGS", "1") + // Set Chrome binary path to avoid snap permission issues + environment("CHROME_BIN", "/usr/bin/google-chrome-stable") + environment("CHROMIUM_BIN", "/usr/bin/chromium") + environment("PUPPETEER_EXECUTABLE_PATH", "/usr/bin/chromium") + } + + tasks.withType { + compilerOptions { + freeCompilerArgs.add("-Xannotation-default-target=param-property") + } + } + + // ------------------------------ + // Detekt & Ktlint default setup + // ------------------------------ + plugins.withId("io.gitlab.arturbosch.detekt") { + extensions.configure(io.gitlab.arturbosch.detekt.extensions.DetektExtension::class.java) { + buildUponDefaultConfig = true + allRules = false + autoCorrect = false + config.setFrom(files(rootProject.file("config/detekt/detekt.yml"))) + basePath = rootDir.absolutePath + } + tasks.withType().configureEach { + jvmTarget = "21" + reports { + xml.required.set(false) + txt.required.set(false) + sarif.required.set(false) + html.required.set(true) + } + } + } + + plugins.withId("org.jlleitschuh.gradle.ktlint") { + extensions.configure(org.jlleitschuh.gradle.ktlint.KtlintExtension::class.java) { + android.set(false) + outputToConsole.set(true) + ignoreFailures.set(false) + reporters { + reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.CHECKSTYLE) + reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.PLAIN) + } + } + } +} + +// ================================================================== +// Architecture Guards (lightweight, fast checks) +// ================================================================== + +// Fails if any source file contains manual Authorization header setting. +// Policy: Authorization must be injected by the DI-provided HttpClient (apiClient). +tasks.register("archGuardForbiddenAuthorizationHeader") { + group = "verification" + description = "Fail build if code sets Authorization header manually." + doLast { + val forbiddenPatterns = + listOf( + ".header(\"Authorization\"", + "setHeader(\"Authorization\"", + "headers[\"Authorization\"]", + "headers[\'Authorization\']", + ) + // Scope: Frontend-only enforcement. Backend/Test code is excluded. + val srcDirs = listOf("clients", "frontend") + val violations = mutableListOf() + srcDirs.map { file(it) } + .filter { it.exists() } + .forEach { rootDir -> + rootDir.walkTopDown() + .filter { it.isFile && (it.extension == "kt" || it.extension == "kts") } + .forEach { f -> + val text = f.readText() + // Skip test sources + val path = f.invariantSeparatorsPath + val isTest = + path.contains("/src/commonTest/") || + path.contains("/src/jsTest/") || + path.contains("/src/jvmTest/") || + path.contains("/src/test/") + if (!isTest && forbiddenPatterns.any { text.contains(it) }) { + violations += f } - shouldRunAfter("test") - // Keep the same JVM settings for consistency - jvmArgs("-Xshare:auto", "-Djdk.instrument.traceUsage=false") - maxHeapSize = "2g" - dependsOn("testClasses") + } + } + if (violations.isNotEmpty()) { + val msg = + buildString { + appendLine("Forbidden manual Authorization header usage found in:") + violations.take(50).forEach { appendLine(" - ${it.path}") } + if (violations.size > 50) appendLine(" ... and ${violations.size - 50} more files") + appendLine() + appendLine("Policy: Use DI-provided apiClient (Koin named \"apiClient\").") } + throw GradleException(msg) } + } +} - // Suppress Node.js deprecation warnings (e.g., DEP0040 punycode) during Kotlin/JS npm/yarn tasks - // Applies to all Exec-based tasks (covers Yarn/NPM invocations used by Kotlin JS plugin) - tasks.withType().configureEach { - // Merge existing NODE_OPTIONS with --no-deprecation - val current = (environment["NODE_OPTIONS"] as String?) ?: System.getenv("NODE_OPTIONS") - val merged = if (current.isNullOrBlank()) "--no-deprecation" else "$current --no-deprecation" - environment("NODE_OPTIONS", merged) - // Also set the legacy switch to silence warnings entirely - environment("NODE_NO_WARNINGS", "1") - // Set Chrome binary path to avoid snap permission issues - environment("CHROME_BIN", "/usr/bin/google-chrome-stable") - environment("CHROMIUM_BIN", "/usr/bin/chromium") - environment("PUPPETEER_EXECUTABLE_PATH", "/usr/bin/chromium") - } +// Aggregate convenience task +tasks.register("archGuards") { + group = "verification" + description = "Run all architecture guard checks" + dependsOn("archGuardForbiddenAuthorizationHeader") +} - tasks.withType { - compilerOptions { - freeCompilerArgs.add("-Xannotation-default-target=param-property") - } - } +// Composite verification task including static analyzers if present +tasks.register("staticAnalysis") { + group = "verification" + description = "Run static analysis (detekt, ktlint) and architecture guards" + // These tasks are provided by plugins; only depend if tasks exist + dependsOn( + tasks.matching { it.name == "detekt" }, + tasks.matching { it.name == "ktlintCheck" }, + tasks.named("archGuards"), + ) } // ################################################################## @@ -117,70 +226,80 @@ subprojects { // Apply Dokka automatically to Kotlin subprojects to enable per-module docs subprojects { - plugins.withId("org.jetbrains.kotlin.jvm") { - apply(plugin = "org.jetbrains.dokka") - } - plugins.withId("org.jetbrains.kotlin.multiplatform") { - apply(plugin = "org.jetbrains.dokka") - } + plugins.withId("org.jetbrains.kotlin.jvm") { + apply(plugin = "org.jetbrains.dokka") + } + plugins.withId("org.jetbrains.kotlin.multiplatform") { + apply(plugin = "org.jetbrains.dokka") + } - // Minimal sourceLink configuration when running in GitHub Actions - tasks.withType(org.jetbrains.dokka.gradle.DokkaTask::class.java).configureEach { - dokkaSourceSets.configureEach { - val repo = System.getenv("GITHUB_REPOSITORY") - if (!repo.isNullOrBlank()) { - sourceLink { - localDirectory.set(project.file("src")) - remoteUrl.set(java.net.URI.create("https://github.com/$repo/blob/main/" + project.path.trimStart(':').replace(':', '/') + "/src").toURL()) - } - } - // Keep module names short and stable - moduleName.set(project.path.trimStart(':')) + // Minimal sourceLink configuration when running in GitHub Actions + tasks.withType(org.jetbrains.dokka.gradle.DokkaTask::class.java).configureEach { + dokkaSourceSets.configureEach { + val repo = System.getenv("GITHUB_REPOSITORY") + if (!repo.isNullOrBlank()) { + sourceLink { + localDirectory.set(project.file("src")) + remoteUrl.set( + java.net.URI.create( + "https://github.com/$repo/blob/main/" + project.path.trimStart(':').replace(':', '/') + "/src", + ).toURL(), + ) } + } + // Keep module names short and stable + moduleName.set(project.path.trimStart(':')) } + } } // Aggregate tasks to build multi-module docs in Markdown (GFM) and HTML -val dokkaGfmAll = tasks.register("dokkaGfmAll") { +val dokkaGfmAll = + tasks.register("dokkaGfmAll") { group = "documentation" description = "Builds Dokka GFM for all modules and aggregates outputs under build/dokka/gfm" // Depend on all dokkaGfm tasks that exist in subprojects - dependsOn(subprojects + dependsOn( + subprojects .filter { it.plugins.hasPlugin("org.jetbrains.dokka") } - .map { "${it.path}:dokkaGfm" }) + .map { "${it.path}:dokkaGfm" }, + ) doLast { - val dest = layout.buildDirectory.dir("dokka/gfm").get().asFile - if (dest.exists()) dest.deleteRecursively() - dest.mkdirs() - subprojects.filter { it.plugins.hasPlugin("org.jetbrains.dokka") }.forEach { p -> - val out = p.layout.buildDirectory.dir("dokka/gfm").get().asFile - if (out.exists()) { - out.copyRecursively(File(dest, p.path.trimStart(':').replace(':', '/')), overwrite = true) - } + val dest = layout.buildDirectory.dir("dokka/gfm").get().asFile + if (dest.exists()) dest.deleteRecursively() + dest.mkdirs() + subprojects.filter { it.plugins.hasPlugin("org.jetbrains.dokka") }.forEach { p -> + val out = p.layout.buildDirectory.dir("dokka/gfm").get().asFile + if (out.exists()) { + out.copyRecursively(File(dest, p.path.trimStart(':').replace(':', '/')), overwrite = true) } - println("[DOKKA] Aggregated GFM into ${dest.absolutePath}") + } + println("[DOKKA] Aggregated GFM into ${dest.absolutePath}") } -} + } -val dokkaHtmlAll = tasks.register("dokkaHtmlAll") { +val dokkaHtmlAll = + tasks.register("dokkaHtmlAll") { group = "documentation" description = "Builds Dokka HTML for all modules and aggregates outputs under build/dokka/html" - dependsOn(subprojects + dependsOn( + subprojects .filter { it.plugins.hasPlugin("org.jetbrains.dokka") } - .map { "${it.path}:dokkaHtml" }) + .map { "${it.path}:dokkaHtml" }, + ) doLast { - val dest = layout.buildDirectory.dir("dokka/html").get().asFile - if (dest.exists()) dest.deleteRecursively() - dest.mkdirs() - subprojects.filter { it.plugins.hasPlugin("org.jetbrains.dokka") }.forEach { p -> - val out = p.layout.buildDirectory.dir("dokka/html").get().asFile - if (out.exists()) { - out.copyRecursively(File(dest, p.path.trimStart(':').replace(':', '/')), overwrite = true) - } + val dest = layout.buildDirectory.dir("dokka/html").get().asFile + if (dest.exists()) dest.deleteRecursively() + dest.mkdirs() + subprojects.filter { it.plugins.hasPlugin("org.jetbrains.dokka") }.forEach { p -> + val out = p.layout.buildDirectory.dir("dokka/html").get().asFile + if (out.exists()) { + out.copyRecursively(File(dest, p.path.trimStart(':').replace(':', '/')), overwrite = true) } - println("[DOKKA] Aggregated HTML into ${dest.absolutePath}") + } + println("[DOKKA] Aggregated HTML into ${dest.absolutePath}") } -} + } // ################################################################## // ### DOKU-AGGREGATOR ### @@ -188,26 +307,26 @@ val dokkaHtmlAll = tasks.register("dokkaHtmlAll") { // Leichter Aggregator im Root-Projekt, ruft die eigentlichen Tasks im :docs Subprojekt auf tasks.register("docs") { - description = "Aggregates documentation tasks from :docs" - group = "documentation" - dependsOn(":docs:generateAllDocs") + description = "Aggregates documentation tasks from :docs" + group = "documentation" + dependsOn(":docs:generateAllDocs") } // Wrapper-Konfiguration // Apply Node warning suppression on root project Exec tasks as well // Ensures aggregated Kotlin/JS tasks created at root (e.g., kotlinNpmInstall) inherit the env tasks.withType().configureEach { - val current = (environment["NODE_OPTIONS"] as String?) ?: System.getenv("NODE_OPTIONS") - val merged = if (current.isNullOrBlank()) "--no-deprecation" else "$current --no-deprecation" - environment("NODE_OPTIONS", merged) - environment("NODE_NO_WARNINGS", "1") - // Set Chrome binary path to avoid snap permission issues - environment("CHROME_BIN", "/usr/bin/google-chrome-stable") - environment("CHROMIUM_BIN", "/usr/bin/chromium") - environment("PUPPETEER_EXECUTABLE_PATH", "/usr/bin/chromium") + val current = (environment["NODE_OPTIONS"] as String?) ?: System.getenv("NODE_OPTIONS") + val merged = if (current.isNullOrBlank()) "--no-deprecation" else "$current --no-deprecation" + environment("NODE_OPTIONS", merged) + environment("NODE_NO_WARNINGS", "1") + // Set Chrome binary path to avoid snap permission issues + environment("CHROME_BIN", "/usr/bin/google-chrome-stable") + environment("CHROMIUM_BIN", "/usr/bin/chromium") + environment("PUPPETEER_EXECUTABLE_PATH", "/usr/bin/chromium") } tasks.wrapper { - gradleVersion = "9.1.0" - distributionType = Wrapper.DistributionType.BIN + gradleVersion = "9.1.0" + distributionType = Wrapper.DistributionType.BIN } diff --git a/clients/auth-feature/build.gradle.kts b/clients/auth-feature/build.gradle.kts index 4bd935e0..620d2db3 100644 --- a/clients/auth-feature/build.gradle.kts +++ b/clients/auth-feature/build.gradle.kts @@ -42,8 +42,8 @@ kotlin { sourceSets { commonMain.dependencies { - // UI Kit - implementation(project(":clients:shared:common-ui")) + // UI Kit (Design System) + implementation(project(":frontend:core:design-system")) // Shared Konfig & Utilities (AppConfig + BuildConfig) implementation(project(":clients:shared")) diff --git a/clients/ping-feature/build.gradle.kts b/clients/ping-feature/build.gradle.kts index 25beb5c3..681cda1b 100644 --- a/clients/ping-feature/build.gradle.kts +++ b/clients/ping-feature/build.gradle.kts @@ -43,10 +43,10 @@ kotlin { sourceSets { commonMain.dependencies { // Contract from backend - implementation(projects.services.ping.pingApi) + implementation(project(":backend:services:ping:ping-api")) - // UI Kit - implementation(project(":clients:shared:common-ui")) + // UI Kit (Design System) + implementation(project(":frontend:core:design-system")) // Shared Konfig & Utilities implementation(project(":clients:shared")) @@ -65,6 +65,9 @@ kotlin { // Coroutines and serialization implementation(libs.bundles.kotlinx.core) + // DI (Koin) for resolving apiClient from container + implementation(libs.koin.core) + // ViewModel lifecycle implementation(libs.bundles.compose.common) diff --git a/clients/shared/build.gradle.kts b/clients/shared/build.gradle.kts index 35b6c6eb..48b71da0 100644 --- a/clients/shared/build.gradle.kts +++ b/clients/shared/build.gradle.kts @@ -63,6 +63,9 @@ kotlin { implementation(libs.koin.compose) implementation(libs.koin.compose.viewmodel) + // Network module (provides DI `apiClient`) + implementation(project(":frontend:core:network")) + // Compose für shared UI components (common) implementation(compose.runtime) implementation(compose.foundation) diff --git a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/di/SharedModule.kt b/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/di/SharedModule.kt index f548bf43..10202c64 100644 --- a/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/di/SharedModule.kt +++ b/clients/shared/src/commonMain/kotlin/at/mocode/clients/shared/di/SharedModule.kt @@ -1,6 +1,7 @@ package at.mocode.clients.shared.di import at.mocode.clients.shared.core.devConfig +import at.mocode.frontend.core.network.networkModule import org.koin.core.context.startKoin import org.koin.dsl.KoinAppDeclaration import org.koin.dsl.module @@ -10,14 +11,16 @@ val configModule = module { single { devConfig } // Später können wir hier PROD/DEV umschalten } -// Alle Module zusammen -val sharedModules = listOf( +// Basismodule, die immer geladen werden sollen (ohne Feature/Core-Cross-Imports) +val baseSharedModules = listOf( configModule, + // Network module provides DI-only HttpClient (safe to be shared across features) networkModule ) // Helper zum Starten von Koin (wird von der App aufgerufen) +// Weitere Module (z. B. networkModule) können über appDeclaration hinzugefügt werden. fun initKoin(appDeclaration: KoinAppDeclaration = {}) = startKoin { + modules(baseSharedModules) appDeclaration() - modules(sharedModules) } diff --git a/compose.hardcoded.yaml b/compose.hardcoded.yaml deleted file mode 100644 index 9f8138ba..00000000 --- a/compose.hardcoded.yaml +++ /dev/null @@ -1,190 +0,0 @@ -name: meldestelle-hardcoded - -services: - # --- DATENBANK --- - postgres: - image: postgres:16-alpine - container_name: meldestelle-postgres - restart: unless-stopped - ports: - - "5432:5432" - environment: - POSTGRES_USER: pg-user - POSTGRES_PASSWORD: pg-password - POSTGRES_DB: meldestelle - volumes: - - postgres-data:/var/lib/postgresql/data - # Falls du Init-Scripte hast, lassen wir die erstmal weg, - # um Fehlerquellen zu reduzieren, oder lassen den Pfad, falls er existiert: - - ./docker/core/postgres:/docker-entrypoint-initdb.d:Z - healthcheck: - test: [ "CMD-SHELL", "pg_isready -U pg-user -d meldestelle" ] - interval: 1s - timeout: 5s - retries: 3 - start_period: 30s - networks: - - meldestelle-network - - # --- DATENBANK-MANAGEMENT-TOOL --- - pgadmin: - image: dpage/pgadmin4:8 - container_name: pgadmin4_container - restart: unless-stopped - ports: - - "8888:80" - environment: - PGADMIN_DEFAULT_EMAIL: user@domain.com - PGADMIN_DEFAULT_PASSWORD: strong-password - volumes: - - pgadmin-data:/var/lib/pgadmin - healthcheck: - test: [ "CMD-SHELL", "wget --spider -q http://localhost:80/ || exit 1" ] - interval: 1s - timeout: 5s - retries: 3 - start_period: 30s - networks: - - meldestelle-network - - # --- CACHE --- - redis: - image: redis:7-alpine - container_name: meldestelle-redis - restart: unless-stopped - ports: - - "6379:6379" - volumes: - - redis-data:/data - command: redis-server --appendonly yes - healthcheck: - test: [ "CMD", "redis-cli" ] - interval: 1s - timeout: 5s - retries: 3 - networks: - - meldestelle-network - - # --- IDENTITY PROVIDER (Wartet auf Postgres) --- - keycloak: - image: quay.io/keycloak/keycloak:26.4 - container_name: meldestelle-keycloak - restart: unless-stopped - environment: - KC_HEALTH_ENABLED: true - KC_METRICS_ENABLED: true - KC_BOOTSTRAP_ADMIN_USERNAME: kc-admin - KC_BOOTSTRAP_ADMIN_PASSWORD: kc-password - KC_DB: postgres - KC_DB_URL: jdbc:postgresql://postgres:5432/meldestelle - KC_DB_USERNAME: pg-user - KC_DB_PASSWORD: pg-password - KC_HOSTNAME: localhost - ports: - - "8180:8080" - depends_on: - postgres: - condition: service_healthy - volumes: - - ./docker/core/keycloak:/opt/keycloak/data/import:Z - command: start-dev --import-realm - healthcheck: - test: [ "CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/9000" ] - interval: 20s - timeout: 10s - retries: 5 - start_period: 60s - networks: - - meldestelle-network - - # --- MONITORING --- - prometheus: - image: prom/prometheus:v2.54.1 - container_name: meldestelle-prometheus - restart: unless-stopped - ports: - - "9090:9090" - volumes: - - prometheus-data:/prometheus - - ./docker/monitoring/prometheus:/etc/prometheus:Z - command: - - --config.file=/etc/prometheus/prometheus.yml - - --storage.tsdb.retention.time=15d - healthcheck: - test: [ "CMD", "wget", "--spider", "-q", "http://localhost:9090/-/healthy" ] - interval: 30s - timeout: 10s - retries: 3 - start_period: 30s - networks: - - meldestelle-network - - grafana: - image: grafana/grafana:11.3.0 - container_name: meldestelle-grafana - environment: - GF_SECURITY_ADMIN_USER: gf-admin - GF_SECURITY_ADMIN_PASSWORD: gf-password - ports: - - "3000:3000" - volumes: - - grafana-data:/var/lib/grafana - - ./docker/monitoring/grafana:/etc/grafana/provisioning:Z - depends_on: - - prometheus - healthcheck: - test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health" ] - interval: 30s - timeout: 10s - retries: 3 - start_period: 30s - networks: - - meldestelle-network - - # --- CLIENTS: WEB APP (Kotlin/JS, no WASM) --- - web-app: - build: - context: . - dockerfile: dockerfiles/clients/web-app/Dockerfile - args: - GRADLE_VERSION: 9.1.0 - JAVA_VERSION: 21 - NODE_VERSION: 22.21.0 - NGINX_IMAGE_TAG: 1.28.0-alpine - WEB_BUILD_PROFILE: dev - container_name: meldestelle-web-app - restart: unless-stopped - ports: - - "4000:4000" - depends_on: - - api-gateway - networks: - - meldestelle-network - - # --- CLIENTS: DESKTOP APP (VNC + noVNC) --- - desktop-app: - build: - context: . - dockerfile: dockerfiles/clients/desktop-app/Dockerfile - container_name: meldestelle-desktop-app - restart: unless-stopped - environment: - - API_BASE_URL=http://api-gateway:8081 - ports: - - "5901:5901" # VNC - - "6080:6080" # noVNC - depends_on: - - api-gateway - networks: - - meldestelle-network - -volumes: - postgres-data: - pgadmin-data: - redis-data: - prometheus-data: - grafana-data: - -networks: - meldestelle-network: - driver: bridge diff --git a/docker/.env.example b/docker/.env.example new file mode 100644 index 00000000..441e1f8c --- /dev/null +++ b/docker/.env.example @@ -0,0 +1,43 @@ +# Core project name used as prefix for container names +COMPOSE_PROJECT_NAME=meldestelle + +# Ports +POSTGRES_PORT=5432:5432 +REDIS_PORT=6379:6379 +KC_PORT=8180:8080 +CONSUL_PORT=8500:8500 +PROMETHEUS_PORT=9090:9090 +GF_PORT=3000:3000 +WEB_APP_PORT=4000:80 +PING_SERVICE_PORT=8082:8082 +PING_DEBUG_PORT=5006:5006 +GATEWAY_PORT=8081:8081 +GATEWAY_DEBUG_PORT=5005:5005 +GATEWAY_SERVER_PORT=8081 +DESKTOP_APP_VNC_PORT=5900:5900 +DESKTOP_APP_NOVNC_PORT=6080:6080 + +# Postgres +POSTGRES_USER=meldestelle +POSTGRES_PASSWORD=meldestelle +POSTGRES_DB=meldestelle + +# Keycloak +KC_ADMIN_USER=admin +KC_ADMIN_PASSWORD=admin +KC_HOSTNAME=localhost + +# PgAdmin +PGADMIN_EMAIL=admin@example.com +PGADMIN_PASSWORD=admin + +# Grafana +GF_ADMIN_USER=admin +GF_ADMIN_PASSWORD=admin + +# Docker build versions (optional overrides) +DOCKER_GRADLE_VERSION=9.1.0 +DOCKER_JAVA_VERSION=21 +DOCKER_NODE_VERSION=22.21.0 +DOCKER_NGINX_VERSION=1.28.0-alpine +WEB_BUILD_PROFILE=dev diff --git a/docker/docker-compose.clients.yml b/docker/docker-compose.clients.yml new file mode 100644 index 00000000..ad189ddb --- /dev/null +++ b/docker/docker-compose.clients.yml @@ -0,0 +1 @@ +services: {} diff --git a/docker/docker-compose.services.yml b/docker/docker-compose.services.yml new file mode 100644 index 00000000..ad189ddb --- /dev/null +++ b/docker/docker-compose.services.yml @@ -0,0 +1 @@ +services: {} diff --git a/compose.yaml b/docker/docker-compose.yml similarity index 94% rename from compose.yaml rename to docker/docker-compose.yml index 4d3e2fc6..8ff0bfba 100644 --- a/compose.yaml +++ b/docker/docker-compose.yml @@ -16,7 +16,7 @@ services: POSTGRES_DB: ${POSTGRES_DB} volumes: - postgres-data:/var/lib/postgresql/data - - ./docker/core/postgres:/docker-entrypoint-initdb.d:Z + - ./core/postgres:/docker-entrypoint-initdb.d:Z healthcheck: test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}" ] interval: 5s @@ -72,7 +72,7 @@ services: postgres: condition: service_healthy volumes: - - ./docker/core/keycloak:/opt/keycloak/data/import:Z + - ./core/keycloak:/opt/keycloak/data/import:Z command: start-dev --import-realm healthcheck: test: [ "CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/9000" ] @@ -112,7 +112,7 @@ services: - "${PROMETHEUS_PORT}" volumes: - prometheus-data:/prometheus - - ./docker/monitoring/prometheus:/etc/prometheus:Z + - ./monitoring/prometheus:/etc/prometheus:Z command: - --config.file=/etc/prometheus/prometheus.yml - --storage.tsdb.retention.time=15d @@ -138,7 +138,7 @@ services: - "${GF_PORT}" volumes: - grafana-data:/var/lib/grafana - - ./docker/monitoring/grafana:/etc/grafana/provisioning:Z + - ./monitoring/grafana:/etc/grafana/provisioning:Z depends_on: - prometheus healthcheck: @@ -175,7 +175,7 @@ services: api-gateway: build: - context: . + context: .. dockerfile: dockerfiles/infrastructure/gateway/Dockerfile args: # Build-Args aus deinen .env Dateien (werden hier statisch benötigt für den Build) @@ -224,7 +224,7 @@ services: # ========================================== ping-service: build: - context: . + context: .. dockerfile: dockerfiles/services/ping-service/Dockerfile args: GRADLE_VERSION: 9.1.0 @@ -246,16 +246,13 @@ services: SPRING_CLOUD_CONSUL_PORT: 8500 SPRING_CLOUD_CONSUL_DISCOVERY_HOSTNAME: ping-service - # --- DATENBANK VERBINDUNG --- - # Wir nutzen die Container-Namen aus deiner .env Variable + # - DATENBANK VERBINDUNG - SPRING_DATASOURCE_URL: jdbc:postgresql://${COMPOSE_PROJECT_NAME}-postgres:5432/${POSTGRES_DB} SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER} SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD} - # WICHTIG: Wir wollen nur validieren, nichts erstellen. SPRING_JPA_HIBERNATE_DDL_AUTO: validate # --- REDIS --- - # Wir nutzen den Service-Namen, genau wie bei Postgres SPRING_DATA_REDIS_HOST: ${COMPOSE_PROJECT_NAME}-redis SPRING_DATA_REDIS_PORT: 6379 depends_on: @@ -275,7 +272,7 @@ services: # ========================================== web-app: build: - context: . + context: .. dockerfile: dockerfiles/clients/web-app/Dockerfile args: GRADLE_VERSION: ${DOCKER_GRADLE_VERSION:-9.1.0} @@ -297,7 +294,7 @@ services: desktop-app: build: - context: . + context: .. dockerfile: dockerfiles/clients/desktop-app/Dockerfile container_name: ${COMPOSE_PROJECT_NAME}-desktop-app restart: unless-stopped diff --git a/dockerfiles/clients/desktop-app/Dockerfile b/dockerfiles/clients/desktop-app/Dockerfile index 09ada4d9..38b69ff7 100644 --- a/dockerfiles/clients/desktop-app/Dockerfile +++ b/dockerfiles/clients/desktop-app/Dockerfile @@ -15,22 +15,21 @@ COPY gradle ./gradle COPY gradlew ./ # Kopiere alle notwendigen Module für Multi-Modul-Projekt -COPY clients ./clients +COPY frontend ./frontend +COPY backend ./backend COPY core ./core COPY domains ./domains COPY platform ./platform -COPY infrastructure ./infrastructure -COPY services ./services COPY docs ./docs # Setze Gradle-Wrapper Berechtigung RUN chmod +x ./gradlew # Dependencies downloaden (für besseres Caching) -RUN ./gradlew :clients:app:dependencies --no-configure-on-demand +RUN ./gradlew :frontend:shells:meldestelle-portal:dependencies --no-configure-on-demand # Desktop-App kompilieren (createDistributable für native Distribution) -RUN ./gradlew :clients:app:createDistributable --no-configure-on-demand +RUN ./gradlew :frontend:shells:meldestelle-portal:createDistributable --no-configure-on-demand # =================================================================== # Stage 2: Runtime Stage - Ubuntu mit VNC + noVNC @@ -59,7 +58,7 @@ RUN apt-get update && apt-get install -y \ WORKDIR /app # Kopiere kompilierte Desktop-App von Build-Stage -COPY --from=builder /app/clients/app/build/compose/binaries/main/desktop/ ./desktop-app/ +COPY --from=builder /app/frontend/shells/meldestelle-portal/build/compose/binaries/main/desktop/ ./desktop-app/ # Kopiere Scripts COPY dockerfiles/clients/desktop-app/entrypoint.sh /entrypoint.sh diff --git a/dockerfiles/clients/web-app/Dockerfile b/dockerfiles/clients/web-app/Dockerfile index 951ef685..0d5f9a13 100644 --- a/dockerfiles/clients/web-app/Dockerfile +++ b/dockerfiles/clients/web-app/Dockerfile @@ -30,29 +30,28 @@ COPY gradle ./gradle COPY gradlew ./ # Kopiere alle notwendigen Module für Multi-Modul-Projekt -COPY clients ./clients +COPY frontend ./frontend +COPY backend ./backend COPY core ./core COPY domains ./domains COPY platform ./platform -COPY infrastructure ./infrastructure -COPY services ./services COPY docs ./docs # Setze Gradle-Wrapper Berechtigung RUN chmod +x ./gradlew # Dependencies downloaden (für besseres Caching) -RUN ./gradlew :clients:app:dependencies --no-configure-on-demand +RUN ./gradlew :frontend:shells:meldestelle-portal:dependencies --no-configure-on-demand # Kotlin/JS Web-App kompilieren (Profil wählbar über WEB_BUILD_PROFILE) # - dev → jsBrowserDevelopmentExecutable (schneller, Source Maps) # - prod → jsBrowserDistribution (minifiziert, optimiert) RUN if [ "$WEB_BUILD_PROFILE" = "prod" ]; then \ - ./gradlew :clients:app:jsBrowserDistribution --no-configure-on-demand -Pproduction=true; \ - mkdir -p /app/web-dist && cp -r clients/app/build/dist/js/productionExecutable/* /app/web-dist/; \ + ./gradlew :frontend:shells:meldestelle-portal:jsBrowserDistribution --no-configure-on-demand -Pproduction=true; \ + mkdir -p /app/web-dist && cp -r frontend/shells/meldestelle-portal/build/dist/js/productionExecutable/* /app/web-dist/; \ else \ - ./gradlew :clients:app:jsBrowserDevelopmentExecutable --no-configure-on-demand; \ - mkdir -p /app/web-dist && cp -r clients/app/build/dist/js/developmentExecutable/* /app/web-dist/; \ + ./gradlew :frontend:shells:meldestelle-portal:jsBrowserDevelopmentExecutable --no-configure-on-demand; \ + mkdir -p /app/web-dist && cp -r frontend/shells/meldestelle-portal/build/dist/js/developmentExecutable/* /app/web-dist/; \ fi # =================================================================== diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 00000000..efb52dc3 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,67 @@ +Repository-Architektur (MP-22) + +Dieses Dokument beschreibt die Zielstruktur und das Mapping vom bisherigen Stand (Ist) zur neuen Struktur (Soll). Es begleitet Epic 2 (MP-22). + +Zielstruktur (Top-Level) + +backend/ Gateway, Discovery (optional), Services + gateway + discovery + services +frontend/ KMP Frontend + shells Ausführbare Apps (Assembler) + features Vertical Slices (kein Feature→Feature) + core Shared Foundation (Design-System, Network, Local-DB, Auth, Domain) +docker/ Docker Compose, .env.example, Monitoring-/Core-Konfiguration +docs/ Architektur, ADRs, C4-Modelle, Guides + +Ist → Soll Mapping (erste Tranche) + +- Frontend + - clients/app → frontend/shells/meldestelle-portal (verschieben in Folge-Commit) + - clients/shared/common-ui → frontend/core/design-system (verschieben in Folge-Commit) + - clients/shared/navigation → frontend/core/navigation (verschieben in Folge-Commit) + +- Backend + - infrastructure/gateway → backend/gateway (verschieben in Folge-Commit) + - services/* → backend/services/* (verschieben in Folge-Commit) + - Discovery (falls genutzt) → backend/discovery + +- Docker + - compose.yaml → docker/docker-compose.yml (neu angelegt, Makefile angepasst) + - .env Handling → docker/.env.example (neu, als Template) + +Build/Gradle + +- settings.gradle.kts bleibt vorerst unverändert. Modul-Verschiebungen folgen in einem separaten Schritt mit angepassten include-Pfaden. +- Version Catalog (gradle/libs.versions.toml) bleibt die einzige Quelle der Versionswahrheit. + +Richtlinien (Kurzfassung) + +- Features kommunizieren ausschließlich über Routen (Navigation) und Shared-Modelle in frontend/core/domain. +- Kein manueller Authorization-Header – nur der DI-verwaltete apiClient aus frontend/core/network (Koin Named Binding). +- SQLDelight als Offline-SSoT: Schema/Migrationen zentral versionieren, UI liest stets lokal und synchronisiert im Hintergrund. + +DI-Policy & Architecture Guards (MP-23) + +- DI-Policy (Frontend) + - Http‑Requests erfolgen ausschließlich über den via Koin bereitgestellten `apiClient` (named Binding) aus `:frontend:core:network`. + - Manuelles Setzen des `Authorization`‑Headers ist verboten. Token‑Handling wird zentral im `apiClient` konfiguriert (Auth‑Plugin/Interceptor). + - Basis‑URL wird plattformspezifisch aufgelöst: + - JVM/Desktop: Env `API_BASE_URL` (Fallback `http://localhost:8081`). + - Web/JS: `globalThis.API_BASE_URL` (z. B. per `index.html` oder Proxy), sonst `window.location.origin`, Fallback `http://localhost:8081`. + +- Architecture Guards (Frontend‑Scope) + - Root‑Task `archGuards` bricht den Build ab, wenn verbotene Muster gefunden werden (manuelle `Authorization`‑Header). Tests sind ausgenommen; Backend ist ausgenommen. + - Statische Analyse verfügbar über `detekt` und `ktlintCheck`; Aggregator `staticAnalysis` führt alles zusammen. + +- Hinweise für Features + - Features importieren keine anderen Features (Kommunikation über Navigation + Shared‑Domain‑Modelle). Eine explizite Detekt‑Regel folgt. + - Netzwerkzugriffe in Features nutzen Koin über die App‑Shell (DI‑Bootstrap). Für schrittweise Migration kann eine Factory den `apiClient` optional beziehen. + +Nächste Schritte (MP-22 Folgetasks) + +1. Physisches Verschieben der Frontend-Module gemäß Mapping und Anpassung von settings.gradle.kts. +2. Physisches Verschieben der Backend-Komponenten in backend/* inkl. evtl. Package-Pfade, sofern notwendig. +3. Ergänzung von docker-compose.services.yml und docker-compose.clients.yml mit echten Overlays. +4. Erstellen der ersten ADRs unter docs/adr (Koin, SQLDelight, Optimistic Locking, Freshness UI, Core Domain). diff --git a/docs/adr/README.md b/docs/adr/README.md new file mode 100644 index 00000000..9d51fc60 --- /dev/null +++ b/docs/adr/README.md @@ -0,0 +1,13 @@ +Architecture Decision Records (ADRs) + +Dieses Verzeichnis enthält Architekturentscheidungen in kurzer, überprüfbarer Form. + +Namensschema: ADR-XXX-title.md mit fortlaufender Nummerierung. + +- ADR-001 Koin als DI +- ADR-002 SQLDelight als Offline-DB +- ADR-003 Optimistic Locking (409) als Konfliktstrategie +- ADR-004 Freshness UI (Ampel) +- ADR-005 Core Domain & Feature Isolation + +Siehe Template: ADR-000-template.md. diff --git a/docs/clients/visionen/AntwortenOffenerFragenArchitekturReview.md b/docs/clients/visionen/AntwortenOffenerFragenArchitekturReview.md new file mode 100644 index 00000000..2e47083a --- /dev/null +++ b/docs/clients/visionen/AntwortenOffenerFragenArchitekturReview.md @@ -0,0 +1,160 @@ +### 1\. Welche DI-Lösung? (Dependency Injection) + +**Entscheidung:** Wir nutzen **Koin**. + +**Begründung (ADR):** + +* **Warum nicht Dagger/Hilt?** Hilt ist stark auf Android (Context, Lifecycles) fixiert. Dagger ist extrem komplex im Setup für Multiplatform (Kapt/KSP Setup über alle Targets). +* **Warum Koin?** Es ist ein reines Kotlin-Framework ("Service Locator" Pattern). Es funktioniert identisch auf JVM (Desktop), JS (Web) und Android. Es benötigt keine Annotation-Processing-Magie, was die Build-Zeiten im Monorepo niedrig hält. + +**Eintrag im Guide:** + +```kotlin +// GUIDELINE: Dependency Injection +// Wir nutzen Koin. Module werden im `di` Package des Features definiert. + +// 1. Definition (Feature Module) +val inventoryModule = module { + // Singletons für Services + single { InventoryRepositoryImpl(get(), get()) } + + // ViewModels (Factory scope) + viewModel { InventoryViewModel(get()) } +} + +// 2. Nutzung des ApiClients (Best Practice) +// Wir injizieren IMMER den "apiClient" (mit Auth-Header), niemals den Default Client. +val networkModule = module { + single(named("apiClient")) { ... } // Konfiguriert in :core:network +} + +val myFeatureModule = module { + single { + // Explizites Holen des authentifizierten Clients + MyFeatureApi(httpClient = get(named("apiClient"))) + } +} +``` + +----- + +### 2\. Welche Offline-DB/ORM? + +**Entscheidung:** Wir nutzen **SQLDelight**. + +**Begründung (ADR):** + +* **Warum nicht Room (KMP)?** Room ist für KMP noch sehr neu (Alpha/Beta Status) und bringt viel Overhead mit sich (SQLite Bundling etc.). +* **Warum SQLDelight?** + 1. **Schema First:** Du schreibst SQL (`.sq`), und Kotlin-Code wird *generiert*. Das zwingt Entwickler dazu, über ihr Datenmodell nachzudenken, bevor sie Code schreiben. + 2. **Performance:** Es ist extrem leichtgewichtig und typ-sicher. + 3. **Migrationen:** SQLDelight hat ein exzellentes System für Schema-Migrationen (`1.sqm`, `2.sqm`), was für Desktop-Apps (die nicht einfach "neu geladen" werden können wie Webseiten) essenziell ist. + +**Eintrag im Guide:** + +> **DB-Guideline:** +> +> * Jedes Feature definiert sein Schema in `:frontend:core:local-db/src/commonMain/sqldelight/...`. +> * Business-Logik darf niemals SQL-Strings enthalten. Nutze die generierten `Queries`-Objekte. +> * Migrationen sind Pflicht bei Schema-Änderungen\! (Kein `DROP TABLE` in Production). + +----- + +### 3\. Konfliktstrategie bei Sync? + +**Entscheidung:** **Optimistic Locking** (Server Wins). + +**Begründung (ADR):** + +* In einem System mit Offline-Clients ist "Last Write Wins" gefährlich (Lagerbestand wird überschrieben). +* **Strategie:** + 1. Jedes Entity hat eine `lastUpdated` (Timestamp) Spalte. + 2. Der Client sendet beim Update die Version mit, die er *kennt*. + 3. Wenn Server-Version \> Client-Version → **HTTP 409 Conflict**. + 4. Client muss Daten neu laden (Refresh) und User fragen/informieren. + +**Eintrag im Guide:** + +```kotlin +// GUIDELINE: Sync & Conflicts +// Das Frontend führt KEIN komplexes Merging durch. + +suspend fun updateStock(item: Item) { + try { + api.update(item.id, item.newStock, currentVersion = item.version) + // Happy Path: DB Update + } catch (e: ConflictException) { // HTTP 409 + // 1. Markiere Item in UI als "Out of Sync" (Rot) + // 2. Trigger automatischen Refresh vom Server + // 3. Zeige User Toast: "Daten waren veraltet. Bitte prüfen." + repo.refreshSingleItem(item.id) + } +} +``` + +----- + +### 4\. Error Budgets / SLIs (Stale Data Indikatoren) + +**Entscheidung:** **Visual Freshness Indicators** (Ampel-System). + +**Begründung (ADR):** + +* Ein User muss wissen, ob der Lagerbestand "live" ist oder "von gestern". +* Wir definieren keine harten Timeouts (App blockieren), sondern weiche UI-Hinweise. + +**Eintrag im Guide:** + +> **UI-Regel "Data Freshness":** +> Jedes Entity in der lokalen DB hat ein Feld `lastSyncedAt`. Das UI reagiert darauf: +> +> * **\< 5 min:** ✅ Normalzustand (Kein Indikator). +> * **\> 5 min:** ⚠️ Kleines gelbes "Wolke"-Icon oder ausgegrauter Text (Warnung). +> * **\> 1 Stunde:** ❌ Roter Banner "Offline-Daten: Bestand nicht garantiert". +> * **Aktion:** Schreibende Operationen sind bei "Rot" für kritische Bereiche (z.B. Inventur-Abschluss) gesperrt, für unkritische (z.B. Notiz anlegen) erlaubt (Queue). + +----- + +### 5\. API-Verträge und Kapselung der Feature-Teams + +**Entscheidung:** **Loose Coupling via Navigation Routes & Shared Data Models (Core)**. + +**Begründung (ADR):** + +* Wir wollen vermeiden, dass Team A (Inventory) direkt Klassen von Team B (Checkout) importiert. Das führt zum "Monolithen-Klumpen". +* Wir nutzen **keine** separaten Gradle-Module pro Feature-API (`:inventory-api`, `:inventory-impl`), da dies den Build-Graph unnötig aufbläht ("Gradle Overhead"). + +**Strategie:** + +1. **Schnittstelle:** Die einzige "Public API" eines Features ist sein `EntryPoint` (Composable) und seine `Route` (String). +2. **Datenaustausch:** + * *Minimal:* Über URL-Parameter (IDs). `navigator.navigate("inventory/details/123")`. + * *Objekte:* Wenn komplexe Objekte geteilt werden müssen (z.B. `UserProfile`), gehören diese in **`:frontend:core:domain`** (Shared Kernel). + +**Eintrag im Guide:** + +```kotlin +// GUIDELINE: Feature Isolation +// 1. Features importieren NIEMALS andere Features im `build.gradle.kts`. +// 2. Kommunikation nur über Navigation (Router). +// 3. Gemeinsam genutzte Datenobjekte (z.B. UserID, ShopID) liegen in :core:domain. + +// FALSCH: +import com.project.features.billing.Invoice // Abhängigkeit zu anderem Feature! + +// RICHTIG: +// Feature A navigiert zu Feature B via Route +navigator.navigateTo("billing/create?orderId=123") +``` + +----- + +### Zusammenfassung für dein Dokument + +Diese 5 Punkte schließen den Kreis: + +1. **Koin** hält den Code sauber. +2. **SQLDelight** hält die Daten sicher. +3. **Optimistic Locking** verhindert Datenmüll. +4. **Freshness UI** managed die Erwartungshaltung des Users. +5. **Core Domain** verhindert Spaghetti-Code zwischen Features. diff --git a/docs/clients/visionen/ProjectArchitecture_StructureGuide.md b/docs/clients/visionen/ProjectArchitecture_StructureGuide.md new file mode 100644 index 00000000..d24c541d --- /dev/null +++ b/docs/clients/visionen/ProjectArchitecture_StructureGuide.md @@ -0,0 +1,155 @@ +# 🏗 Project Architecture & Structure Guide + +> **"Code is liability. Structure is asset."** +> Wir bauen dieses System nicht für den schnellsten Start, sondern für die **Wartbarkeit über Jahre**, Offline-Fähigkeit und Skalierbarkeit über mehrere Teams hinweg. + +----- + +## 1\. Die Große Übersicht: The Monorepo Strategy + +Wir organisieren Backend und Frontend in einem einzigen Repository (Monorepo). + +### **Warum Monorepo? (Decision Record)** + +* ❌ **Alternative:** Getrennte Repositories für Backend, Web-Frontend, Desktop-App. +* **Problem dabei:** "Version Hell". Backend ändert API v1 zu v2, aber Frontend-Repo ist noch auf v1. Refactorings über die ganze Kette sind schmerzhaft. +* ✅ **Unsere Entscheidung:** Monorepo. + * **Atomic Commits:** Ein Pull Request enthält Backend-Änderungen UND die dazugehörige Frontend-Anpassung. + * **Single Versioning:** Wir nutzen `gradle/libs.versions.toml` als einzige Quelle der Wahrheit für Library-Versionen (z.B. Kotlin Version) über das gesamte System hinweg. + +----- + +## 2\. Der "Deep Dive" in die Ordnerstruktur + +Hier ist der detaillierte Aufriss unseres Dateisystems. Jeder Ordner hat einen spezifischen architektonischen Zweck. + +```text +/my-project-root +│ +├── ⚙️ docker-compose.yml <-- Die lokale "Cloud". Startet DBs, Gateway & Services. +├── 📄 settings.gradle.kts <-- Definiert die Module (Frontend & Backend). +├── 📂 gradle +│ └── libs.versions.toml <-- 🛑 STOP! Hier werden Versionen definiert. Nirgendwo sonst. +│ +├── 📂 backend <-- ARCHITEKTUR: Hexagonal / DDD +│ ├── 📂 gateway <-- Der "Türsteher". Routing & Auth-Check. +│ ├── 📂 discovery <-- Das "Telefonbuch" (Consul/Service Registry). +│ └── 📂 services <-- Die Business Logic (Microservices) +│ ├── 📂 inventory-service +│ │ ├── 📄 Dockerfile <-- Jedes Service ist ein isolierter Container! +│ │ └── 📂 src/main/kotlin/.../domain <-- Reine Logik, kein Spring! +│ └── 📂 auth-service +│ +└── 📂 frontend <-- ARCHITEKTUR: Kotlin Multiplatform (KMP) + │ + ├── 📂 shells <-- 💡 CONCEPT: "The Assembler" + │ │ Das sind die ausführbaren Anwendungen. Sie enthalten KEINE Logik. + │ │ Sie "kleben" nur Features zusammen und konfigurieren DI. + │ │ + │ ├── 📂 warehouse-app <-- Desktop-App (Windows/Linux) für Lageristen + │ │ └── build.gradle.kts (bindet :features:inventory ein) + │ └── 📂 admin-portal <-- Web-App (JS/Wasm) für Management + │ └── build.gradle.kts (bindet alle Features ein) + │ + ├── 📂 features <-- 💡 CONCEPT: "Vertical Slices" (Micro-Frontends) + │ │ Hier passiert die Arbeit. Ein Feature gehört einem Team. + │ │ + │ ├── 📂 inventory-feature + │ │ ├── 📂 src/commonMain + │ │ │ ├── 📂 api <-- Public Interface (Der Vertrag nach außen) + │ │ │ ├── 📂 ui <-- Screens & Components (Internal) + │ │ │ └── 📂 data <-- Repository & SSoT (Internal) + │ │ └── build.gradle.kts + │ └── 📂 auth-feature + │ + └── 📂 core <-- 💡 CONCEPT: "Shared Foundation" + │ Code, der sich selten ändert, aber überall genutzt wird. + │ + ├── 📂 design-system <-- UI-Baukasten (Farben, Typo, Buttons) + ├── 📂 network <-- HTTP Clients & Auth-Interceptor + ├── 📂 local-db <-- SQLDelight Schemas (Die Offline-Wahrheit) + └── 📂 auth <-- OAuth2 Logik (Browser Bridge für Desktop) +``` + +----- + +## 3\. Architectural Decision Records (ADRs) + +Warum haben wir das so gebaut? Hier sind die Antworten auf die "Warum nicht X?" Fragen. + +### ADR 001: Kotlin Multiplatform vs. Electron / Web-Wrapper + +* **Kontext:** Wir brauchen eine Web-App UND eine Desktop-App. +* **Entscheidung:** Wir nutzen **Kotlin Multiplatform (Compose)**. +* **Begründung:** + * *Performance:* Electron braucht pro App \~200MB RAM (Chromium Instanz). Unsere Desktop-Apps (Lager, Kasse) laufen auf schwacher Hardware. JVM/Native ist effizienter. + * *Type Safety:* Wir teilen Business-Logik (Validation, SSoT) zwischen Web und Desktop. Mit JS/Electron müssten wir Logik duplizieren oder transpilen. + * *Offline:* Echte SQL-Datenbank (SQLite) Integration ist in nativem Code robuster als im Browser-Storage. + +### ADR 002: Multiple App Shells vs. One "Super-App" + +* **Kontext:** Wir haben Lagerarbeiter, Kassierer und Manager. +* **Entscheidung:** Wir bauen **pro Rolle eine eigene "Shell"** (Executable). +* **Begründung:** + * *Security (Web):* "Tree Shaking". Wenn der Code für "Admin-User-Löschen" gar nicht erst in der `warehouse-app.js` enthalten ist, kann er auch nicht gehackt werden. + * *Focus (Desktop):* Die Lager-App startet schneller und hat weniger Bugs, weil sie den Code für das Rechnungswesen gar nicht lädt. + * *Flexibilität:* Wir können Features wiederverwenden. Das Feature `auth-feature` ist in ALLEN Apps, `inventory-feature` nur in zweien. + +### ADR 003: Single Source of Truth (SSoT) via Database + +* **Kontext:** Desktop-Apps werden in Hallen mit schlechtem WLAN genutzt. +* **Entscheidung:** **Database First Architecture**. +* **Begründung:** + * Klassisch (`UI -> API -> UI`) führt zu weißen Screens und Ladekreisen bei Netzschwankungen. + * Wir nutzen `UI -> Local DB <- Sync -> API`. + * Das UI zeigt **immer** Daten an (auch wenn sie 10 Minuten alt sind). Der User kann arbeiten. Sync passiert transparent im Hintergrund. + +### ADR 004: Docker für alles (außer Desktop Runtime) + +* **Kontext:** "Bei mir läuft's aber..." Probleme. +* **Entscheidung:** Das gesamte Backend + Web-Frontend Build-Pipeline läuft in Docker. +* **Begründung:** + * Die `docker-compose.yml` ist die Wahrheit. + * Für die Desktop-Entwicklung nutzen wir Gradle lokal, aber der Server, gegen den entwickelt wird, läuft im Container. Das garantiert Identität zwischen Dev und Prod. + +----- + +## 4\. Guidelines: Wo gehört mein Code hin? + +Wenn du neuen Code schreibst, stelle dir diese Fragen: + +### Q1: Ist es Business Logik (z.B. "Preis berechnen")? + +* ➡️ Gehört in **`/backend/services/.../domain`** (Server-Side Validierung ist Pflicht). +* ➡️ UND optional in **`/frontend/features/.../domain`** (für schnelle UI-Feedback, aber Server hat das letzte Wort). + +### Q2: Ist es ein UI-Element (z.B. "Runder Button")? + +* ➡️ Gehört in **`/frontend/core/design-system`**. +* 🛑 *Stop\!* Baue keine Custom Buttons in deinem Feature-Ordner. Nutze das Design System. Wenn etwas fehlt, erweitere das Design System. + +### Q3: Ich brauche Daten von einem anderen Service. + +* **Szenario:** Im "Checkout" (Kasse) brauche ich den Produktnamen aus dem "Inventory". +* ❌ **Falsch:** `CheckoutService` ruft `InventoryService` Datenbank direkt ab. +* ✅ **Richtig (Backend):** `CheckoutService` ruft `InventoryService` via REST/gRPC über das Gateway. +* ✅ **Richtig (Frontend):** Das `Checkout-Feature` kennt das `Inventory-Feature` nicht. Es bekommt nur eine `productId`. Wenn es Details anzeigen muss, nutzt es entweder ein eigenes minimales Datenmodell oder fragt das Backend. + +### Q4: Auth Token Handling + +* ❌ **Niemals:** `httpClient.header("Authorization", token)` manuell aufrufen. +* ✅ **Immer:** Nutze den konfigurierten Client aus dem DI-Container: `get(named("apiClient"))`. Die Architektur kümmert sich um Refresh und Injection. + +----- + +## 5\. Das "Mental Model" für Entwickler + +Stell dir unsere App wie einen **Lego-Baukasten** vor. + +1. **Core (Platte):** Das Fundament (Auth, Network, Design). Muss immer da sein. +2. **Features (Steine):** Bunte Bausteine (Inventory, Cart, Profile). Sie berühren sich seitlich nicht (keine direkten Abhängigkeiten). +3. **Shells (Modelle):** Das fertige Haus. + * Haus A (Admin Portal) nutzt alle Steine. + * Haus B (Lager App) nutzt nur die grünen Steine (Inventory). + +Dein Job als Entwickler ist es meistens, **einen neuen Stein (Feature)** zu bauen oder einen bestehenden zu verbessern. Du musst dich selten um das Fundament oder das fertige Haus kümmern. diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 00000000..3f12a19d --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,7 @@ +# Frontend + +Kotlin Multiplatform Frontend layer. + +- shells: ausführbare Anwendungen (Assembler) +- features: Vertical Slices (kein Feature→Feature Import) +- core: gemeinsame Basis (Design-System, Network, Local-DB, Auth, Domain) diff --git a/frontend/core/.gitkeep b/frontend/core/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/clients/shared/common-ui/build.gradle.kts b/frontend/core/design-system/build.gradle.kts similarity index 100% rename from clients/shared/common-ui/build.gradle.kts rename to frontend/core/design-system/build.gradle.kts diff --git a/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/AppFooter.kt b/frontend/core/design-system/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/AppFooter.kt similarity index 100% rename from clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/AppFooter.kt rename to frontend/core/design-system/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/AppFooter.kt diff --git a/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/AppHeader.kt b/frontend/core/design-system/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/AppHeader.kt similarity index 100% rename from clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/AppHeader.kt rename to frontend/core/design-system/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/AppHeader.kt diff --git a/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/AppScaffold.kt b/frontend/core/design-system/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/AppScaffold.kt similarity index 100% rename from clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/AppScaffold.kt rename to frontend/core/design-system/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/AppScaffold.kt diff --git a/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/LoadingIndicator.kt b/frontend/core/design-system/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/LoadingIndicator.kt similarity index 100% rename from clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/LoadingIndicator.kt rename to frontend/core/design-system/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/LoadingIndicator.kt diff --git a/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/MeldestelleButton.kt b/frontend/core/design-system/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/MeldestelleButton.kt similarity index 100% rename from clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/MeldestelleButton.kt rename to frontend/core/design-system/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/MeldestelleButton.kt diff --git a/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/MeldestelleTextField.kt b/frontend/core/design-system/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/MeldestelleTextField.kt similarity index 100% rename from clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/MeldestelleTextField.kt rename to frontend/core/design-system/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/MeldestelleTextField.kt diff --git a/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/NotificationCard.kt b/frontend/core/design-system/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/NotificationCard.kt similarity index 100% rename from clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/NotificationCard.kt rename to frontend/core/design-system/src/commonMain/kotlin/at/mocode/clients/shared/commonui/components/NotificationCard.kt diff --git a/clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/theme/AppTheme.kt b/frontend/core/design-system/src/commonMain/kotlin/at/mocode/clients/shared/commonui/theme/AppTheme.kt similarity index 100% rename from clients/shared/common-ui/src/commonMain/kotlin/at/mocode/clients/shared/commonui/theme/AppTheme.kt rename to frontend/core/design-system/src/commonMain/kotlin/at/mocode/clients/shared/commonui/theme/AppTheme.kt diff --git a/clients/shared/navigation/build.gradle.kts b/frontend/core/navigation/build.gradle.kts similarity index 100% rename from clients/shared/navigation/build.gradle.kts rename to frontend/core/navigation/build.gradle.kts diff --git a/clients/shared/navigation/src/commonMain/kotlin/at/mocode/clients/shared/navigation/AppScreen.kt b/frontend/core/navigation/src/commonMain/kotlin/at/mocode/clients/shared/navigation/AppScreen.kt similarity index 100% rename from clients/shared/navigation/src/commonMain/kotlin/at/mocode/clients/shared/navigation/AppScreen.kt rename to frontend/core/navigation/src/commonMain/kotlin/at/mocode/clients/shared/navigation/AppScreen.kt diff --git a/frontend/features/.gitkeep b/frontend/features/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/clients/app/build.gradle.kts b/frontend/shells/meldestelle-portal/build.gradle.kts similarity index 93% rename from clients/app/build.gradle.kts rename to frontend/shells/meldestelle-portal/build.gradle.kts index bf8828be..92b80868 100644 --- a/clients/app/build.gradle.kts +++ b/frontend/shells/meldestelle-portal/build.gradle.kts @@ -76,11 +76,15 @@ kotlin { commonMain.dependencies { // Shared modules implementation(project(":clients:shared")) - implementation(project(":clients:shared:common-ui")) - implementation(project(":clients:shared:navigation")) + implementation(project(":frontend:core:design-system")) + implementation(project(":frontend:core:navigation")) + implementation(project(":frontend:core:network")) implementation(project(":clients:auth-feature")) implementation(project(":clients:ping-feature")) + // DI (Koin) needed to call initKoin { modules(...) } + implementation(libs.koin.core) + // Compose Multiplatform implementation(compose.runtime) implementation(compose.foundation) @@ -100,6 +104,7 @@ kotlin { implementation(compose.desktop.currentOs) implementation(libs.kotlinx.coroutines.swing) implementation(libs.kotlinx.coroutines.core) + implementation(libs.koin.core) } jsMain.dependencies { diff --git a/clients/app/src/commonMain/kotlin/DevelopmentMode.kt b/frontend/shells/meldestelle-portal/src/commonMain/kotlin/DevelopmentMode.kt similarity index 100% rename from clients/app/src/commonMain/kotlin/DevelopmentMode.kt rename to frontend/shells/meldestelle-portal/src/commonMain/kotlin/DevelopmentMode.kt diff --git a/clients/app/src/commonMain/kotlin/MainApp.kt b/frontend/shells/meldestelle-portal/src/commonMain/kotlin/MainApp.kt similarity index 100% rename from clients/app/src/commonMain/kotlin/MainApp.kt rename to frontend/shells/meldestelle-portal/src/commonMain/kotlin/MainApp.kt diff --git a/clients/app/src/commonTest/kotlin/ComposeAppCommonTest.kt b/frontend/shells/meldestelle-portal/src/commonTest/kotlin/ComposeAppCommonTest.kt similarity index 100% rename from clients/app/src/commonTest/kotlin/ComposeAppCommonTest.kt rename to frontend/shells/meldestelle-portal/src/commonTest/kotlin/ComposeAppCommonTest.kt diff --git a/clients/app/src/jsMain/kotlin/DevelopmentMode.js.kt b/frontend/shells/meldestelle-portal/src/jsMain/kotlin/DevelopmentMode.js.kt similarity index 100% rename from clients/app/src/jsMain/kotlin/DevelopmentMode.js.kt rename to frontend/shells/meldestelle-portal/src/jsMain/kotlin/DevelopmentMode.js.kt diff --git a/clients/app/src/jsMain/kotlin/main.kt b/frontend/shells/meldestelle-portal/src/jsMain/kotlin/main.kt similarity index 59% rename from clients/app/src/jsMain/kotlin/main.kt rename to frontend/shells/meldestelle-portal/src/jsMain/kotlin/main.kt index 7c71a485..69f1f198 100644 --- a/clients/app/src/jsMain/kotlin/main.kt +++ b/frontend/shells/meldestelle-portal/src/jsMain/kotlin/main.kt @@ -2,10 +2,40 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.ComposeViewport import kotlinx.browser.document import org.w3c.dom.HTMLElement +import at.mocode.clients.shared.di.initKoin +import at.mocode.frontend.core.network.networkModule +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch +import org.koin.core.context.GlobalContext +import org.koin.core.qualifier.named +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.get @OptIn(ExperimentalComposeUiApi::class) fun main() { console.log("[WebApp] main() entered") + // Initialize DI (Koin) with shared modules + network module + try { + initKoin { modules(networkModule) } + console.log("[WebApp] Koin initialized with networkModule") + } catch (e: dynamic) { + console.warn("[WebApp] Koin initialization warning:", e) + } + // Simple smoke request using DI apiClient + try { + val client = GlobalContext.get().get(named("apiClient")) + MainScope().launch { + try { + val resp: String = client.get("/api/ping/health").body() + console.log("[WebApp] /api/ping/health → ", resp) + } catch (e: dynamic) { + console.warn("[WebApp] /api/ping/health failed:", e?.message ?: e) + } + } + } catch (e: dynamic) { + console.warn("[WebApp] Unable to resolve apiClient from Koin:", e) + } fun startApp() { try { console.log("[WebApp] startApp(): readyState=", document.asDynamic().readyState) diff --git a/clients/app/src/jsMain/resources/icons/icon-192.png b/frontend/shells/meldestelle-portal/src/jsMain/resources/icons/icon-192.png similarity index 100% rename from clients/app/src/jsMain/resources/icons/icon-192.png rename to frontend/shells/meldestelle-portal/src/jsMain/resources/icons/icon-192.png diff --git a/clients/app/src/jsMain/resources/icons/icon-512.png b/frontend/shells/meldestelle-portal/src/jsMain/resources/icons/icon-512.png similarity index 100% rename from clients/app/src/jsMain/resources/icons/icon-512.png rename to frontend/shells/meldestelle-portal/src/jsMain/resources/icons/icon-512.png diff --git a/clients/app/src/jsMain/resources/index.html b/frontend/shells/meldestelle-portal/src/jsMain/resources/index.html similarity index 56% rename from clients/app/src/jsMain/resources/index.html rename to frontend/shells/meldestelle-portal/src/jsMain/resources/index.html index 47fb586b..9c7f76f0 100644 --- a/clients/app/src/jsMain/resources/index.html +++ b/frontend/shells/meldestelle-portal/src/jsMain/resources/index.html @@ -12,6 +12,24 @@

Loading...
+