diff --git a/docs/PLAN-Linux-Portierung.md b/docs/PLAN-Linux-Portierung.md index fc748f3..b310855 100644 --- a/docs/PLAN-Linux-Portierung.md +++ b/docs/PLAN-Linux-Portierung.md @@ -309,12 +309,22 @@ Beide Betriebsarten laufen an: | `Predictalytics.Shell.exe` (GUI) | startet, Lizenzfenster erscheint, kein Absturz | | Konsolen-Kodierung | UTF-8 explizit gesetzt — ohne das zeigt die Windows-Konsole nur Fragezeichen statt Rahmen und Emoji | -### ⚠️ Nicht verifiziert: das Hauptfenster +### Nachtrag 2026-08-07: Hauptfenster verifiziert ✅ -Die hinterlegte Lizenz ist **abgelaufen** (`Expired: License has expired`). Beide -Betriebsarten bleiben deshalb an der Lizenzschranke stehen — `MainWindow`, Terminal, -Einstellungsansicht und die Wartungsaktionen sind compilergeprüft, aber **noch nie -angezeigt worden**. Nach Erneuerung der Lizenz nachzuholen. +Zunächst blockierte eine abgelaufene Lizenz die Prüfung. Nach der Umstellung auf das +Deploymentcenter (Abschnitt 5a) startet die GUI vollständig durch — belegt durch die +Logdateien des Laufs: + +``` +[INF] Shell initialisiert. Bereit. +[INF] 'Start Webserver' startet die WebUI auf http://localhost:5000 +[INF] Einstellungen: C:\Users\bergm\AppData\Roaming\Predictalytics\settings.json +``` + +Damit sind Lizenzschranke, Serilog-Aufbau mit Terminal-Sink, `MainWindowViewModel.Start()` +und der neue Einstellungspfad nachgewiesen. **Noch nicht durchgeklickt**: die einzelnen +Wartungsaktionen und die Einstellungsansicht — dafür fehlen die in Phase 1 verlorenen +DB-Zugangsdaten. ### Umsetzung @@ -433,6 +443,55 @@ Lizenzdialog. --- +## 5a. Zusatz — Umstellung auf das Deploymentcenter-Lizenzsystem ✅ (2026-08-07) + +**Nicht ursprünglich geplant.** Beim Aktivieren der neuen Lizenz stellte sich heraus, dass +Predictalytics noch am alten Lizenzsystem hing (LicenseLabrador, `license.mhdf.de`), +während die Lizenzen inzwischen im Deploymentcenter (`dc.mhdf.de`) gepflegt werden. + +Vor dem Umbau mit einem Wegwerf-Probelauf gegen den echten Client abgesichert: +Slug `predictalytics`, HWID v2 aus `machine-guid`, Server meldet `valid`. + +### Was sich geändert hat + +| | vorher (LicenseLabrador) | jetzt (Deploymentcenter) | +|---|---|---| +| Endpunkt | `license.mhdf.de/public/api/v1` | `dc.mhdf.de/api/license/v1` | +| Client | `LicenseLabrador.Client` (netstandard2.0) | `Deploymentcenter.Client` (netstandard2.0;net8.0) | +| Authentifizierung | Ed25519-Signaturprüfung + Basic Auth im Quelltext | serverseitig, kein Geheimnis im Client | +| Hardware-ID | SHA-256 aus MachineGuid **+ Rechnername** | **v2**: `2::`, ohne Rechnername | +| Linux-HWID | `/etc/machine-id`, sonst MAC | `/etc/machine-id` → dbus → DMI-UUID → MAC → Schlüsseldatei | +| Container | nicht vorgesehen | `DEPLOYMENTCENTER_HWID` überschreibt | +| Cache | DPAPI, **stiller Klartext-Rückfall unter Linux** | AES-256-GCM, HKDF aus HWID+Slug, **kein Klartext-Rückfall**, `0600` unter Linux | +| Ablageort | `%APPDATA%` | zusätzlich `XDG_CONFIG_HOME` unter Linux | + +**Damit erledigt sich Analyse-Punkt 3.2 Nr. 2 von selbst:** der stille Klartext-Rückfall +des Lizenz-Caches unter Linux existiert im neuen Client nicht mehr. Der Rechnername steckt +außerdem nicht mehr im Hash — Umbenennen verbraucht keinen Aktivierungsplatz. + +### Neue CLI-Schalter (`Services/LicenseCli.cs`) + +Aus dem Deploymentcenter-Integrationsleitfaden übernommen, für Server ohne Oberfläche: + +```bash +Predictalytics.Shell --license-status # HWID, Cache-Ablage, Serverprüfung +Predictalytics.Shell --license-set-key # einmalig aktivieren +Predictalytics.Shell --license-deactivate # Aktivierungsplatz freigeben +``` + +Die Schalter werden **vor** jeder Avalonia-Initialisierung ausgewertet, wie `--headless`. +Der Schlüssel wird in der Ausgabe maskiert (`LLAB3-*****-*****-*****-AABB2`). + +### ⚠️ Offen: Watchdog läuft noch auf dem alten System + +Das Deploymentcenter bündelt License, Watchdog, UpdateService und Bugtracker. +`WatchdogHeartbeatService` zeigt weiterhin auf `watchdog.mhdf.de` mit dem Header +`X-Watchdog-Key`. Das funktioniert, ist aber dieselbe Altlast wie eben beim Lizenzsystem. +Migration nach `dc.mhdf.de/api/watchdog/v1` als eigener Schritt einplanen — +Leitfaden liegt unter `Deploymentcenter/docs/WATCHDOG_INTEGRATION_GUIDE.md`. + +--- + ## 6. Phase 4 — Aufräumen und Absichern (1–1,5 PT) ### AP 4.1 — Kestrel-Binding und Absicherung ⚠️ diff --git a/src/Predictalytics.Hosting/LicenseGuard.cs b/src/Predictalytics.Hosting/LicenseGuard.cs index 14333a2..ecf45de 100644 --- a/src/Predictalytics.Hosting/LicenseGuard.cs +++ b/src/Predictalytics.Hosting/LicenseGuard.cs @@ -1,71 +1,96 @@ -using LicenseLabrador.Client; +using Deploymentcenter.Client; using Serilog; namespace Predictalytics.Hosting; /// -/// Lizenzpruefung gegen den LicenseLabrador-Server. +/// Lizenzpruefung gegen das Deploymentcenter (dc.mhdf.de). /// -/// Endpunkt, Produkt-Slug und der Ed25519-Public-Key sind bewusst einkompiliert -/// (keine Benutzerkonfiguration): ein konfigurierbarer Endpunkt bzw. Key wuerde es -/// erlauben, die Anwendung auf einen gefaelschten Lizenzserver zu zeigen. +/// Endpunkt und Produkt-Slug sind bewusst einkompiliert (keine Benutzer- +/// konfiguration): ein konfigurierbarer Endpunkt wuerde es erlauben, die +/// Anwendung auf einen gefaelschten Lizenzserver zu zeigen. /// /// -/// Enthaelt keinen Dialog: die interaktive Aktivierung liegt beim Host (WinForms- -/// bzw. Avalonia-Dialog), der Headless-Modus bezieht den Schluessel aus der -/// Konfiguration. +/// Enthaelt keinen Dialog: die interaktive Aktivierung liegt beim Host +/// (Avalonia- bzw. WinForms-Fenster), der Headless-Modus bezieht den +/// Schluessel aus der Umgebungsvariable oder dem verschluesselten Cache. /// /// public static class LicenseGuard { - private const string ProductSlug = "predictalytics"; - private const string PublicKeyBase64 = "L7YR1wMKk8+lNefatzL+DMvAtHFVkZWYXAxXGrro+/U="; - private const string BasicAuthUser = "Labrador"; - private const string BasicAuthPassword = "Labrador02763!"; - // HTTPS ist Pflicht, nicht Kosmetik: license.mhdf.de leitet http→https um, und .NET - // macht bei einem Redirect aus dem POST ein GET. Der Server antwortet darauf mit 405, - // das SDK wertet das als "unerreichbar" und meldet irrefuehrend NoLicense. - // Ausserdem gingen die BasicAuth-Credentials sonst im Klartext ueber die Leitung. - private static readonly string[] Endpoints = { "https://license.mhdf.de/public/api/v1" }; + /// In dc_projects hinterlegter Slug. + public const string ProductSlug = "predictalytics"; + + private const string ServerUrl = "https://dc.mhdf.de"; /// Re-check interval while the app is running (12 h). public static readonly TimeSpan RevalidationInterval = TimeSpan.FromHours(12); - public static LicenseClient CreateClient() + private static readonly LicenseClient Client = new(); + + /// Hardware-ID v2 dieser Maschine (Format 2:<plattform>:<hex>). + public static HardwareIdResult GetHardwareInfo() => HardwareId.GetHardwareId(ProductSlug); + + /// Ablageort des verschluesselten Lizenz-Caches. + public static string StorageDirectory => LicenseConfig.GetStorageDirectory(ProductSlug); + + /// + /// Zwischengespeicherter Lizenzschluessel, oder null wenn noch nie aktiviert wurde. + /// Der Cache ist AES-GCM-verschluesselt und an Hardware-ID und Produkt gebunden. + /// + public static string? GetCachedLicenseKey() { - var config = new LicenseConfig + try { - ProductSlug = ProductSlug, - PublicKeyBase64 = PublicKeyBase64, - Endpoints = Endpoints, - HttpBasicAuthUser = BasicAuthUser, - HttpBasicAuthPassword = BasicAuthPassword, - OfflineGraceHoursFallback = 168 // 7 Tage offline nutzbar, danach Serverkontakt nötig - }; - return new LicenseClient(config); + var cache = StateStore.Load(ProductSlug, GetHardwareInfo().HardwareId); + return string.IsNullOrWhiteSpace(cache?.LicenseKey) ? null : cache!.LicenseKey; + } + catch (Exception ex) + { + Log.Debug(ex, "Lizenz-Cache konnte nicht gelesen werden"); + return null; + } } /// - /// Prueft die zwischengespeicherte Lizenz. Liefert das Ergebnis; ob es nutzbar ist, - /// beantwortet . + /// Validiert einen Lizenzschluessel am Server und legt ihn bei Erfolg + /// verschluesselt ab. Bei Netzwerkfehlern faellt der Client auf den + /// Cache zurueck (Status valid_offline). /// - public static Task RevalidateAsync(LicenseClient client) => client.RevalidateAsync(); - - /// Nutzbar heisst: gueltig und mit passender Pruefsumme. - public static bool IsUsable(LicenseClient client, LicenseResult? result) => - result != null && result.IsUsable && client.VerifyChecksum(result); + public static Task ValidateAsync(string licenseKey) => + Client.ValidateAsync(ProductSlug, Normalize(licenseKey), ServerUrl); /// - /// Startet die periodische Revalidierung im Hintergrund. Erkennt Widerruf/Ablauf - /// waehrend des Betriebs. + /// Validiert mit dem zwischengespeicherten Schluessel. + /// Liefert null, wenn noch keiner hinterlegt ist. + /// + public static async Task ValidateCachedAsync() + { + var key = GetCachedLicenseKey(); + return key == null ? null : await ValidateAsync(key); + } + + /// Gibt den Aktivierungsplatz dieser Maschine am Server wieder frei. + public static async Task DeactivateAsync() + { + var key = GetCachedLicenseKey(); + if (key == null) return false; + return await Client.DeactivateAsync(ProductSlug, key, ServerUrl); + } + + /// Schreibweise vereinheitlichen — die Schluessel sind durchgaengig gross. + public static string Normalize(string licenseKey) => licenseKey.Trim().ToUpperInvariant(); + + /// + /// Startet die periodische Revalidierung im Hintergrund. Erkennt Widerruf und + /// Ablauf waehrend des Betriebs. /// /// /// Wird genau einmal aufgerufen, wenn die Lizenz endgueltig nicht mehr nutzbar ist. - /// Der Host entscheidet, was dann passiert (Meldung anzeigen, Anwendung beenden). - /// Transiente Fehler (Netz) loesen den Rueckruf nicht aus — dafuer gibt es die - /// Offline-Gnadenfrist des SDK. + /// Der Host entscheidet, was dann passiert. Netzwerkfehler loesen den Rueckruf + /// nicht aus — dafuer gibt es den verschluesselten Offline-Cache des Clients. /// - public static IDisposable StartPeriodicRevalidation(LicenseClient client, Action onUnusable) + public static IDisposable StartPeriodicRevalidation(Action onUnusable) { var cts = new CancellationTokenSource(); @@ -78,26 +103,39 @@ public static class LicenseGuard { try { - var result = await client.RevalidateAsync(); - if (IsUsable(client, result)) + var result = await ValidateCachedAsync(); + if (result == null) { - if (result.State == LicenseState.ValidOffline) + Log.Warning("Lizenz-Revalidierung: kein zwischengespeicherter Schlüssel vorhanden."); + continue; + } + + if (result.IsValid) + { + if (result.IsCached) { - Log.Warning("Lizenzserver nicht erreichbar — Offline-Gnadenfrist läuft bis {GraceUntil}.", - result.GraceUntil); + Log.Warning("Lizenzserver nicht erreichbar — Lizenz gilt über den lokalen Cache weiter ({Status}).", + result.Status); } continue; } - Log.Fatal("Lizenzprüfung fehlgeschlagen ({State}): {Message} — Anwendung wird beendet.", - result.State, result.Message); + // network_error heisst: Server weg UND kein brauchbarer Cache. + // Das ist kein Widerruf — beim naechsten Durchlauf erneut versuchen. + if (result.Status.Equals("network_error", StringComparison.OrdinalIgnoreCase)) + { + Log.Warning("Lizenz-Revalidierung fehlgeschlagen (Netzwerk): {Message}", result.Message); + continue; + } + + Log.Fatal("Lizenzprüfung fehlgeschlagen ({Status}): {Message} — Anwendung wird beendet.", + result.Status, result.Message); onUnusable(result); return; } catch (Exception ex) { - // Transiente Fehler (Netz etc.) faengt die Offline-Gnadenfrist des SDK ab — - // eine unerwartete Ausnahme darf die Anwendung niemals beenden. + // Eine unerwartete Ausnahme darf die Anwendung niemals beenden. Log.Warning(ex, "Periodische Lizenz-Revalidierung fehlgeschlagen (wird erneut versucht)."); } } diff --git a/src/Predictalytics.Hosting/Predictalytics.Hosting.csproj b/src/Predictalytics.Hosting/Predictalytics.Hosting.csproj index 11acea6..1fb0588 100644 --- a/src/Predictalytics.Hosting/Predictalytics.Hosting.csproj +++ b/src/Predictalytics.Hosting/Predictalytics.Hosting.csproj @@ -28,8 +28,9 @@ - - + + diff --git a/src/Predictalytics.Shell/App.axaml.cs b/src/Predictalytics.Shell/App.axaml.cs index 449b06d..c810c2b 100644 --- a/src/Predictalytics.Shell/App.axaml.cs +++ b/src/Predictalytics.Shell/App.axaml.cs @@ -43,12 +43,11 @@ public partial class App : global::Avalonia.Application private async Task StartupAsync(IClassicDesktopStyleApplicationLifetime desktop) { // ─── Lizenzschranke: ohne nutzbare Lizenz keine Anwendung ─── - var client = LicenseGuard.CreateClient(); - var result = await LicenseGuard.RevalidateAsync(client); + var result = await LicenseGuard.ValidateCachedAsync(); - if (!LicenseGuard.IsUsable(client, result)) + if (result?.IsValid != true) { - var licenseWindow = new LicenseWindow(client, result); + var licenseWindow = new LicenseWindow(result); desktop.MainWindow = licenseWindow; licenseWindow.Show(); @@ -75,11 +74,11 @@ public partial class App : global::Avalonia.Application viewModel.Start(); // Waehrend des Betriebs alle 12 h nachpruefen (Widerruf, Ablauf, Offline-Frist). - _licenseWatch = LicenseGuard.StartPeriodicRevalidation(client, unusable => + _licenseWatch = LicenseGuard.StartPeriodicRevalidation(unusable => Dispatcher.UIThread.Post(async () => { await Dialogs.ShowErrorAsync(window, "Lizenzfehler", - $"Die Lizenz ist nicht mehr gültig ({unusable.State}):\n{unusable.Message}\n\nPredictalytics wird beendet."); + $"Die Lizenz ist nicht mehr gültig ({unusable.Status}):\n{unusable.Message}\n\nPredictalytics wird beendet."); desktop.Shutdown(); })); diff --git a/src/Predictalytics.Shell/Predictalytics.Shell.csproj b/src/Predictalytics.Shell/Predictalytics.Shell.csproj index f5564e6..496a8a0 100644 --- a/src/Predictalytics.Shell/Predictalytics.Shell.csproj +++ b/src/Predictalytics.Shell/Predictalytics.Shell.csproj @@ -21,7 +21,7 @@ - + diff --git a/src/Predictalytics.Shell/Program.cs b/src/Predictalytics.Shell/Program.cs index 8892a19..eb7f52d 100644 --- a/src/Predictalytics.Shell/Program.cs +++ b/src/Predictalytics.Shell/Program.cs @@ -10,7 +10,13 @@ internal static class Program { // Die Argumentauswertung steht bewusst vor jeder Avalonia-Initialisierung: // auf einem Server ohne X11 wuerde der Prozess sonst beim Aufbau des - // Fenstersystems sterben, bevor --headless ueberhaupt greift. + // Fenstersystems sterben, bevor die Schalter ueberhaupt greifen. + if (LicenseCli.IsLicenseCommand(args)) + { + ConsoleAttach.TryAttachToParent(); + return LicenseCli.RunAsync(args).GetAwaiter().GetResult(); + } + if (args.Any(a => string.Equals(a, "--headless", StringComparison.OrdinalIgnoreCase))) { ConsoleAttach.TryAttachToParent(); diff --git a/src/Predictalytics.Shell/Services/HeadlessRunner.cs b/src/Predictalytics.Shell/Services/HeadlessRunner.cs index 55f9f6d..237187b 100644 --- a/src/Predictalytics.Shell/Services/HeadlessRunner.cs +++ b/src/Predictalytics.Shell/Services/HeadlessRunner.cs @@ -29,17 +29,16 @@ internal static class HeadlessRunner try { - var client = await EnsureLicensedAsync(); - if (client == null) return ExitNoLicense; + if (!await EnsureLicensedAsync()) return ExitNoLicense; var host = new PredictalyticsHost(options); using var cts = new CancellationTokenSource(); using var shutdownSignals = RegisterShutdown(cts); - using var licenseWatch = LicenseGuard.StartPeriodicRevalidation(client, result => + using var licenseWatch = LicenseGuard.StartPeriodicRevalidation(result => { - Log.Fatal("Lizenz nicht mehr gültig ({State}): {Message} — Dienst wird beendet.", - result.State, result.Message); + Log.Fatal("Lizenz nicht mehr gültig ({Status}): {Message} — Dienst wird beendet.", + result.Status, result.Message); cts.Cancel(); }); @@ -76,33 +75,36 @@ internal static class HeadlessRunner /// Lizenzpruefung ohne Dialog: zuerst der zwischengespeicherte Schluessel, sonst /// einmalige Aktivierung mit dem Schluessel aus der Umgebungsvariable. /// - private static async Task EnsureLicensedAsync() + private static async Task EnsureLicensedAsync() { - var client = LicenseGuard.CreateClient(); - - var result = await LicenseGuard.RevalidateAsync(client); - if (LicenseGuard.IsUsable(client, result)) return client; + var result = await LicenseGuard.ValidateCachedAsync(); + if (result?.IsValid == true) + { + Log.Information("Lizenz gültig ({Status}{Cached}).", + result.Status, result.IsCached ? ", aus lokalem Cache" : ""); + return true; + } var key = Environment.GetEnvironmentVariable(LicenseKeyVariable); if (string.IsNullOrWhiteSpace(key)) { - Log.Fatal("Keine nutzbare Lizenz ({State}: {Message}) und {Variable} ist nicht gesetzt. " + + Log.Fatal("Keine nutzbare Lizenz ({Status}) und {Variable} ist nicht gesetzt. " + "Im Headless-Betrieb gibt es keinen Aktivierungsdialog — bitte den Lizenzschlüssel " + - "über die Umgebungsvariable bereitstellen.", - result.State, result.Message, LicenseKeyVariable); - return null; + "über die Umgebungsvariable bereitstellen oder einmalig mit --license-set-key aktivieren.", + result?.Status ?? "kein Schlüssel hinterlegt", LicenseKeyVariable); + return false; } Log.Information("Aktiviere Lizenz mit dem Schlüssel aus {Variable}…", LicenseKeyVariable); - result = await client.ValidateAsync(key.Trim().ToUpperInvariant()); - if (LicenseGuard.IsUsable(client, result)) + result = await LicenseGuard.ValidateAsync(key); + if (result.IsValid) { - Log.Information("Lizenz aktiviert ({State}).", result.State); - return client; + Log.Information("Lizenz aktiviert ({Status}).", result.Status); + return true; } - Log.Fatal("Aktivierung fehlgeschlagen ({State}): {Message}", result.State, result.Message); - return null; + Log.Fatal("Aktivierung fehlgeschlagen ({Status}): {Message}", result.Status, result.Message); + return false; } private static WatchdogHeartbeatService? StartWatchdog(PredictalyticsOptions options, PredictalyticsHost host) diff --git a/src/Predictalytics.Shell/Services/LicenseCli.cs b/src/Predictalytics.Shell/Services/LicenseCli.cs new file mode 100644 index 0000000..6484823 --- /dev/null +++ b/src/Predictalytics.Shell/Services/LicenseCli.cs @@ -0,0 +1,104 @@ +using Predictalytics.Hosting; + +namespace Predictalytics.Shell.Services; + +/// +/// Lizenzverwaltung von der Kommandozeile — die im Deploymentcenter-Integrations- +/// leitfaden vorgesehenen Schalter. Gedacht fuer Server ohne Oberflaeche, wo es +/// keinen Aktivierungsdialog geben kann. +/// +internal static class LicenseCli +{ + public const string StatusSwitch = "--license-status"; + public const string SetKeySwitch = "--license-set-key"; + public const string DeactivateSwitch = "--license-deactivate"; + + public static bool IsLicenseCommand(string[] args) => + args.Length > 0 && args[0] is StatusSwitch or SetKeySwitch or DeactivateSwitch; + + public static async Task RunAsync(string[] args) + { + switch (args[0]) + { + case StatusSwitch: return await ShowStatusAsync(); + case SetKeySwitch: return await SetKeyAsync(args.Length > 1 ? args[1] : null); + case DeactivateSwitch: return await DeactivateAsync(); + default: return 1; + } + } + + private static async Task ShowStatusAsync() + { + var hw = LicenseGuard.GetHardwareInfo(); + Console.WriteLine("Predictalytics — Lizenzstatus"); + Console.WriteLine($" Produkt : {LicenseGuard.ProductSlug}"); + Console.WriteLine($" Hardware-ID : {hw.HardwareId}"); + Console.WriteLine($" HWID-Quelle : {hw.HwidSource} (v{hw.HwidVersion}, {hw.Platform})"); + Console.WriteLine($" Cache-Ablage : {LicenseGuard.StorageDirectory}"); + + var cachedKey = LicenseGuard.GetCachedLicenseKey(); + if (cachedKey == null) + { + Console.WriteLine(" Schlüssel : keiner hinterlegt"); + Console.WriteLine(); + Console.WriteLine($"Aktivierung: {SetKeySwitch} "); + return HeadlessRunner.ExitNoLicense; + } + + Console.WriteLine($" Schlüssel : {Mask(cachedKey)}"); + Console.WriteLine(); + Console.Write("Prüfe am Server… "); + + var result = await LicenseGuard.ValidateCachedAsync(); + Console.WriteLine(result is { IsValid: true } + ? $"gültig ({result.Status}{(result.IsCached ? ", aus lokalem Cache" : "")})" + : $"NICHT gültig ({result?.Status}): {result?.Message}"); + + return result is { IsValid: true } ? HeadlessRunner.ExitOk : HeadlessRunner.ExitNoLicense; + } + + private static async Task SetKeyAsync(string? key) + { + if (string.IsNullOrWhiteSpace(key)) + { + Console.Error.WriteLine($"Aufruf: {SetKeySwitch} "); + return HeadlessRunner.ExitFailed; + } + + Console.Write($"Aktiviere {Mask(LicenseGuard.Normalize(key))} … "); + var result = await LicenseGuard.ValidateAsync(key); + + if (result.IsValid) + { + Console.WriteLine($"erfolgreich ({result.Status})"); + Console.WriteLine($"Hardware-ID: {result.HardwareId}"); + return HeadlessRunner.ExitOk; + } + + Console.Error.WriteLine($"fehlgeschlagen ({result.Status}): {result.Message}"); + return HeadlessRunner.ExitNoLicense; + } + + private static async Task DeactivateAsync() + { + if (LicenseGuard.GetCachedLicenseKey() == null) + { + Console.Error.WriteLine("Kein hinterlegter Schlüssel — nichts zu deaktivieren."); + return HeadlessRunner.ExitFailed; + } + + Console.Write("Gebe Aktivierungsplatz am Server frei… "); + var ok = await LicenseGuard.DeactivateAsync(); + Console.WriteLine(ok ? "erfolgreich" : "fehlgeschlagen"); + return ok ? HeadlessRunner.ExitOk : HeadlessRunner.ExitFailed; + } + + /// Nur die Segmentstruktur zeigen, nicht den vollstaendigen Schluessel. + private static string Mask(string key) + { + var parts = key.Split('-'); + if (parts.Length < 2) return "*****"; + for (var i = 1; i < parts.Length - 1; i++) parts[i] = new string('*', parts[i].Length); + return string.Join('-', parts); + } +} diff --git a/src/Predictalytics.Shell/Views/LicenseWindow.axaml b/src/Predictalytics.Shell/Views/LicenseWindow.axaml index 2bdd849..3ae5e93 100644 --- a/src/Predictalytics.Shell/Views/LicenseWindow.axaml +++ b/src/Predictalytics.Shell/Views/LicenseWindow.axaml @@ -2,14 +2,14 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="Predictalytics.Shell.Views.LicenseWindow" Title="Predictalytics — Lizenzaktivierung" - Width="560" SizeToContent="Height" + Width="580" SizeToContent="Height" CanResize="False" WindowStartupLocation="CenterScreen"> - + @@ -29,5 +29,16 @@