Files
PolyTraderSharp/Program.cs
T
RichardandClaude Opus 5 ca750a06f5 Watchdog-Heartbeat + LicenseLabrador-Lizenzpruefung eingebunden
Slices W-A/W-B/W-C und L-A aus UMSETZUNGSPLAN-Watchdog-LicenseLabrador-Integration.

Watchdog (Dead-Man's-Switch, externer Server):
- WatchdogHeartbeatService als BackgroundService + DI-Singleton; jeder Sendeversuch
  gekapselt, ein Ausfall des Watchdogs beeintraechtigt PolyTrader nie.
- Eigene Implementierung statt Test-Client des Fremdprojekts: TLS-Pruefung bleibt
  aktiv, http:// nur fuer localhost (Agent-Token nicht im Klartext ins Netz).
- Status aus dem App-Log abgeleitet (Error mit 5-Minuten-Sticky-Fenster, entprellt),
  Lifecycle-Events started/stopping.
- Konfiguration in ServerSettings; Agent-Token [Browsable(false)] mit maskierter
  Eingabe + Statusanzeige, bei gesetztem Master-Key verschluesselt (enc:v1:).

Lizenz (LicenseLabrador, Ed25519):
- LicenseGate.RunStartupGate prueft beim Start; bei ungueltiger Lizenz wird die
  Modulliste leer gebaut, sodass nur die Core-Shell (Terminal/Einstellungen)
  startet. Bewusst kein Environment.Exit - ein Trading-Bot darf nicht mitten im
  Lauf hart sterben. TamperSuspected gilt als nicht nutzbar.
- LicenseDialog (partial + .Designer.cs) fuer Start- und Verwalten-Modus, mit
  Hardware-ID zum Kopieren; Smoke-UI konstruiert beide Modi headless.
- Master-Key wird jetzt VOR dem Host-Build geladen, da auch der Lizenzschluessel
  entschluesselt werden muss; derselbe TerminalLogger wird als Singleton
  weitergereicht, damit die Startmeldungen im Terminal-Fenster erscheinen.

Der Lizenz-SDK-Client wird per Cross-Repo-Projektreferenz auf
..\..\LicenseLabrador eingebunden, damit SDK-Fixes ohne Kopie einfliessen.

484 Zeilen Tests fuer den Heartbeat; Suite gruen (438 Tests).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 09:04:14 +02:00

575 lines
24 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<ThreemaService>();
services.AddSingleton<JobManager>();
// 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<StartupHydrationService>();
// 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<ThreemaService>());
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,
CreateForm = () =>
{
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,
CreateForm = () =>
{
var view = new PolyTraderSharp.Ui.Views.SettingsView();
view.Initialize(
viewServices.GetRequiredService<ThreemaService>(),
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,
CreateForm = () =>
{
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,
CreateForm = () =>
{
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) ihr Menü-Icon aus den App-Ressourcen zu
/// gemappt über die stabile View-ID. Bereits gesetzte Icons bleiben erhalten.
/// </summary>
private static void AssignMenuIcons(PolyTraderSharp.Ui.ShellUiHost uiHost)
{
var map = new System.Collections.Generic.Dictionary<string, System.Drawing.Image>
{
["core.dashboard"] = Properties.Resources.dashboard,
["core.settings"] = Properties.Resources.setting_tools,
["core.terminal"] = Properties.Resources.error_log,
["core.jobs"] = Properties.Resources.system_time,
["accounting.main"] = Properties.Resources.coins_in_hand,
["copytrading.main"] = Properties.Resources.cross_reference,
["resolutionfarming.main"] = Properties.Resources.file_start_workflow,
["supervisor.main"] = Properties.Resources.emotion_batman,
};
foreach (var view in uiHost.Views)
if (view.Icon == null && map.TryGetValue(view.Id, out var img))
view.Icon = img;
}
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<StartupHydrationService>();
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<StartupHydrationService>()
.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 = 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<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;
}
}