From e7e5faeba86f243e07baa0820051c627a381cb7e Mon Sep 17 00:00:00 2001 From: Sebastian Hedtrich Date: Tue, 18 Aug 2026 23:38:46 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20eigener=20Push=20lie=C3=9F=20Client=20se?= =?UTF-8?q?inen=20Pull-Cursor=20=C3=BCber=20fremde=20Ereignisse=20springen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SyncEngine.PushAsync setzte den lokalen Pull-Cursor bisher aus PushResponse.ServerSequenceNr - dem globalen Zähler über alle Geräte nach dem eigenen Push, nicht dem tatsächlich zugestellten Stand. War beim Server zu diesem Zeitpunkt bereits ein noch nicht abgeholtes Ereignis eines anderen Geräts mit niedrigerer ServerSeq vorhanden, sprang der Cursor darüber hinweg und der direkt folgende Pull bekam 0 Ereignisse, ohne es je angewendet zu haben - ein zweiter, unabhängiger Cursor-Bug mit demselben Symptom wie der vorherige Wasserzeichen-Fix, diesmal client- statt serverseitig. Ergänzt außerdem einen "Vollständigen Sync erzwingen"-Button in den Sync-Einstellungen, damit bereits durch diesen Bug zu weit vorgerückte Geräte ihren Fortschritt manuell zurücksetzen und alle Ereignisse erneut laden können. Co-Authored-By: Claude Sonnet 5 --- .../ViewModels/Settings/SettingsViewModel.cs | 31 ++++++++++- .../Views/Settings/SettingsView.axaml | 11 ++++ LehrerApp.Sync.Tests/SyncEngineTests.cs | 54 +++++++++++++++++++ LehrerApp.Sync/SyncEngine.cs | 16 ++++-- TODO.md | 30 +++++++++++ 5 files changed, 138 insertions(+), 4 deletions(-) diff --git a/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs b/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs index 182a976..ca4e28a 100644 --- a/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs @@ -195,6 +195,8 @@ public partial class SettingsViewModel : ObservableObject [ObservableProperty] private string _syncLoginError = ""; [ObservableProperty] private bool _syncIsLoggedIn; [ObservableProperty] private string _syncConnectionStatus = ""; + [ObservableProperty] private string _syncForceResyncStatus = ""; + [ObservableProperty] private bool _syncForceResyncBusy; public ObservableCollection SyncConflicts { get; } = []; @@ -229,6 +231,7 @@ public partial class SettingsViewModel : ObservableObject private readonly SyncSettingsService _syncSettings; private readonly SyncAuthService _syncAuth; private readonly EventQueue _eventQueue; + private readonly SyncEngine? _syncEngine; private readonly SnapshotService? _snapshotService; private readonly CompetencyCatalogImportService _catalogImport; private readonly AppLogger _logger; @@ -243,7 +246,7 @@ public partial class SettingsViewModel : ObservableObject ISupervisionDutyRepository supervisionDuties, LetterTemplateService letterTemplates, AiSettingsService aiSettings, AiPlanningService aiPlanning, SyncSettingsService syncSettings, SyncAuthService syncAuth, EventQueue eventQueue, - AppLogger logger, SnapshotService? snapshotService = null) + AppLogger logger, SnapshotService? snapshotService = null, SyncEngine? syncEngine = null) { _logger = logger; _subjects = subjects; @@ -270,6 +273,7 @@ public partial class SettingsViewModel : ObservableObject _syncAuth = syncAuth; _eventQueue = eventQueue; _snapshotService = snapshotService; + _syncEngine = syncEngine; _catalogImport = new CompetencyCatalogImportService(domainRepo); LoadSubjects(); LoadShorthandCodes(); @@ -489,6 +493,31 @@ public partial class SettingsViewModel : ObservableObject AppBootstrapper.RestartApplication(); } + /// + /// Setzt den lokalen Sync-Fortschritt zurück und lädt danach alle Ereignisse dieses Nutzers + /// erneut vom Server - manueller Reparaturweg, falls dieses Gerät wiederholt keine Änderungen + /// eines anderen Geräts erhält (z.B. nach einem lokal bereits zu weit vorgerückten Cursor, + /// siehe TODO 10.1.10). Sicher wiederholbar: EventApplier wendet jedes Ereignis über Upsert/ + /// Delete-by-Id idempotent an. + /// + [RelayCommand] + private async Task SyncForceFullResync() + { + if (_syncEngine is null) { SyncForceResyncStatus = "Sync nicht konfiguriert."; return; } + SyncForceResyncBusy = true; + SyncForceResyncStatus = "Lade alle Ereignisse erneut…"; + try + { + _eventQueue.SetLastServerSeq(0); + var result = await _syncEngine.SyncNowAsync(); + SyncForceResyncStatus = result.Success + ? $"Abgeschlossen - {result.EventsPulled} Ereignis(se) erneut geladen." + : $"Fehlgeschlagen: {result.Reason}"; + LoadSyncConflicts(); + } + finally { SyncForceResyncBusy = false; } + } + // ── Synchronisation: Konflikte ──────────────────────────────────────────── // // Zeigt, was ConflictResolver bereits entschieden hat (welche Seite gewonnen hat) — kein diff --git a/LehrerApp.Desktop/Views/Settings/SettingsView.axaml b/LehrerApp.Desktop/Views/Settings/SettingsView.axaml index b20d081..055b9fd 100644 --- a/LehrerApp.Desktop/Views/Settings/SettingsView.axaml +++ b/LehrerApp.Desktop/Views/Settings/SettingsView.axaml @@ -775,6 +775,17 @@ + + + + +