v2.53.1 — MED-1: Tombstone-Race behoben (Snapshot vor listSongs + Server-Zeit)
## 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)
This commit is contained in:
@@ -790,15 +790,22 @@ class _CloudScreenState extends State<CloudScreen>
|
||||
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);
|
||||
setState(() {
|
||||
_letzterSyncTs = letzterTs;
|
||||
_letzterSync = letzterTs != null
|
||||
? _formatZeit(DateTime.tryParse(letzterTs) ?? DateTime.now())
|
||||
: _formatZeit(DateTime.now());
|
||||
_berechneNaechstenSync();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user