using IBKRTrader.Core.AI; using IBKRTrader.Core.Budget; using IBKRTrader.Core.Configuration; using IBKRTrader.Core.DependencyInjection; using IBKRTrader.Core.IBKR; using IBKRTrader.Core.Logging; using IBKRTrader.Core.Modularity; using IBKRTrader.Core.Security; using IBKRTrader.Core.Settings; using IBKRTrader.Core.Trading; using IBKRTrader.Core.Workers; using IBKRTrader.Core.Workers.BuiltIn; using IBKRTrader.Modules.CongressTrading; using IBKRTrader.UI; using IBKRTrader.UI.Views; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace IBKRTrader; internal static class Program { public static IHost? AppHost { get; private set; } [STAThread] static void Main(string[] args) { // Headless-Smoke-Test der UI (konstruiert jede View + Launcher, ohne Message-Loop). if (args.Length > 0 && string.Equals(args[0], "--smoke-ui", StringComparison.OrdinalIgnoreCase)) { Environment.ExitCode = RunSmokeUi(); return; } // Diagnose: gibt die MySQL/MariaDB-Serverversion aus (für das EF-ServerVersion-Pinning). if (args.Length > 0 && string.Equals(args[0], "--db-version", StringComparison.OrdinalIgnoreCase)) { RunDbVersion(); return; } ApplicationConfiguration.Initialize(); var modules = new List { new CongressTradingModule() }; AppHost = Host.CreateDefaultBuilder() .UseContentRoot(AppContext.BaseDirectory) .ConfigureAppConfiguration((_, config) => config.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: false)) .ConfigureServices((context, services) => { RegisterCoreServices(services, context.Configuration); foreach (var module in modules) { services.AddSingleton(module); module.RegisterServices(services, context.Configuration); } services.AddSingleton(); services.AddSingleton(sp => sp.GetRequiredService()); services.AddSingleton(); }) .Build(); // Sicherheit: Master-Key laden (VOR jeder Entschlüsselung) und DB-TLS prüfen. var startupLog = AppHost.Services.GetRequiredService(); ConfigureSecretProtection(startupLog); WarnIfDbTlsNotEnforced(AppHost.Services, startupLog); // Zirkuläre Abhängigkeit auflösen: WebApiService braucht die Engine-Referenz (vor dem Start). AppHost.Services.GetRequiredService() .SetEngine(AppHost.Services.GetRequiredService()); // Host starten → alle Worker/Services (IHostedService) laufen an. AppHost.Start(); // Views registrieren (Core + Module), dann Launcher starten. var uiHost = AppHost.Services.GetRequiredService(); RegisterCoreViews(uiHost, AppHost.Services); foreach (var module in modules) module.RegisterUi(uiHost, AppHost.Services); AssignViewIcons(uiHost); Application.Run(AppHost.Services.GetRequiredService()); AppHost.StopAsync().GetAwaiter().GetResult(); } /// Registriert alle Core-Services im DI-Container. private static void RegisterCoreServices(IServiceCollection services, IConfiguration configuration) { // EF-Core-Persistenz (Connection aus appsettings.Local.json). services.AddCorePersistence(new DatabaseOptions { MySqlConnectionString = configuration["Database:MySqlConnectionString"] ?? string.Empty }); // Settings zuerst laden (eine Quelle, als Singleton weitergereicht). var settingsService = new SettingsService(); settingsService.Load(); services.AddSingleton(settingsService); services.AddSingleton(); services.AddSingleton(); // core_settings via EF services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); // Trading-Kern services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); // Sicherer Standard-Broker: handelt nicht, bis der echte IBKR-Adapter verifiziert ist. services.AddSingleton(); // Core-Worker/Services services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); // Als IWorker registrieren → die WorkerEngine erhält alle über IEnumerable (nur UI/Registry). services.AddSingleton(sp => sp.GetRequiredService()); services.AddSingleton(sp => sp.GetRequiredService()); services.AddSingleton(sp => sp.GetRequiredService()); services.AddSingleton(sp => sp.GetRequiredService()); services.AddSingleton(sp => sp.GetRequiredService()); // Lebenszyklus über den Generic Host (jeder Worker ist ein IHostedService). services.AddHostedService(sp => sp.GetRequiredService()); services.AddHostedService(sp => sp.GetRequiredService()); services.AddHostedService(sp => sp.GetRequiredService()); services.AddHostedService(sp => sp.GetRequiredService()); services.AddHostedService(sp => sp.GetRequiredService()); services.AddSingleton(); } /// /// Weist den registrierten Views ihr Button-/Menü-Icon aus den App-Ressourcen zu (über die stabile /// View-ID). Icons stammen aus PolytraderSharp; nicht passende können später ausgetauscht werden. /// Bereits gesetzte Icons bleiben erhalten. /// private static void AssignViewIcons(IModuleUiHost uiHost) { var map = new Dictionary { ["core.dashboard"] = Properties.Resources.dashboard, ["core.workers"] = Properties.Resources.system_time, ["core.logs"] = Properties.Resources.error_log, ["core.settings"] = Properties.Resources.setting_tools, ["congresstrading.main"] = Properties.Resources.cross_reference, }; foreach (var view in uiHost.Views) if (view.Icon is null && map.TryGetValue(view.Id, out var img)) view.Icon = img; } /// Registriert die Core-Views (Logs, Settings, Workers) bei der Shell. private static void RegisterCoreViews(IModuleUiHost uiHost, IServiceProvider sp) { uiHost.RegisterView(new ModuleView { Id = "core.dashboard", Title = "Dashboard", Group = "Core", Order = 5, CreateForm = () => new DashboardView( sp.GetRequiredService(), sp.GetRequiredService(), sp.GetServices(), sp.GetRequiredService(), sp.GetServices()) }); uiHost.RegisterView(new ModuleView { Id = "core.workers", Title = "Workers / Services", Group = "Core", Order = 10, CreateForm = () => new WorkersView(sp.GetRequiredService()) }); uiHost.RegisterView(new ModuleView { Id = "core.logs", Title = "Logs", Group = "Core", Order = 20, CreateForm = () => new LogsView(sp.GetRequiredService()) }); uiHost.RegisterView(new ModuleView { Id = "core.settings", Title = "Settings", Group = "Core", Order = 30, CreateForm = () => new SettingsView(sp.GetRequiredService()) }); } /// /// Lädt den Master-Key (env IBKRTRADER_MASTER_KEY, sonst gitignorierte master.key) und aktiviert die /// at-rest-Verschlüsselung. Ohne Key läuft die App mit Klartext – mit deutlicher Warnung. /// private static void ConfigureSecretProtection(LoggingService logger) { var masterKey = Environment.GetEnvironmentVariable("IBKRTRADER_MASTER_KEY"); if (string.IsNullOrWhiteSpace(masterKey)) { var keyFile = Path.Combine(AppContext.BaseDirectory, "master.key"); if (File.Exists(keyFile)) masterKey = File.ReadAllText(keyFile).Trim(); } SecretProtection.Configure(masterKey); if (SecretProtection.IsConfigured) logger.Info("Core", "🔐 Secret-Verschlüsselung aktiv – sensible Daten werden at-rest verschlüsselt (AES-256-GCM)."); else logger.Warn("Core", "⚠️ SICHERHEIT: Kein IBKRTRADER_MASTER_KEY gesetzt – sensible Daten würden UNVERSCHLÜSSELT gespeichert. " + "Master-Key setzen (env IBKRTRADER_MASTER_KEY oder Datei master.key)."); } /// Warnt, wenn der DB-Connection-String keine TLS-Option (SslMode) enthält. Der String wird NICHT geloggt. private static void WarnIfDbTlsNotEnforced(IServiceProvider services, LoggingService logger) { try { var conn = services.GetService()?["Database:MySqlConnectionString"] ?? string.Empty; if (string.IsNullOrEmpty(conn)) return; if (conn.IndexOf("sslmode", StringComparison.OrdinalIgnoreCase) < 0) logger.Warn("Core", "⚠️ SICHERHEIT: DB-Verbindung ohne SslMode – Transportverschlüsselung nicht erzwungen. " + "Im Connection-String 'SslMode=Required' setzen."); } catch { /* best-effort, darf den Start nie stören */ } } /// Diagnose: öffnet die DB (aus settings.json) und gibt die Serverversion aus. Kein UI. private static void RunDbVersion() { var settings = new SettingsService(); settings.Load(); var conn = settings.Settings.Database.BuildConnectionString(); try { using var c = new MySqlConnector.MySqlConnection(conn); c.Open(); Console.WriteLine($"ServerVersion: {c.ServerVersion}"); } catch (Exception ex) { Console.WriteLine($"FEHLER: {ex.GetType().Name}: {ex.Message}"); } } /// /// Headless-Smoke-Test: baut den Host, registriert Views + Modul-UI und konstruiert jede /// registrierte View sowie den Launcher – ohne Message-Loop. Gibt die Fehleranzahl zurück. /// private static int RunSmokeUi() { ApplicationConfiguration.Initialize(); var modules = new List { new CongressTradingModule() }; using var host = Host.CreateDefaultBuilder() .UseContentRoot(AppContext.BaseDirectory) .ConfigureAppConfiguration((_, config) => config.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: false)) .ConfigureServices((context, services) => { RegisterCoreServices(services, context.Configuration); foreach (var module in modules) { services.AddSingleton(module); module.RegisterServices(services, context.Configuration); } services.AddSingleton(); services.AddSingleton(s => s.GetRequiredService()); services.AddSingleton(); }) .Build(); host.Services.GetRequiredService() .SetEngine(host.Services.GetRequiredService()); var uiHost = host.Services.GetRequiredService(); RegisterCoreViews(uiHost, host.Services); foreach (var module in modules) module.RegisterUi(uiHost, host.Services); var 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; } }