From 6f478e876481f455316dba4d6c6fc59ba769ba01 Mon Sep 17 00:00:00 2001 From: Richard Date: Mon, 6 Jul 2026 09:42:56 +0200 Subject: [PATCH] Fix: appsettings.Local.json im Host laden + headless UI-Smoke-Test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - KRITISCH: Host.CreateDefaultBuilder lädt appsettings.Local.json nicht (nur appsettings.json + appsettings.{Environment}.json). Dadurch war Database:MySqlConnectionString zur Laufzeit leer -> Pomelo fiel auf localhost:3306 zurück ("Connect Timeout expired"), die GUI-App hätte am Server nie mit MySQL verbunden. Fix: appsettings.Local.json explizit per ConfigureAppConfiguration ergänzen (in Main und im Smoke-Test). - Neuer CLI-Modus --smoke-ui: baut einen minimalen Host, hydriert den State aus MySQL und konstruiert jede registrierte View + den Launcher (ohne Message-Loop / Trading-Services). Deckte den obigen Bug auf. - Verifiziert: 3 Accounts / 32 Trader hydriert; alle 3 Modul-Views + Launcher konstruieren fehlerfrei mit echten Daten. Co-Authored-By: Claude Opus 4.8 --- Program.cs | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/Program.cs b/Program.cs index ea433fa..8309d27 100644 --- a/Program.cs +++ b/Program.cs @@ -40,6 +40,14 @@ internal static class Program return; } + // Headless-Smoke-Test der UI: konstruiert jede View + den Launcher (ohne Message-Loop + // und ohne Trading-/WSS-Services). Fängt Laufzeit-Konstruktionsfehler ab. Kein Fenster. + if (args.Length > 0 && string.Equals(args[0], "--smoke-ui", StringComparison.OrdinalIgnoreCase)) + { + Environment.ExitCode = RunSmokeUi(); + return; + } + ApplicationConfiguration.Initialize(); var modules = new System.Collections.Generic.List @@ -47,7 +55,13 @@ internal static class Program new CopyTradingModule() }; - AppHost = Host.CreateDefaultBuilder().ConfigureServices(delegate(HostBuilderContext context, IServiceCollection services) + AppHost = Host.CreateDefaultBuilder() + // Host.CreateDefaultBuilder lädt appsettings.Local.json NICHT (nur appsettings.json + // + appsettings.{Environment}.json). Die gitignorierte Local-Datei hält aber die + // MySQL-Connection – daher hier explizit ergänzen, sonst bleibt sie leer. + .ConfigureAppConfiguration((context, config) => + config.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: false)) + .ConfigureServices(delegate(HostBuilderContext context, IServiceCollection services) { var databaseOptions = new DatabaseOptions { @@ -237,4 +251,87 @@ internal static class Program Services.ConfigMigrator.VerifyMySql(mySql); } + + /// + /// Headless-Smoke-Test: baut einen minimalen Host (Persistenz + State + Modul-Registrierung + /// + Shell/Launcher), hydriert den State aus MySQL und konstruiert jede registrierte View + /// sowie den Launcher – ohne Message-Loop und ohne Trading-/WSS-Services zu starten. + /// Gibt 0 zurück, wenn alles fehlerfrei konstruiert, sonst die Fehleranzahl. + /// + private static int RunSmokeUi() + { + ApplicationConfiguration.Initialize(); + + var modules = new System.Collections.Generic.List { new CopyTradingModule() }; + + using var host = Host.CreateDefaultBuilder() + .ConfigureAppConfiguration((context, config) => + config.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: false)) + .ConfigureServices((context, services) => + { + var databaseOptions = new DatabaseOptions + { + MySqlConnectionString = context.Configuration["Database:MySqlConnectionString"] ?? string.Empty + }; + services.Configure(context.Configuration.GetSection(DatabaseOptions.SectionName)); + services.AddCorePersistence(databaseOptions); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + foreach (var module in modules) + { + services.AddSingleton(module); + module.RegisterServices(services, context.Configuration); + } + + services.AddSingleton(); + services.AddTransient(); + }).Build(); + + // State aus MySQL laden (Accounts/Trader/Settings), ohne die BackgroundServices zu starten. + try + { + host.Services.GetRequiredService() + .StartAsync(default).GetAwaiter().GetResult(); + } + catch (Exception ex) + { + Console.WriteLine($"[WARN] Hydration übersprungen: {ex.Message}"); + } + + var uiHost = host.Services.GetRequiredService(); + foreach (var module in modules) + module.RegisterUi(uiHost, host.Services); + + int failures = 0; + Console.WriteLine("=== Smoke-UI: View-Konstruktion ==="); + foreach (var view in uiHost.Views) + { + try + { + using var form = view.CreateForm(); + Console.WriteLine($"[OK] {view.Id} ({view.Title})"); + } + catch (Exception ex) + { + failures++; + Console.WriteLine($"[FEHLER] {view.Id}: {ex.GetType().Name}: {ex.Message}"); + } + } + + try + { + using var launcher = host.Services.GetRequiredService(); + Console.WriteLine("[OK] LauncherForm konstruiert"); + } + catch (Exception ex) + { + failures++; + Console.WriteLine($"[FEHLER] LauncherForm: {ex.GetType().Name}: {ex.Message}"); + } + + Console.WriteLine(failures == 0 ? "=== Smoke-UI OK ===" : $"=== Smoke-UI: {failures} Fehler ==="); + return failures; + } }