diff --git a/IBKRTrader.Tests/Modules/CongressTradingModuleTests.cs b/IBKRTrader.Tests/Modules/CongressTradingModuleTests.cs index 903a17a..2821764 100644 --- a/IBKRTrader.Tests/Modules/CongressTradingModuleTests.cs +++ b/IBKRTrader.Tests/Modules/CongressTradingModuleTests.cs @@ -1,8 +1,13 @@ using FluentAssertions; +using IBKRTrader.Core.Database; +using IBKRTrader.Core.Logging; +using IBKRTrader.Core.Settings; +using IBKRTrader.Core.Workers; using IBKRTrader.Modules.CongressTrading; using IBKRTrader.Modules.CongressTrading.Database; using IBKRTrader.Modules.CongressTrading.Scraper; using IBKRTrader.Modules.CongressTrading.Workers; +using IBKRTrader.UI; using Microsoft.Extensions.DependencyInjection; namespace IBKRTrader.Tests.Modules; @@ -39,11 +44,22 @@ public class CongressTradingModuleTests } [Fact] - public void CreateWindow_ReturnsForm() + public void CreateWindow_ReturnsModuleForm() { - using var form = new CongressTradingModule().CreateWindow(new ServiceCollection().BuildServiceProvider()); + // Minimaler DI-Container mit allen von CreateWindow benötigten Abhängigkeiten. + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton>(_ => Array.Empty()); + services.AddSingleton(); + var module = new CongressTradingModule(); + module.RegisterServices(services); - form.Should().NotBeNull(); - form.Text.Should().Contain("CT"); + using var provider = services.BuildServiceProvider(); + using var form = module.CreateWindow(provider); + + form.Should().BeAssignableTo(); + ((ModuleFormBase)form).ModuleKey.Should().Be("CT"); } } diff --git a/IBKRTrader.Tests/UI/WindowManagerTests.cs b/IBKRTrader.Tests/UI/WindowManagerTests.cs new file mode 100644 index 0000000..b05a685 --- /dev/null +++ b/IBKRTrader.Tests/UI/WindowManagerTests.cs @@ -0,0 +1,100 @@ +using FluentAssertions; +using IBKRTrader.UI; + +namespace IBKRTrader.Tests.UI; + +[Trait("cat", "unit")] +public class WindowManagerTests +{ + /// + /// Testbare Variante: unterdrückt echte Show-/Focus-Aufrufe (kein Message-Loop), + /// zählt aber die Focus-Aufrufe. + /// + private sealed class TestWindowManager : WindowManager + { + public int FocusCount; + protected override void Present(Form form) { /* kein Show im Test */ } + protected override void Focus(Form form) => FocusCount++; + } + + [Fact] + public void OpenOrFocus_NewKey_InvokesFactory_AndTracks() + { + var wm = new TestWindowManager(); + var created = 0; + + using var form = wm.OpenOrFocus("CT", () => { created++; return new Form(); }); + + created.Should().Be(1); + wm.IsOpen("CT").Should().BeTrue(); + wm.OpenCount.Should().Be(1); + } + + [Fact] + public void OpenOrFocus_SameKey_FocusesExisting_DoesNotRecreate() + { + var wm = new TestWindowManager(); + var created = 0; + + using var first = wm.OpenOrFocus("CT", () => { created++; return new Form(); }); + var second = wm.OpenOrFocus("CT", () => { created++; return new Form(); }); + + created.Should().Be(1); + wm.FocusCount.Should().Be(1); + second.Should().BeSameAs(first); + wm.OpenCount.Should().Be(1); + } + + [Fact] + public void OpenOrFocus_DifferentKeys_TracksSeparately() + { + var wm = new TestWindowManager(); + + using var a = wm.OpenOrFocus("A", () => new Form()); + using var b = wm.OpenOrFocus("B", () => new Form()); + + wm.OpenCount.Should().Be(2); + wm.IsOpen("A").Should().BeTrue(); + wm.IsOpen("B").Should().BeTrue(); + } + + [Fact] + public void OpenOrFocus_AfterDisposed_RecreatesWindow() + { + var wm = new TestWindowManager(); + var created = 0; + + var first = wm.OpenOrFocus("CT", () => { created++; return new Form(); }); + first.Dispose(); + + wm.IsOpen("CT").Should().BeFalse(); + + using var second = wm.OpenOrFocus("CT", () => { created++; return new Form(); }); + + created.Should().Be(2); + second.Should().NotBeSameAs(first); + } + + [Fact] + public void CloseAll_RemovesAllWindows() + { + var wm = new TestWindowManager(); + wm.OpenOrFocus("A", () => new Form()); + wm.OpenOrFocus("B", () => new Form()); + + wm.CloseAll(); + + wm.OpenCount.Should().Be(0); + wm.IsOpen("A").Should().BeFalse(); + } + + [Fact] + public void OpenOrFocus_BlankKey_Throws() + { + var wm = new TestWindowManager(); + + var act = () => wm.OpenOrFocus(" ", () => new Form()); + + act.Should().Throw(); + } +} diff --git a/Form1.Designer.cs b/LauncherForm.Designer.cs similarity index 88% rename from Form1.Designer.cs rename to LauncherForm.Designer.cs index ae32242..a04fdb8 100644 --- a/Form1.Designer.cs +++ b/LauncherForm.Designer.cs @@ -1,6 +1,6 @@ namespace IBKRTrader { - partial class Form1 + partial class LauncherForm { /// /// Required designer variable. @@ -28,14 +28,15 @@ /// private void InitializeComponent() { - System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Form1)); + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(LauncherForm)); menuStrip1 = new MenuStrip(); toolStrip1 = new ToolStrip(); statusStrip1 = new StatusStrip(); tabControl1 = new TabControl(); tabPage_dash = new TabPage(); tabPage_sett = new TabPage(); - tabPage_mod_congresstrade = new TabPage(); + tabPage_modules = new TabPage(); + flp_modules = new FlowLayoutPanel(); btn_activateTrading = new ToolStripButton(); tabPage_history = new TabPage(); tabPage_workers = new TabPage(); @@ -89,7 +90,7 @@ tabControl1.Controls.Add(tabPage_sett); tabControl1.Controls.Add(tabPage_logs); tabControl1.Controls.Add(tabPage_workers); - tabControl1.Controls.Add(tabPage_mod_congresstrade); + tabControl1.Controls.Add(tabPage_modules); tabControl1.Location = new Point(12, 52); tabControl1.Name = "tabControl1"; tabControl1.SelectedIndex = 0; @@ -116,15 +117,27 @@ tabPage_sett.TabIndex = 1; tabPage_sett.Text = "Settings"; tabPage_sett.UseVisualStyleBackColor = true; - // - // tabPage_mod_congresstrade - // - tabPage_mod_congresstrade.Location = new Point(4, 34); - tabPage_mod_congresstrade.Name = "tabPage_mod_congresstrade"; - tabPage_mod_congresstrade.Size = new Size(2421, 1104); - tabPage_mod_congresstrade.TabIndex = 2; - tabPage_mod_congresstrade.Text = "Module: CongressTrading"; - tabPage_mod_congresstrade.UseVisualStyleBackColor = true; + // + // tabPage_modules + // + tabPage_modules.Controls.Add(flp_modules); + tabPage_modules.Location = new Point(4, 34); + tabPage_modules.Name = "tabPage_modules"; + tabPage_modules.Padding = new Padding(3); + tabPage_modules.Size = new Size(2421, 1104); + tabPage_modules.TabIndex = 2; + tabPage_modules.Text = "Module"; + tabPage_modules.UseVisualStyleBackColor = true; + // + // flp_modules + // + flp_modules.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + flp_modules.AutoScroll = true; + flp_modules.Location = new Point(6, 6); + flp_modules.Name = "flp_modules"; + flp_modules.Padding = new Padding(6); + flp_modules.Size = new Size(2409, 1092); + flp_modules.TabIndex = 0; // // btn_activateTrading // @@ -223,8 +236,8 @@ Controls.Add(toolStrip1); Controls.Add(menuStrip1); MainMenuStrip = menuStrip1; - Name = "Form1"; - Text = "IBKRTrader"; + Name = "LauncherForm"; + Text = "IBKRTrader — Launcher"; toolStrip1.ResumeLayout(false); toolStrip1.PerformLayout(); tabControl1.ResumeLayout(false); @@ -247,7 +260,8 @@ private TabControl tabControl1; private TabPage tabPage_dash; private TabPage tabPage_sett; - private TabPage tabPage_mod_congresstrade; + private TabPage tabPage_modules; + private FlowLayoutPanel flp_modules; private TabPage tabPage_history; private TabPage tabPage_workers; private DataGridView dgv_workerlist; diff --git a/Form1.cs b/LauncherForm.cs similarity index 55% rename from Form1.cs rename to LauncherForm.cs index 1786811..d3a0f2e 100644 --- a/Form1.cs +++ b/LauncherForm.cs @@ -4,27 +4,35 @@ using IBKRTrader.Core.Modules; using IBKRTrader.Core.Settings; using IBKRTrader.Core.Workers; using IBKRTrader.UI; -using Microsoft.Extensions.DependencyInjection; namespace IBKRTrader; -public partial class Form1 : Form +/// +/// Launcher – das Basis-Fenster der Anwendung. +/// Enthält die Core-Panels (Workers, Logs, Settings) und eine Modul-Liste, +/// aus der jedes Modul als eigenständiges Fenster geöffnet wird. +/// +public partial class LauncherForm : Form { - private readonly LoggingService _logger; - private readonly WorkerEngine _workerEngine; - private readonly SettingsService _settings; - private readonly CoreMigrations _migrations; - private readonly IBKRMigrations _ibkrMigrations; + private readonly LoggingService _logger; + private readonly WorkerEngine _workerEngine; + private readonly SettingsService _settings; + private readonly CoreMigrations _migrations; + private readonly IBKRMigrations _ibkrMigrations; + private readonly ModuleRegistry _modules; + private readonly WindowManager _windows; private readonly IServiceProvider _services; private LogPanelController? _logPanel; - public Form1( + public LauncherForm( LoggingService logger, WorkerEngine workerEngine, SettingsService settings, CoreMigrations migrations, IBKRMigrations ibkrMigrations, + ModuleRegistry modules, + WindowManager windows, IServiceProvider services) { InitializeComponent(); @@ -34,6 +42,8 @@ public partial class Form1 : Form _settings = settings; _migrations = migrations; _ibkrMigrations = ibkrMigrations; + _modules = modules; + _windows = windows; _services = services; } @@ -45,13 +55,15 @@ public partial class Form1 : Form InitializeLogPanel(); InitializeWorkerList(); InitializeSettingsGrid(); + InitializeModulePanel(); _ = StartupAsync(); } protected override void OnFormClosing(FormClosingEventArgs e) { base.OnFormClosing(e); - // Worker sauber beenden (feuert & vergisst – Form muss kurz warten) + // Offene Modul-Fenster schließen, dann Worker sauber beenden. + _windows.CloseAll(); _workerEngine.StopAllAsync().GetAwaiter().GetResult(); } @@ -79,6 +91,83 @@ public partial class Form1 : Form pg_settings.SelectedObject = _settings.Settings; } + /// Baut je Modul eine Karte mit „Fenster öffnen"-Button. + private void InitializeModulePanel() + { + flp_modules.FlowDirection = FlowDirection.LeftToRight; + flp_modules.WrapContents = true; + flp_modules.Controls.Clear(); + + foreach (var module in _modules.Modules) + flp_modules.Controls.Add(BuildModuleCard(module)); + } + + private Control BuildModuleCard(IModule module) + { + var card = new Panel + { + Width = 320, + Height = 150, + Margin = new Padding(8), + BorderStyle = BorderStyle.FixedSingle + }; + + var title = new Label + { + Text = $"{module.DisplayName} [{module.Key}]", + Font = new Font(Font.FontFamily, 11f, FontStyle.Bold), + Location = new Point(10, 10), + AutoSize = true + }; + + var desc = new Label + { + Text = module.Description, + Location = new Point(10, 42), + Size = new Size(300, 60), + AutoEllipsis = true + }; + + var open = new Button + { + Text = "Fenster öffnen", + Location = new Point(10, 108), + Width = 140, + Tag = module + }; + open.Click += (_, _) => OpenModuleWindow(module); + + var version = new Label + { + Text = $"v{module.Version}", + Location = new Point(240, 113), + AutoSize = true, + ForeColor = SystemColors.GrayText + }; + + card.Controls.Add(title); + card.Controls.Add(desc); + card.Controls.Add(open); + card.Controls.Add(version); + return card; + } + + /// Öffnet (oder fokussiert) das eigenständige Fenster eines Moduls. + private void OpenModuleWindow(IModule module) + { + try + { + _windows.OpenOrFocus(module.Key, () => module.CreateWindow(_services)); + } + catch (Exception ex) + { + _logger.Error(module.Key, $"{module.DisplayName}: Fenster konnte nicht geöffnet werden.", ex); + MessageBox.Show(this, + $"Fenster für '{module.DisplayName}' konnte nicht geöffnet werden:\n{ex.Message}", + "Modul-Fenster", MessageBoxButtons.OK, MessageBoxIcon.Warning); + } + } + // ─── Async Startup ──────────────────────────────────────────────────────── private async Task StartupAsync() @@ -99,8 +188,7 @@ public partial class Form1 : Form } // Modul-Migrationen: über die Registry iterieren (Core kennt kein Modul). - var registry = _services.GetRequiredService(); - foreach (var module in registry.Modules) + foreach (var module in _modules.Modules) { try { diff --git a/Form1.resx b/LauncherForm.resx similarity index 100% rename from Form1.resx rename to LauncherForm.resx diff --git a/Modules/CongressTrading/CongressTradingModule.cs b/Modules/CongressTrading/CongressTradingModule.cs index 4dac160..cbdb0ba 100644 --- a/Modules/CongressTrading/CongressTradingModule.cs +++ b/Modules/CongressTrading/CongressTradingModule.cs @@ -1,8 +1,9 @@ -using System.Drawing; +using IBKRTrader.Core.Logging; using IBKRTrader.Core.Modules; using IBKRTrader.Core.Workers; using IBKRTrader.Modules.CongressTrading.Database; using IBKRTrader.Modules.CongressTrading.Scraper; +using IBKRTrader.Modules.CongressTrading.UI; using IBKRTrader.Modules.CongressTrading.Workers; using Microsoft.Extensions.DependencyInjection; @@ -58,24 +59,10 @@ public sealed class CongressTradingModule : IModule provider.GetRequiredService() ]; - /// - /// Erzeugt das Modul-Fenster. Platzhalter – die vollständige UI folgt in Phase 2. - /// + /// Erzeugt das eigenständige Modul-Fenster. public Form CreateWindow(IServiceProvider provider) - { - var form = new Form - { - Text = $"{DisplayName} ({Key})", - Width = 800, - Height = 500, - StartPosition = FormStartPosition.CenterParent - }; - form.Controls.Add(new Label - { - Dock = DockStyle.Fill, - TextAlign = ContentAlignment.MiddleCenter, - Text = $"{DisplayName}\n\nModul-UI folgt in Phase 2." - }); - return form; - } + => new CongressTradingForm( + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService()); } diff --git a/Modules/CongressTrading/UI/CongressTradingForm.cs b/Modules/CongressTrading/UI/CongressTradingForm.cs new file mode 100644 index 0000000..908ec7f --- /dev/null +++ b/Modules/CongressTrading/UI/CongressTradingForm.cs @@ -0,0 +1,107 @@ +using IBKRTrader.Core.Logging; +using IBKRTrader.Core.Workers; +using IBKRTrader.Modules.CongressTrading.Database; +using IBKRTrader.UI; + +namespace IBKRTrader.Modules.CongressTrading.UI; + +/// +/// Eigenständiges Fenster des CongressTrading-Moduls. +/// Phase 2: Grundgerüst mit DB-Kennzahlen und manuellem Scrape-Trigger. +/// Die vollständige Trade-/Positions-Ansicht folgt in Phase 4. +/// +public sealed class CongressTradingForm : ModuleFormBase +{ + private const string ScrapeWorkerName = "CT-ScrapeWorker"; + + private readonly CongressRepository _repo; + private readonly WorkerEngine _engine; + private readonly LoggingService _logger; + + private readonly Label _lblTrades = new() { AutoSize = true, Location = new Point(20, 70) }; + private readonly Label _lblMembers = new() { AutoSize = true, Location = new Point(20, 100) }; + private readonly Button _btnRefresh = new() { Text = "Aktualisieren", Location = new Point(20, 140), Width = 140 }; + private readonly Button _btnScrape = new() { Text = "Scrape jetzt", Location = new Point(170, 140), Width = 140 }; + private readonly Label _lblStatus = new() { AutoSize = true, Location = new Point(20, 185), ForeColor = SystemColors.GrayText }; + + public CongressTradingForm(CongressRepository repo, WorkerEngine engine, LoggingService logger) + : base(CongressTradingModule.ModuleKey) + { + _repo = repo; + _engine = engine; + _logger = logger; + + Text = "Congress Trading [CT]"; + BuildLayout(); + } + + private void BuildLayout() + { + var title = new Label + { + Text = "Congress Trading", + Font = new Font(Font.FontFamily, 14f, FontStyle.Bold), + Location = new Point(18, 20), + AutoSize = true + }; + + var hint = new Label + { + Text = "Vollständige Trade- und Positions-Ansicht folgt in Phase 4.", + Location = new Point(20, 230), + AutoSize = true, + ForeColor = SystemColors.GrayText + }; + + _btnRefresh.Click += async (_, _) => await RefreshStatsAsync(); + _btnScrape.Click += async (_, _) => await TriggerScrapeAsync(); + + Controls.Add(title); + Controls.Add(_lblTrades); + Controls.Add(_lblMembers); + Controls.Add(_btnRefresh); + Controls.Add(_btnScrape); + Controls.Add(_lblStatus); + Controls.Add(hint); + } + + protected override async void OnShown(EventArgs e) + { + base.OnShown(e); + await RefreshStatsAsync(); + } + + private async Task RefreshStatsAsync() + { + try + { + var trades = await _repo.GetTradeCountAsync(); + var members = await _repo.GetMemberCountAsync(); + _lblTrades.Text = $"Trades in DB: {trades:N0}"; + _lblMembers.Text = $"Mitglieder in DB: {members:N0}"; + _lblStatus.Text = $"Aktualisiert: {DateTime.Now:HH:mm:ss}"; + } + catch (Exception ex) + { + _lblTrades.Text = "Trades in DB: n/v"; + _lblMembers.Text = "Mitglieder in DB: n/v"; + _lblStatus.Text = $"DB nicht erreichbar: {ex.Message}"; + _logger.Warn(CongressTradingModule.ModuleKey, $"Kennzahlen konnten nicht geladen werden: {ex.Message}"); + } + } + + private async Task TriggerScrapeAsync() + { + try + { + _lblStatus.Text = "Scrape angestoßen..."; + await _engine.TriggerWorkerAsync(ScrapeWorkerName); + _logger.Info(CongressTradingModule.ModuleKey, "Scrape-Worker manuell ausgelöst (aus Modul-Fenster)."); + } + catch (Exception ex) + { + _lblStatus.Text = $"Scrape fehlgeschlagen: {ex.Message}"; + _logger.Error(CongressTradingModule.ModuleKey, "Manueller Scrape-Trigger fehlgeschlagen.", ex); + } + } +} diff --git a/Program.cs b/Program.cs index adc3688..30dd263 100644 --- a/Program.cs +++ b/Program.cs @@ -10,6 +10,7 @@ using IBKRTrader.Core.Trading; using IBKRTrader.Core.Workers; using IBKRTrader.Core.Workers.BuiltIn; using IBKRTrader.Modules.CongressTrading; +using IBKRTrader.UI; using Microsoft.Extensions.DependencyInjection; namespace IBKRTrader; @@ -84,7 +85,10 @@ internal static class Program .ToArray()); services.AddSingleton(); - services.AddSingleton(); + + // UI: Launcher + Fenster-Verwaltung + services.AddSingleton(); + services.AddSingleton(); var provider = services.BuildServiceProvider(); @@ -93,6 +97,6 @@ internal static class Program var engine = provider.GetRequiredService(); webApi.SetEngine(engine); - Application.Run(provider.GetRequiredService()); + Application.Run(provider.GetRequiredService()); } } \ No newline at end of file diff --git a/UI/ModuleFormBase.cs b/UI/ModuleFormBase.cs new file mode 100644 index 0000000..e453768 --- /dev/null +++ b/UI/ModuleFormBase.cs @@ -0,0 +1,25 @@ +namespace IBKRTrader.UI; + +/// +/// Basisklasse für eigenständige Modul-Fenster. +/// Stellt gemeinsame Grundeinstellungen (Größe, Startposition) bereit und +/// hält den Modul-Key, damit der Launcher/WindowManager das Fenster zuordnen kann. +/// +public class ModuleFormBase : Form +{ + /// Kürzel des zugehörigen Moduls (z. B. "CT"). + public string ModuleKey { get; } + + // Parameterloser Ctor nur für den WinForms-Designer. + protected ModuleFormBase() : this("") { } + + protected ModuleFormBase(string moduleKey) + { + ModuleKey = moduleKey; + Width = 900; + Height = 600; + StartPosition = FormStartPosition.CenterScreen; + ShowInTaskbar = true; + MinimumSize = new Size(600, 400); + } +} diff --git a/UI/WindowManager.cs b/UI/WindowManager.cs new file mode 100644 index 0000000..a6d8940 --- /dev/null +++ b/UI/WindowManager.cs @@ -0,0 +1,82 @@ +namespace IBKRTrader.UI; + +/// +/// Verwaltet die eigenständigen Modul-Fenster des Launchers. +/// Pro Modul-Key wird höchstens ein Fenster gehalten: erneutes Öffnen +/// fokussiert das bestehende Fenster statt ein zweites zu erzeugen. +/// +/// Die tatsächlichen UI-Aktionen (Show/Focus) sind in überschreibbare +/// Methoden gekapselt, damit die Tracking-Logik ohne Message-Loop testbar ist. +/// +public class WindowManager +{ + private readonly Dictionary _open = new(StringComparer.OrdinalIgnoreCase); + + /// Öffnet das Fenster zum Key oder fokussiert ein bereits offenes. + public Form OpenOrFocus(string key, Func
factory) + { + ArgumentException.ThrowIfNullOrWhiteSpace(key); + ArgumentNullException.ThrowIfNull(factory); + + if (_open.TryGetValue(key, out var existing)) + { + if (!existing.IsDisposed) + { + Focus(existing); + return existing; + } + _open.Remove(key); + } + + var form = factory(); + _open[key] = form; + form.FormClosed += OnFormClosed; + Present(form); + return form; + } + + /// True, wenn zum Key aktuell ein nicht-disposetes Fenster offen ist. + public bool IsOpen(string key) + => _open.TryGetValue(key, out var f) && !f.IsDisposed; + + /// Anzahl aktuell offener Fenster. + public int OpenCount => _open.Values.Count(f => !f.IsDisposed); + + /// Schließt alle offenen Fenster (z. B. beim Beenden des Launchers). + public void CloseAll() + { + foreach (var f in _open.Values.ToList()) + { + if (!f.IsDisposed) + { + f.FormClosed -= OnFormClosed; + f.Close(); + } + } + _open.Clear(); + } + + // ─── Überschreibbare UI-Aktionen (für Tests) ────────────────────────────── + + /// Zeigt ein neu erzeugtes Fenster an. + protected virtual void Present(Form form) => form.Show(); + + /// Bringt ein bestehendes Fenster in den Vordergrund. + protected virtual void Focus(Form form) + { + if (form.WindowState == FormWindowState.Minimized) + form.WindowState = FormWindowState.Normal; + form.Activate(); + form.BringToFront(); + } + + private void OnFormClosed(object? sender, FormClosedEventArgs e) + { + if (sender is not Form f) return; + f.FormClosed -= OnFormClosed; + + var entry = _open.FirstOrDefault(p => ReferenceEquals(p.Value, f)); + if (entry.Key is not null) + _open.Remove(entry.Key); + } +} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 2a86768..7c52fb8 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -109,12 +109,13 @@ WinForms selbst wird **nicht** unit-getestet – Logik in Services/Manager halte - [x] Testbarkeits-Seams: `WorkerBase.BeginRunLogAsync/EndRunLogAsync`, `CapitolTradesScraper.ParseTradesFromHtml` - [x] Tests: `ModuleRegistry`, `CongressTradingModule`, `WorkerBase`, `CapitolTradesScraper` (gegen `ct_raw.html`) → **15/15 grün** -### Phase 2 – Launcher-UI + Modul-Fenster -- [ ] `Form1` → `LauncherForm`; Kern-Panels behalten (Workers, Logs, Settings, Core-Status) -- [ ] Modul-Panel: Liste aus `ModuleRegistry`, je Modul „Fenster öffnen" -- [ ] `UI/ModuleFormBase.cs` + `UI/WindowManager.cs` (Fenster-Tracking) -- [ ] `Modules/CongressTrading/UI/CongressTradingForm.cs` (erstes Modul-Fenster) -- [ ] Tests: `WindowManager` +### Phase 2 – Launcher-UI + Modul-Fenster ✅ +- [x] `Form1` → `LauncherForm` (Dateien via `git mv`, Designer/resx angepasst); Kern-Panels behalten +- [x] Modul-Tab: Karten aus `ModuleRegistry`, je Modul „Fenster öffnen" +- [x] `UI/ModuleFormBase.cs` + `UI/WindowManager.cs` (Fenster-Tracking, Re-Open fokussiert, `CloseAll`) +- [x] `Modules/CongressTrading/UI/CongressTradingForm.cs` (DB-Kennzahlen + „Scrape jetzt") +- [x] `WindowManager` in DI; Launcher schließt Modul-Fenster beim Beenden +- [x] Tests: `WindowManager` (6) → **21/21 grün**; Launcher-Start verifiziert ### Phase 3 – Trading-Kern (Core) - [ ] `Core/Trading/IIbkrClient.cs` (+ Adapter auf `IBKRGatewayService`)