WinForms-Host durch die Avalonia-Shell abgeloest

Merge von main: die dort entstandene Deployment-Center-Integration (Lizenz,
Heartbeat mit DB-Health-Check, Fehler-Stream, UpdateService) ist jetzt Teil
des plattformneutralen Kerns. Predictalytics.WinFormsHost ist entfernt.

Nach Predictalytics.Hosting gezogen:
- DcConfig, DcApiClient, DcErrorSink, DcHeartbeatService, DcUpdateService
  unveraendert - sie waren bereits plattformneutral
- DcErrorReporter ohne Application.ThreadException und MessageBox; der
  UI-Thread-Handler liegt jetzt beim Host und ruft ReportUiThreadException
- LicenseGuard/LicenseSession ohne Dialog und ohne WinForms-Timer. Neu:
  TryUseCachedAsync, ActivateAsync, StartPeriodicRevalidation ueber
  PeriodicTimer. Die Unterscheidung transienter Fehler und die Warnung vor
  ablaufender Gnadenfrist sind unveraendert uebernommen.
- Dc-Einstellungen von AppSettings nach PredictalyticsOptions; die
  Watchdog-Einstellungen entfallen
- DcErrorSink im LoggingSetup, Startbanner nutzt DcConfig.AppVersion

BuildInfo.targets wird jetzt von Predictalytics.Hosting importiert.

In der Avalonia-Shell nachgezogen:
- Menue Deployment Center mit Update-Suche und Lizenzstatus
- Einstellungsgruppe Deployment Center statt Watchdog, Update-Kanal als
  ComboBox, Server-URL nur zur Anzeige
- Heartbeat-Snapshot mit SELECT-1-Probe wie in der WinForms-Fassung
- Update-Pruefung still beim Start und interaktiv ueber das Menue, mit
  NotifyStopping vor dem Start des Update-Agenten
- Lizenzfenster wertet IsTransient aus: bei fehlender Serververbindung wird
  nicht behauptet, die Lizenz sei ungueltig
- TextBox.Watermark auf PlaceholderText (in Avalonia 12 veraltet)

Build: 0 Fehler, 8 Warnungen (alle vorbestehend).
Tests: 100 bestanden, 0 Fehler, 1 uebersprungen.
Verifiziert: --license-status meldet gueltig samt Gnadenfrist; die GUI
startet durch, prueft die Lizenz und laeuft gegen den UpdateService.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-08-08 21:07:13 +02:00
co-authored by Claude Opus 5
35 changed files with 1694 additions and 1578 deletions
+41 -22
View File
@@ -1,3 +1,4 @@
using Deploymentcenter.Client;
using Predictalytics.Hosting;
namespace Predictalytics.Shell.Services;
@@ -18,26 +19,30 @@ internal static class LicenseCli
public static async Task<int> RunAsync(string[] args)
{
switch (args[0])
LicenseClient.DefaultAppVersion = DcConfig.AppVersion;
return args[0] switch
{
case StatusSwitch: return await ShowStatusAsync();
case SetKeySwitch: return await SetKeyAsync(args.Length > 1 ? args[1] : null);
case DeactivateSwitch: return await DeactivateAsync();
default: return 1;
}
StatusSwitch => await ShowStatusAsync(),
SetKeySwitch => await SetKeyAsync(args.Length > 1 ? args[1] : null),
DeactivateSwitch => await DeactivateAsync(),
_ => HeadlessRunner.ExitFailed
};
}
private static async Task<int> ShowStatusAsync()
{
var hw = LicenseGuard.GetHardwareInfo();
Console.WriteLine("Predictalytics — Lizenzstatus");
Console.WriteLine($" Produkt : {LicenseGuard.ProductSlug}");
Console.WriteLine($" Produkt : {DcConfig.ProductSlug}");
Console.WriteLine($" Version : {DcConfig.AppVersion} ({DcConfig.GitCommitShort})");
Console.WriteLine($" Server : {DcConfig.BaseUrl}");
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)
var cachedKey = LicenseClient.TryGetCachedKey(DcConfig.ProductSlug);
if (string.IsNullOrWhiteSpace(cachedKey))
{
Console.WriteLine(" Schlüssel : keiner hinterlegt");
Console.WriteLine();
@@ -45,16 +50,28 @@ internal static class LicenseCli
return HeadlessRunner.ExitNoLicense;
}
Console.WriteLine($" Schlüssel : {Mask(cachedKey)}");
Console.WriteLine($" Schlüssel : {Mask(cachedKey!)}");
Console.WriteLine();
Console.Write("Prüfe am Server… ");
Console.Write("Prüfe am Deployment Center… ");
var result = await LicenseGuard.ValidateCachedAsync();
Console.WriteLine(result is { IsValid: true }
? $"gültig ({result.Status}{(result.IsCached ? ", aus lokalem Cache" : "")})"
var check = await LicenseGuard.TryUseCachedAsync();
var result = check.LastResult;
if (check.IsUsable)
{
Console.WriteLine($"gültig ({result!.Status}{(result.IsCached ? ", aus lokalem Cache" : "")})");
if (result.CacheExpiresAt is { } expiresAt && expiresAt > 0)
{
Console.WriteLine($" Offline-Gnadenfrist bis: {DateTimeOffset.FromUnixTimeSeconds(expiresAt).UtcDateTime:yyyy-MM-dd HH:mm} UTC");
}
return HeadlessRunner.ExitOk;
}
// Transient heisst: kein Urteil, nur eine gescheiterte Verbindung.
Console.WriteLine(result?.IsTransient == true
? $"unentschieden ({result.Status}): {result.Message}"
: $"NICHT gültig ({result?.Status}): {result?.Message}");
return result is { IsValid: true } ? HeadlessRunner.ExitOk : HeadlessRunner.ExitNoLicense;
return HeadlessRunner.ExitNoLicense;
}
private static async Task<int> SetKeyAsync(string? key)
@@ -65,10 +82,10 @@ internal static class LicenseCli
return HeadlessRunner.ExitFailed;
}
Console.Write($"Aktiviere {Mask(LicenseGuard.Normalize(key))} … ");
var result = await LicenseGuard.ValidateAsync(key);
Console.Write($"Aktiviere {Mask(key.Trim().ToUpperInvariant())} … ");
var (session, result) = await LicenseGuard.ActivateAsync(LicenseGuard.CreateClient(), key);
if (result.IsValid)
if (session is not null)
{
Console.WriteLine($"erfolgreich ({result.Status})");
Console.WriteLine($"Hardware-ID: {result.HardwareId}");
@@ -81,14 +98,16 @@ internal static class LicenseCli
private static async Task<int> DeactivateAsync()
{
if (LicenseGuard.GetCachedLicenseKey() == null)
var key = LicenseClient.TryGetCachedKey(DcConfig.ProductSlug);
if (string.IsNullOrWhiteSpace(key))
{
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.Write("Gebe Aktivierungsplatz am Deployment Center frei… ");
var ok = await LicenseGuard.CreateClient()
.DeactivateAsync(DcConfig.ProductSlug, key!, DcConfig.BaseUrl);
Console.WriteLine(ok ? "erfolgreich" : "fehlgeschlagen");
return ok ? HeadlessRunner.ExitOk : HeadlessRunner.ExitFailed;
}