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:
2026-06-08 12:54:01 +02:00
co-authored by Claude Opus 4.8
parent 5a0203e49e
commit cf2970a41a
5 changed files with 242 additions and 3 deletions
+47 -3
View File
@@ -16,6 +16,7 @@ namespace StarfaceOutlookSync.UI
{
private readonly ProfileManager _profileManager = new ProfileManager();
private readonly SyncEngine _syncEngine = new SyncEngine();
private readonly ConflictNotifier _conflictNotifier = new ConflictNotifier();
private NotifyIcon _trayIcon;
private ContextMenuStrip _trayMenu;
private ListView _profileList;
@@ -349,10 +350,11 @@ namespace StarfaceOutlookSync.UI
}
SyncLock crossLock = null;
var sharedDir = UserSettings.Load().SharedDirectory;
try
{
// Clientuebergreifende Sperre (falls gemeinsames Verzeichnis konfiguriert).
crossLock = await AcquireCrossClientLock();
crossLock = await AcquireCrossClientLock(sharedDir);
if (crossLock == null)
{
SetStatus("Anderer Arbeitsplatz synchronisiert gerade - uebersprungen.");
@@ -381,8 +383,14 @@ namespace StarfaceOutlookSync.UI
detail += $"\n... und {result.Conflicts.Count - 5} weitere";
_trayIcon.ShowBalloonTip(10000,
$"Konflikt bei {result.Conflicts.Count} Kontakt(en)", detail, ToolTipIcon.Warning);
// Andere Arbeitsplaetze ueber das gemeinsame Verzeichnis informieren.
_conflictNotifier.Write(sharedDir, result.Conflicts);
}
// Konflikt-Hinweise von ANDEREN Arbeitsplaetzen (zu meinen Kontakten) anzeigen.
ShowRemoteConflictNotices(sharedDir);
SetStatus(msg);
}
catch (Exception ex)
@@ -406,9 +414,8 @@ namespace StarfaceOutlookSync.UI
/// Ist kein Verzeichnis konfiguriert oder es ist nicht erreichbar, wird
/// ohne clientuebergreifende Sperre fortgefahren (lokaler Schutz bleibt).
/// </summary>
private async Task<SyncLock> AcquireCrossClientLock()
private async Task<SyncLock> AcquireCrossClientLock(string dir)
{
var dir = UserSettings.Load().SharedDirectory;
if (string.IsNullOrWhiteSpace(dir))
return SyncLock.NoOp();
@@ -439,6 +446,43 @@ namespace StarfaceOutlookSync.UI
}
}
/// <summary>
/// Zeigt Konflikt-Hinweise an, die andere Arbeitsplaetze ueber das
/// gemeinsame Verzeichnis hinterlassen haben und Kontakte betreffen, die
/// auch dieser Client gemappt hat.
/// </summary>
private void ShowRemoteConflictNotices(string sharedDir)
{
if (string.IsNullOrWhiteSpace(sharedDir)) return;
try
{
var pending = _conflictNotifier.GetPending(sharedDir, AllMappedStarfaceIds());
if (pending.Count == 0) return;
var detail = string.Join("\n", pending.Take(5).Select(p => p.ToString()));
if (pending.Count > 5)
detail += $"\n... und {pending.Count - 5} weitere";
_trayIcon.ShowBalloonTip(10000,
$"Konflikt an anderem Arbeitsplatz ({pending.Count})", detail, ToolTipIcon.Warning);
}
catch { }
}
/// <summary>Alle von diesem Client gemappten StarfaceIds (ueber alle Profile).</summary>
private HashSet<string> AllMappedStarfaceIds()
{
var ids = new HashSet<string>(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);
}
catch { }
return ids;
}
private void SetStatus(string text)
{
if (InvokeRequired)