fix: Kontowechsel ließ Push dauerhaft mit 404 scheitern

Nach einem Login mit korrigierter Groß-/Kleinschreibung (siehe vorheriger Fix zur userId-
Stabilität) bezog sich die lokale Versionsverfolgung je Entität (BasedOnServerSeq-Cache) und der
Pull-Cursor weiterhin auf das alte Konto - ServerSeq-Werte sind aber nur innerhalb des Event-Logs
EINES Kontos gültig. Der Push wurde zu Recht abgelehnt, der Server kannte die Entität unter der
neuen userId aber gar nicht (404 beim Nachladen), und HandleRejectedAsync gab bei einem 404
bisher einfach auf, ohne den veralteten Cache-Eintrag zu bereinigen - derselbe Fehlschlag bei
jedem weiteren Sync-Versuch.

Dreiteiliger Fix: (1) ein 404 beim Nachladen löscht jetzt den stale Cache-Eintrag, sodass der
nächste Push die Entität korrekt als neu behandelt und selbstheilend durchgeht; (2) der
"Vollständigen Sync erzwingen"-Button setzt jetzt auch die Push-Versionsverfolgung zurück, nicht
nur den Pull-Cursor; (3) SyncLogin erkennt einen echten Kontowechsel künftig proaktiv anhand der
kanonischen userId aus der Server-Antwort und resettet automatisch, bevor der Folgefehler
überhaupt auftreten kann.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 00:05:24 +02:00
co-authored by Claude Sonnet 5
parent ba8786906a
commit 979511ab6a
9 changed files with 229 additions and 13 deletions
+14
View File
@@ -81,6 +81,20 @@ public class EventQueue : IDisposable
_entityVersions.Upsert(new EntityVersion
{ Key = EntityVersionKey(entityType, entityId), ServerSeq = serverSeq });
/// <summary>Löscht den bekannten Stand EINER Entität — z.B. wenn der Server auf eine
/// BasedOnServerSeq-Ablehnung hin meldet, die Entität gar nicht zu kennen (404 bei
/// GetLatestForEntity): der lokale Cache war dann stale, siehe SyncEngine.HandleRejectedAsync
/// und TODO 10.3.5.</summary>
public void ClearKnownServerSeq(string entityType, string entityId) =>
_entityVersions.Delete(EntityVersionKey(entityType, entityId));
/// <summary>Verwirft die GESAMTE lokale Versionsverfolgung — nötig nach einem Kontowechsel
/// (siehe SettingsViewModel.SyncLogin/SyncForceFullResync), da ServerSeq-Werte ausschließlich
/// innerhalb des Event-Logs EINES Server-Kontos bedeutungsvoll sind (TODO 10.3.5). Sicher: der
/// nächste Push behandelt jede Entität dann als "erstmals für dieses Konto", der Server nimmt
/// sie an, solange er sie unter der aktuellen userId selbst noch nicht kennt.</summary>
public void ResetKnownServerSeqs() => _entityVersions.DeleteAll();
private static string EntityVersionKey(string entityType, string entityId) => $"{entityType}:{entityId}";
// ── Anhang-Warteliste (getrennt von der JSON-Ereignis-Outbox, siehe AttachmentSyncer) ────
+18 -3
View File
@@ -1,3 +1,4 @@
using System.Net;
using System.Net.Http.Json;
using LehrerApp.Core.Services;
using LehrerApp.Sync.Models;
@@ -150,11 +151,10 @@ public class SyncEngine : IDisposable
{
foreach (var local in rejected)
{
SyncEvent? remote;
HttpResponseMessage resp;
try
{
remote = await _http.GetFromJsonAsync<SyncEvent>(
$"/api/sync/entity/{local.EntityType}/{local.EntityId}");
resp = await _http.GetAsync($"/api/sync/entity/{local.EntityType}/{local.EntityId}");
}
catch (Exception ex)
{
@@ -162,6 +162,21 @@ public class SyncEngine : IDisposable
"aktueller Server-Stand konnte nicht nachgeladen werden.", ex);
continue;
}
if (resp.StatusCode == HttpStatusCode.NotFound)
{
// Der Server kennt diese Entität unter der aktuellen userId gar nicht - der lokal
// zwischengespeicherte BasedOnServerSeq war stale (z.B. nach einem Kontowechsel,
// siehe TODO 10.3.5, oder wenn dieses Gerät die Entität nie zuvor unter diesem
// Konto gepusht hat). Cache löschen, statt endlos mit demselben 404 zu scheitern -
// der nächste Push behandelt die Entität dann korrekt als neu für dieses Konto.
_queue.ClearKnownServerSeq(local.EntityType, local.EntityId);
_logger?.Warn($"Sync: Push-Konflikt bei {local.EntityType}/{local.EntityId} - Server " +
"kennt diese Entität nicht. Lokale Versionsverfolgung zurückgesetzt, " +
"nächster Push behandelt sie als neu.");
continue;
}
resp.EnsureSuccessStatusCode();
var remote = await resp.Content.ReadFromJsonAsync<SyncEvent>();
if (remote is null)
{
_logger?.Warn($"Sync: Push-Konflikt bei {local.EntityType}/{local.EntityId}, aber " +