diff --git a/docs/06_Frontend/README.md b/docs/06_Frontend/README.md index 97f8a0ec..6a4f1551 100644 --- a/docs/06_Frontend/README.md +++ b/docs/06_Frontend/README.md @@ -40,6 +40,7 @@ Das `frontend`-Verzeichnis ist wie folgt strukturiert, um eine klare Trennung de ## Wichtige Dokumente +* **[State-Management Strategie (UDF)](state-management-strategy.md):** Beschreibt die empfohlene Strategie für komplexe Screens. * **[ADR-0010: SQLDelight für Cross-Platform-Persistenz](../01_Architecture/adr/0010-sqldelight-for-cross-platform-persistence.md):** Beschreibt die Entscheidung für SQLDelight. * **[ADR-0011: Koin für Dependency Injection](../01_Architecture/adr/0011-koin-for-dependency-injection.md):** Beschreibt die Entscheidung für Koin. * **[Offline-First-Architektur](offline-first-architecture.md):** Detaillierte Beschreibung der Offline-First-Strategie. diff --git a/docs/06_Frontend/state-management-strategy.md b/docs/06_Frontend/state-management-strategy.md new file mode 100644 index 00000000..d13b2309 --- /dev/null +++ b/docs/06_Frontend/state-management-strategy.md @@ -0,0 +1,98 @@ +--- +type: Reference +status: DRAFT +owner: Frontend Expert +last_update: 2026-01-28 +--- + +# Frontend State-Management Strategie (UDF) + +Dieses Dokument beschreibt die empfohlene Strategie für das State Management in komplexen UI-Komponenten wie Formularen und Reports, um die Skalierbarkeit und Wartbarkeit des Frontends sicherzustellen. + +## Problemstellung: Grenzen des einfachen State Managements + +Für einfache Screens ist die Verwendung von mehreren `StateFlow`s in einem ViewModel ein valider Ansatz. + +```kotlin +// Beispiel für einen einfachen Ansatz +class SimpleViewModel : ViewModel() { + private val _name = MutableStateFlow("") + val name: StateFlow = _name + + private val _isLoading = MutableStateFlow(false) + val isLoading: StateFlow = _isLoading +} +``` + +Bei wachsender Komplexität (viele Felder, Validierungsregeln, asynchrone Aktionen) führt dieser Ansatz zu Problemen: +* **Inkonsistente Zustände:** Es ist leicht, einen State zu vergessen (z.B. `isLoading` auf `false` zu setzen, aber die Fehlermeldung nicht zu löschen), was zu schwer nachvollziehbaren UI-Bugs führt. +* **Schwere Testbarkeit:** Die Logik ist über viele Funktionen verteilt, die einzelne State-Variablen mutieren. +* **Race Conditions:** Mehrere gleichzeitige, asynchrone Updates können den State unvorhersehbar machen. + +## Lösungsstrategie: Unidirectional Data Flow (UDF) + +Um diese Probleme zu lösen, wird die Einführung eines **Unidirectional Data Flow (UDF)** Musters für alle neuen, komplexen Features empfohlen. + +UDF erzwingt einen strikten, vorhersagbaren Kreislauf: + +1. **State:** Ein **einziges, unveränderliches (immutable) `data class`** repräsentiert den gesamten Zustand der UI. Dies ist die *Single Source of Truth*. +2. **Event/Intent:** Die UI ändert den Zustand niemals direkt. Stattdessen sendet sie ein **Event** (z.B. `SaveButtonClicked`), um eine Benutzeraktion zu signalisieren. +3. **Logic (Reducer):** Eine zentrale Logik-Komponente (im ViewModel) empfängt das Event, führt Geschäftslogik aus und produziert einen **neuen, kompletten State**. + +### Beispiel-Implementierung + +**1. Ein einziges State-Objekt:** +```kotlin +data class FormState( + val name: String = "", + val email: String = "", + val isSaving: Boolean = false, + val saveError: String? = null, + val isFormValid: Boolean = false +) +``` + +**2. Klare Events von der UI:** +```kotlin +sealed interface FormEvent { + data class NameChanged(val newName: String) : FormEvent + object SaveButtonClicked : FormEvent + object ErrorDismissed : FormEvent +} +``` + +**3. Zentrale Logik im ViewModel:** +```kotlin +class FormViewModel : ViewModel() { + private val _state = MutableStateFlow(FormState()) + val state: StateFlow = _state + + fun onEvent(event: FormEvent) { + when (event) { + is FormEvent.NameChanged -> { + _state.update { it.copy(name = event.newName, isFormValid = /*...*/) } + } + is FormEvent.SaveButtonClicked -> { + _state.update { it.copy(isSaving = true) } + // ... + } + } + } +} +``` + +## Empfohlene Bibliotheken + +Die manuelle Implementierung von UDF ist möglich, aber dedizierte Bibliotheken bieten eine bewährte Struktur und reduzieren Boilerplate. + +1. **Voyager:** + * **Primär eine Navigationsbibliothek**, die aber ein leichtgewichtetes und pragmatisches UDF-System (`ScreenModel`) mitbringt. + * **Empfehlung:** **Der empfohlene Einstiegspunkt.** Da das Projekt ohnehin eine Navigationslösung benötigt, ist dies die effizienteste Wahl. + +2. **MVIKotlin:** + * Eine sehr mächtige, explizite MVI-Bibliothek (eine Form von UDF). + * **Empfehlung:** **In der Hinterhand behalten.** Für extrem komplexe Features, bei denen erweiterte Funktionen wie Time-Travel-Debugging den Mehraufwand rechtfertigen, kann MVIKotlin gezielt eingesetzt werden. + +## Nächste Schritte + +Diese Strategie muss nicht sofort für bestehende Screens umgesetzt werden. Sie soll als **Blaupause für alle zukünftigen, fachlichen Features** dienen, sobald das grundlegende SQLDelight-Sync-Problem gelöst ist. diff --git a/docs/99_Journal/2026-01-28_Session_Log.md b/docs/99_Journal/2026-01-28_Session_Log.md new file mode 100644 index 00000000..f9bbe2f8 --- /dev/null +++ b/docs/99_Journal/2026-01-28_Session_Log.md @@ -0,0 +1,38 @@ +--- +type: Journal +status: COMPLETED +owner: Curator +date: 2026-01-28 +participants: + - Lead Architect + - Frontend Expert +--- + +# Session Log: 28. Jänner 2026 - Lösung des SQLDelight-Sync-Problems + +## Zielsetzung +Systematische Analyse und Behebung des kritischen SQLDelight-Bugs in der Web-App (JS/Wasm), der einen echten Delta-Sync verhinderte. + +## Durchgeführte Arbeiten + +### 1. Analyse & Fehlerreproduktion +* **Ausgangslage:** Der `PingEventRepositoryImpl` enthielt einen Workaround, der `getLatestSince()` immer `null` zurückgeben ließ, um einen Full-Sync zu erzwingen. +* **Reproduktion:** Der Workaround wurde entfernt und durch den ursprünglichen Code (`db.appDatabaseQueries.selectLatestPingEventId().executeAsOneOrNull()`) ersetzt. +* **Ergebnis:** Der Fehler `The driver used with SQLDelight is asynchronous...` wurde wie erwartet in der Browser-Konsole reproduziert. + +### 2. Systematische Ursachenforschung +* **Hypothese 1 (Konfiguration):** Die Build-Konfiguration (`frontend/core/local-db/build.gradle.kts`) wurde überprüft. `generateAsync.set(true)` war korrekt gesetzt. Die Fehlermeldung war also eine falsche Fährte. +* **Hypothese 2 (API-Nutzung):** Die Analyse ergab, dass `.executeAsOneOrNull()` eine **blockierende** API ist, die mit dem asynchronen Web-Worker-Treiber in Konflikt steht. +* **Lösung:** Die korrekte, **nicht-blockierende** API aus der SQLDelight-Coroutines-Erweiterung muss verwendet werden. + +### 3. Implementierung des Fixes +* Der Aufruf in `PingEventRepositoryImpl` wurde von `executeAsOneOrNull()` auf `awaitAsOneOrNull()` geändert. +* Der korrekte Import-Pfad `app.cash.sqldelight.async.coroutines.awaitAsOneOrNull` wurde hinzugefügt. + +## Ergebnis & Status +* **Erfolg:** Das SQLDelight-Sync-Problem ist **gelöst**. +* Die Web-App führt nun einen korrekten **Delta-Sync** durch, was durch den Aufruf des `/api/ping/sync?since=...` Endpunkts im Netzwerk-Log bestätigt wurde. +* Die wichtigste technische Schuld im Frontend wurde beseitigt. + +## Nächste Schritte (Diskutiert) +* **Docker-Integration:** Das Frontend (Build & Deployment) soll in die bestehende Docker-Konstruktion des Projekts integriert werden. diff --git a/docs/ScreenShots/event-log_2026-01-28 12-20-22.png b/docs/ScreenShots/event-log_2026-01-28 12-20-22.png new file mode 100644 index 00000000..47741561 Binary files /dev/null and b/docs/ScreenShots/event-log_2026-01-28 12-20-22.png differ diff --git a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/ping/feature/data/PingEventRepositoryImpl.kt b/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/ping/feature/data/PingEventRepositoryImpl.kt index e82e9cba..1dd88bf2 100644 --- a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/ping/feature/data/PingEventRepositoryImpl.kt +++ b/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/ping/feature/data/PingEventRepositoryImpl.kt @@ -3,6 +3,7 @@ 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 app.cash.sqldelight.async.coroutines.awaitAsOneOrNull // ARCH-BLUEPRINT: This repository implements the generic SyncableRepository // for a specific entity, bridging the gap between the sync core and the local database. @@ -12,12 +13,9 @@ class PingEventRepositoryImpl( // The `since` parameter for our sync is the ID of the last event, not a timestamp. override suspend fun getLatestSince(): String? { - println("PingEventRepositoryImpl: getLatestSince called") - // WORKAROUND: executeAsOneOrNull() fails with "driver is asynchronous" error. - // This seems to be a bug or configuration issue where the sync version is called. - // Since we are in Phase 2 (Tracer Bullet), we can live with a full sync for now. - // We return null to force a full sync, which works because upsert() works. - return null + println("PingEventRepositoryImpl: getLatestSince called - using corrected async implementation") + // FIX: Use .awaitAsOneOrNull() for async drivers instead of the blocking .executeAsOneOrNull() + return db.appDatabaseQueries.selectLatestPingEventId().awaitAsOneOrNull() } override suspend fun upsert(items: List) {