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:
@@ -41,7 +41,18 @@ namespace PolyTrader.App.Avalonia
|
||||
bool headless = args.Any(a => string.Equals(a, "--headless", StringComparison.OrdinalIgnoreCase));
|
||||
bool smoke = args.Any(a => string.Equals(a, "--smoke-ui", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var host = BuildHost(out var bootLog);
|
||||
// D-2: Lizenz-CLI-Schalter (--license-status/--license-set-key/--license-deactivate)
|
||||
// laufen VOR dem Host-Aufbau und beenden sich selbst - kein Sinn, dafür Trading-Dienste
|
||||
// hochzufahren.
|
||||
if (Licensing.LicenseCli.TryHandle(args, out int cliExitCode))
|
||||
return cliExitCode;
|
||||
|
||||
var dcOptions = LoadDeploymentcenterOptions();
|
||||
|
||||
// Konstruktionsprüfung (--smoke-ui) testet Fensteraufbau, keine Lizenzlogik - dort
|
||||
// bewusst ALLE Module registrieren, sonst würde die Prüfung auf einem Build-Server
|
||||
// (nie lizenziert) still einen Großteil ihrer Abdeckung verlieren.
|
||||
var host = BuildHost(dcOptions, enforceLicense: !smoke, out var bootLog);
|
||||
|
||||
// Konstruktionspruefung der Fenster: Host NICHT starten. Sonst laufen Trading-Engine,
|
||||
// WSS-Clients und Jobs gegen die echten Boersen-Endpunkte an - fuer einen reinen
|
||||
@@ -82,11 +93,35 @@ namespace PolyTrader.App.Avalonia
|
||||
.WithInterFont()
|
||||
.LogToTrace();
|
||||
|
||||
/// <summary>
|
||||
/// Lädt NUR den Deploymentcenter-Konfigurationsblock, bevor der Generic Host existiert –
|
||||
/// die Lizenzprüfung (D-2) muss VOR der Modul-Liste laufen, also bevor
|
||||
/// <see cref="Host.CreateDefaultBuilder"/> seine eigene <see cref="IConfiguration"/> baut.
|
||||
/// Liest dieselben Dateien wie der Host später (appsettings.json + appsettings.Local.json),
|
||||
/// nur eigenständig und minimal. Von <see cref="Licensing.LicenseCli"/> mitbenutzt.
|
||||
/// </summary>
|
||||
internal static DeploymentcenterOptions LoadDeploymentcenterOptions()
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.SetBasePath(AppContext.BaseDirectory)
|
||||
.AddJsonFile("appsettings.json", optional: true)
|
||||
.AddJsonFile("appsettings.Local.json", optional: true)
|
||||
.Build();
|
||||
|
||||
var options = new DeploymentcenterOptions();
|
||||
config.GetSection(DeploymentcenterOptions.SectionName).Bind(options);
|
||||
return options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Baut den Anwendungs-Host: Konfiguration, Master-Key, Persistenz, Core-Dienste und die
|
||||
/// aktiven Module. Kennt die Oberfläche nicht.
|
||||
/// </summary>
|
||||
private static IHost BuildHost(out TerminalLogger bootLog)
|
||||
/// <param name="enforceLicense">
|
||||
/// D-2: Wenn <c>false</c> (nur <c>--smoke-ui</c>), werden immer alle Module registriert –
|
||||
/// die Konstruktionsprüfung testet Fensteraufbau, keine Lizenzlogik.
|
||||
/// </param>
|
||||
private static IHost BuildHost(DeploymentcenterOptions dcOptions, bool enforceLicense, out TerminalLogger bootLog)
|
||||
{
|
||||
var serverSettings = ServerSettings.Load(ServerSettingsPath);
|
||||
var logger = new TerminalLogger();
|
||||
@@ -115,6 +150,16 @@ namespace PolyTrader.App.Avalonia
|
||||
new AccountingModule()
|
||||
};
|
||||
var disabled = new HashSet<string>(serverSettings.DisabledModules, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// D-2: ohne gültige Lizenz startet nur die Core-Shell (Terminal, Einstellungen) - alle
|
||||
// Trading-/Analyse-Module bleiben unregistriert. Kein Environment.Exit (Entscheidung
|
||||
// 31.07.2026): der Schlüssel lässt sich im Einstellungsfenster nachtragen.
|
||||
bool licensed = !enforceLicense || Licensing.LicenseGate.CheckUsable(dcOptions, serverSettings, logger);
|
||||
if (!licensed)
|
||||
{
|
||||
foreach (var m in allModules) disabled.Add(m.Name);
|
||||
}
|
||||
|
||||
var modules = allModules.Where(m => !disabled.Contains(m.Name)).ToList();
|
||||
|
||||
return Host.CreateDefaultBuilder()
|
||||
@@ -123,6 +168,7 @@ namespace PolyTrader.App.Avalonia
|
||||
config.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: false))
|
||||
.ConfigureServices((context, services) =>
|
||||
{
|
||||
services.AddSingleton(dcOptions);
|
||||
var databaseOptions = new DatabaseOptions
|
||||
{
|
||||
MySqlConnectionString = context.Configuration["Database:MySqlConnectionString"] ?? string.Empty
|
||||
@@ -147,7 +193,22 @@ namespace PolyTrader.App.Avalonia
|
||||
services.AddSingleton<INotificationSink>(
|
||||
sp => new LogNotificationSink(sp.GetRequiredService<TerminalLogger>()));
|
||||
|
||||
services.AddSingleton(sp => new WatchdogHeartbeatService(sp.GetRequiredService<TerminalLogger>()));
|
||||
services.AddSingleton(sp =>
|
||||
{
|
||||
var watchdog = new WatchdogHeartbeatService(sp.GetRequiredService<TerminalLogger>());
|
||||
|
||||
// D-1: erster "checks"-Eintrag - beweist die Mechanik mit einem real
|
||||
// verfügbaren Signal. db/market_feed folgen, sobald die jeweiligen Dienste
|
||||
// ein Gesundheitssignal nach außen geben (siehe
|
||||
// docs/umsetzungsplaene/UMSETZUNGSPLAN-Deploymentcenter-Integration.md, D-1).
|
||||
var vpn = sp.GetRequiredService<MullvadVpnService>();
|
||||
var serverSettings = sp.GetRequiredService<ServerSettings>();
|
||||
watchdog.RegisterCheck("vpn", () => serverSettings.VpnEnabled
|
||||
? (vpn.IsConnected, vpn.IsConnected ? null : "Mullvad VPN nicht verbunden")
|
||||
: (true, null)); // VPN bewusst aus -> kein Fehlalarm
|
||||
|
||||
return watchdog;
|
||||
});
|
||||
|
||||
// MUSS vor den Modulen registriert werden: hydriert die Accounts, bevor die
|
||||
// Trading-Dienste anlaufen (Reihenfolge = Registrierungsreihenfolge).
|
||||
@@ -159,9 +220,15 @@ namespace PolyTrader.App.Avalonia
|
||||
module.RegisterServices(services, context.Configuration);
|
||||
}
|
||||
|
||||
// D-3: Fehler-Reporting - rein ereignisgesteuert (AppDomain.UnhandledException,
|
||||
// TaskScheduler.UnobservedTaskException, TerminalLogger-Fehler), kein eigener Takt.
|
||||
services.AddSingleton(sp => new DeploymentcenterErrorReporter(
|
||||
sp.GetRequiredService<TerminalLogger>(), sp.GetRequiredService<DeploymentcenterOptions>()));
|
||||
|
||||
services.AddHostedService<MarketSyncService>();
|
||||
services.AddHostedService(sp => sp.GetRequiredService<MullvadVpnService>());
|
||||
services.AddHostedService(sp => sp.GetRequiredService<WatchdogHeartbeatService>());
|
||||
services.AddHostedService(sp => sp.GetRequiredService<DeploymentcenterErrorReporter>());
|
||||
|
||||
// Shell-Zustand (Fensterverwaltung). Auch im kopflosen Betrieb harmlos.
|
||||
services.AddSingleton<AvaloniaUiHost>();
|
||||
@@ -189,7 +256,7 @@ namespace PolyTrader.App.Avalonia
|
||||
ViewIcons.AssignDefaults(uiHost);
|
||||
|
||||
int failures = 0;
|
||||
Console.WriteLine("=== Smoke-UI: Fenster-Konstruktion (Avalonia) ===");
|
||||
Console.WriteLine("=== Smoke-UI: Seiten-Konstruktion (Avalonia) ===");
|
||||
foreach (var view in uiHost.Views)
|
||||
{
|
||||
try
|
||||
@@ -204,15 +271,17 @@ namespace PolyTrader.App.Avalonia
|
||||
}
|
||||
}
|
||||
|
||||
// Die Startseite steckt seit der Einfenster-Shell in der Schleife oben (core.start);
|
||||
// separat geprueft wird nur noch das Shell-Fenster selbst.
|
||||
try
|
||||
{
|
||||
_ = new Views.LauncherWindow(uiHost, host.Services);
|
||||
Console.WriteLine("[OK] LauncherWindow konstruiert");
|
||||
_ = new Views.ShellWindow(uiHost, host.Services);
|
||||
Console.WriteLine("[OK] ShellWindow konstruiert");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failures++;
|
||||
Console.WriteLine($"[FEHLER] LauncherWindow: {ex.GetType().Name}: {ex.Message}");
|
||||
Console.WriteLine($"[FEHLER] ShellWindow: {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
|
||||
try
|
||||
@@ -255,9 +324,12 @@ namespace PolyTrader.App.Avalonia
|
||||
// Schluessel faellt sonst nicht auf - ThemeManager.Brush liefert dann still Grau.
|
||||
string[] themeKeys =
|
||||
{
|
||||
"AppSurfaceBrush", "AppSurfaceAltBrush", "AppCardBrush", "AppBorderBrush",
|
||||
"AppMutedTextBrush", "AppCaptionTextBrush", "AppReadOnlyTextBrush",
|
||||
"AppBgBrush", "AppSurfaceBrush", "AppSurfaceAltBrush", "AppCardBrush",
|
||||
"AppBorderBrush", "AppDividerBrush",
|
||||
"AppTextBrush", "AppMutedTextBrush", "AppCaptionTextBrush", "AppReadOnlyTextBrush",
|
||||
"AppAccentBrush", "AppAccentAltBrush", "AppAccentStrongBrush", "AppAccentSoftBrush",
|
||||
"AppPositiveBrush", "AppNegativeBrush", "AppWarningBrush",
|
||||
"AppNotifyWarnBrush", "AppNotifyErrorBrush",
|
||||
"AppTradeLossBrush", "AppTradeSmallWinBrush", "AppTradeBigWinBrush",
|
||||
"AppToggleActiveBrush", "AppToggleSellOnlyBrush", "AppToggleInactiveBrush",
|
||||
"AppChatUserBrush", "AppChatAgentBrush"
|
||||
@@ -298,17 +370,23 @@ namespace PolyTrader.App.Avalonia
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Lädt den Master-Key (Umgebungsvariable, sonst gitignorierte <c>master.key</c>) und aktiviert
|
||||
/// die at-rest-Verschlüsselung. Muss VOR jeder Credential-Entschlüsselung laufen.
|
||||
/// Lädt den Master-Key (Umgebungsvariable, sonst gitignorierte <c>master.key</c>, sonst wird
|
||||
/// einer erzeugt – D-5/D-10) und aktiviert die at-rest-Verschlüsselung. Muss VOR jeder
|
||||
/// Credential-Entschlüsselung laufen.
|
||||
/// </summary>
|
||||
private static void ConfigureSecretProtection(TerminalLogger logger)
|
||||
{
|
||||
string? masterKey = Environment.GetEnvironmentVariable("POLYTRADER_MASTER_KEY");
|
||||
if (string.IsNullOrWhiteSpace(masterKey))
|
||||
{
|
||||
string keyFile = Path.Combine(AppContext.BaseDirectory, "master.key");
|
||||
if (File.Exists(keyFile)) masterKey = File.ReadAllText(keyFile).Trim();
|
||||
}
|
||||
string keyFile = Path.Combine(AppContext.BaseDirectory, "master.key");
|
||||
string? masterKey = MasterKeyResolver.Resolve(
|
||||
keyFile,
|
||||
() => Environment.GetEnvironmentVariable("POLYTRADER_MASTER_KEY"),
|
||||
onGenerated: _ => logger.Warning(
|
||||
"🔐 Kein Master-Key gefunden – ein neuer wurde erzeugt und in \"master.key\" " +
|
||||
"gespeichert (App-Ordner, gitignored). WICHTIG: Diese Datei SOFORT separat sichern " +
|
||||
"(Passwort-Manager/Offline-Backup) – ihr Verlust macht jedes ab jetzt verschlüsselte " +
|
||||
"Secret unwiederbringlich unlesbar."),
|
||||
onWarning: msg => logger.Warning($"⚠️ {msg}"));
|
||||
|
||||
SecretProtection.Configure(masterKey);
|
||||
|
||||
if (SecretProtection.IsConfigured)
|
||||
|
||||
Reference in New Issue
Block a user