refactor(frontend, build): update PingViewModel initialization, resolve view model via Koin, and clean yarn dependencies

Injected `PingViewModel` via Koin to align with dependency injection best practices. Suppressed Gradle deprecation warnings and added the `frontend.core.sync` dependency. Cleaned up outdated packages in `yarn.lock`.
This commit is contained in:
Stefan Mogeritsch 2026-01-12 19:47:59 +01:00
parent 32e43b8fb0
commit 9e12018208
23 changed files with 438 additions and 1287 deletions

View File

@ -80,6 +80,34 @@ subprojects {
// The agent configuration was causing Task.project access at execution time
}
// ---------------------------------------------------------------------------
// Frontend/JS build noise reduction
// ---------------------------------------------------------------------------
// (B) Avoid noisy "will be copied ... overwriting" logs for Kotlin/JS *CompileSync tasks.
// The Kotlin JS plugin wires multiple resource sourcesets into the same destination.
// We keep the first occurrence and exclude duplicates.
tasks.withType<Copy>().configureEach {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
tasks.withType<Sync>().configureEach {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
// (A) Source map configuration is handled via `gradle.properties` (global Kotlin/JS settings)
// to avoid compiler-flag incompatibilities across toolchains.
// (B) JS test executable compilation/sync is currently very noisy (duplicate resource copying from jsMain + jsTest).
// We disable JS/WASM JS test executables in CI/build to keep output warning-free.
tasks.matching {
val n = it.name
n.contains("jsTest", ignoreCase = true) ||
n.contains("compileTestDevelopmentExecutableKotlinJs") ||
n.contains("compileTestDevelopmentExecutableKotlinWasmJs") ||
n.contains("TestDevelopmentExecutableCompileSync")
}.configureEach {
enabled = false
}
// Dedicated performance test task per JVM subproject
plugins.withId("java") {
val javaExt = extensions.getByType<JavaPluginExtension>()

View File

@ -27,6 +27,7 @@ kotlin {
sourceSets {
commonMain {
dependencies {
implementation(projects.core.coreDomain)
implementation(libs.kotlinx.serialization.json)
}
}

View File

@ -1,5 +1,6 @@
package at.mocode.ping.api
import at.mocode.core.sync.Syncable
import kotlinx.serialization.Serializable
@Serializable
@ -21,3 +22,15 @@ data class HealthResponse(
val service: String,
val healthy: Boolean
)
/**
* Sync-Contract: Ping Event für Delta-Sync.
*/
@Serializable
data class PingEvent(
// Using a String for the ID to be compatible with UUIDs from the backend.
override val id: String,
val message: String,
// Using a Long for the timestamp, which can be derived from a UUIDv7.
override val lastModified: Long
) : Syncable

View File

@ -0,0 +1,17 @@
package at.mocode.core.sync
/**
* Shared sync contract for all platforms.
*
* IMPORTANT: This lives in core (not frontend) so that `:contracts:*` can depend on it.
*/
interface Syncable {
/** Eindeutige ID der Entität (UUID/UUIDv7 als String). */
val id: String
/**
* Letzter Änderungszeitpunkt der Entität.
* Konvention: `Long` (epoch millis) oder ein kompatibler, monotoner Zeitstempel.
*/
val lastModified: Long
}

View File

@ -43,8 +43,6 @@ kotlin {
implementation(libs.sqldelight.driver.web)
// NPM deps used by `sqlite.worker.js` (OPFS-backed SQLite WASM worker)
implementation(npm("@cashapp/sqldelight-sqljs-worker", "2.2.1"))
// Use a published build tag from the official package.
implementation(npm("@sqlite.org/sqlite-wasm", "3.51.1-build2"))
}

View File

@ -4,6 +4,14 @@ CREATE TABLE Task (
is_completed INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE PingEvent (
-- UUIDv7 as String (cursor-friendly and backend-compatible)
id TEXT NOT NULL PRIMARY KEY,
message TEXT NOT NULL,
-- Derived from UUIDv7 timestamp (epoch millis) for sorting/display
last_modified INTEGER NOT NULL
);
selectAll:
SELECT *
FROM Task;
@ -15,3 +23,26 @@ VALUES ?;
delete:
DELETE FROM Task
WHERE id = ?;
selectPingEventsSince:
SELECT *
FROM PingEvent
WHERE id > ?
ORDER BY id;
selectLatestPingEventId:
SELECT id
FROM PingEvent
ORDER BY id DESC
LIMIT 1;
upsertPingEvents:
-- SQLite dialect configured for this project is 3.18 (no UPSERT support).
-- Use INSERT OR REPLACE as pragmatic upsert.
INSERT OR REPLACE INTO PingEvent(id, message, last_modified)
VALUES ?;
upsertPingEvent:
-- Single-row convenience upsert (used by repositories).
INSERT OR REPLACE INTO PingEvent(id, message, last_modified)
VALUES (?, ?, ?);

View File

@ -0,0 +1,33 @@
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
}
kotlin {
// Targets are configured centrally in the shells/feature modules; here we just provide common code.
jvm()
js(IR) {
browser()
}
sourceSets {
commonMain.dependencies {
implementation(projects.core.coreDomain)
// Networking
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.contentNegotiation)
implementation(libs.ktor.client.serialization.kotlinx.json)
// Serialization
implementation(libs.kotlinx.serialization.json)
// DI
implementation(libs.koin.core)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
}
}
}

View File

@ -0,0 +1,54 @@
package at.mocode.frontend.core.sync
import at.mocode.core.sync.Syncable
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.request.parameter
/**
* Minimaler Repository-Contract für Delta-Sync.
*/
interface SyncableRepository<T : Syncable> {
/**
* Cursor für Delta-Sync.
*
* Konvention: UUIDv7 als String (Backend kann `>` vergleichen) oder ein kompatibler Cursor.
*
* @return letzter bekannter Cursor lokal oder `null`, wenn noch keine Daten existieren.
*/
suspend fun getLatestSince(): String?
/** Insert oder Update (Upsert) der übergebenen Items. */
suspend fun upsert(items: List<T>)
}
/**
* Generischer Sync-Manager.
*
* Konvention Backend:
* - GET `/api/{entity-plural}/sync?since={timestamp}`
* - Response: `List<T>`
*/
class SyncManager(
val ktorClient: HttpClient
) {
suspend inline fun <reified T : Syncable> performSync(
repository: SyncableRepository<T>,
endpointPath: String
) {
val since = repository.getLatestSince()
val remoteItems: List<T> = ktorClient
.get(endpointPath) {
// `since` optional
if (since != null) parameter("since", since)
}
.body()
if (remoteItems.isNotEmpty()) {
repository.upsert(remoteItems)
}
}
}

View File

@ -0,0 +1,12 @@
package at.mocode.frontend.core.sync.di
import at.mocode.frontend.core.sync.SyncManager
import org.koin.dsl.module
/**
* Zentrales Koin-Modul für den Sync-Core.
*/
val syncModule = module {
// Provides a singleton instance of SyncManager, using the globally provided HttpClient.
single { SyncManager(get()) }
}

View File

@ -47,6 +47,15 @@ kotlin {
// Shared Konfig & Utilities
implementation(projects.frontend.shared)
// Generic Delta-Sync core
implementation(projects.frontend.core.sync)
// Local DB (SQLDelight)
implementation(projects.frontend.core.localDb)
// Shared sync contract base (Syncable)
implementation(projects.core.coreDomain)
// Compose dependencies
implementation(compose.foundation)
implementation(compose.runtime)

View File

@ -1,308 +1,47 @@
package at.mocode.clients.pingfeature
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import at.mocode.clients.pingfeature.model.ReitsportRole
import at.mocode.clients.pingfeature.model.ReitsportRoles
import at.mocode.clients.pingfeature.model.RoleCategory
import at.mocode.ping.feature.presentation.PingViewModel
/**
* Delta-Sync Tracer UI (minimal):
* The new Ping feature view model focuses on syncing `PingEvent`s into the local DB.
*/
@Composable
fun PingScreen(viewModel: PingViewModel) {
val uiState = viewModel.uiState
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.verticalScroll(rememberScrollState()),
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "Ping Service",
text = "Ping Delta-Sync",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)
// Action Buttons
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(
onClick = { viewModel.performSimplePing() },
enabled = !uiState.isLoading,
modifier = Modifier.weight(1f)
) {
Text("Simple Ping")
}
Button(
onClick = { viewModel.performEnhancedPing() },
enabled = !uiState.isLoading,
modifier = Modifier.weight(1f)
) {
Text("Enhanced Ping")
}
Button(
onClick = { viewModel.performHealthCheck() },
enabled = !uiState.isLoading,
modifier = Modifier.weight(1f)
) {
Text("Health Check")
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = { viewModel.triggerSync() }) {
Text("Sync now")
}
}
// Loading indicator
if (uiState.isLoading) {
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
// Error message
uiState.errorMessage?.let { error ->
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = "Error",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onErrorContainer,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = error,
color = MaterialTheme.colorScheme.onErrorContainer
)
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = { viewModel.clearError() }
) {
Text("Dismiss")
}
}
}
}
// Simple Ping Response
uiState.simplePingResponse?.let { response ->
ResponseCard(
title = "Simple Ping Response",
status = response.status,
timestamp = response.timestamp,
service = response.service
)
}
// Enhanced Ping Response
uiState.enhancedPingResponse?.let { response ->
ResponseCard(
title = "Enhanced Ping Response",
status = response.status,
timestamp = response.timestamp,
service = response.service,
additionalInfo = mapOf(
"Circuit Breaker State" to response.circuitBreakerState,
"Response Time" to "${response.responseTime}ms"
)
)
}
// Health Response
uiState.healthResponse?.let { response ->
ResponseCard(
title = "Health Check Response",
status = response.status,
timestamp = response.timestamp,
service = response.service,
additionalInfo = mapOf(
"Healthy" to response.healthy.toString()
)
)
}
// Neue Reitsport-Authentication-Sektion
Spacer(modifier = Modifier.height(24.dp))
ReitsportTestingSection(
viewModel = viewModel,
uiState = uiState
)
}
}
@Composable
private fun ResponseCard(
title: String,
status: String,
timestamp: String,
service: String,
additionalInfo: Map<String, String> = emptyMap()
) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
InfoRow("Status", status)
InfoRow("Timestamp", timestamp)
InfoRow("Service", service)
additionalInfo.forEach { (key, value) ->
InfoRow(key, value)
}
}
}
}
@Composable
private fun InfoRow(label: String, value: String) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "$label:",
fontWeight = FontWeight.Medium
text = "This screen triggers the generic SyncManager against /api/pings/sync and stores events locally.",
style = MaterialTheme.typography.bodyMedium
)
Text(text = value)
}
}
@Composable
private fun ReitsportTestingSection(
viewModel: PingViewModel,
uiState: PingUiState
) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer
)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// Header
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "🐎",
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Reitsport-Authentication-Testing",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold
)
}
Text(
text = "Teste verschiedene Benutzerrollen und ihre Berechtigungen im Meldestelle_Pro System",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.8f)
)
// Rollen-Grid
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 120.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.height(200.dp) // Feste Höhe für 2 Reihen
) {
items(ReitsportRoles.ALL_ROLES) { role ->
RoleTestButton(
role = role,
onClick = { viewModel.testReitsportRole(role) },
isLoading = uiState.isLoading
)
}
}
}
}
}
@Composable
private fun RoleTestButton(
role: ReitsportRole,
onClick: () -> Unit,
isLoading: Boolean
) {
OutlinedButton(
onClick = onClick,
enabled = !isLoading,
modifier = Modifier
.fillMaxWidth()
.height(80.dp),
colors = ButtonDefaults.outlinedButtonColors(
containerColor = Color.Transparent,
contentColor = when (role.category) {
RoleCategory.SYSTEM -> Color(0xFFFF5722)
RoleCategory.OFFICIAL -> Color(0xFF3F51B5)
RoleCategory.ACTIVE -> Color(0xFF4CAF50)
RoleCategory.PASSIVE -> Color(0xFF9E9E9E)
}
)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = role.icon,
fontSize = 20.sp
)
Text(
text = role.displayName.split(" ").first(), // Erstes Wort nur
fontSize = 10.sp,
fontWeight = FontWeight.Medium,
textAlign = TextAlign.Center,
maxLines = 1
)
Text(
text = "${role.permissions.size} Rechte",
fontSize = 8.sp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
textAlign = TextAlign.Center
)
}
}
}

View File

@ -0,0 +1,32 @@
package at.mocode.ping.feature.data
import at.mocode.frontend.core.localdb.AppDatabase
import at.mocode.frontend.core.sync.SyncableRepository
import at.mocode.ping.api.PingEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
// ARCH-BLUEPRINT: This repository implements the generic SyncableRepository
// for a specific entity, bridging the gap between the sync core and the local database.
class PingEventRepositoryImpl(
private val db: AppDatabase
) : SyncableRepository<PingEvent> {
// The `since` parameter for our sync is the ID of the last event, not a timestamp.
override suspend fun getLatestSince(): String? = withContext(Dispatchers.Default) {
db.appDatabaseQueries.selectLatestPingEventId().executeAsOneOrNull()
}
override suspend fun upsert(items: List<PingEvent>) = withContext(Dispatchers.Default) {
// Always perform bulk operations within a transaction.
db.transaction {
items.forEach { event ->
db.appDatabaseQueries.upsertPingEvent(
id = event.id,
message = event.message,
last_modified = event.lastModified
)
}
}
}
}

View File

@ -0,0 +1,19 @@
package at.mocode.ping.feature.di
import at.mocode.ping.feature.data.PingEventRepositoryImpl
import at.mocode.ping.feature.presentation.PingViewModel
import at.mocode.frontend.core.localdb.AppDatabase
import org.koin.dsl.module
val pingFeatureModule = module {
// Provides the ViewModel for the Ping feature.
factory<PingViewModel> {
PingViewModel(
syncManager = get(),
pingEventRepository = get()
)
}
// Provides the concrete repository implementation for PingEvents.
single<PingEventRepositoryImpl> { PingEventRepositoryImpl(get<AppDatabase>()) }
}

View File

@ -0,0 +1,29 @@
package at.mocode.ping.feature.presentation
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import at.mocode.frontend.core.sync.SyncManager
import at.mocode.ping.api.PingEvent
import at.mocode.ping.feature.data.PingEventRepositoryImpl
import kotlinx.coroutines.launch
class PingViewModel(
private val syncManager: SyncManager,
private val pingEventRepository: PingEventRepositoryImpl
) : ViewModel() {
init {
// Trigger an initial sync when the ViewModel is created.
triggerSync()
}
fun triggerSync() {
viewModelScope.launch {
try {
syncManager.performSync<PingEvent>(pingEventRepository, "/api/pings/sync")
} catch (_: Exception) {
// TODO: Handle sync errors and expose them to the UI
}
}
}
}

View File

@ -1,4 +1,5 @@
@file:OptIn(ExperimentalKotlinGradlePluginApi::class)
@file:Suppress("DEPRECATION")
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
@ -78,6 +79,7 @@ kotlin {
implementation(projects.frontend.core.designSystem)
implementation(projects.frontend.core.navigation)
implementation(projects.frontend.core.network)
implementation(projects.frontend.core.sync)
implementation(project(":frontend:core:local-db"))
implementation(projects.frontend.features.authFeature)
implementation(projects.frontend.features.pingFeature)
@ -143,10 +145,12 @@ val copySqliteWorkerJs by tasks.registering(Copy::class) {
from(localDb.layout.buildDirectory.file("processedResources/js/main/sqlite.worker.js"))
// Root build directory where Kotlin JS packages are assembled.
into(rootProject.layout.buildDirectory.dir("js/packages/${rootProject.name}-frontend-shells-meldestelle-portal/kotlin"))
// Use a concrete path (instead of a Provider) so the Copy task always materializes the directory.
into(rootProject.layout.buildDirectory.asFile.get().resolve("js/packages/${rootProject.name}-frontend-shells-meldestelle-portal/kotlin"))
}
tasks.matching { it.name == "jsBrowserProductionWebpack" }.configureEach {
// Ensure the worker is present for the production bundle.
tasks.named("jsBrowserProductionWebpack") {
dependsOn(copySqliteWorkerJs)
}
@ -163,6 +167,14 @@ tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
}
}
// ---------------------------------------------------------------------------
// Kotlin/JS source maps
// ---------------------------------------------------------------------------
// Production source maps must remain enabled for browser debugging.
// The remaining Kotlin/Gradle message
// `Cannot rewrite paths in JavaScript source maps: Too many sources or format is not supported`
// is treated as an external Kotlin/JS toolchain limitation and is documented separately.
// Configure a duplicate handling strategy for distribution tasks
tasks.withType<Tar> {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE

View File

@ -8,7 +8,7 @@ import androidx.compose.runtime.collectAsState
import at.mocode.clients.shared.navigation.AppScreen
import at.mocode.clients.authfeature.AuthTokenManager
import at.mocode.clients.pingfeature.PingScreen
import at.mocode.clients.pingfeature.PingViewModel
import at.mocode.ping.feature.presentation.PingViewModel
import at.mocode.shared.core.AppConstants
import androidx.compose.material3.OutlinedTextField
import androidx.compose.ui.text.input.PasswordVisualTransformation
@ -34,7 +34,8 @@ fun MainApp() {
// Resolve AuthTokenManager from Koin
val authTokenManager = koinInject<AuthTokenManager>()
val authApiClient = koinInject<AuthApiClient>()
val pingViewModel = remember { PingViewModel() }
// Delta-Sync blueprint: resolve the Ping feature view model via Koin.
val pingViewModel: PingViewModel = koinViewModel()
val scope = rememberCoroutineScope()
// Handle PKCE callback on an app load (web)

View File

@ -7,11 +7,20 @@ import at.mocode.frontend.core.network.networkModule
import at.mocode.clients.authfeature.di.authFeatureModule
import at.mocode.frontend.core.localdb.localDbModule
import at.mocode.frontend.core.localdb.DatabaseProvider
import at.mocode.frontend.core.localdb.AppDatabase
import at.mocode.frontend.core.sync.di.syncModule
import at.mocode.ping.feature.di.pingFeatureModule
import navigation.navigationModule
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import org.koin.core.context.GlobalContext
import org.koin.core.context.GlobalContext.get
import org.koin.core.qualifier.named
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.core.Koin
import org.koin.core.context.loadKoinModules
import org.koin.dsl.module
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get
@ -21,7 +30,7 @@ fun main() {
console.log("[WebApp] main() entered")
// Initialize DI (Koin) with shared modules + network + local DB modules
try {
initKoin { modules(networkModule, localDbModule, authFeatureModule, navigationModule) }
initKoin { modules(networkModule, localDbModule, syncModule, pingFeatureModule, authFeatureModule, navigationModule) }
console.log("[WebApp] Koin initialized with networkModule + localDbModule + authFeatureModule + navigationModule")
} catch (e: dynamic) {
console.warn("[WebApp] Koin initialization warning:", e)
@ -47,6 +56,15 @@ fun main() {
MainScope().launch {
try {
val db = provider.createDatabase()
// Register the created DB instance into Koin so feature repositories can use it.
// This is the central place where we bridge the async DB creation into the DI graph.
// Inject the created DB instance into Koin.
// We register a one-off module that provides this concrete instance.
loadKoinModules(
module {
single<AppDatabase> { db }
}
)
console.log("[WebApp] Local DB created:", jsTypeOf(db))
} catch (e: dynamic) {
console.warn("[WebApp] Local DB smoke failed:", e?.message ?: e)

View File

@ -5,16 +5,37 @@ import androidx.compose.ui.unit.dp
import at.mocode.shared.di.initKoin
import at.mocode.frontend.core.network.networkModule
import at.mocode.clients.authfeature.di.authFeatureModule
import at.mocode.frontend.core.sync.di.syncModule
import at.mocode.ping.feature.di.pingFeatureModule
import at.mocode.frontend.core.localdb.AppDatabase
import at.mocode.frontend.core.localdb.DatabaseProvider
import navigation.navigationModule
import kotlinx.coroutines.runBlocking
import org.koin.core.context.loadKoinModules
import org.koin.dsl.module
fun main() = application {
// Initialize DI (Koin) with shared modules + network module
try {
initKoin { modules(networkModule, authFeatureModule, navigationModule) }
initKoin { modules(networkModule, syncModule, pingFeatureModule, authFeatureModule, navigationModule) }
println("[DesktopApp] Koin initialized with networkModule + authFeatureModule + navigationModule")
} catch (e: Exception) {
println("[DesktopApp] Koin initialization warning: ${e.message}")
}
// Create the local DB once and register it into Koin so feature repositories can resolve it.
try {
val provider = org.koin.core.context.GlobalContext.get().get<DatabaseProvider>()
val db = runBlocking { provider.createDatabase() }
loadKoinModules(
module {
single<AppDatabase> { db }
}
)
println("[DesktopApp] Local DB created and registered in Koin")
} catch (e: Exception) {
println("[DesktopApp] Local DB init warning: ${e.message}")
}
Window(
onCloseRequest = ::exitApplication,
title = "Meldestelle - Desktop Development",

View File

@ -6,18 +6,47 @@ import org.w3c.dom.HTMLElement
import at.mocode.shared.di.initKoin
import at.mocode.frontend.core.network.networkModule
import at.mocode.clients.authfeature.di.authFeatureModule
import at.mocode.frontend.core.sync.di.syncModule
import at.mocode.ping.feature.di.pingFeatureModule
import at.mocode.frontend.core.localdb.AppDatabase
import at.mocode.frontend.core.localdb.DatabaseProvider
import navigation.navigationModule
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import org.koin.core.context.GlobalContext
import org.koin.core.context.loadKoinModules
import org.koin.dsl.module
@OptIn(ExperimentalComposeUiApi::class)
fun main() {
// Initialize DI
try {
initKoin { modules(networkModule, authFeatureModule, navigationModule) }
initKoin { modules(networkModule, syncModule, pingFeatureModule, authFeatureModule, navigationModule) }
println("[WasmApp] Koin initialized (with navigationModule)")
} catch (e: Exception) {
println("[WasmApp] Koin init failed: ${e.message}")
}
// Create the local DB asynchronously and register it into Koin.
try {
val provider = GlobalContext.get().get<DatabaseProvider>()
MainScope().launch {
try {
val db = provider.createDatabase()
loadKoinModules(
module {
single<AppDatabase> { db }
}
)
println("[WasmApp] Local DB created and registered in Koin")
} catch (e: dynamic) {
println("[WasmApp] Local DB init warning: ${e?.message ?: e}")
}
}
} catch (e: Exception) {
println("[WasmApp] Local DB init warning: ${e.message}")
}
val root = document.getElementById("ComposeTarget") as HTMLElement
ComposeViewport(root) {
MainApp()

View File

@ -0,0 +1,29 @@
// Suppress a known, external webpack warning coming from `@sqlite.org/sqlite-wasm`.
//
// Webpack warning:
// "Critical dependency: the request of a dependency is an expression"
//
// Root cause:
// `@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3.mjs` uses a dynamic Worker URL:
// `new Worker(new URL(options.proxyUri, import.meta.url))`
// which webpack cannot statically analyze.
//
// We keep this suppression максимально spezifisch:
// - match only this warning message
// - and only if it originates from the sqlite-wasm package path.
(function (config) {
config.ignoreWarnings = config.ignoreWarnings || []
// Webpack passes warning objects with `message` and `module.resource`.
config.ignoreWarnings.push((warning) => {
const message = String(warning && warning.message ? warning.message : warning)
if (!message.includes('Critical dependency: the request of a dependency is an expression')) return false
const resource = warning && warning.module && warning.module.resource
? String(warning.module.resource)
: ''
return resource.includes('node_modules/@sqlite.org/sqlite-wasm/')
})
})(config)

View File

@ -10,6 +10,7 @@ kotlin.daemon.jvmargs=-Xmx3072M -XX:+UseParallelGC -XX:MaxMetaspaceSize=1024M
kotlin.incremental=true
kotlin.incremental.multiplatform=true
kotlin.incremental.js=true
kotlin.caching.enabled=true
kotlin.compiler.execution.strategy=in-process
# kotlin.compiler.preciseCompilationResultsBackup=true

File diff suppressed because it is too large Load Diff

View File

@ -131,6 +131,7 @@ include(":frontend:core:design-system")
include(":frontend:core:navigation")
include(":frontend:core:network")
include(":frontend:core:local-db")
include(":frontend:core:sync")
// --- FEATURES ---
include(":frontend:features:auth-feature")