Arbeitsstand gesichert: Deploymentcenter-Integration (D-0 bis D-5) und Einfenster-Shell

Sicherungscommit vor dem Aufraeumen des Repos, damit nachvollziehbar bleibt,
welcher Stand vor der Bereinigung galt. Build gruen, 476 Tests gruen.

Zwei Straenge, die sich ueber .csproj, Program.cs und appsettings.json
ueberschneiden und darum gemeinsam abgelegt werden:

Deploymentcenter-Integration (P3c, Plan D-0 bis D-5 code-seitig fertig):
- Deploymentcenter.Client 2.5.0 als lokales Paket, Source-Mapping erweitert
- DeploymentcenterOptions, DeploymentcenterErrorReporter, LicenseGate/LicenseCli
- WatchdogHeartbeatService auf die Deploymentcenter-API umgestellt
  (version, os, checks, metrics, status stopped)
- Security: MasterKeyResolver, SecretRedactor, FilePermissions
- Directory.Build.props mit zentraler Version 0.1.0 (Packager-Versionsdisziplin)
- deploy/: Packager-Vorlage und systemd-Unit; echte Zugangsdaten bleiben
  ueber .gitignore aussen vor
- setup.json fuer die Erstinstallation
- UMSETZUNGSPLAN-Deploymentcenter-Integration.md; ANALYSE-Linux-Portierung.md
  verweist auf den neuen Plan

Einfenster-Shell (UI-Redesign):
- ShellWindow + ShellNavModel als Seitenleisten-Shell
- WindowMenuBar und WindowMenuModel entfallen
- Modul- und Kernfenster auf die Shell-Einbettung angepasst

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-08-22 10:42:22 +02:00
co-authored by Claude Opus 5
parent ac5e16f0a6
commit dd8da3fd3f
58 changed files with 3932 additions and 650 deletions
@@ -0,0 +1,143 @@
using System;
using System.Linq;
using Deploymentcenter.Client;
using PolyTrader.Core;
using PolyTrader.Core.Security;
using PolyTraderSharp.Models;
namespace PolyTrader.App.Avalonia.Licensing
{
/// <summary>
/// Kopflose Lizenzverwaltung von der Kommandozeile aus (D-2), nach der Konvention aus
/// LICENSE_INTEGRATION_GUIDE §4: <c>--license-status</c>, <c>--license-set-key</c>,
/// <c>--license-deactivate</c>. Diese Schalter baut die Anwendung selbst, das SDK bringt nur die
/// Bausteine mit. Läuft VOR dem Host-Aufbau und beendet den Prozess über den Rückgabewert.
/// </summary>
internal static class LicenseCli
{
/// <summary>Liefert <c>true</c>, wenn einer der Lizenz-Schalter erkannt und abgearbeitet wurde.</summary>
public static bool TryHandle(string[] args, out int exitCode)
{
exitCode = 0;
if (args.Any(a => string.Equals(a, "--license-status", StringComparison.OrdinalIgnoreCase)))
{
exitCode = Status();
return true;
}
int setIdx = Array.FindIndex(args, a => string.Equals(a, "--license-set-key", StringComparison.OrdinalIgnoreCase));
if (setIdx >= 0)
{
string? key = setIdx + 1 < args.Length ? args[setIdx + 1] : null;
exitCode = SetKey(key);
return true;
}
if (args.Any(a => string.Equals(a, "--license-deactivate", StringComparison.OrdinalIgnoreCase)))
{
exitCode = Deactivate();
return true;
}
return false;
}
private static int Status()
{
var options = Program.LoadDeploymentcenterOptions();
var hw = HardwareId.GetHardwareId(options.ProjectSlug);
var cached = LicenseClient.TryGetCachedState(options.ProjectSlug);
Console.WriteLine($"HWID v2: {hw.HardwareId} ({hw.HwidSource})");
Console.WriteLine($"Produkt: {options.ProjectSlug}");
Console.WriteLine($"Server: {(string.IsNullOrWhiteSpace(options.BaseUrl) ? "(keine URL konfiguriert)" : options.BaseUrl)}");
Console.WriteLine($"Cache: {(cached == null ? "leer" : cached.Status)}");
if (cached != null && cached.CacheExpiresAt > 0)
{
var until = DateTimeOffset.FromUnixTimeSeconds(cached.CacheExpiresAt);
Console.WriteLine($"Offline noch bis: {until:u}");
}
var settings = ServerSettings.Load("server_settings.xml");
Console.WriteLine($"Gespeicherter Schlüssel (server_settings.xml): {(string.IsNullOrWhiteSpace(settings.LicenseKey) ? "keiner" : settings.LicenseKeyStatus)}");
return 0;
}
private static int SetKey(string? key)
{
if (string.IsNullOrWhiteSpace(key))
{
Console.Error.WriteLine("Fehler: --license-set-key erwartet den Schlüssel als nächstes Argument.");
return 1;
}
var options = Program.LoadDeploymentcenterOptions();
if (string.IsNullOrWhiteSpace(options.BaseUrl))
{
Console.Error.WriteLine("Fehler: Keine Deploymentcenter-URL konfiguriert (appsettings.json: Deploymentcenter:BaseUrl).");
return 1;
}
LicenseClient.DefaultAppVersion = BuildInfo.Version;
var client = new LicenseClient();
LicenseValidationResult result = client
.ValidateAsync(options.ProjectSlug, key.Trim(), options.BaseUrl, BuildInfo.Version)
.GetAwaiter().GetResult();
Console.WriteLine($"{result.Status}: {result.Message}");
if (!result.IsValid)
{
return 1;
}
const string settingsPath = "server_settings.xml";
var settings = ServerSettings.Load(settingsPath);
settings.LicenseKey = SecretProtection.Protect(key.Trim());
settings.Save(settingsPath);
Console.WriteLine($"Lizenz gültig und in {settingsPath} gespeichert " +
$"({(SecretProtection.IsEncrypted(settings.LicenseKey) ? "verschlüsselt" : "Klartext")}).");
return 0;
}
private static int Deactivate()
{
var options = Program.LoadDeploymentcenterOptions();
var settings = ServerSettings.Load("server_settings.xml");
string key;
try { key = SecretProtection.Unprotect(settings.LicenseKey); }
catch (Exception ex)
{
Console.Error.WriteLine($"Fehler: gespeicherter Schlüssel nicht lesbar: {ex.Message}");
return 1;
}
if (string.IsNullOrWhiteSpace(key))
{
Console.Error.WriteLine("Fehler: kein Lizenzschlüssel hinterlegt.");
return 1;
}
// Die Deaktivierung gibt einen Aktivierungsplatz frei und ist deshalb geschützt - der
// shared_key gehört auf den Administrationsrechner, nicht in die ausgelieferte
// Anwendung (siehe LICENSE_INTEGRATION_GUIDE). Ohne ihn: klarer Hinweis statt Versuch.
string? sharedKey = Environment.GetEnvironmentVariable("DC_SHARED_KEY");
if (string.IsNullOrWhiteSpace(sharedKey))
{
Console.Error.WriteLine("Fehler: DC_SHARED_KEY nicht gesetzt. Die Deaktivierung braucht den " +
"administrativen Shared Key - alternativ im Deploymentcenter-WebUI unter " +
"der Hardware-Liste auf „Freigeben\" klicken.");
return 1;
}
var client = new LicenseClient();
bool ok = client.DeactivateAsync(options.ProjectSlug, key, options.BaseUrl, sharedKey)
.GetAwaiter().GetResult();
Console.WriteLine(ok ? "Aktivierung freigegeben." : "Deaktivierung fehlgeschlagen.");
return ok ? 0 : 1;
}
}
}