feat: Erfolgs-Protokollierung für die gesamte Sync-Kette

SyncEventPublisher/SyncEngine/EventApplier protokollierten bisher
ausschließlich Fehlschläge - ein sauberes Log bewies nur "nichts ist
abgestürzt", nicht ob eine Änderung tatsächlich hoch-/heruntergeladen
wurde. Jetzt wird auch der Erfolgspfad geloggt: Einreihen in die Outbox
(mit SequenceNr), Push/Pull mit Anzahl und Entitätstypen sowie der vom
Server bestätigten ServerSequenceNr, und jedes tatsächlich angewendete
Ereignis. Damit lässt sich anhand der Log-Dateien beider Geräte
nachvollziehen, an welcher Stelle der Kette eine Änderung verloren geht.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 22:20:33 +02:00
co-authored by Claude Sonnet 5
parent dea7da8cca
commit 270c4d40cd
5 changed files with 44 additions and 6 deletions
+20 -3
View File
@@ -44,13 +44,17 @@ public class SyncEngine : IDisposable
if (Status.State == SyncState.Syncing)
return new() { Skipped = true, Reason = "Sync bereits aktiv" };
SetState(SyncState.Syncing);
_logger?.Info($"Sync: Start ({(isAutomatic ? "automatisch" : "manuell")}), Gerät={_config.DeviceId}, " +
$"{_queue.PendingCount()} lokal ausstehend");
try
{
var (pushed, _) = await PushAsync();
var (pushed, pushConflicts) = await PushAsync();
await _attachments.UploadPendingAsync(_queue);
var (pulled, conflicts) = await PullAsync();
_queue.SetLastSyncAt(DateTime.UtcNow);
SetState(SyncState.Idle);
_logger?.Info($"Sync: Fertig - {pushed} gepusht ({pushConflicts} Push-Konflikte), " +
$"{pulled} gepullt ({conflicts} Pull-Konflikte)");
return new() { Success = true, EventsPushed = pushed, EventsPulled = pulled, Conflicts = conflicts };
}
catch (HttpRequestException ex)
@@ -74,14 +78,18 @@ public class SyncEngine : IDisposable
{
var pending = _queue.GetPending();
if (pending.Count == 0) return (0, 0);
_logger?.Info($"Sync: Push - {pending.Count} Ereignis(se) ausstehend: " +
string.Join(", ", pending.Select(e => $"{e.EntityType}/{e.Operation}")));
var resp = await _http.PostAsJsonAsync("/api/sync/push", pending);
resp.EnsureSuccessStatusCode();
var result = await resp.Content.ReadFromJsonAsync<PushResponse>();
if (result is null) return (0, 0);
if (result is null) { _logger?.Warn("Sync: Push - leere Server-Antwort."); return (0, 0); }
_queue.Acknowledge(pending
.Where(e => !result.ConflictingEventIds.Contains(e.EventId))
.Select(e => e.EventId));
_queue.SetLastServerSeq(result.ServerSequenceNr);
_logger?.Info($"Sync: Push - vom Server bestätigt bis ServerSequenceNr={result.ServerSequenceNr}, " +
$"{result.ConflictingEventIds.Count} vom Server abgelehnt (Konflikt).");
return (pending.Count - result.ConflictingEventIds.Count,
result.ConflictingEventIds.Count);
}
@@ -89,9 +97,16 @@ public class SyncEngine : IDisposable
private async Task<(int Pulled, int Conflicts)> PullAsync()
{
var since = _queue.GetLastServerSeq();
_logger?.Info($"Sync: Pull - frage Server nach Ereignissen seit ServerSequenceNr={since}.");
var resp = await _http.GetFromJsonAsync<PullResponse>(
$"/api/sync/pull?since={since}&deviceId={_config.DeviceId}");
if (resp is null || resp.Events.Count == 0) return (0, 0);
if (resp is null || resp.Events.Count == 0)
{
_logger?.Info("Sync: Pull - keine neuen Ereignisse vom Server.");
return (0, 0);
}
_logger?.Info($"Sync: Pull - {resp.Events.Count} Ereignis(se) vom Server erhalten: " +
string.Join(", ", resp.Events.Select(e => $"{e.EntityType}/{e.Operation}")));
var conflicts = 0;
foreach (var evt in resp.Events)
{
@@ -99,6 +114,8 @@ public class SyncEngine : IDisposable
if (c is null) { await _applier.ApplyAsync(evt); continue; }
_queue.AddConflict(c);
conflicts++;
_logger?.Info($"Sync: Pull - Konflikt bei {evt.EntityType}/{evt.EntityId}, " +
$"Auflösung={c.Resolution}");
if (c.Resolution == "RemoteWon") await _applier.ApplyAsync(evt);
}
_queue.SetLastServerSeq(resp.ServerSequenceNr);