Files
PolyTraderSharp/Program.cs
T
RichardandClaude Opus 5 cd59e5c0a5 Avalonia-Grundgeruest: plattformneutrale App laeuft (Shell + erstes Fenster)
Neues Projekt src/PolyTrader.App.Avalonia (net10.0, Avalonia 12.1.1, LiveCharts2 2.0.5)
- laeuft unter Windows und Linux aus derselben Quelle.

Enthalten:
- Program.cs mit bewusst getrenntem Aufbau: BuildHost() stellt Persistenz, Dienste und
  Module ohne jeden UI-Bezug zusammen, erst Main haengt Avalonia daran. Damit ist der
  kopflose Linux-Betrieb (--headless, Stufe L2) ohne Umbau erreichbar - der Schalter ist
  bereits drin.
- AvaloniaUiHost als IModuleUiHost: gleiche Semantik wie die WinForms-Shell (ein Fenster
  je View, offene nach vorn holen, alles maximiert).
- Fenster-Menueleiste vollstaendig DEKLARATIV (Controls/WindowMenuBar.axaml + ItemsSource
  auf WindowMenuModel.Entries). Loest die alte Fassung ab, die menu.Items zur Laufzeit
  leerte und neu befuellte - genau der Punkt, den die neue Layout-Regel verbietet.
- ViewIcons fuer Avalonia: dieselben Schluessel und dieselben PNGs wie zuvor, Core und
  Module bleiben unveraendert.
- LauncherWindow, JobsWindow, ShutdownConfirmWindow (inkl. der 10-Sekunden-Sperre).
- --smoke-ui als Nachfolger der WinForms-Konstruktionspruefung; startet den Host bewusst
  NICHT, damit ein reiner UI-Test nicht die Trading-Engine gegen echte Endpunkte anwirft.

Dabei aufgeraeumt:
- JobManager.Jobs: BindingList -> ObservableCollection. BindingList implementiert kein
  INotifyCollectionChanged; neu registrierte Jobs waeren in Avalonia unsichtbar geblieben.
- StartupHydrationService aufgeteilt in CoreStateHydrationService (Core: Accounts +
  Demo-Positionen) und CopyTradingHydrationService (Modul: Settings + Trader). Behebt einen
  latenten Fehler: bei deaktiviertem Copytrading-Modul waeren die Accounts gar nicht mehr
  hydriert worden, obwohl sie zum Core gehoeren.

Verifiziert: Solution baut, 442 Tests gruen, --smoke-ui gruen, die App laeuft real mit
Fenster und allen Trading-Diensten (Market-Sync, Master-Trader-Analyse, RF-Scanner),
und publisht fuer linux-x64.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:55:05 +02:00

566 lines
23 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Channels;
using PolyTrader.Core.Persistence;
using PolyTrader.Core.Security;
using System.Windows.Forms;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using PolyTrader.Core.Configuration;
using PolyTrader.Core.DependencyInjection;
using PolyTrader.Core.Modularity;
using PolyTrader.Modules.CopyTrading;
using PolyTrader.Modules.CopyTrading.Persistence;
using PolyTrader.Modules.ResolutionFarming;
using PolyTrader.Modules.Supervisor;
using PolyTrader.Modules.Accounting;
using PolyTraderSharp.Models;
using PolyTraderSharp.Services;
namespace PolyTraderSharp;
internal static class Program
{
public static IHost? AppHost { get; private set; }
/// <summary>Pfad der (gitignorierten) Server-Settings-Datei Quelle u.a. für die Modul-Aktivierung.</summary>
private const string ServerSettingsPath = "server_settings.xml";
[STAThread]
private static void Main(string[] args)
{
// Config-Migration aus mongoexport-JSON (Ordner, Default "MongoDB") -> MySQL.
if (args.Length > 0 && string.Equals(args[0], "--migrate-json", StringComparison.OrdinalIgnoreCase))
{
var folder = args.Length > 1 ? args[1] : "MongoDB";
RunConfigMigrationFromJson(folder);
return;
}
// Readback-Verifikation der migrierten Config aus MySQL. Kein UI-Start.
if (args.Length > 0 && string.Equals(args[0], "--verify-mysql", StringComparison.OrdinalIgnoreCase))
{
RunVerifyMySql();
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;
}
// Diagnose: gibt die MySQL-Serverversion aus (für das ServerVersion-Pinning). Kein UI.
if (args.Length > 0 && string.Equals(args[0], "--db-version", StringComparison.OrdinalIgnoreCase))
{
RunDbVersion();
return;
}
ApplicationConfiguration.Initialize();
// Server-Settings früh laden (VOR der DI-Registrierung), damit deaktivierte Module gar nicht
// erst geladen werden. Dieselbe Instanz wird als Singleton weitergereicht (eine Quelle).
var serverSettings = PolyTraderSharp.Models.ServerSettings.Load(ServerSettingsPath);
// Früher Logger: wird unten als DI-Singleton weitergereicht, damit die Startmeldungen
// (Master-Key, Lizenz) später im Terminal-Fenster erscheinen (History-Replay).
var bootLog = new TerminalLogger();
// F1 (Sicherheit): Master-Key VOR jeder Entschlüsselung laden wird sowohl für die
// Account-Credentials als auch für den gespeicherten Lizenzschlüssel gebraucht.
ConfigureSecretProtection(bootLog);
// Alle bekannten Module; „modules" enthält nur die aktiven (nicht in DisabledModules).
var allModules = new System.Collections.Generic.List<IPolyTraderModule>
{
new CopyTradingModule(),
new ResolutionFarmingModule(),
new SupervisorModule(),
new AccountingModule()
};
var disabledModules = new System.Collections.Generic.HashSet<string>(
serverSettings.DisabledModules, StringComparer.OrdinalIgnoreCase);
// B.4: Lizenzprüfung beim Start. Bei ungültiger Lizenz zeigt der Gate einen Dialog und
// gibt false zurück → es werden KEINE Module registriert (nur die Core-Shell startet,
// damit Terminal und Einstellungen zum Eintragen einer Lizenz erreichbar bleiben).
// Bewusst KEIN Environment.Exit ein Trading-Bot darf nicht mitten im Lauf hart sterben.
bool licensed = PolyTraderSharp.Licensing.LicenseGate.RunStartupGate(serverSettings, bootLog, ServerSettingsPath);
var modules = licensed
? allModules.Where(m => !disabledModules.Contains(m.Name)).ToList()
: new System.Collections.Generic.List<IPolyTraderModule>();
AppHost = Host.CreateDefaultBuilder()
// Config immer neben der EXE suchen (nicht im Arbeitsverzeichnis), damit die App
// auch beim Start aus bin/ oder per Doppelklick ihre appsettings findet.
.UseContentRoot(AppContext.BaseDirectory)
// 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
{
MySqlConnectionString = context.Configuration["Database:MySqlConnectionString"] ?? string.Empty
};
services.Configure<DatabaseOptions>(context.Configuration.GetSection(DatabaseOptions.SectionName));
services.AddCorePersistence(databaseOptions);
services.AddSingleton(serverSettings);
services.AddSingleton<TradingState>();
services.AddSingleton(delegate(IServiceProvider sp)
{
TerminalLogger requiredService2 = sp.GetRequiredService<TerminalLogger>();
var httpHandler = new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(2), MaxConnectionsPerServer = 100 };
return new PolymarketApiService(requiredService2, new HttpClient(httpHandler)
{
DefaultRequestHeaders =
{
{ "User-Agent", "py_clob_client" },
{ "Accept", "*/*" }
}
});
});
services.AddSingleton(delegate(IServiceProvider sp)
{
TerminalLogger requiredService2 = sp.GetRequiredService<TerminalLogger>();
var httpHandler = new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(2), MaxConnectionsPerServer = 100 };
return new PolymarketClobClient(requiredService2, new HttpClient(httpHandler)
{
DefaultRequestHeaders =
{
{ "User-Agent", "py_clob_client" },
{ "Accept", "*/*" }
}
});
});
// Denselben früh erzeugten Logger als Singleton weiterreichen, damit die
// Master-Key-/Lizenz-Meldungen von oben im Terminal-Fenster auftauchen.
services.AddSingleton(bootLog);
services.AddSingleton<MullvadVpnService>();
services.AddSingleton<JobManager>();
// Benachrichtigungen laufen über eine neutrale Senke. Threema ist entfallen; bis RocketChat
// und Telegram angebunden sind, landen Meldungen im Terminal-Log (LogNotificationSink).
services.AddSingleton<PolyTrader.Core.Notifications.INotificationSink>(
sp => new PolyTrader.Core.Notifications.LogNotificationSink(sp.GetRequiredService<TerminalLogger>()));
// Watchdog-Heartbeat (Dead-Man's-Switch). Als Singleton registriert, damit das
// Settings-Fenster denselben Dienst für ReloadSettings/Test-Heartbeat nutzt.
services.AddSingleton(sp => new WatchdogHeartbeatService(sp.GetRequiredService<TerminalLogger>()));
// MUSS als erster HostedService laufen: hydriert den State, bevor die
// Trading-Services gegen einen leeren State anlaufen.
services.AddHostedService<CoreStateHydrationService>();
// Module registrieren ihre eigenen Services/Channels/State selbst.
foreach (var module in modules)
{
services.AddSingleton(module);
module.RegisterServices(services, context.Configuration);
}
services.AddHostedService<MarketSyncService>();
services.AddHostedService((IServiceProvider sp) => sp.GetRequiredService<MullvadVpnService>());
services.AddHostedService((IServiceProvider sp) => sp.GetRequiredService<WatchdogHeartbeatService>());
services.AddSingleton<PolyTraderSharp.Ui.ShellUiHost>();
services.AddTransient<PolyTraderSharp.Ui.LauncherForm>();
}).Build();
// (Master-Key wurde bereits VOR dem Host-Build geladen siehe ConfigureSecretProtection(bootLog) oben.)
// F5 (Sicherheit): warnen, wenn die (remote) DB-Verbindung keine TLS-Erzwingung hat.
WarnIfDbTlsNotEnforced(AppHost.Services);
try
{
// Trade-Nummerierung fortsetzen: höchste bestehende TradeId serverseitig lesen
// (M3: kein Full-Table-Load mehr über Find(_=>true).Max()). Nur wenn CopyTrading aktiv ist
// sonst sind dessen Services (CopyTradingState/…) gar nicht registriert.
if (modules.Any(m => m is CopyTradingModule))
{
var copyState = AppHost.Services.GetRequiredService<CopyTradingState>();
var tradeLog = AppHost.Services.GetRequiredService<ICopyTradeLogRepository>();
copyState.TotalCopyTrades = tradeLog.GetMaxTradeId();
}
}
catch (Exception ex)
{
// M3: NICHT still schlucken. Bliebe der Zähler bei 0, kollidierten neue TradeIds mit
// historischen (PK ist ValueGeneratedNever) → stiller Verlust aller Session-Trades.
var log = AppHost.Services.GetRequiredService<TerminalLogger>();
log.Error($"❌ KRITISCH: TradeId-Fortsetzung konnte nicht initialisiert werden: {ex.Message}. " +
"Trade-Nummerierung ist NICHT sicher neue Trades könnten mit bestehenden kollidieren. Bitte DB prüfen.");
}
AppHost.Start();
// F1: einmalige, idempotente Verschlüsselung evtl. vorhandener Klartext-Credentials in der DB.
ReencryptAccountCredentials(AppHost.Services);
// Shell-Views registrieren (Core-App-Views; Module folgen via module.RegisterUi).
var uiHost = AppHost.Services.GetRequiredService<PolyTraderSharp.Ui.ShellUiHost>();
var viewServices = AppHost.Services;
uiHost.RegisterView(new PolyTrader.Core.Modularity.ModuleView
{
Id = "core.terminal",
Title = "Terminal",
Group = "Core",
Order = 10,
CreateView = () =>
{
var view = new PolyTraderSharp.Ui.Views.TerminalView();
view.Initialize(viewServices.GetRequiredService<TerminalLogger>());
return view;
}
});
uiHost.RegisterView(new PolyTrader.Core.Modularity.ModuleView
{
Id = "core.settings",
Title = "Server Settings",
Group = "Core",
Order = 20,
CreateView = () =>
{
var view = new PolyTraderSharp.Ui.Views.SettingsView();
view.Initialize(
viewServices.GetRequiredService<MullvadVpnService>(),
viewServices.GetRequiredService<TerminalLogger>(),
viewServices.GetRequiredService<PolyTrader.Core.Persistence.IAccountRepository>(),
viewServices.GetRequiredService<TradingState>(),
viewServices.GetRequiredService<WatchdogHeartbeatService>());
return view;
}
});
uiHost.RegisterView(new PolyTrader.Core.Modularity.ModuleView
{
Id = "core.jobs",
Title = "Server Jobs",
Group = "Core",
Order = 30,
CreateView = () =>
{
var view = new PolyTraderSharp.Ui.Views.JobsView();
view.Initialize(viewServices.GetRequiredService<JobManager>());
return view;
}
});
uiHost.RegisterView(new PolyTrader.Core.Modularity.ModuleView
{
Id = "core.dashboard",
Title = "Dashboard",
Group = "Core",
Order = 5,
CreateView = () =>
{
var view = new PolyTraderSharp.Ui.Views.DashboardView();
var config = viewServices.GetRequiredService<Microsoft.Extensions.Configuration.IConfiguration>();
// Alle Module (auch deaktivierte) fürs Dashboard: läuft-Status = ist in dieser Session geladen.
var moduleInfos = allModules
.Select(m => new PolyTraderSharp.Ui.Views.ModuleActivationInfo(
m.Name, modules.Contains(m), m.GetActivationBlocker(config)))
.ToList();
view.Initialize(
viewServices.GetRequiredService<PolyTrader.Core.Persistence.ITradeLogRepository>(),
viewServices.GetRequiredService<TradingState>(),
moduleInfos,
ServerSettingsPath);
return view;
}
});
// Modul-UI registrieren (Module steuern ihre Views selbst bei).
foreach (var module in modules)
{
module.RegisterUi(uiHost, viewServices);
}
// Menü-Icons zentral zuweisen: Die App kennt alle Fenster-Ressourcen; Core und Module
// referenzieren sie nicht. So erscheint jedes Fenster mit Icon in der obersten Menüleiste.
AssignMenuIcons(uiHost);
var launcher = AppHost.Services.GetRequiredService<PolyTraderSharp.Ui.LauncherForm>();
Application.Run(launcher);
AppHost.StopAsync().GetAwaiter().GetResult();
}
/// <summary>
/// Weist den registrierten Views (Core + Module) ihren Standard-Symbolschlüssel zu.
/// Die Auflösung Schlüssel → Bildressource liegt in <see cref="PolyTraderSharp.Ui.ViewIcons"/>;
/// Core und Module kennen nur noch den Schlüssel (toolkit-neutral, siehe ModuleView.IconKey).
/// </summary>
private static void AssignMenuIcons(PolyTraderSharp.Ui.ShellUiHost uiHost)
=> PolyTraderSharp.Ui.ViewIcons.AssignDefaults(uiHost);
private static void RunConfigMigrationFromJson(string folder)
{
var config = new Microsoft.Extensions.Configuration.ConfigurationBuilder()
.SetBasePath(System.IO.Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true)
.AddJsonFile("appsettings.Local.json", optional: true)
.AddEnvironmentVariables()
.Build();
var mySql = config["Database:MySqlConnectionString"] ?? string.Empty;
if (string.IsNullOrWhiteSpace(mySql))
{
Console.WriteLine("FEHLER: Database:MySqlConnectionString fehlt (appsettings.Local.json).");
return;
}
if (!System.IO.Path.IsPathRooted(folder))
folder = System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), folder);
Services.ConfigMigrator.RunFromJson(folder, mySql);
}
private static void RunVerifyMySql()
{
var config = new Microsoft.Extensions.Configuration.ConfigurationBuilder()
.SetBasePath(System.IO.Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true)
.AddJsonFile("appsettings.Local.json", optional: true)
.AddEnvironmentVariables()
.Build();
var mySql = config["Database:MySqlConnectionString"] ?? string.Empty;
if (string.IsNullOrWhiteSpace(mySql))
{
Console.WriteLine("FEHLER: Database:MySqlConnectionString fehlt (appsettings.Local.json).");
return;
}
Services.ConfigMigrator.VerifyMySql(mySql);
}
private static void RunDbVersion()
{
var config = new Microsoft.Extensions.Configuration.ConfigurationBuilder()
.SetBasePath(System.IO.Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true)
.AddJsonFile("appsettings.Local.json", optional: true)
.AddEnvironmentVariables()
.Build();
var conn = config["Database:MySqlConnectionString"] ?? string.Empty;
if (string.IsNullOrWhiteSpace(conn))
{
Console.WriteLine("FEHLER: Database:MySqlConnectionString fehlt (appsettings.Local.json).");
return;
}
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}");
}
}
/// <summary>
/// F1: Lädt den Master-Key (POLYTRADER_MASTER_KEY, sonst gitignorierte master.key) und aktiviert die
/// at-rest-Verschlüsselung. Ohne Key läuft die App wie bisher mit Klartext mit deutlicher Warnung
/// (kein stiller Sicherheitsverlust). 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();
}
SecretProtection.Configure(masterKey);
if (SecretProtection.IsConfigured)
logger.Info("🔐 Secret-Verschlüsselung aktiv Account-Credentials werden at-rest verschlüsselt (AES-256-GCM).");
else
logger.Warning("⚠️ SICHERHEIT: Kein POLYTRADER_MASTER_KEY gesetzt Account-Credentials liegen UNVERSCHLÜSSELT in der DB. " +
"Master-Key setzen (env POLYTRADER_MASTER_KEY oder master.key), siehe docs/sicherheit.");
}
/// <summary>
/// F5: Warnt, wenn der DB-Connection-String keine TLS-Erzwingung (SslMode) enthält. Der String
/// selbst wird NICHT geloggt (enthält das Passwort) nur das Fehlen der TLS-Option.
/// </summary>
private static void WarnIfDbTlsNotEnforced(IServiceProvider services)
{
try
{
var config = services.GetService<Microsoft.Extensions.Configuration.IConfiguration>();
string conn = config?["Database:MySqlConnectionString"] ?? string.Empty;
if (string.IsNullOrEmpty(conn)) return;
if (conn.IndexOf("sslmode", StringComparison.OrdinalIgnoreCase) < 0)
{
services.GetRequiredService<TerminalLogger>().Warning(
"⚠️ SICHERHEIT: DB-Verbindung ohne SslMode Transportverschlüsselung zur (remote) MySQL nicht erzwungen. " +
"Im Connection-String 'SslMode=Required' (oder VerifyFull) setzen, siehe docs/sicherheit.");
}
}
catch { /* Warnung ist best-effort; darf den Start nie stören */ }
}
/// <summary>
/// F1: Verschlüsselt einmalig/idempotent evtl. vorhandene Klartext-Credentials in der DB (Alt-Bestand
/// wird beim Re-Save durch den EF-Converter verschlüsselt). Nur wenn ein Master-Key gesetzt ist.
/// Fehler blockieren den Start nicht (Log); künftige Saves verschlüsseln ohnehin.
/// </summary>
private static void ReencryptAccountCredentials(IServiceProvider services)
{
if (!SecretProtection.IsConfigured) return;
var logger = services.GetRequiredService<TerminalLogger>();
try
{
var accountRepo = services.GetRequiredService<IAccountRepository>();
var accounts = accountRepo.GetAll(); // Converter entschlüsselt/passt Klartext durch
foreach (var acc in accounts) accountRepo.Upsert(acc); // Re-Save → Converter verschlüsselt
logger.Info($"🔐 Account-Credentials at-rest gesichert ({accounts.Count} Account(s)).");
}
catch (Exception ex)
{
logger.Error($"Re-Encryption der Account-Credentials fehlgeschlagen: {ex.Message}. " +
"Stimmt der Master-Key mit den bereits verschlüsselten Daten überein?");
}
}
/// <summary>
/// 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.
/// </summary>
private static int RunSmokeUi()
{
ApplicationConfiguration.Initialize();
var modules = new System.Collections.Generic.List<IPolyTraderModule> { new CopyTradingModule(), new ResolutionFarmingModule(), new SupervisorModule(), new AccountingModule() };
using var host = Host.CreateDefaultBuilder()
.UseContentRoot(AppContext.BaseDirectory)
.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<DatabaseOptions>(context.Configuration.GetSection(DatabaseOptions.SectionName));
services.AddCorePersistence(databaseOptions);
services.AddSingleton<TerminalLogger>();
services.AddSingleton<TradingState>();
services.AddSingleton<CoreStateHydrationService>();
services.AddSingleton<CopyTradingHydrationService>();
foreach (var module in modules)
{
services.AddSingleton(module);
module.RegisterServices(services, context.Configuration);
}
services.AddSingleton<PolyTraderSharp.Ui.ShellUiHost>();
services.AddTransient<PolyTraderSharp.Ui.LauncherForm>();
}).Build();
// F1: Master-Key laden, damit die Hydration verschlüsselte Credentials entschlüsseln kann.
ConfigureSecretProtection(host.Services.GetRequiredService<TerminalLogger>());
// State aus MySQL laden (Accounts/Trader/Settings), ohne die BackgroundServices zu starten.
try
{
host.Services.GetRequiredService<CoreStateHydrationService>()
.StartAsync(default).GetAwaiter().GetResult();
host.Services.GetRequiredService<CopyTradingHydrationService>()
.StartAsync(default).GetAwaiter().GetResult();
}
catch (Exception ex)
{
Console.WriteLine($"[WARN] Hydration übersprungen: {ex.Message}");
}
var uiHost = host.Services.GetRequiredService<PolyTraderSharp.Ui.ShellUiHost>();
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 = (System.Windows.Forms.Form)view.CreateView();
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<PolyTraderSharp.Ui.LauncherForm>();
Console.WriteLine("[OK] LauncherForm konstruiert");
}
catch (Exception ex)
{
failures++;
Console.WriteLine($"[FEHLER] LauncherForm: {ex.GetType().Name}: {ex.Message}");
}
// Core-Dashboard direkt konstruieren (im Smoke nicht via uiHost registriert) - prueft das ScottPlot-Rendering headless.
try
{
using var dash = new PolyTraderSharp.Ui.Views.DashboardView();
dash.Initialize(host.Services.GetRequiredService<ITradeLogRepository>(), host.Services.GetRequiredService<TradingState>());
Console.WriteLine("[OK] core.dashboard konstruiert (inkl. Charts)");
}
catch (Exception ex)
{
failures++;
Console.WriteLine($"[FEHLER] core.dashboard: {ex.GetType().Name}: {ex.Message}");
}
// Core-Views mit hand-erstellten Grid-Spalten direkt konstruieren (nicht im uiHost registriert):
// faengt VS-Re-Serialisierungs-Regressionen (fallengelassene DataGridView-Spalten) ab.
foreach (var (id, make) in new (string, Func<System.Windows.Forms.Form>)[]
{
("core.jobs", () => new PolyTraderSharp.Ui.Views.JobsView()),
("core.terminal", () => new PolyTraderSharp.Ui.Views.TerminalView()),
("core.settings", () => new PolyTraderSharp.Ui.Views.SettingsView()),
})
{
try { using var f = make(); Console.WriteLine($"[OK] {id} konstruiert"); }
catch (Exception ex) { failures++; Console.WriteLine($"[FEHLER] {id}: {ex.GetType().Name}: {ex.Message}"); }
}
// B.4: Lizenzdialog headless konstruieren (kein Netz, keine Message-Loop) fängt
// Designer-/Konstruktionsregressionen ab.
try
{
var licClient = new LicenseLabrador.Client.LicenseClient(PolyTraderSharp.Licensing.LicenseGate.BuildConfig());
var dummy = new LicenseLabrador.Client.LicenseResult(
LicenseLabrador.Client.LicenseState.NoLicense, null, null, "Smoke-Test", string.Empty);
using (var licDlg = new PolyTraderSharp.Ui.LicenseDialog(licClient, string.Empty, dummy, startupContext: true))
using (var licDlgManage = new PolyTraderSharp.Ui.LicenseDialog(licClient, string.Empty, null, startupContext: false))
Console.WriteLine("[OK] license.dialog konstruiert (Start- und Verwalten-Modus)");
}
catch (Exception ex) { failures++; Console.WriteLine($"[FEHLER] license.dialog: {ex.GetType().Name}: {ex.Message}"); }
Console.WriteLine(failures == 0 ? "=== Smoke-UI OK ===" : $"=== Smoke-UI: {failures} Fehler ===");
return failures;
}
}