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
@@ -2,11 +2,13 @@ using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Deploymentcenter.Client;
using PolyTrader.App.Avalonia.Controls;
using PolyTrader.Core;
using PolyTrader.Core.Configuration;
using PolyTrader.Core.Modularity;
using PolyTrader.Core.Persistence;
using PolyTrader.Core.Security;
@@ -21,7 +23,7 @@ namespace PolyTrader.App.Avalonia.Views
/// den <see cref="SettingsEditor"/> anstelle der früheren PropertyGrids.
/// Layout vollständig in SettingsWindow.axaml.
/// </summary>
public partial class SettingsWindow : Window
public partial class SettingsWindow : UserControl
{
private const string SettingsPath = "server_settings.xml";
@@ -33,20 +35,20 @@ namespace PolyTrader.App.Avalonia.Views
private WatchdogHeartbeatService? _watchdog;
private MullvadVpnService? _vpn;
private TerminalLogger? _logger;
private DeploymentcenterOptions? _dcOptions;
public SettingsWindow() => AvaloniaXamlLoader.Load(this);
public SettingsWindow(IModuleUiHost host, IAccountRepository accountRepo, TradingState state,
TerminalLogger logger, WatchdogHeartbeatService? watchdog = null,
MullvadVpnService? vpn = null) : this()
MullvadVpnService? vpn = null, DeploymentcenterOptions? dcOptions = null) : this()
{
this.FindControl<Controls.WindowMenuBar>("menuBar")!.Attach(host, "core.settings", this);
_accountRepo = accountRepo;
_state = state;
_logger = logger;
_watchdog = watchdog;
_vpn = vpn;
_dcOptions = dcOptions;
var grid = this.FindControl<DataGrid>("gridAccounts")!;
grid.ItemsSource = _accounts;
@@ -59,6 +61,8 @@ namespace PolyTrader.App.Avalonia.Views
this.FindControl<Button>("btnOpenRouterKey")!.Click += async (_, _) => await SetOpenRouterKeyAsync();
this.FindControl<Button>("btnWatchdogToken")!.Click += async (_, _) => await SetWatchdogTokenAsync();
this.FindControl<Button>("btnTestWatchdog")!.Click += async (_, _) => await TestWatchdogAsync();
this.FindControl<Button>("btnLicenseKey")!.Click += async (_, _) => await SetLicenseKeyAsync();
this.FindControl<Button>("btnCheckLicense")!.Click += async (_, _) => await CheckLicenseAsync();
this.FindControl<Button>("btnAccNew")!.Click += (_, _) => AddAccount();
this.FindControl<Button>("btnAccDelete")!.Click += async (_, _) => await DeleteAccountAsync();
@@ -199,19 +203,8 @@ namespace PolyTrader.App.Avalonia.Views
/// mit der üblichen umask sonst <c>644</c> an – die Datei wäre für alle lesbar. Unter Windows
/// greift die NTFS-Vererbung, dort ist nichts zu tun.
/// </summary>
private void RestrictToOwner(string path)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return;
try
{
File.SetUnixFileMode(path, UnixFileMode.UserRead | UnixFileMode.UserWrite);
}
catch (Exception ex)
{
_logger?.Warning($"⚠️ Dateirechte für {Path.GetFileName(path)} konnten nicht gesetzt werden: {ex.Message}. " +
"Bitte manuell prüfen (chmod 600).");
}
}
private void RestrictToOwner(string path) =>
FilePermissions.RestrictToOwner(path, msg => _logger?.Warning($"⚠️ {msg}"));
// ===== OpenRouter-API-Key (Supervisor) =====
@@ -323,5 +316,111 @@ namespace PolyTrader.App.Avalonia.Views
btn.IsEnabled = true;
}
}
// ===== Lizenz (D-2) =====
private async System.Threading.Tasks.Task SetLicenseKeyAsync()
{
bool exists = !string.IsNullOrWhiteSpace(_settings.LicenseKey);
string? key = await DialogWindow.Prompt(this, "Deploymentcenter-Lizenzschlüssel",
"Lizenzschlüssel eingeben.\n" +
"Wird in der gitignorierten server_settings.xml gespeichert (leer = entfernen).\n" +
"Wirkt erst beim nächsten Start von PolyTrader (keine Laufzeit-Revalidierung).\n" +
(exists ? "Aktuell ist ein Schlüssel gesetzt." : "Aktuell ist KEIN Schlüssel gesetzt."));
if (key == null) return; // Abbruch
try
{
_settings.LicenseKey = string.IsNullOrWhiteSpace(key)
? string.Empty
: SecretProtection.Protect(key.Trim());
_settings.Save(SettingsPath);
this.FindControl<SettingsEditor>("editorServer")!.Show(_settings);
if (_settings.LicenseKey.Length == 0)
{
_logger?.Info("Lizenzschlüssel entfernt.");
Status("Lizenzschlüssel entfernt – PolyTrader startet beim nächsten Mal im eingeschränkten Modus.");
return;
}
bool encrypted = SecretProtection.IsEncrypted(_settings.LicenseKey);
_logger?.Info($"Lizenzschlüssel gespeichert ({(encrypted ? "verschlüsselt" : "Klartext")}).");
Status((encrypted
? "Lizenzschlüssel gespeichert (verschlüsselt). "
: "Lizenzschlüssel gespeichert – ACHTUNG: ohne Master-Key im Klartext. ") +
"Mit „Lizenz jetzt prüfen\" testen, wirkt für Module erst beim nächsten Start.");
}
catch (Exception ex)
{
await DialogWindow.Info(this, "Fehler", $"Fehler beim Speichern des Lizenzschlüssels: {ex.Message}");
}
}
/// <summary>
/// Prüft den GESPEICHERTEN Schlüssel gegen das Deploymentcenter und zeigt Urteil und
/// Hardware-ID (zum Kopieren, falls Richard sie für eine Aktivierung im Admin braucht).
/// Ungespeicherte Änderungen wirken bewusst nicht, aus demselben Grund wie beim Watchdog-Test.
/// </summary>
private async System.Threading.Tasks.Task CheckLicenseAsync()
{
if (_dcOptions == null) return;
var btn = this.FindControl<Button>("btnCheckLicense")!;
btn.IsEnabled = false;
try
{
if (string.IsNullOrWhiteSpace(_dcOptions.BaseUrl))
{
await DialogWindow.Info(this, "Lizenz", "Keine Deploymentcenter-URL konfiguriert (appsettings.json).");
return;
}
string key;
try { key = SecretProtection.Unprotect(_settings.LicenseKey); }
catch (Exception ex)
{
await DialogWindow.Info(this, "Lizenz", $"Gespeicherter Schlüssel nicht lesbar: {ex.Message}");
return;
}
var hw = HardwareId.GetHardwareId(_dcOptions.ProjectSlug);
if (string.IsNullOrWhiteSpace(key))
{
await DialogWindow.Info(this, "Lizenz",
$"Kein Lizenzschlüssel gesetzt.\n\nHardware-ID dieser Maschine:\n{hw.HardwareId}");
Status("Kein Lizenzschlüssel gesetzt.");
return;
}
Status("Prüfe Lizenz …");
LicenseClient.DefaultAppVersion = BuildInfo.Version;
var client = new LicenseClient();
var result = await client.ValidateAsync(_dcOptions.ProjectSlug, key, _dcOptions.BaseUrl, BuildInfo.Version);
string detail = $"Status: {result.Status}\n{result.Message}\n\nHardware-ID dieser Maschine:\n{hw.HardwareId}";
if (result.IsValid)
{
_logger?.Info($"🔑 Lizenz geprüft: gültig ({result.Status}).");
Status($"Lizenz gültig ({result.Status}).");
await DialogWindow.Info(this, "Lizenz gültig", detail);
}
else
{
_logger?.Warning($"⚠️ Lizenz geprüft: nicht gültig ({result.Status}).");
Status($"Lizenz NICHT gültig ({result.Status}).");
await DialogWindow.Info(this, "Lizenz nicht gültig", detail);
}
}
catch (Exception ex)
{
await DialogWindow.Info(this, "Fehler", $"Lizenzprüfung fehlgeschlagen: {ex.Message}");
}
finally
{
btn.IsEnabled = true;
}
}
}
}