Drittes Fenster: KPI-Kacheln, drei Diagramme, Tradehistorie mit Suche/Filter und Modul-Aktivierung. Aufbau nach docs/UI-SPEZIFIKATION-WinForms.md. - Diagramme sind jetzt echte Steuerelemente (LiveCharts2) statt nach Bitmap gerenderter ScottPlot-Bilder: interaktiv (Tooltips/Zoom) und ohne System.Drawing. Die Auswertung kommt unveraendert aus TradeAnalytics - der Chart-Wechsel beruehrte keine Fachlogik. Das war der Zweck der bestehenden Trennung und hat sich hier ausgezahlt. - ModuleActivationInfo vom UI-Typ in den Core verschoben (PolyTrader.Core.Modularity): Aussage ueber die Modularitaet, keine Darstellungsfrage - beide Shells brauchen sie. - Modul-Tab listet auch NICHT geladene Module, sonst liessen sie sich nie reaktivieren. Blockierte Module haben eine deaktivierte Schaltflaeche statt eines Hinweisdialogs; der Grund steht ohnehin in der Spalte 'Hinweis'. WICHTIG - Avalonia 12 -> 11.3.19 zurueckgenommen: Der erste Wurf zog per Version='*' Avalonia 12.1.1. LiveCharts2 2.0.5 (die aktuellste Version) ist gegen Avalonia 11 gebaut und bricht dort zur Laufzeit: MissingFieldException 'Avalonia.Input.Gestures.PinchEvent'. Avalonia 12 ist dem Chart-Oekosystem voraus. Jetzt 11.3.19 (DataGrid folgt eigener Reihe: 11.3.13). Erst wieder anheben, wenn LiveCharts2 Avalonia 12 unterstuetzt. Verifiziert: Solution baut, 442 Tests gruen, --smoke-ui gruen (alle 4 Fenster), App laeuft real mit allen Trading-Diensten, publisht fuer linux-x64. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
566 lines
23 KiB
C#
566 lines
23 KiB
C#
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 PolyTrader.Core.Modularity.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;
|
||
}
|
||
}
|