--- type: Concept status: DRAFT owner: Lead Architect last_update: 2026-04-03 --- # Konzept: Offline-First Synchronisation (Desktop ↔ Backend) ## Ziel und Rahmen Dieses Dokument definiert das Synchronisations-Konzept zwischen der Compose Desktop App (Meldestelle-Zentrale) und dem Backend in einem Offline-First Szenario. Es baut auf ADR-0021 (Tenant-Isolation) und ADR-0022 (LAN-Sync mit Lamport-Uhren) auf und erweitert sie um die WAN/Backend-Synchronisation. Nicht-Ziele: Cloud-Realtime für Endnutzer, kollaboratives Editieren außerhalb des Veranstaltungsbetriebs. ## Leitprinzipien 1. Offline-First: Die Desktop-App ist voll funktionsfähig ohne Netzwerk; Synchronisation erfolgt opportunistisch. 2. Event-isoliert: Pro Veranstaltung eigener Datenraum (gemäß ADR-0021). Keine Vermischung von Events. 3. Einheitliches Änderungsmodell: Wiederverwendung des `SyncEvent`-Logs (ADR-0022) für Desktop↔Backend. 4. Domänen-Mastership: Klare Schreibhoheiten reduzieren Konflikte, fachliche Regeln haben Vorrang vor rein technischen Timestamps. 5. Deterministische Konfliktauflösung: Lamport-Uhren + Regel-Matrix; keine Abhängigkeit von Systemuhren. ## Topologie & Rollen - Backend (Zentrale Plattform): - Master für: Stammdaten (Reiter, Pferde, Vereine, Funktionäre), Identität/Rollen, Gebührenkataloge, globale Referenzen. - Aggregations-/Archiv-Quelle nach Veranstaltungsende (finale Ergebnisse, Abrechnungen). - Desktop (Meldestelle-Zentrale): - Master während der Veranstaltung für: Nennungen (operativ), Startreihenfolgen, Startlisten-Status, Ergebnisse/Protokolle (falls Richter nicht direkt am Backend), Kassa-Operationen vor Ort. - Hält lokales `SyncEvent`-Log + Snapshots (vgl. ADR-0022) und synchronisiert mit Backend, sobald Konnektivität besteht. Hinweis Mehrfach-Desktops: Genau eine „Zentrale“ pro Veranstaltung besitzt Schreibhoheit (Konfig-Flag `isEventAuthority=true`). Weitere Geräte sind Replikate/Clients. ## Datenkategorien & Mastership | Kategorie | Master | Desktop Rechte | Backend Rechte | |--------------------------|----------------|--------------------------------|-----------------------------------| | Stammdaten (Actor) | Backend | Lesen, lokal „provisional“ anlegen (Temp-ID) | Vollzugriff, ID-Zuteilung, Merge | | Veranstaltungs-Stammdaten| Backend | Lesen | Vollzugriff | | Nennungen operativ | Desktop | Vollzugriff | Lesen, Import nach Sync | | Startreihenfolge/Status | Desktop | Vollzugriff | Lesen, Import nach Sync | | Bewertungen/Ergebnisse | Desktop/Richter| Vollzugriff (Eventzeitraum) | Lesen, Publikation/Archiv | | Kassa/Finanzen vor Ort | Desktop | Vollzugriff | Lesen, Abgleich Summen | Konflikte über Kategoriegrenzen werden durch Mastership-Regeln verhindert; verbleibende Konflikte werden per Regel-Matrix gelöst. ## Änderungsmodell (Wiederverwendung `SyncEvent`) Struktur wie in ADR-0022 beschrieben: ```kotlin data class SyncEvent( val eventId: String, val turnierId: String?, val sequenceNumber: Long, // Lamport val originNodeId: String, // Desktop-ID oder Backend-Node-ID val aggregateType: String, // z. B. "Nennung", "Bewertung", "Start" val aggregateId: String, val eventType: String, val payload: ByteArray, val createdAt: Instant, val checksum: String, val schemaVersion: Int, ) ``` - Erweiterung: `schemaVersion` ist Pflichtfeld für WAN-Sync (Schema-Evolution, Rolling Upgrades). - Persistenz: `sync_events`, `sync_snapshots` lokal (SQLDelight) und im Backend (pro Tenant-Schema) gespiegelt. ## Lamport-Uhren & Vector-Clock (Optional) - Primär: Lamport-Uhren wie ADR-0022. Gleichstand → lexikografisch größere `originNodeId` gewinnt (Determinismus). - Optional für feingranulare Erkennung: Per-Aggregat Vector-Clock (`Map`) zur Diagnose; Entscheidungsgrundlage bleibt Lamport + Fachregeln. ## Sync-Protokoll Desktop ↔ Backend (HTTPS) Transport: HTTPS (HTTP/2), JSON oder Protobuf, idempotente Endpunkte. Auth: mTLS zwischen Desktop und Backend ODER OAuth2 Client Credentials + Signatur der Batch-Payload. Empfohlene Endpunkte (pro `eventId`): ``` POST /api/sync/{eventId}/hello → { nodeId, lastKnownSeq } → { backendNodeId, currentSeq, minSupportedSchema } POST /api/sync/{eventId}/pull → { sinceSeq, limit } → [ SyncEvent... ], { nextSeq } POST /api/sync/{eventId}/push → [ SyncEvent... ] → { ackedMaxSeq, rejected:[ids...] } POST /api/sync/{eventId}/snapshot/request → { scope } → { snapshotBlob, snapshotSeq } POST /api/sync/{eventId}/diagnostics → { stats } → { advice } ``` Batching: bis 512 Events oder 1 MiB pro Batch. Serverseitiges Paging über `sinceSeq`/`nextSeq`. Idempotenz: Jeder `SyncEvent` wird durch `(eventId, originNodeId, sequenceNumber, checksum)` dedupliziert. ## Konfliktauflösung 1) Strukturkonflikte (gleiches Aggregat, konkurrierende Events): - Wenn eine Seite nicht Master ist → Event wird angenommen, aber als `PENDING_REVIEW` markiert; fachliche Entscheidung erforderlich (Backend-UI oder Desktop-Review-Queue). - Beide Master (Sonderfälle, z. B. Ergebnisse während parallelem Backend-Fix): - Lamport höher gewinnt. - Gleichstand → `originNodeId`-Tiebreaker. - Zusätzlich fachliche Heuristik optional: „Korrektur-Events“ (z. B. `ErgebnisKorrigiert`) schlagen normale `ErgebnisErfasst` bei gleichem Lamport. 2) Identitätskonflikte (provisionale Stammdaten): - Desktop darf temporäre Einträge (Temp-ID `tmp-...`) erzeugen. - Beim Push führt Backend `Upsert+Merge` aus, weist finale IDs zu und liefert `IdMapping { tmpId -> finalId }` zurück. - Desktop ersetzt Referenzen transaktional und emittiert ein lokales `IdRemapped`-Event (kein Re-Upload nötig, außer für Diagnose). 3) Reihenfolge-/Kausalitätskonflikte: - Bei fehlenden Vorgänger-Events antwortet Backend mit `rejected: [id]` und `requiredSinceSeq`. Desktop zieht Delta (`pull`) und wiederholt den `push`. ## Snapshots & Recovery - Snapshot-Intervall: standardmäßig 100 Events pro `(aggregateType, scope)` (wie ADR-0022), für WAN-Sync zusätzlich Full-State-Snapshot pro Veranstaltung vor Event-Abschluss. - Recovery: Desktop kann mit leerem Log starten → `snapshot/request` → Full-State + `snapshotSeq` → weitere Deltas über `pull`. - USB-Fallback (Notbetrieb): Export/Import von `sync_events` und `sync_snapshots` als verschlüsselte Archive (`.msync`). Offene Spezifikation; separater PoC. ## Sicherheit - Mandantentrennung: Jeder Request trägt `X-Event-Id` (ADR-0021). Backend validiert gegen `control.tenants`. - Transport: `https` + mTLS (bevorzugt) oder `https` + OAuth2 Client Credentials. Payload-Signatur (HMAC-SHA256) empfohlen. - Integrität: `checksum` pro Event wird serverseitig geprüft; Mismatch → Reject. - Rechte: Backend erzwingt Mastership-Regeln serverseitig; Verstöße → `PENDING_REVIEW` + Audit-Log. ## Fehlerfälle & Resilienz - Netzwerkfehler: Exponentielles Backoff (bis 5 min), Offline-Queue unbegrenzt (bounded by disk quota), Telemetrie im UI. - Schema-Divergenz: `minSupportedSchema` aus `hello`; Desktop migriert vor weiterem Sync oder schaltet in Read-Only. - Duplikate: Idempotenz-Keys verhindern Doppelverarbeitung. ACK enthält höchste verarbeitete `sequenceNumber`. ## Observability - Metriken: `sync_push_events_total`, `sync_pull_events_total`, `sync_rejected_total`, `sync_latency_ms` (p50/p95), `offline_duration_s`. - Logs: pro Event `tenant`, `originNodeId`, `seq`, `aggType`, `eventType`, `result`. - UI: Status-Anzeige (Verbunden, Getrennt, Ausstehend X), Konflikt-Review-Queue. ## Einführungsplan (Auszug) 1. Core: `SyncEvent` in Shared-KMP-Modul härten (`schemaVersion`), Persistenzschicht Desktop/Backend angleichen. 2. Backend-API: `hello/pull/push/snapshot` Endpunkte implementieren (Spring Boot/Ktor), Mandantentrennung. 3. Desktop-Client: Batch-Sync, Retry, Id-Mapping-Mechanismus. 4. Review-UI: `PENDING_REVIEW`-Queue im Backend (Admin) und Anzeige im Desktop. 5. E2E-Tests: Offline-Phase, Reconnect, Konflikt, Provisionals-Merge, Schema-Rolling-Upgrade. ## Referenzen - [ADR-0021: Tenant-Resolution-Strategie](adr/0021-tenant-resolution-strategy-de.md) - [ADR-0022: LAN-Sync-Protokoll (Meldestelle ↔ Richter-Turm)](adr/0022-lan-sync-protocol-de.md)