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;
|
return;
|
||||||
}
|
}
|
||||||
final erfolgreich = await _syncAlles();
|
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 p = await SharedPreferences.getInstance();
|
||||||
final now = DateTime.now();
|
final letzterTs = p.getString('cloud_last_sync_ts');
|
||||||
final zeit = _formatZeit(now);
|
|
||||||
await p.setString('cloud_last_sync', zeit);
|
|
||||||
await p.setString('cloud_last_sync_ts', now.toIso8601String());
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() => _letzterSync = zeit);
|
setState(() {
|
||||||
_berechneNaechstenSync();
|
_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) ──
|
// ── Phase 1: Songs (inkl. Tombstone-Sync, Sprint E) ──
|
||||||
onFortschritt?.call('Lade Songs…', 0.1);
|
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
|
// MED-1 (Tombstone-Race): letzterSync-Snapshot VOR listSongs lesen.
|
||||||
// Server-Löschung NEUER als der letzte Sync ist.
|
// 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 prefs = await SharedPreferences.getInstance();
|
||||||
final letzterSyncRaw = prefs.getString('cloud_last_sync_ts');
|
final letzterSyncRaw = prefs.getString('cloud_last_sync_ts');
|
||||||
final letzterSync =
|
final letzterSync =
|
||||||
letzterSyncRaw != null ? DateTime.tryParse(letzterSyncRaw) : null;
|
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 ──
|
// ── Tombstones zuerst: serverseitig gelöschte Songs lokal nachziehen ──
|
||||||
// (keine Zombie-Songs: Offline-Geräte löschen nach dem Sync ebenfalls)
|
// (keine Zombie-Songs: Offline-Geräte löschen nach dem Sync ebenfalls)
|
||||||
@@ -402,11 +409,14 @@ class SyncService {
|
|||||||
// ── Phase 4: Sync-Metadaten + letzter Sync ──
|
// ── Phase 4: Sync-Metadaten + letzter Sync ──
|
||||||
onFortschritt?.call('Speichere Sync-Zeitpunkt...', 0.9);
|
onFortschritt?.call('Speichere Sync-Zeitpunkt...', 0.9);
|
||||||
await cloud.syncAll();
|
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);
|
await _db.syncMetaSet('last_full_sync', now);
|
||||||
// Nur bei Erfolg persistieren (Auto-Sync-Fälligkeit hängt daran)
|
// Nur bei Erfolg persistieren (Auto-Sync-Fälligkeit hängt daran)
|
||||||
final p = await SharedPreferences.getInstance();
|
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', zeit);
|
||||||
await p.setString('cloud_last_sync_ts', now);
|
await p.setString('cloud_last_sync_ts', now);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user