From 257b6fb33dadfbc602267d52a0f1209f0d5c0032 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Mon, 8 Jun 2026 13:01:02 +0200 Subject: [PATCH] Only notify clients actually affected by a conflict Bisher bekam jeder Arbeitsplatz, der den Konflikt-Kontakt im Adressbuch hat, den Hinweis - auch wenn er den Wert gar nicht selbst gepflegt hat. Jetzt zeigt ein Client eine fremde Konflikt-Notiz nur, wenn er den Kontakt hat UND sein eigener Feldwert vom uebernommenen (Gewinner-)Wert abweicht. Die Pruefung laeuft VOR dem Sync (gegen den eigenen Mapping-Snapshot), bevor der Sync den Stand auf den Gewinner-Wert angleicht. - FieldConflict/ConflictNotice: stabiler FieldKey zusaetzlich zum Anzeige-Label. - ContactMerger: GetValue/ValuesEqual per FieldKey (telefon-normalisiert). - ConflictNotifier.GetPending: Filter "eigener Wert != Gewinner-Wert", bekommt StarfaceId -> eigener Kontaktstand. - MainForm zeigt die Hinweise jetzt vor dem Sync und liefert den eigenen Stand aus den Mapping-Snapshots. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 11 ++-- README.md | 11 ++-- .../Models/ConflictNotice.cs | 1 + src/StarfaceOutlookSync/Models/SyncProfile.cs | 3 +- .../Services/ConflictNotifier.cs | 36 ++++++++++-- .../Services/ContactMerger.cs | 57 ++++++++++++------- src/StarfaceOutlookSync/UI/MainForm.cs | 26 +++++---- 7 files changed, 98 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff080c09..1ffb4c0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,11 +40,12 @@ Versionsschema ist `x.x.x.x` (siehe `release.sh`). - **Clientuebergreifende Konflikt-Hinweise (Mehrplatz).** Wird ein echter Feld-Konflikt aufgeloest, legt der Client eine Notiz im gemeinsamen Verzeichnis - (`conflicts/`) ab - verschluesselt nach Kontakt (StarfaceId). Jeder andere - Arbeitsplatz, der denselben Kontakt pflegt, bekommt die Meldung beim naechsten - Sync als Tray-Hinweis angezeigt (auch der, dessen Wert ueberschrieben wurde). - Bereits gezeigte Notizen merkt sich jeder Client lokal; alte Notizen werden - nach 7 Tagen aufgeraeumt. + (`conflicts/`) ab - zugeordnet nach Kontakt (StarfaceId). Ein anderer + Arbeitsplatz zeigt die Meldung beim naechsten Sync nur an, wenn er den Kontakt + selbst hat UND sein eigener Feldwert vom uebernommenen Wert abweicht - so wird + der ueberschriebene Arbeitsplatz gewarnt, waehrend bereits aktuelle Clients + nicht benachrichtigt werden. Bereits gezeigte Notizen merkt sich jeder Client + lokal; alte Notizen werden nach 7 Tagen aufgeraeumt. - **Clientuebergreifende Sync-Sperre (Mehrplatz).** In den Einstellungen laesst sich ein gemeinsames Verzeichnis (Netzlaufwerk/UNC) hinterlegen. Synct ein Arbeitsplatz, legt er dort eine Lock-Datei an (atomar via `CreateNew`); andere diff --git a/README.md b/README.md index cad88178..81fd0cf0 100644 --- a/README.md +++ b/README.md @@ -301,11 +301,12 @@ umgekehrt), wird der zweite Lauf uebersprungen. Atomar per `Interlocked`. **Konflikt-Hinweise:** Wird im bidirektionalen Modus ein echter Feld-Konflikt aufgeloest (dasselbe Feld auf beiden Seiten unterschiedlich geaendert, Outlook gewinnt), legt der Client eine Notiz im Unterordner `conflicts/` des gemeinsamen -Verzeichnisses ab - zugeordnet ueber die StarfaceId des Kontakts. Andere -Arbeitsplaetze, die denselben Kontakt pflegen, zeigen die Meldung beim naechsten -Sync als Tray-Hinweis an (so erfaehrt auch der Arbeitsplatz, dessen Wert -ueberschrieben wurde, davon). Gezeigte Notizen merkt sich jeder Client lokal, -veraltete Notizen werden nach 7 Tagen entfernt. +Verzeichnisses ab - zugeordnet ueber die StarfaceId des Kontakts. Beim naechsten +Sync zeigt ein anderer Arbeitsplatz die Meldung als Tray-Hinweis - aber **nur, +wenn er den Kontakt selbst hat UND sein eigener Feldwert vom uebernommenen Wert +abweicht** (so erfaehrt der ueberschriebene Arbeitsplatz davon, waehrend Clients, +die den Wert ohnehin schon haben, nicht benachrichtigt werden). Gezeigte Notizen +merkt sich jeder Client lokal, veraltete Notizen werden nach 7 Tagen entfernt. > Hinweis: Im Mehrplatz-Betrieb sollte die **bidirektionale** Sync-Richtung > verwendet werden. Die Ein-Richtungs-Modi ("Ersetzen") wuerden die von anderen diff --git a/src/StarfaceOutlookSync/Models/ConflictNotice.cs b/src/StarfaceOutlookSync/Models/ConflictNotice.cs index 712921ba..8b92b03d 100644 --- a/src/StarfaceOutlookSync/Models/ConflictNotice.cs +++ b/src/StarfaceOutlookSync/Models/ConflictNotice.cs @@ -14,6 +14,7 @@ namespace StarfaceOutlookSync.Models public string StarfaceId { get; set; } = ""; public string ContactName { get; set; } = ""; public string Field { get; set; } = ""; + public string FieldKey { get; set; } = ""; public string OutlookValue { get; set; } = ""; public string StarfaceValue { get; set; } = ""; public string Winner { get; set; } = ""; diff --git a/src/StarfaceOutlookSync/Models/SyncProfile.cs b/src/StarfaceOutlookSync/Models/SyncProfile.cs index 4d56b11c..0d0a39aa 100644 --- a/src/StarfaceOutlookSync/Models/SyncProfile.cs +++ b/src/StarfaceOutlookSync/Models/SyncProfile.cs @@ -82,7 +82,8 @@ namespace StarfaceOutlookSync.Models { public string StarfaceId { get; set; } = ""; public string ContactName { get; set; } = ""; - public string Field { get; set; } = ""; + public string Field { get; set; } = ""; // Anzeige-Label, z.B. "E-Mail" + public string FieldKey { get; set; } = ""; // stabiler Schluessel, z.B. "Email" public string OutlookValue { get; set; } = ""; public string StarfaceValue { get; set; } = ""; public string Winner { get; set; } = ""; // "Outlook" oder "Starface" diff --git a/src/StarfaceOutlookSync/Services/ConflictNotifier.cs b/src/StarfaceOutlookSync/Services/ConflictNotifier.cs index de9b3d91..2a231ab1 100644 --- a/src/StarfaceOutlookSync/Services/ConflictNotifier.cs +++ b/src/StarfaceOutlookSync/Services/ConflictNotifier.cs @@ -57,6 +57,7 @@ namespace StarfaceOutlookSync.Services StarfaceId = c.StarfaceId, ContactName = c.ContactName, Field = c.Field, + FieldKey = c.FieldKey, OutlookValue = c.OutlookValue, StarfaceValue = c.StarfaceValue, Winner = c.Winner @@ -74,10 +75,13 @@ namespace StarfaceOutlookSync.Services /// /// Liefert ungesehene Konflikt-Notizen, die Kontakte dieses Clients - /// betreffen (StarfaceId in myStarfaceIds) und nicht von ihm selbst - /// stammen. Markiert sie als gesehen und raeumt veraltete Notizen auf. + /// betreffen und nicht von ihm selbst stammen. Es werden nur die Notizen + /// zurueckgegeben, bei denen der EIGENE aktuelle Feldwert vom uebernommenen + /// (Gewinner-)Wert abweicht - wer den Wert ohnehin schon hat, wird nicht + /// benachrichtigt. myContacts bildet StarfaceId -> eigener Kontaktstand ab. + /// Markiert verarbeitete Notizen als gesehen und raeumt veraltete auf. /// - public List GetPending(string sharedDir, ICollection myStarfaceIds) + public List GetPending(string sharedDir, IDictionary myContacts) { var result = new List(); if (string.IsNullOrWhiteSpace(sharedDir)) return result; @@ -117,9 +121,29 @@ namespace StarfaceOutlookSync.Services seen.Add(n.Id); // selbst erzeugt -> nicht erneut anzeigen continue; } - if (myStarfaceIds != null && !string.IsNullOrEmpty(n.StarfaceId) - && !myStarfaceIds.Contains(n.StarfaceId)) - continue; // betrifft mich nicht + + // Habe ich diesen Kontakt ueberhaupt? + UnifiedContact mine = null; + bool haveContact = !string.IsNullOrEmpty(n.StarfaceId) + && myContacts != null + && myContacts.TryGetValue(n.StarfaceId, out mine); + if (!haveContact) + continue; // betrifft mich nicht (kein seen -> evtl. spaeter relevant) + + // Bin ich wirklich betroffen? Nur wenn mein aktueller Feldwert vom + // uebernommenen Wert abweicht. Wer den Gewinner-Wert schon hat, wird + // nicht benachrichtigt. + if (mine != null && !string.IsNullOrEmpty(n.FieldKey)) + { + var winnerValue = string.Equals(n.Winner, "Outlook", StringComparison.OrdinalIgnoreCase) + ? n.OutlookValue : n.StarfaceValue; + var myValue = ContactMerger.GetValue(mine, n.FieldKey); + if (ContactMerger.ValuesEqual(n.FieldKey, myValue, winnerValue)) + { + seen.Add(n.Id); // nicht betroffen -> als erledigt merken + continue; + } + } result.Add(n); seen.Add(n.Id); diff --git a/src/StarfaceOutlookSync/Services/ContactMerger.cs b/src/StarfaceOutlookSync/Services/ContactMerger.cs index efd5703f..4f7ce1c5 100644 --- a/src/StarfaceOutlookSync/Services/ContactMerger.cs +++ b/src/StarfaceOutlookSync/Services/ContactMerger.cs @@ -15,6 +15,7 @@ namespace StarfaceOutlookSync.Services { private class FieldDef { + public string Key; public string Label; public Func Get; public Action Set; @@ -23,28 +24,43 @@ namespace StarfaceOutlookSync.Services private static readonly FieldDef[] Fields = new[] { - new FieldDef { Label = "Vorname", Get = c => c.FirstName, Set = (c, v) => c.FirstName = v }, - new FieldDef { Label = "Nachname", Get = c => c.LastName, Set = (c, v) => c.LastName = v }, - new FieldDef { Label = "Firma", Get = c => c.Company, Set = (c, v) => c.Company = v }, - new FieldDef { Label = "Position", Get = c => c.JobTitle, Set = (c, v) => c.JobTitle = v }, - new FieldDef { Label = "E-Mail", Get = c => c.Email, Set = (c, v) => c.Email = v }, - new FieldDef { Label = "E-Mail 2", Get = c => c.EmailSecondary, Set = (c, v) => c.EmailSecondary = v }, - new FieldDef { Label = "Telefon", Get = c => c.PhoneWork, Set = (c, v) => c.PhoneWork = v, IsPhone = true }, - new FieldDef { Label = "Mobil", Get = c => c.PhoneMobile, Set = (c, v) => c.PhoneMobile = v, IsPhone = true }, - new FieldDef { Label = "Telefon privat", Get = c => c.PhoneHome, Set = (c, v) => c.PhoneHome = v, IsPhone = true }, - new FieldDef { Label = "Fax", Get = c => c.Fax, Set = (c, v) => c.Fax = v, IsPhone = true }, - new FieldDef { Label = "Strasse", Get = c => c.Street, Set = (c, v) => c.Street = v }, - new FieldDef { Label = "Ort", Get = c => c.City, Set = (c, v) => c.City = v }, - new FieldDef { Label = "PLZ", Get = c => c.PostalCode, Set = (c, v) => c.PostalCode = v }, - new FieldDef { Label = "Bundesland", Get = c => c.State, Set = (c, v) => c.State = v }, - new FieldDef { Label = "Land", Get = c => c.Country, Set = (c, v) => c.Country = v }, - new FieldDef { Label = "Webseite", Get = c => c.Website, Set = (c, v) => c.Website = v }, - new FieldDef { Label = "Notizen", Get = c => c.Notes, Set = (c, v) => c.Notes = v }, - new FieldDef { Label = "Anrede", Get = c => c.Salutation, Set = (c, v) => c.Salutation = v }, - new FieldDef { Label = "Titel", Get = c => c.Title, Set = (c, v) => c.Title = v }, - new FieldDef { Label = "Geburtstag", Get = c => c.Birthday, Set = (c, v) => c.Birthday = v }, + new FieldDef { Key = "FirstName", Label = "Vorname", Get = c => c.FirstName, Set = (c, v) => c.FirstName = v }, + new FieldDef { Key = "LastName", Label = "Nachname", Get = c => c.LastName, Set = (c, v) => c.LastName = v }, + new FieldDef { Key = "Company", Label = "Firma", Get = c => c.Company, Set = (c, v) => c.Company = v }, + new FieldDef { Key = "JobTitle", Label = "Position", Get = c => c.JobTitle, Set = (c, v) => c.JobTitle = v }, + new FieldDef { Key = "Email", Label = "E-Mail", Get = c => c.Email, Set = (c, v) => c.Email = v }, + new FieldDef { Key = "EmailSecondary", Label = "E-Mail 2", Get = c => c.EmailSecondary, Set = (c, v) => c.EmailSecondary = v }, + new FieldDef { Key = "PhoneWork", Label = "Telefon", Get = c => c.PhoneWork, Set = (c, v) => c.PhoneWork = v, IsPhone = true }, + new FieldDef { Key = "PhoneMobile", Label = "Mobil", Get = c => c.PhoneMobile, Set = (c, v) => c.PhoneMobile = v, IsPhone = true }, + new FieldDef { Key = "PhoneHome", Label = "Telefon privat", Get = c => c.PhoneHome, Set = (c, v) => c.PhoneHome = v, IsPhone = true }, + new FieldDef { Key = "Fax", Label = "Fax", Get = c => c.Fax, Set = (c, v) => c.Fax = v, IsPhone = true }, + new FieldDef { Key = "Street", Label = "Strasse", Get = c => c.Street, Set = (c, v) => c.Street = v }, + new FieldDef { Key = "City", Label = "Ort", Get = c => c.City, Set = (c, v) => c.City = v }, + new FieldDef { Key = "PostalCode", Label = "PLZ", Get = c => c.PostalCode, Set = (c, v) => c.PostalCode = v }, + new FieldDef { Key = "State", Label = "Bundesland", Get = c => c.State, Set = (c, v) => c.State = v }, + new FieldDef { Key = "Country", Label = "Land", Get = c => c.Country, Set = (c, v) => c.Country = v }, + new FieldDef { Key = "Website", Label = "Webseite", Get = c => c.Website, Set = (c, v) => c.Website = v }, + new FieldDef { Key = "Notes", Label = "Notizen", Get = c => c.Notes, Set = (c, v) => c.Notes = v }, + new FieldDef { Key = "Salutation", Label = "Anrede", Get = c => c.Salutation, Set = (c, v) => c.Salutation = v }, + new FieldDef { Key = "Title", Label = "Titel", Get = c => c.Title, Set = (c, v) => c.Title = v }, + new FieldDef { Key = "Birthday", Label = "Geburtstag", Get = c => c.Birthday, Set = (c, v) => c.Birthday = v }, }; + /// Liest den Wert eines Feldes per stabilem Schluessel. + public static string GetValue(UnifiedContact c, string key) + { + if (c == null || string.IsNullOrEmpty(key)) return ""; + var f = Fields.FirstOrDefault(x => x.Key == key); + return f == null ? "" : (f.Get(c) ?? ""); + } + + /// Vergleicht zwei Feldwerte (telefon-normalisiert je nach Feld). + public static bool ValuesEqual(string key, string a, string b) + { + var f = Fields.FirstOrDefault(x => x.Key == key); + return Equal(a ?? "", b ?? "", f?.IsPhone ?? false); + } + /// /// Fuehrt den Outlook- und Starface-Stand gegen ihre jeweilige Baseline /// zusammen. outlookWins entscheidet bei echten Feld-Konflikten. @@ -95,6 +111,7 @@ namespace StarfaceOutlookSync.Services StarfaceId = starface.StarfaceId, ContactName = outlook.DisplayName, Field = f.Label, + FieldKey = f.Key, OutlookValue = olv, StarfaceValue = sfv, Winner = outlookWins ? "Outlook" : "Starface" diff --git a/src/StarfaceOutlookSync/UI/MainForm.cs b/src/StarfaceOutlookSync/UI/MainForm.cs index 1a6ad107..466921a7 100644 --- a/src/StarfaceOutlookSync/UI/MainForm.cs +++ b/src/StarfaceOutlookSync/UI/MainForm.cs @@ -361,6 +361,11 @@ namespace StarfaceOutlookSync.UI return; } + // Konflikt-Hinweise von ANDEREN Arbeitsplaetzen anzeigen, BEVOR der + // Sync den eigenen Stand auf den Gewinner-Wert aktualisiert (sonst + // koennte man nicht mehr erkennen, ob man betroffen war). + ShowRemoteConflictNotices(sharedDir); + SetStatus($"Synchronisiere '{profile.Name}'..."); _trayIcon.ShowBalloonTip(2000, "Starface Sync", $"Synchronisiere '{profile.Name}'...", ToolTipIcon.Info); @@ -388,9 +393,6 @@ namespace StarfaceOutlookSync.UI _conflictNotifier.Write(sharedDir, result.Conflicts); } - // Konflikt-Hinweise von ANDEREN Arbeitsplaetzen (zu meinen Kontakten) anzeigen. - ShowRemoteConflictNotices(sharedDir); - SetStatus(msg); } catch (Exception ex) @@ -456,7 +458,7 @@ namespace StarfaceOutlookSync.UI if (string.IsNullOrWhiteSpace(sharedDir)) return; try { - var pending = _conflictNotifier.GetPending(sharedDir, AllMappedStarfaceIds()); + var pending = _conflictNotifier.GetPending(sharedDir, MyContactsByStarfaceId()); if (pending.Count == 0) return; var detail = string.Join("\n", pending.Take(5).Select(p => p.ToString())); @@ -468,19 +470,23 @@ namespace StarfaceOutlookSync.UI catch { } } - /// Alle von diesem Client gemappten StarfaceIds (ueber alle Profile). - private HashSet AllMappedStarfaceIds() + /// + /// Eigener Kontaktstand je StarfaceId (ueber alle Profile), aus den + /// Mapping-Snapshots. Dient dazu, bei Konflikt-Notizen zu pruefen, ob der + /// eigene Feldwert ueberhaupt vom uebernommenen Wert abweicht. + /// + private Dictionary MyContactsByStarfaceId() { - var ids = new HashSet(StringComparer.OrdinalIgnoreCase); + var map = new Dictionary(StringComparer.OrdinalIgnoreCase); try { foreach (var p in _profileManager.GetProfiles()) foreach (var m in _profileManager.GetMappings(p.Id)) - if (!string.IsNullOrEmpty(m.StarfaceId)) - ids.Add(m.StarfaceId); + if (!string.IsNullOrEmpty(m.StarfaceId) && !map.ContainsKey(m.StarfaceId)) + map[m.StarfaceId] = m.LastOutlook; } catch { } - return ids; + return map; } private void SetStatus(string text)