From 1e9ff63833c0118eb85ce18efac62a0ac2adaa7a Mon Sep 17 00:00:00 2001 From: duffyduck Date: Mon, 8 Jun 2026 11:59:12 +0200 Subject: [PATCH] Propagate deletions in bidirectional sync via baseline tombstone Im Both-Modus wurde ein auf einer Seite geloeschter Kontakt bisher auf der anderen Seite einfach wieder angelegt, statt die Loeschung zu spiegeln. Jetzt wird anhand der gespeicherten Baseline (LastOutlookHash / LastStarfaceHash) entschieden: - Gegenseite seit letztem Sync unveraendert -> es war eine Loeschung -> auf der anderen Seite ebenfalls loeschen. - Gegenseite wurde geaendert -> Bearbeitung gewinnt -> neu anlegen (kein Datenverlust). In den Ein-Richtungs-Modi bleibt die Quelle fuehrend: eine Loeschung im Ziel wird aus der Quelle wiederhergestellt (StarfaceToOutlook legt einen in Outlook geloeschten Kontakt jetzt ebenfalls wieder an statt ein totes Mapping zu behalten). Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 7 ++ .../Services/SyncEngine.cs | 82 +++++++++++++++---- 2 files changed, 71 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58cbefed..b5e96fe9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,13 @@ Versionsschema ist `x.x.x.x` (siehe `release.sh`). ### Geaendert +- **Bidirektionale Loeschungen** werden jetzt erkannt. Wird ein Kontakt auf + einer Seite geloescht und ist die andere Seite seit dem letzten Sync + unveraendert (Abgleich ueber die gespeicherte Baseline), wird die Loeschung + auf die andere Seite gespiegelt – statt den Kontakt wieder anzulegen. Wurde + die andere Seite zwischenzeitlich bearbeitet, gewinnt die Bearbeitung und der + Kontakt wird neu angelegt (kein Datenverlust). In den Ein-Richtungs-Modi + bleibt die jeweilige Quelle fuehrend (Loeschung im Ziel wird wiederhergestellt). - Starface-Kontaktdetails werden beim Laden parallel abgerufen (8 gleichzeitig) statt einzeln nacheinander – deutlich schneller bei grossen Adressbuechern. - `UpdateContact` (Outlook) und `UpdateContactAsync` (Starface) geben jetzt den diff --git a/src/StarfaceOutlookSync/Services/SyncEngine.cs b/src/StarfaceOutlookSync/Services/SyncEngine.cs index 52189041..1bd7682b 100644 --- a/src/StarfaceOutlookSync/Services/SyncEngine.cs +++ b/src/StarfaceOutlookSync/Services/SyncEngine.cs @@ -163,19 +163,46 @@ namespace StarfaceOutlookSync.Services continue; } - // Wirklich geloescht -> in Starface auch loeschen - if (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.OutlookToStarface) + // Wirklich in Outlook geloescht. + if (profile.SyncDirection == SyncDirection.OutlookToStarface) { + // Outlook ist fuehrend -> Loeschung nach Starface spiegeln. if (await starface.DeleteContactAsync(mapping.StarfaceId)) { result.Updated++; Log($" Geloescht (OL->SF): {sc.DisplayName}"); } + continue; } - else + + if (profile.SyncDirection == SyncDirection.Both) { - newMappings.Add(mapping); + // Bidirektional: anhand der Baseline pruefen, ob die + // Starface-Seite seit dem letzten Sync unveraendert ist. + bool sfUnchanged = !string.IsNullOrEmpty(mapping.LastStarfaceHash) + && sc.GetHash() == mapping.LastStarfaceHash; + if (sfUnchanged) + { + // Unveraendert + in Outlook geloescht -> Loeschung gilt + // -> auch aus Starface entfernen. + if (await starface.DeleteContactAsync(mapping.StarfaceId)) + { + result.Updated++; + Log($" Geloescht (OL->SF): {sc.DisplayName}"); + } + continue; + } + // In Outlook geloescht, aber in Starface geaendert -> + // Bearbeitung gewinnt, in Outlook neu anlegen (Phase 3). + Log($" In Outlook geloescht, in Starface geaendert -> neu anlegen: {sc.DisplayName}"); + processedStarfaceIds.Remove(sc.StarfaceId); + continue; } + + // StarfaceToOutlook: Starface ist alleinige Quelle -> in Outlook + // neu anlegen (Loeschung im Ziel zaehlt nicht). + Log($" Outlook-Kontakt geloescht, wird neu angelegt: {sc.DisplayName}"); + processedStarfaceIds.Remove(sc.StarfaceId); continue; } @@ -197,21 +224,10 @@ namespace StarfaceOutlookSync.Services continue; } - // Wirklich geloescht. - if (profile.SyncDirection == SyncDirection.Both - || profile.SyncDirection == SyncDirection.OutlookToStarface) + // Wirklich in Starface geloescht. + if (profile.SyncDirection == SyncDirection.StarfaceToOutlook) { - // Outlook ist (mit-)fuehrend -> Kontakt in Starface neu - // anlegen. Mapping verwerfen und oc wieder freigeben, - // damit Phase 2 ihn anlegt (inkl. Duplikat-Pruefung). - Log($" Starface-Kontakt geloescht, wird neu angelegt: {oc.DisplayName}"); - processedOutlookIds.Remove(oc.OutlookEntryId); - continue; - } - else - { - // StarfaceToOutlook: Starface ist fuehrend, Loeschung - // nach Outlook spiegeln. + // Starface ist fuehrend -> Loeschung nach Outlook spiegeln. if (_outlookService.DeleteContact(oc.OutlookEntryId)) { result.Updated++; @@ -219,6 +235,36 @@ namespace StarfaceOutlookSync.Services } continue; } + + if (profile.SyncDirection == SyncDirection.Both) + { + // Bidirektional: anhand der Baseline entscheiden, ob der + // Outlook-Kontakt seit dem letzten Sync unveraendert ist. + bool olUnchanged = !string.IsNullOrEmpty(mapping.LastOutlookHash) + && oc.GetHash() == mapping.LastOutlookHash; + if (olUnchanged) + { + // Unveraendert + in Starface geloescht -> Loeschung gilt + // -> aus Outlook entfernen. + if (_outlookService.DeleteContact(oc.OutlookEntryId)) + { + result.Updated++; + Log($" Geloescht (SF->OL): {oc.DisplayName}"); + } + continue; + } + // In Starface geloescht, aber in Outlook geaendert -> + // Bearbeitung gewinnt, in Starface neu anlegen. + Log($" In Starface geloescht, in Outlook geaendert -> neu anlegen: {oc.DisplayName}"); + processedOutlookIds.Remove(oc.OutlookEntryId); + continue; + } + + // OutlookToStarface: Outlook ist alleinige Quelle -> Kontakt + // in Starface neu anlegen (Loeschung im Ziel zaehlt nicht). + Log($" Starface-Kontakt geloescht, wird neu angelegt: {oc.DisplayName}"); + processedOutlookIds.Remove(oc.OutlookEntryId); + continue; } if (oc != null && sc != null)