257b6fb33d
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) <noreply@anthropic.com>
179 lines
7.2 KiB
C#
179 lines
7.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using Newtonsoft.Json;
|
|
using StarfaceOutlookSync.Models;
|
|
|
|
namespace StarfaceOutlookSync.Services
|
|
{
|
|
/// <summary>
|
|
/// Verteilt Konflikt-Hinweise ueber das gemeinsame Verzeichnis an alle
|
|
/// Arbeitsplaetze. Wer einen echten Feld-Konflikt aufloest, legt eine Notiz
|
|
/// ab; andere Clients zeigen beim naechsten Sync die Notizen zu Kontakten,
|
|
/// die sie selbst gemappt haben (per StarfaceId). Bereits gezeigte Notizen
|
|
/// merkt sich jeder Client lokal, damit sie nicht doppelt erscheinen.
|
|
/// </summary>
|
|
public class ConflictNotifier
|
|
{
|
|
private static readonly TimeSpan Ttl = TimeSpan.FromDays(7);
|
|
|
|
private readonly string _seenFile;
|
|
|
|
public ConflictNotifier()
|
|
{
|
|
var dir = Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
|
"StarfaceOutlookSync");
|
|
try { Directory.CreateDirectory(dir); } catch { }
|
|
_seenFile = Path.Combine(dir, "seen-conflicts.json");
|
|
}
|
|
|
|
private static string ConflictsDir(string sharedDir) => Path.Combine(sharedDir, "conflicts");
|
|
|
|
/// <summary>Legt fuer jeden Konflikt eine Notiz im gemeinsamen Verzeichnis ab.</summary>
|
|
public void Write(string sharedDir, IEnumerable<FieldConflict> conflicts)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(sharedDir)) return;
|
|
var list = conflicts?.ToList();
|
|
if (list == null || list.Count == 0) return;
|
|
|
|
try
|
|
{
|
|
var dir = ConflictsDir(sharedDir);
|
|
Directory.CreateDirectory(dir);
|
|
var host = Environment.MachineName;
|
|
var user = Environment.UserName;
|
|
var ts = DateTime.UtcNow.ToString("o");
|
|
|
|
foreach (var c in list)
|
|
{
|
|
var notice = new ConflictNotice
|
|
{
|
|
Id = Guid.NewGuid().ToString("N"),
|
|
ByHost = host,
|
|
ByUser = user,
|
|
TimestampUtc = ts,
|
|
StarfaceId = c.StarfaceId,
|
|
ContactName = c.ContactName,
|
|
Field = c.Field,
|
|
FieldKey = c.FieldKey,
|
|
OutlookValue = c.OutlookValue,
|
|
StarfaceValue = c.StarfaceValue,
|
|
Winner = c.Winner
|
|
};
|
|
try
|
|
{
|
|
File.WriteAllText(Path.Combine(dir, notice.Id + ".json"),
|
|
JsonConvert.SerializeObject(notice, Formatting.Indented));
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Liefert ungesehene Konflikt-Notizen, die Kontakte dieses Clients
|
|
/// 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.
|
|
/// </summary>
|
|
public List<ConflictNotice> GetPending(string sharedDir, IDictionary<string, UnifiedContact> myContacts)
|
|
{
|
|
var result = new List<ConflictNotice>();
|
|
if (string.IsNullOrWhiteSpace(sharedDir)) return result;
|
|
|
|
var dir = ConflictsDir(sharedDir);
|
|
string[] files;
|
|
try
|
|
{
|
|
if (!Directory.Exists(dir)) return result;
|
|
files = Directory.GetFiles(dir, "*.json");
|
|
}
|
|
catch { return result; }
|
|
|
|
var seen = LoadSeen();
|
|
var host = Environment.MachineName;
|
|
var existingIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
foreach (var f in files)
|
|
{
|
|
ConflictNotice n = null;
|
|
try { n = JsonConvert.DeserializeObject<ConflictNotice>(File.ReadAllText(f)); }
|
|
catch { }
|
|
if (n == null || string.IsNullOrEmpty(n.Id)) continue;
|
|
|
|
// Veraltete Notizen entfernen.
|
|
if (DateTime.TryParse(n.TimestampUtc, out var ts)
|
|
&& DateTime.UtcNow - ts.ToUniversalTime() > Ttl)
|
|
{
|
|
try { File.Delete(f); } catch { }
|
|
continue;
|
|
}
|
|
existingIds.Add(n.Id);
|
|
|
|
if (seen.Contains(n.Id)) continue; // schon gezeigt
|
|
if (string.Equals(n.ByHost, host, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
seen.Add(n.Id); // selbst erzeugt -> nicht erneut anzeigen
|
|
continue;
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// Gesehen-Liste auf noch vorhandene Notizen eindampfen.
|
|
seen.IntersectWith(existingIds);
|
|
SaveSeen(seen);
|
|
|
|
return result;
|
|
}
|
|
|
|
private HashSet<string> LoadSeen()
|
|
{
|
|
try
|
|
{
|
|
if (File.Exists(_seenFile))
|
|
return new HashSet<string>(
|
|
JsonConvert.DeserializeObject<List<string>>(File.ReadAllText(_seenFile)) ?? new List<string>(),
|
|
StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
catch { }
|
|
return new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private void SaveSeen(HashSet<string> seen)
|
|
{
|
|
try { File.WriteAllText(_seenFile, JsonConvert.SerializeObject(seen.ToList())); }
|
|
catch { }
|
|
}
|
|
}
|
|
}
|