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);