Phase 2: Launcher-UI + eigenstaendige Modul-Fenster

- Form1 -> LauncherForm (Dateien via git mv, Designer/resx angepasst)
- Modul-Tab: Karten aus ModuleRegistry mit "Fenster oeffnen"-Button je Modul
- UI/WindowManager: Fenster-Tracking (Key->Form), Re-Open fokussiert, CloseAll
- UI/ModuleFormBase: Basisklasse fuer eigenstaendige Modul-Fenster
- CongressTradingForm: DB-Kennzahlen + manueller Scrape-Trigger (Phase-4-Ausbau folgt)
- WindowManager in DI; Launcher schliesst Modul-Fenster beim Beenden
- Tests: WindowManager (6) -> 21/21 gruen; Launcher-Start verifiziert

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
This commit is contained in:
Richard
2026-07-27 10:39:50 +02:00
parent 5815d3258f
commit d0bc833235
11 changed files with 483 additions and 59 deletions
@@ -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<CongressScrapeWorker>()
];
/// <summary>
/// Erzeugt das Modul-Fenster. Platzhalter die vollständige UI folgt in Phase 2.
/// </summary>
/// <summary>Erzeugt das eigenständige Modul-Fenster.</summary>
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<CongressRepository>(),
provider.GetRequiredService<WorkerEngine>(),
provider.GetRequiredService<LoggingService>());
}
@@ -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;
/// <summary>
/// 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.
/// </summary>
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);
}
}
}