Add cross-client conflict notifications (step 3)
Wird ein echter Feld-Konflikt aufgeloest, erfahren jetzt auch die ANDEREN Arbeitsplaetze davon - nicht nur der aufloesende Client. - ConflictNotice-Modell + ConflictNotifier: schreibt pro Konflikt eine Notiz in <shared>/conflicts/ (nach StarfaceId), liest beim Sync ungesehene Notizen zu eigenen Kontakten, zeigt sie als Tray-Hinweis und merkt sich gezeigte lokal (seen-conflicts.json). Veraltete Notizen (>7 Tage) werden aufgeraeumt. - MainForm: schreibt nach einem Sync mit Konflikten die Notizen und zeigt ausstehende Notizen anderer Clients (gefiltert auf eigene gemappte StarfaceIds). AcquireCrossClientLock nimmt jetzt das gemeinsame Verzeichnis als Parameter. - README/CHANGELOG aktualisiert. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
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,
|
||||
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 (StarfaceId in myStarfaceIds) und nicht von ihm selbst
|
||||
/// stammen. Markiert sie als gesehen und raeumt veraltete Notizen auf.
|
||||
/// </summary>
|
||||
public List<ConflictNotice> GetPending(string sharedDir, ICollection<string> myStarfaceIds)
|
||||
{
|
||||
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;
|
||||
}
|
||||
if (myStarfaceIds != null && !string.IsNullOrEmpty(n.StarfaceId)
|
||||
&& !myStarfaceIds.Contains(n.StarfaceId))
|
||||
continue; // betrifft mich nicht
|
||||
|
||||
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 { }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user