From 3fe20e1c7b04408788bcd81fcd5f8563a7bc1550 Mon Sep 17 00:00:00 2001 From: Dustin Date: Wed, 5 Aug 2026 11:22:36 +0200 Subject: [PATCH] =?UTF-8?q?v2.53.1=20=E2=80=94=20MED-1:=20Tombstone-Race?= =?UTF-8?q?=20behoben=20(Snapshot=20vor=20listSongs=20+=20Server-Zeit)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## MED-1 (Review-Fix) - sync_service.dart: letzterSync-Snapshot wird VOR listSongs gelesen und am Sync-Ende persistiert (Sync-BEGINN statt now-am-Ende) — Löschungen, die zwischen listSongs und Sync-Ende auf dem Server ankommen, werden im nächsten Sync korrekt angewendet statt als „bereits gesehen“ übersprungen - cloud_screen.dart: _download schreibt cloud_last_sync_ts nicht mehr nachträglich mit now-am-Ende (redundant + Race-Wieder-Einführung); UI-Anzeige liest den von SyncService persistierten Snapshot - melo_cloud.py: handle_delete stempelt deleted_at IMMER mit Server-Zeit (datetime('now')) — Client-Timestamps werden ignoriert (Uhr-Drift würde Löschungen dauerhaft überspringen lassen; konsistente Uhr für alle Geräte) --- lib/screens/cloud_screen.dart | 21 ++++++++++++++------- lib/services/sync_service.dart | 26 ++++++++++++++++++-------- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/lib/screens/cloud_screen.dart b/lib/screens/cloud_screen.dart index 3d89c44..6c4b52f 100644 --- a/lib/screens/cloud_screen.dart +++ b/lib/screens/cloud_screen.dart @@ -790,15 +790,22 @@ class _CloudScreenState extends State return; } final erfolgreich = await _syncAlles(); - if (!erfolgreich) return; // cloud_last_sync nur bei Erfolg schreiben + if (!erfolgreich) return; // SyncService persistiert cloud_last_sync nur bei Erfolg + // UI: letzten Sync anzeigen. KEINE zweite cloud_last_sync_ts-Schreibung + // mit now-am-Ende hier — SyncService persistiert bereits den + // Sync-BEGINN-Snapshot (MED-1: Tombstone-Race). Eine spätere Schreibung + // würde die Löschungen zwischen listSongs und Sync-Ende wieder als + // „bereits gesehen“ markieren. final p = await SharedPreferences.getInstance(); - final now = DateTime.now(); - final zeit = _formatZeit(now); - await p.setString('cloud_last_sync', zeit); - await p.setString('cloud_last_sync_ts', now.toIso8601String()); + final letzterTs = p.getString('cloud_last_sync_ts'); if (mounted) { - setState(() => _letzterSync = zeit); - _berechneNaechstenSync(); + setState(() { + _letzterSyncTs = letzterTs; + _letzterSync = letzterTs != null + ? _formatZeit(DateTime.tryParse(letzterTs) ?? DateTime.now()) + : _formatZeit(DateTime.now()); + _berechneNaechstenSync(); + }); } } diff --git a/lib/services/sync_service.dart b/lib/services/sync_service.dart index 15bda44..ccfe5a2 100644 --- a/lib/services/sync_service.dart +++ b/lib/services/sync_service.dart @@ -217,17 +217,24 @@ class SyncService { // ── Phase 1: Songs (inkl. Tombstone-Sync, Sprint E) ── onFortschritt?.call('Lade Songs…', 0.1); - final serverSongs = await cloud.listSongs(); - final dir = - Directory('${(await getApplicationDocumentsDirectory()).path}/music'); - if (!await dir.exists()) await dir.create(recursive: true); - // Letzter Sync-Zeitpunkt: Tombstones werden nur angewendet, wenn die - // Server-Löschung NEUER als der letzte Sync ist. + // MED-1 (Tombstone-Race): letzterSync-Snapshot VOR listSongs lesen. + // Der persistierte „letzter Sync“ darf nie später liegen als die + // letzte Beobachtung der Server-Liste — sonst gilt eine Löschung, die + // zwischen listSongs und Sync-Ende auf dem Server ankommt, im nächsten + // Sync als „bereits gesehen“ (deletedAt < letzterSync) und wird + // dauerhaft übersprungen (Zombie-Song). Der Snapshot (Sync-BEGINN, + // nicht now-am-Ende) wird am Ende persistiert. final prefs = await SharedPreferences.getInstance(); final letzterSyncRaw = prefs.getString('cloud_last_sync_ts'); final letzterSync = letzterSyncRaw != null ? DateTime.tryParse(letzterSyncRaw) : null; + final syncSnapshot = DateTime.now(); + + final serverSongs = await cloud.listSongs(); + final dir = + Directory('${(await getApplicationDocumentsDirectory()).path}/music'); + if (!await dir.exists()) await dir.create(recursive: true); // ── Tombstones zuerst: serverseitig gelöschte Songs lokal nachziehen ── // (keine Zombie-Songs: Offline-Geräte löschen nach dem Sync ebenfalls) @@ -402,11 +409,14 @@ class SyncService { // ── Phase 4: Sync-Metadaten + letzter Sync ── onFortschritt?.call('Speichere Sync-Zeitpunkt...', 0.9); await cloud.syncAll(); - final now = DateTime.now().toIso8601String(); + // Zeitstempel = Sync-BEGINN (Snapshot VOR listSongs), NICHT das + // Sync-Ende: Der Tombstone-Vergleich anderer Geräte hängt am + // „letzten Beobachtungszeitpunkt der Server-Liste“ (MED-1). + final now = syncSnapshot.toIso8601String(); await _db.syncMetaSet('last_full_sync', now); // Nur bei Erfolg persistieren (Auto-Sync-Fälligkeit hängt daran) final p = await SharedPreferences.getInstance(); - final zeit = _formatZeit(DateTime.now()); + final zeit = _formatZeit(syncSnapshot); await p.setString('cloud_last_sync', zeit); await p.setString('cloud_last_sync_ts', now);