Compare commits

...

4 Commits

Author SHA1 Message Date
duffyduck b8a7fda7bf Release v0.0.0.28 2026-06-08 13:02:51 +02:00
duffyduck 257b6fb33d 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) <noreply@anthropic.com>
2026-06-08 13:01:02 +02:00
duffyduck cf2970a41a 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>
2026-06-08 12:54:01 +02:00
duffyduck 5a0203e49e Document multi-client sync lock in README
Beschreibt Lock-Datei starface-sync.lock: atomares Anlegen (CreateNew),
nur waehrend des Syncs vorhanden, Inhalt (Host/User/Zeit/PID), Warte- und
Absturz-Verhalten, sowie den Hinweis auf bidirektionalen Modus im Mehrplatz.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 12:47:28 +02:00
10 changed files with 356 additions and 29 deletions
+8
View File
@@ -38,6 +38,14 @@ Versionsschema ist `x.x.x.x` (siehe `release.sh`).
### Hinzugefuegt ### Hinzugefuegt
- **Clientuebergreifende Konflikt-Hinweise (Mehrplatz).** Wird ein echter
Feld-Konflikt aufgeloest, legt der Client eine Notiz im gemeinsamen Verzeichnis
(`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 - **Clientuebergreifende Sync-Sperre (Mehrplatz).** In den Einstellungen laesst
sich ein gemeinsames Verzeichnis (Netzlaufwerk/UNC) hinterlegen. Synct ein sich ein gemeinsames Verzeichnis (Netzlaufwerk/UNC) hinterlegen. Synct ein
Arbeitsplatz, legt er dort eine Lock-Datei an (atomar via `CreateNew`); andere Arbeitsplatz, legt er dort eine Lock-Datei an (atomar via `CreateNew`); andere
+47
View File
@@ -265,6 +265,53 @@ Alle Daten werden lokal pro Benutzer gespeichert:
Self-signed Zertifikate der Starface werden automatisch akzeptiert. Es ist kein manueller Zertifikat-Import erforderlich. Self-signed Zertifikate der Starface werden automatisch akzeptiert. Es ist kein manueller Zertifikat-Import erforderlich.
### Mehrplatz-Betrieb / Sync-Sperre
Mehrere Arbeitsplaetze koennen dasselbe Starface-Adressbuch pflegen. Damit sich
gleichzeitig laufende Syncs nicht ins Gehege kommen (Dubletten, ueberschriebene
Aenderungen), gibt es zwei Schutzebenen:
**1. Lokal (immer aktiv):** Auf einem PC kann nie mehr als ein Sync gleichzeitig
laufen. Startet der Auto-Sync-Timer, waehrend ein manueller Sync laeuft (oder
umgekehrt), wird der zweite Lauf uebersprungen. Atomar per `Interlocked`.
**2. Clientuebergreifend (optional):** In den Einstellungen kann ein
**gemeinsames Verzeichnis** (Netzlaufwerk oder UNC-Pfad wie
`\\server\freigabe\sync`) hinterlegt werden. Funktionsweise:
- Beim Start eines Syncs legt der Client dort die Datei **`starface-sync.lock`**
an - **atomar** ueber `FileMode.CreateNew`: das Anlegen schlaegt fehl, wenn
die Datei bereits existiert. Damit kann immer nur ein Client gleichzeitig die
Sperre halten.
- Die Datei wird waehrend des gesamten Syncs **offen gehalten** und am Ende
wieder **geloescht**. Sie existiert also nur, solange tatsaechlich gerade
synchronisiert wird (bei kleinen Adressbuechern nur sehr kurz).
- Inhalt der Datei: `Rechnername | Benutzer | UTC-Zeitstempel | Prozess-ID` -
damit man sieht, wer gerade synct.
- Findet ein anderer Client die Sperre vor, **wartet** er (Abfrage alle 3
Sekunden, bis zu 2 Minuten) und synct erst danach. Wird die Sperre nicht frei,
wird der Lauf uebersprungen und beim naechsten Intervall erneut versucht.
- **Absturzsicher:** Stuerzt ein Client mitten im Sync ab, gibt Windows das
Datei-Handle frei. Die verwaiste Lock-Datei gilt nach 15 Minuten als veraltet
und wird vom naechsten Client uebernommen. Lebt der Halter noch, scheitert das
Loeschen am offenen Handle - die Sperre bleibt also korrekt bestehen.
- Ist **kein** Verzeichnis konfiguriert oder es ist nicht erreichbar, laeuft der
Sync ohne diese Sperre weiter (die lokale Sperre bleibt aktiv).
**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. 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
> Arbeitsplaetzen gepflegten Kontakte loeschen.
## Lizenz ## Lizenz
Proprietaer - Alle Rechte vorbehalten. Proprietaer - Alle Rechte vorbehalten.
+1 -1
View File
@@ -2,7 +2,7 @@
; Erfordert Inno Setup 6.x (https://jrsoftware.org/isinfo.php) ; Erfordert Inno Setup 6.x (https://jrsoftware.org/isinfo.php)
#define MyAppName "Starface Outlook Sync" #define MyAppName "Starface Outlook Sync"
#define MyAppVersion "0.0.0.27" #define MyAppVersion "0.0.0.28"
#define MyAppPublisher "HackerSoft - Hacker-Net Telekommunikation" #define MyAppPublisher "HackerSoft - Hacker-Net Telekommunikation"
#define MyAppURL "https://www.hacker-net.de" #define MyAppURL "https://www.hacker-net.de"
#define MyAppExeName "StarfaceOutlookSync.exe" #define MyAppExeName "StarfaceOutlookSync.exe"
@@ -0,0 +1,26 @@
namespace StarfaceOutlookSync.Models
{
/// <summary>
/// Hinweis ueber einen aufgeloesten Feld-Konflikt, der im gemeinsamen
/// Verzeichnis abgelegt wird, damit auch andere Arbeitsplaetze (die denselben
/// Kontakt pflegen) erfahren, dass ein Wert ueberschrieben wurde.
/// </summary>
public class ConflictNotice
{
public string Id { get; set; } = "";
public string ByHost { get; set; } = "";
public string ByUser { get; set; } = "";
public string TimestampUtc { get; set; } = "";
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; } = "";
public override string ToString() =>
$"{ContactName}: Feld '{Field}' geaendert an Arbeitsplatz {ByHost} " +
$"(Outlook: '{OutlookValue}' / Starface: '{StarfaceValue}') -> {Winner} uebernommen";
}
}
@@ -82,7 +82,8 @@ namespace StarfaceOutlookSync.Models
{ {
public string StarfaceId { get; set; } = ""; public string StarfaceId { get; set; } = "";
public string ContactName { 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 OutlookValue { get; set; } = "";
public string StarfaceValue { get; set; } = ""; public string StarfaceValue { get; set; } = "";
public string Winner { get; set; } = ""; // "Outlook" oder "Starface" public string Winner { get; set; } = ""; // "Outlook" oder "Starface"
@@ -0,0 +1,178 @@
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 { }
}
}
}
@@ -15,6 +15,7 @@ namespace StarfaceOutlookSync.Services
{ {
private class FieldDef private class FieldDef
{ {
public string Key;
public string Label; public string Label;
public Func<UnifiedContact, string> Get; public Func<UnifiedContact, string> Get;
public Action<UnifiedContact, string> Set; public Action<UnifiedContact, string> Set;
@@ -23,28 +24,43 @@ namespace StarfaceOutlookSync.Services
private static readonly FieldDef[] Fields = new[] private static readonly FieldDef[] Fields = new[]
{ {
new FieldDef { Label = "Vorname", Get = c => c.FirstName, Set = (c, v) => c.FirstName = v }, new FieldDef { Key = "FirstName", 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 { Key = "LastName", 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 { Key = "Company", 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 { Key = "JobTitle", 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 { Key = "Email", 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 { Key = "EmailSecondary", 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 { Key = "PhoneWork", 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 { Key = "PhoneMobile", 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 { Key = "PhoneHome", 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 { Key = "Fax", 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 { Key = "Street", 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 { Key = "City", 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 { Key = "PostalCode", 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 { Key = "State", 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 { Key = "Country", 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 { Key = "Website", 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 { Key = "Notes", 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 { Key = "Salutation", 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 { Key = "Title", 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 = "Birthday", Label = "Geburtstag", Get = c => c.Birthday, Set = (c, v) => c.Birthday = v },
}; };
/// <summary>Liest den Wert eines Feldes per stabilem Schluessel.</summary>
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) ?? "");
}
/// <summary>Vergleicht zwei Feldwerte (telefon-normalisiert je nach Feld).</summary>
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);
}
/// <summary> /// <summary>
/// Fuehrt den Outlook- und Starface-Stand gegen ihre jeweilige Baseline /// Fuehrt den Outlook- und Starface-Stand gegen ihre jeweilige Baseline
/// zusammen. outlookWins entscheidet bei echten Feld-Konflikten. /// zusammen. outlookWins entscheidet bei echten Feld-Konflikten.
@@ -95,6 +111,7 @@ namespace StarfaceOutlookSync.Services
StarfaceId = starface.StarfaceId, StarfaceId = starface.StarfaceId,
ContactName = outlook.DisplayName, ContactName = outlook.DisplayName,
Field = f.Label, Field = f.Label,
FieldKey = f.Key,
OutlookValue = olv, OutlookValue = olv,
StarfaceValue = sfv, StarfaceValue = sfv,
Winner = outlookWins ? "Outlook" : "Starface" Winner = outlookWins ? "Outlook" : "Starface"
@@ -7,9 +7,9 @@
<AssemblyTitle>Starface Outlook Sync</AssemblyTitle> <AssemblyTitle>Starface Outlook Sync</AssemblyTitle>
<Company>HackerSoft - Hacker-Net Telekommunikation</Company> <Company>HackerSoft - Hacker-Net Telekommunikation</Company>
<Product>Starface Outlook Sync</Product> <Product>Starface Outlook Sync</Product>
<Version>0.0.0.27</Version> <Version>0.0.0.28</Version>
<AssemblyVersion>0.0.0.27</AssemblyVersion> <AssemblyVersion>0.0.0.28</AssemblyVersion>
<FileVersion>0.0.0.27</FileVersion> <FileVersion>0.0.0.28</FileVersion>
<Description>Synchronisiert Outlook-Kontakte mit Starface Telefonanlage</Description> <Description>Synchronisiert Outlook-Kontakte mit Starface Telefonanlage</Description>
<Copyright>Stefan Hacker - HackerSoft</Copyright> <Copyright>Stefan Hacker - HackerSoft</Copyright>
<RuntimeIdentifier>win-x64</RuntimeIdentifier> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
+1 -1
View File
@@ -27,7 +27,7 @@ namespace StarfaceOutlookSync.UI
var lblVersion = new Label var lblVersion = new Label
{ {
Text = "Version 0.0.0.27", Text = "Version 0.0.0.28",
Left = 0, Top = 56, Width = 340, Height = 20, Left = 0, Top = 56, Width = 340, Height = 20,
TextAlign = ContentAlignment.MiddleCenter, TextAlign = ContentAlignment.MiddleCenter,
ForeColor = Color.Gray ForeColor = Color.Gray
+53 -3
View File
@@ -16,6 +16,7 @@ namespace StarfaceOutlookSync.UI
{ {
private readonly ProfileManager _profileManager = new ProfileManager(); private readonly ProfileManager _profileManager = new ProfileManager();
private readonly SyncEngine _syncEngine = new SyncEngine(); private readonly SyncEngine _syncEngine = new SyncEngine();
private readonly ConflictNotifier _conflictNotifier = new ConflictNotifier();
private NotifyIcon _trayIcon; private NotifyIcon _trayIcon;
private ContextMenuStrip _trayMenu; private ContextMenuStrip _trayMenu;
private ListView _profileList; private ListView _profileList;
@@ -349,16 +350,22 @@ namespace StarfaceOutlookSync.UI
} }
SyncLock crossLock = null; SyncLock crossLock = null;
var sharedDir = UserSettings.Load().SharedDirectory;
try try
{ {
// Clientuebergreifende Sperre (falls gemeinsames Verzeichnis konfiguriert). // Clientuebergreifende Sperre (falls gemeinsames Verzeichnis konfiguriert).
crossLock = await AcquireCrossClientLock(); crossLock = await AcquireCrossClientLock(sharedDir);
if (crossLock == null) if (crossLock == null)
{ {
SetStatus("Anderer Arbeitsplatz synchronisiert gerade - uebersprungen."); SetStatus("Anderer Arbeitsplatz synchronisiert gerade - uebersprungen.");
return; 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}'..."); SetStatus($"Synchronisiere '{profile.Name}'...");
_trayIcon.ShowBalloonTip(2000, "Starface Sync", _trayIcon.ShowBalloonTip(2000, "Starface Sync",
$"Synchronisiere '{profile.Name}'...", ToolTipIcon.Info); $"Synchronisiere '{profile.Name}'...", ToolTipIcon.Info);
@@ -381,6 +388,9 @@ namespace StarfaceOutlookSync.UI
detail += $"\n... und {result.Conflicts.Count - 5} weitere"; detail += $"\n... und {result.Conflicts.Count - 5} weitere";
_trayIcon.ShowBalloonTip(10000, _trayIcon.ShowBalloonTip(10000,
$"Konflikt bei {result.Conflicts.Count} Kontakt(en)", detail, ToolTipIcon.Warning); $"Konflikt bei {result.Conflicts.Count} Kontakt(en)", detail, ToolTipIcon.Warning);
// Andere Arbeitsplaetze ueber das gemeinsame Verzeichnis informieren.
_conflictNotifier.Write(sharedDir, result.Conflicts);
} }
SetStatus(msg); SetStatus(msg);
@@ -406,9 +416,8 @@ namespace StarfaceOutlookSync.UI
/// Ist kein Verzeichnis konfiguriert oder es ist nicht erreichbar, wird /// Ist kein Verzeichnis konfiguriert oder es ist nicht erreichbar, wird
/// ohne clientuebergreifende Sperre fortgefahren (lokaler Schutz bleibt). /// ohne clientuebergreifende Sperre fortgefahren (lokaler Schutz bleibt).
/// </summary> /// </summary>
private async Task<SyncLock> AcquireCrossClientLock() private async Task<SyncLock> AcquireCrossClientLock(string dir)
{ {
var dir = UserSettings.Load().SharedDirectory;
if (string.IsNullOrWhiteSpace(dir)) if (string.IsNullOrWhiteSpace(dir))
return SyncLock.NoOp(); return SyncLock.NoOp();
@@ -439,6 +448,47 @@ 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, MyContactsByStarfaceId());
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>
/// 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.
/// </summary>
private Dictionary<string, UnifiedContact> MyContactsByStarfaceId()
{
var map = new Dictionary<string, UnifiedContact>(StringComparer.OrdinalIgnoreCase);
try
{
foreach (var p in _profileManager.GetProfiles())
foreach (var m in _profileManager.GetMappings(p.Id))
if (!string.IsNullOrEmpty(m.StarfaceId) && !map.ContainsKey(m.StarfaceId))
map[m.StarfaceId] = m.LastOutlook;
}
catch { }
return map;
}
private void SetStatus(string text) private void SetStatus(string text)
{ {
if (InvokeRequired) if (InvokeRequired)