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:
parent
32e43b8fb0
commit
9e12018208
|
|
@ -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>()
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ kotlin {
|
|||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (?, ?, ?);
|
||||
|
|
|
|||
33
frontend/core/sync/build.gradle.kts
Normal file
33
frontend/core/sync/build.gradle.kts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()) }
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>()) }
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user