L1a: Core und Module von WinForms entkoppeln - net10.0 statt net10.0-windows
Core, alle drei Module und das Testprojekt tragen keinen UI-Code mehr und bauen fuer linux-x64. Nur noch IBKRTrader.App ist Windows-gebunden. UI-Contract toolkit-neutral (Vorbild: PolytraderSharp): - ModuleView.CreateForm (Func<Form>) -> CreateView (Func<object>) - ModuleView.Icon (System.Drawing.Image) -> IconKey (string). System.Drawing.Common ist seit .NET 7 Windows-only und wirft auf Linux. - WindowMenu.cs war reine WinForms-Umsetzung -> in die Shell verschoben. LoggingService haelt keine RichTextBox mehr, sondern meldet Eintraege ueber event EntryWritten. Einfaerbung und UI-Thread-Wechsel liegen jetzt im LogPanelController der Shell. Nebenbei: ToUpper() -> ToUpperInvariant() (tr-TR haette aus "info" ein "İNFO" gemacht) und \r\n -> Environment.NewLine. Die drei Modul-Fenster liegen jetzt unter UI/Views/Modules/; RegisterUi der Module ist bewusst leer, die Shell registriert sie zentral ueber UI/ModuleViews.cs (nur fuer tatsaechlich geladene Module). ViewIcons loest IconKey gegen die PNG-Ressourcen auf - dieselben Schluessel bekommt spaeter die Avalonia-Shell. UiConstructionTests entfernt: die Konstruktionspruefung deckt --smoke-ui ab, das Testprojekt braucht dafuer keine UI-Referenz mehr. Der Test RegisterUi_RegistersMainView haelt jetzt das Gegenteil fest - das Modul darf nichts registrieren, sonst waere es wieder toolkit-gebunden. Verifiziert: Build 0 Fehler/0 Warnungen, 163 Tests gruen, --smoke-ui konstruiert alle 7 Fenster, und Core + 3 Module + Tests bauen fuer linux-x64. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -60,7 +60,7 @@ public sealed class LauncherForm : Form
|
|||||||
foreach (var view in _uiHost.Views.OrderBy(v => v.Order).ThenBy(v => v.Title))
|
foreach (var view in _uiHost.Views.OrderBy(v => v.Order).ThenBy(v => v.Title))
|
||||||
{
|
{
|
||||||
var id = view.Id;
|
var id = view.Id;
|
||||||
var btn = new ToolStripButton(view.Title, view.Icon)
|
var btn = new ToolStripButton(view.Title, ViewIcons.Resolve(view.IconKey))
|
||||||
{
|
{
|
||||||
DisplayStyle = ToolStripItemDisplayStyle.ImageAndText,
|
DisplayStyle = ToolStripItemDisplayStyle.ImageAndText,
|
||||||
ImageScaling = ToolStripItemImageScaling.None,
|
ImageScaling = ToolStripItemImageScaling.None,
|
||||||
|
|||||||
+9
-28
@@ -84,7 +84,8 @@ internal static class Program
|
|||||||
RegisterCoreViews(uiHost, AppHost.Services);
|
RegisterCoreViews(uiHost, AppHost.Services);
|
||||||
foreach (var module in modules)
|
foreach (var module in modules)
|
||||||
module.RegisterUi(uiHost, AppHost.Services);
|
module.RegisterUi(uiHost, AppHost.Services);
|
||||||
AssignViewIcons(uiHost);
|
ModuleViews.Register(uiHost, AppHost.Services);
|
||||||
|
ViewIcons.AssignDefaults(uiHost);
|
||||||
|
|
||||||
Application.Run(AppHost.Services.GetRequiredService<LauncherForm>());
|
Application.Run(AppHost.Services.GetRequiredService<LauncherForm>());
|
||||||
AppHost.StopAsync().GetAwaiter().GetResult();
|
AppHost.StopAsync().GetAwaiter().GetResult();
|
||||||
@@ -163,35 +164,13 @@ internal static class Program
|
|||||||
services.AddSingleton<WorkerEngine>();
|
services.AddSingleton<WorkerEngine>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Weist den registrierten Views ihr Button-/Menü-Icon aus den App-Ressourcen zu (über die stabile
|
|
||||||
/// View-ID). Icons stammen aus PolytraderSharp; nicht passende können später ausgetauscht werden.
|
|
||||||
/// Bereits gesetzte Icons bleiben erhalten.
|
|
||||||
/// </summary>
|
|
||||||
private static void AssignViewIcons(IModuleUiHost uiHost)
|
|
||||||
{
|
|
||||||
var map = new Dictionary<string, Image>
|
|
||||||
{
|
|
||||||
["core.dashboard"] = Properties.Resources.dashboard,
|
|
||||||
["core.workers"] = Properties.Resources.system_time,
|
|
||||||
["core.logs"] = Properties.Resources.error_log,
|
|
||||||
["core.settings"] = Properties.Resources.setting_tools,
|
|
||||||
["congresstrading.main"] = Properties.Resources.cross_reference,
|
|
||||||
["accounting.main"] = Properties.Resources.coins_in_hand,
|
|
||||||
["supervisor.main"] = Properties.Resources.token_quantifier,
|
|
||||||
};
|
|
||||||
foreach (var view in uiHost.Views)
|
|
||||||
if (view.Icon is null && map.TryGetValue(view.Id, out var img))
|
|
||||||
view.Icon = img;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Registriert die Core-Views (Logs, Settings, Workers) bei der Shell.</summary>
|
/// <summary>Registriert die Core-Views (Logs, Settings, Workers) bei der Shell.</summary>
|
||||||
private static void RegisterCoreViews(IModuleUiHost uiHost, IServiceProvider sp)
|
private static void RegisterCoreViews(IModuleUiHost uiHost, IServiceProvider sp)
|
||||||
{
|
{
|
||||||
uiHost.RegisterView(new ModuleView
|
uiHost.RegisterView(new ModuleView
|
||||||
{
|
{
|
||||||
Id = "core.dashboard", Title = "Dashboard", Group = "Core", Order = 5,
|
Id = "core.dashboard", Title = "Dashboard", Group = "Core", Order = 5,
|
||||||
CreateForm = () => new DashboardView(
|
CreateView = () => new DashboardView(
|
||||||
sp.GetRequiredService<DashboardService>(),
|
sp.GetRequiredService<DashboardService>(),
|
||||||
sp.GetRequiredService<SettingsService>(),
|
sp.GetRequiredService<SettingsService>(),
|
||||||
sp.GetServices<IModule>(),
|
sp.GetServices<IModule>(),
|
||||||
@@ -201,17 +180,17 @@ internal static class Program
|
|||||||
uiHost.RegisterView(new ModuleView
|
uiHost.RegisterView(new ModuleView
|
||||||
{
|
{
|
||||||
Id = "core.workers", Title = "Workers / Services", Group = "Core", Order = 10,
|
Id = "core.workers", Title = "Workers / Services", Group = "Core", Order = 10,
|
||||||
CreateForm = () => new WorkersView(sp.GetRequiredService<WorkerEngine>())
|
CreateView = () => new WorkersView(sp.GetRequiredService<WorkerEngine>())
|
||||||
});
|
});
|
||||||
uiHost.RegisterView(new ModuleView
|
uiHost.RegisterView(new ModuleView
|
||||||
{
|
{
|
||||||
Id = "core.logs", Title = "Logs", Group = "Core", Order = 20,
|
Id = "core.logs", Title = "Logs", Group = "Core", Order = 20,
|
||||||
CreateForm = () => new LogsView(sp.GetRequiredService<LoggingService>())
|
CreateView = () => new LogsView(sp.GetRequiredService<LoggingService>())
|
||||||
});
|
});
|
||||||
uiHost.RegisterView(new ModuleView
|
uiHost.RegisterView(new ModuleView
|
||||||
{
|
{
|
||||||
Id = "core.settings", Title = "Settings", Group = "Core", Order = 30,
|
Id = "core.settings", Title = "Settings", Group = "Core", Order = 30,
|
||||||
CreateForm = () => new SettingsView(sp.GetRequiredService<SettingsService>())
|
CreateView = () => new SettingsView(sp.GetRequiredService<SettingsService>())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,12 +282,14 @@ internal static class Program
|
|||||||
RegisterCoreViews(uiHost, host.Services);
|
RegisterCoreViews(uiHost, host.Services);
|
||||||
foreach (var module in modules)
|
foreach (var module in modules)
|
||||||
module.RegisterUi(uiHost, host.Services);
|
module.RegisterUi(uiHost, host.Services);
|
||||||
|
ModuleViews.Register(uiHost, host.Services);
|
||||||
|
ViewIcons.AssignDefaults(uiHost);
|
||||||
|
|
||||||
var failures = 0;
|
var failures = 0;
|
||||||
Console.WriteLine("=== Smoke-UI: View-Konstruktion ===");
|
Console.WriteLine("=== Smoke-UI: View-Konstruktion ===");
|
||||||
foreach (var view in uiHost.Views)
|
foreach (var view in uiHost.Views)
|
||||||
{
|
{
|
||||||
try { using var form = view.CreateForm(); Console.WriteLine($"[OK] {view.Id} ({view.Title})"); }
|
try { using var form = (Form)view.CreateView(); Console.WriteLine($"[OK] {view.Id} ({view.Title})"); }
|
||||||
catch (Exception ex) { failures++; Console.WriteLine($"[FEHLER] {view.Id}: {ex.GetType().Name}: {ex.Message}"); }
|
catch (Exception ex) { failures++; Console.WriteLine($"[FEHLER] {view.Id}: {ex.GetType().Name}: {ex.Message}"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,19 @@ using IBKRTrader.Core.Logging;
|
|||||||
namespace IBKRTrader.UI;
|
namespace IBKRTrader.UI;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Steuert das Log-Panel (RichTextBox im Logs-Tab).
|
/// Bindet eine <see cref="RichTextBox"/> an den <see cref="LoggingService"/> und färbt die Zeilen
|
||||||
/// Bietet Clear- und Filter-Funktionalität.
|
/// nach Log-Level ein.
|
||||||
|
///
|
||||||
|
/// <para>Einfärbung und Wechsel auf den UI-Thread liegen bewusst hier und nicht mehr im
|
||||||
|
/// <see cref="LoggingService"/>: der Core trägt seit der Linux-Portierung keine UI-Abhängigkeit
|
||||||
|
/// (weder WinForms noch <c>System.Drawing</c>). Der Dienst meldet nur noch Einträge.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class LogPanelController
|
public sealed class LogPanelController : IDisposable
|
||||||
{
|
{
|
||||||
|
private static readonly Color ColorInfo = Color.FromArgb(150, 210, 150);
|
||||||
|
private static readonly Color ColorWarn = Color.FromArgb(255, 190, 60);
|
||||||
|
private static readonly Color ColorError = Color.FromArgb(255, 80, 80);
|
||||||
|
|
||||||
private readonly RichTextBox _rtb;
|
private readonly RichTextBox _rtb;
|
||||||
private readonly LoggingService _logger;
|
private readonly LoggingService _logger;
|
||||||
|
|
||||||
@@ -16,15 +24,53 @@ public class LogPanelController
|
|||||||
_rtb = rtb;
|
_rtb = rtb;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
// Logging-Service mit RichTextBox verbinden
|
// Dunkles Theme fürs Log-Panel.
|
||||||
_logger.AttachRichTextBox(rtb);
|
|
||||||
|
|
||||||
// Hintergrund der RTB auf dunkles Theme setzen
|
|
||||||
_rtb.BackColor = Color.FromArgb(20, 20, 30);
|
_rtb.BackColor = Color.FromArgb(20, 20, 30);
|
||||||
_rtb.ForeColor = Color.FromArgb(200, 200, 200);
|
_rtb.ForeColor = Color.FromArgb(200, 200, 200);
|
||||||
_rtb.Font = new Font("Consolas", 9f);
|
_rtb.Font = new Font("Consolas", 9f);
|
||||||
_rtb.ReadOnly = true;
|
_rtb.ReadOnly = true;
|
||||||
_rtb.WordWrap = false;
|
_rtb.WordWrap = false;
|
||||||
|
|
||||||
|
_logger.EntryWritten += OnEntryWritten;
|
||||||
|
_rtb.Disposed += (_, _) => Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => _logger.EntryWritten -= OnEntryWritten;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wird aus beliebigen Worker-Threads gerufen – deshalb der Wechsel auf den UI-Thread. Ein
|
||||||
|
/// Fehler hier darf den schreibenden Worker niemals mitreißen.
|
||||||
|
/// </summary>
|
||||||
|
private void OnEntryWritten(LogEntry e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_rtb.IsDisposed) return;
|
||||||
|
|
||||||
|
var color = e.Level switch
|
||||||
|
{
|
||||||
|
AppLogLevel.Warn => ColorWarn,
|
||||||
|
AppLogLevel.Error => ColorError,
|
||||||
|
_ => ColorInfo
|
||||||
|
};
|
||||||
|
var text = LoggingService.Format(e) + Environment.NewLine;
|
||||||
|
|
||||||
|
if (_rtb.InvokeRequired) _rtb.BeginInvoke(() => AppendColored(text, color));
|
||||||
|
else AppendColored(text, color);
|
||||||
|
}
|
||||||
|
catch { /* Fenster wird gerade geschlossen */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AppendColored(string text, Color color)
|
||||||
|
{
|
||||||
|
if (_rtb.IsDisposed) return;
|
||||||
|
_rtb.SelectionStart = _rtb.TextLength;
|
||||||
|
_rtb.SelectionLength = 0;
|
||||||
|
_rtb.SelectionColor = color;
|
||||||
|
_rtb.AppendText(text);
|
||||||
|
_rtb.SelectionColor = _rtb.ForeColor;
|
||||||
|
if (_rtb.TextLength > 0)
|
||||||
|
_rtb.ScrollToCaret();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Clear() => _rtb.Clear();
|
public void Clear() => _rtb.Clear();
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
using IBKRTrader.Core.Analytics;
|
||||||
|
using IBKRTrader.Core.Logging;
|
||||||
|
using IBKRTrader.Core.Modularity;
|
||||||
|
using IBKRTrader.Core.Trading;
|
||||||
|
using IBKRTrader.Core.Workers;
|
||||||
|
using IBKRTrader.Modules.Accounting.Persistence;
|
||||||
|
using IBKRTrader.Modules.Accounting.Services;
|
||||||
|
using IBKRTrader.Modules.CongressTrading.Database;
|
||||||
|
using IBKRTrader.Modules.Supervisor.Agent;
|
||||||
|
using IBKRTrader.Modules.Supervisor.Persistence;
|
||||||
|
using IBKRTrader.Modules.Supervisor.Services;
|
||||||
|
using IBKRTrader.UI.Views.Modules;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace IBKRTrader.UI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registriert die Fenster der Module bei der Shell.
|
||||||
|
///
|
||||||
|
/// <para><b>Warum hier und nicht im Modul?</b> Ein Modul, das sein eigenes Fenster erzeugt, muss das
|
||||||
|
/// UI-Toolkit referenzieren – und wäre damit an WinForms bzw. Avalonia gebunden. Genau das verhindert
|
||||||
|
/// den kopflosen Linux-Betrieb. Die Modulprojekte bleiben deshalb frei von UI-Code; ihr
|
||||||
|
/// <c>RegisterUi</c> ist leer, und die Shell verdrahtet die Fenster zentral. Die Modul-Dienste
|
||||||
|
/// kommen unverändert aus dem DI-Container.</para>
|
||||||
|
///
|
||||||
|
/// <para>Registriert wird nur, was auch geladen ist: fehlt ein Modul in dieser Sitzung, entfällt
|
||||||
|
/// sein Fenster, und der Launcher zeigt es gar nicht erst an.</para>
|
||||||
|
/// </summary>
|
||||||
|
public static class ModuleViews
|
||||||
|
{
|
||||||
|
public static void Register(IModuleUiHost host, IServiceProvider sp)
|
||||||
|
{
|
||||||
|
RegisterIfLoaded(sp, "CongressTrading", () => host.RegisterView(new ModuleView
|
||||||
|
{
|
||||||
|
Id = "congresstrading.main", Title = "Congress Trading", Group = "CongressTrading", Order = 100,
|
||||||
|
CreateView = () => new CongressTradingForm(
|
||||||
|
sp.GetRequiredService<CongressRepository>(),
|
||||||
|
sp.GetRequiredService<WorkerEngine>(),
|
||||||
|
sp.GetRequiredService<IPortfolioService>(),
|
||||||
|
sp.GetRequiredService<LoggingService>())
|
||||||
|
}));
|
||||||
|
|
||||||
|
RegisterIfLoaded(sp, "Supervisor", () => host.RegisterView(new ModuleView
|
||||||
|
{
|
||||||
|
Id = "supervisor.main", Title = "Supervisor", Group = "Supervisor", Order = 300,
|
||||||
|
CreateView = () => new SupervisorMainForm(
|
||||||
|
sp.GetRequiredService<SupervisorAgent>(),
|
||||||
|
sp.GetRequiredService<DossierService>(),
|
||||||
|
sp.GetRequiredService<ISupervisorReportRepository>(),
|
||||||
|
sp.GetRequiredService<LoggingService>())
|
||||||
|
}));
|
||||||
|
|
||||||
|
RegisterIfLoaded(sp, "Accounting", () => host.RegisterView(new ModuleView
|
||||||
|
{
|
||||||
|
Id = "accounting.main", Title = "Accounting", Group = "Accounting", Order = 400,
|
||||||
|
CreateView = () => new AccountingMainForm(
|
||||||
|
sp.GetRequiredService<ILedgerRepository>(),
|
||||||
|
sp.GetRequiredService<IIngestRunRepository>(),
|
||||||
|
sp.GetRequiredService<AccountingReportService>(),
|
||||||
|
sp.GetRequiredService<AccountingIngestService>(),
|
||||||
|
sp.GetRequiredService<LoggingService>())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Registriert die Ansicht nur, wenn das Modul in dieser Sitzung geladen ist.</summary>
|
||||||
|
private static void RegisterIfLoaded(IServiceProvider sp, string moduleName, Action register)
|
||||||
|
{
|
||||||
|
if (sp.GetServices<IModule>().Any(m => string.Equals(m.Name, moduleName, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
register();
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-1
@@ -69,7 +69,9 @@ public sealed class ShellUiHost : IModuleUiHost
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var form = view.CreateForm();
|
// Der Core-Contract ist toolkit-neutral (CreateView liefert object). Ein anderer Typ als
|
||||||
|
// Form ist ein Programmierfehler und soll laut scheitern, nicht still ein leeres Fenster geben.
|
||||||
|
var form = (Form)view.CreateView();
|
||||||
if (string.IsNullOrEmpty(form.Text) || form.Text == form.Name)
|
if (string.IsNullOrEmpty(form.Text) || form.Text == form.Name)
|
||||||
form.Text = view.Title;
|
form.Text = view.Title;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
using System.Drawing;
|
||||||
|
using IBKRTrader.Core.Modularity;
|
||||||
|
|
||||||
|
namespace IBKRTrader.UI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Löst die toolkit-neutralen <see cref="ModuleView.IconKey"/>-Schlüssel gegen die Bildressourcen
|
||||||
|
/// der Shell auf. Der Core kennt seit der Linux-Portierung keine Bilddaten mehr
|
||||||
|
/// (<c>System.Drawing.Image</c> ist seit .NET 7 Windows-only) – er liefert nur noch den Schlüssel,
|
||||||
|
/// die jeweilige Shell das Bild.
|
||||||
|
///
|
||||||
|
/// <para>Die Avalonia-Shell bekommt ein gleichnamiges Gegenstück mit <b>denselben Schlüsseln</b>
|
||||||
|
/// und denselben PNG-Dateien, sodass Core und Module unverändert bleiben.</para>
|
||||||
|
/// </summary>
|
||||||
|
public static class ViewIcons
|
||||||
|
{
|
||||||
|
/// <summary>Symbol-Schlüssel → Bildressource aus <c>Resources/</c>.</summary>
|
||||||
|
private static readonly Dictionary<string, Image> ImageByKey = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["dashboard"] = Properties.Resources.dashboard,
|
||||||
|
["workers"] = Properties.Resources.system_time,
|
||||||
|
["logs"] = Properties.Resources.error_log,
|
||||||
|
["settings"] = Properties.Resources.setting_tools,
|
||||||
|
["congresstrading"] = Properties.Resources.cross_reference,
|
||||||
|
["accounting"] = Properties.Resources.coins_in_hand,
|
||||||
|
["supervisor"] = Properties.Resources.token_quantifier,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>Standard-Symbolschlüssel je View-ID – greift für Views, die keinen eigenen setzen.</summary>
|
||||||
|
private static readonly Dictionary<string, string> DefaultKeyByViewId = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["core.dashboard"] = "dashboard",
|
||||||
|
["core.workers"] = "workers",
|
||||||
|
["core.logs"] = "logs",
|
||||||
|
["core.settings"] = "settings",
|
||||||
|
["congresstrading.main"] = "congresstrading",
|
||||||
|
["accounting.main"] = "accounting",
|
||||||
|
["supervisor.main"] = "supervisor",
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>Bild zum Schlüssel, oder <c>null</c> (kein Symbol / unbekannter Schlüssel).</summary>
|
||||||
|
public static Image? Resolve(string? iconKey) =>
|
||||||
|
iconKey != null && ImageByKey.TryGetValue(iconKey, out var img) ? img : null;
|
||||||
|
|
||||||
|
/// <summary>Setzt bei allen registrierten Views den Standard-Schlüssel, falls noch keiner gesetzt ist.</summary>
|
||||||
|
public static void AssignDefaults(IModuleUiHost host)
|
||||||
|
{
|
||||||
|
foreach (var view in host.Views)
|
||||||
|
if (view.IconKey is null && DefaultKeyByViewId.TryGetValue(view.Id, out var key))
|
||||||
|
view.IconKey = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -3,7 +3,7 @@ using IBKRTrader.Modules.Accounting.Logic;
|
|||||||
using IBKRTrader.Modules.Accounting.Persistence;
|
using IBKRTrader.Modules.Accounting.Persistence;
|
||||||
using IBKRTrader.Modules.Accounting.Services;
|
using IBKRTrader.Modules.Accounting.Services;
|
||||||
|
|
||||||
namespace IBKRTrader.Modules.Accounting.Ui;
|
namespace IBKRTrader.UI.Views.Modules;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fenster des Accounting-Moduls: Übersicht/BWA, Ledger, Steuer (Platzhalter), Abrechnung/Export,
|
/// Fenster des Accounting-Moduls: Übersicht/BWA, Ledger, Steuer (Platzhalter), Abrechnung/Export,
|
||||||
+2
-1
@@ -1,9 +1,10 @@
|
|||||||
using IBKRTrader.Core.Logging;
|
using IBKRTrader.Core.Logging;
|
||||||
using IBKRTrader.Core.Trading;
|
using IBKRTrader.Core.Trading;
|
||||||
using IBKRTrader.Core.Workers;
|
using IBKRTrader.Core.Workers;
|
||||||
|
using IBKRTrader.Modules.CongressTrading;
|
||||||
using IBKRTrader.Modules.CongressTrading.Database;
|
using IBKRTrader.Modules.CongressTrading.Database;
|
||||||
|
|
||||||
namespace IBKRTrader.Modules.CongressTrading.UI;
|
namespace IBKRTrader.UI.Views.Modules;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Eigenständiges Fenster des CongressTrading-Moduls: DB-Kennzahlen, manueller Scrape-Trigger
|
/// Eigenständiges Fenster des CongressTrading-Moduls: DB-Kennzahlen, manueller Scrape-Trigger
|
||||||
+1
-1
@@ -6,7 +6,7 @@ using IBKRTrader.Modules.Supervisor.Agent;
|
|||||||
using IBKRTrader.Modules.Supervisor.Persistence;
|
using IBKRTrader.Modules.Supervisor.Persistence;
|
||||||
using IBKRTrader.Modules.Supervisor.Services;
|
using IBKRTrader.Modules.Supervisor.Services;
|
||||||
|
|
||||||
namespace IBKRTrader.Modules.Supervisor.Ui;
|
namespace IBKRTrader.UI.Views.Modules;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fenster des Supervisor-Moduls: Analyse (Chat mit dem Agenten, Tool-Aufrufe sichtbar), Dossier-Browser,
|
/// Fenster des Supervisor-Moduls: Analyse (Chat mit dem Agenten, Tool-Aufrufe sichtbar), Dossier-Browser,
|
||||||
@@ -1,12 +1,17 @@
|
|||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
|
using IBKRTrader.Core.Modularity;
|
||||||
|
|
||||||
namespace IBKRTrader.Core.Modularity;
|
namespace IBKRTrader.UI;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Baut das gemeinsame Fenster-Menü, das auf JEDEM Fenster erscheint und das Wechseln zwischen allen
|
/// Baut das gemeinsame Fenster-Menü, das auf JEDEM Fenster erscheint und das Wechseln zwischen allen
|
||||||
/// Fenstern (Launcher + Core + Module) erlaubt. Da es nur den Core-Contract <see cref="IModuleUiHost"/>
|
/// Fenstern (Launcher + Core + Module) erlaubt. Es nutzt nur den Core-Contract
|
||||||
/// nutzt, funktioniert es auch aus Modul-Fenstern (die die App nicht kennen).
|
/// <see cref="IModuleUiHost"/> und funktioniert deshalb aus jedem Fenster.
|
||||||
|
///
|
||||||
|
/// <para>Lag früher im Core. Verschoben in die Shell, weil er reine WinForms-Umsetzung ist – der
|
||||||
|
/// Core soll keine UI-Abhängigkeit tragen (Linux-Portierung). Die Avalonia-Shell bekommt ein
|
||||||
|
/// eigenes Gegenstück gegen denselben Contract.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class WindowMenu
|
public static class WindowMenu
|
||||||
{
|
{
|
||||||
@@ -46,11 +51,12 @@ public static class WindowMenu
|
|||||||
foreach (var view in host.Views.OrderBy(v => v.Order).ThenBy(v => v.Title))
|
foreach (var view in host.Views.OrderBy(v => v.Order).ThenBy(v => v.Title))
|
||||||
{
|
{
|
||||||
bool isCurrent = view.Id == currentViewId;
|
bool isCurrent = view.Id == currentViewId;
|
||||||
|
var icon = ViewIcons.Resolve(view.IconKey);
|
||||||
var item = new ToolStripMenuItem(view.Title)
|
var item = new ToolStripMenuItem(view.Title)
|
||||||
{
|
{
|
||||||
Image = view.Icon,
|
Image = icon,
|
||||||
ImageScaling = ToolStripItemImageScaling.SizeToFit,
|
ImageScaling = ToolStripItemImageScaling.SizeToFit,
|
||||||
DisplayStyle = view.Icon != null
|
DisplayStyle = icon != null
|
||||||
? ToolStripItemDisplayStyle.ImageAndText
|
? ToolStripItemDisplayStyle.ImageAndText
|
||||||
: ToolStripItemDisplayStyle.Text,
|
: ToolStripItemDisplayStyle.Text,
|
||||||
Checked = isCurrent || host.IsOpen(view.Id)
|
Checked = isCurrent || host.IsOpen(view.Id)
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0-windows</TargetFramework>
|
<!-- Plattformneutral: derselbe Core läuft unter Windows und Linux. Der UI-Contract
|
||||||
|
(ModuleView/IModuleUiHost) ist toolkit-neutral (Func<object> statt Func<Form>, IconKey
|
||||||
|
statt System.Drawing.Image) – hier hängt weder WinForms noch System.Drawing.Common,
|
||||||
|
letzteres ist seit .NET 7 Windows-only und wirft auf Linux. -->
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<!-- Der Core stellt den UI-Contract (ModuleFormBase/WindowManager, später IModuleUiHost/ModuleView)
|
|
||||||
bereit, damit Module designbare Forms beitragen können. -->
|
|
||||||
<UseWindowsForms>true</UseWindowsForms>
|
|
||||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -4,22 +4,32 @@ namespace IBKRTrader.Core.Logging;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Thread-sicherer Logging-Service.
|
/// Thread-sicherer Logging-Service.
|
||||||
/// – Schreibt farbig in die RichTextBox (UI-Thread-safe via BeginInvoke)
|
/// – Schreibt in Logs\{Module}\{Level}-dd-MM-yy.txt sowie strukturiert nach Logs\{Datum}.jsonl
|
||||||
/// – Schreibt in Logs\{Module}\{Level}-dd-MM-yy.txt
|
/// – Meldet jeden Eintrag über <see cref="EntryWritten"/> an interessierte Senken (z. B. die
|
||||||
|
/// Live-Log-Ansicht der Oberfläche)
|
||||||
|
///
|
||||||
|
/// <para><b>Bewusst ohne UI-Bezug:</b> Früher hielt dieser Dienst direkt eine
|
||||||
|
/// <c>RichTextBox</c> samt <c>System.Drawing.Color</c> und marshallte selbst auf den UI-Thread.
|
||||||
|
/// Damit hing der Core an WinForms. Jetzt kennt er nur noch das Ereignis; Einfärbung und
|
||||||
|
/// Thread-Wechsel sind Sache der jeweiligen Oberfläche.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class LoggingService
|
public class LoggingService
|
||||||
{
|
{
|
||||||
private RichTextBox? _rtb;
|
|
||||||
private AppLogLevel _minLevel = AppLogLevel.Info;
|
private AppLogLevel _minLevel = AppLogLevel.Info;
|
||||||
private readonly object _fileLock = new();
|
private readonly object _fileLock = new();
|
||||||
private readonly object _jsonlLock = new();
|
private readonly object _jsonlLock = new();
|
||||||
|
|
||||||
private static readonly string LogBaseDir =
|
private static readonly string LogBaseDir =
|
||||||
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
|
Path.Combine(AppContext.BaseDirectory, "Logs");
|
||||||
|
|
||||||
// ─── Konfiguration ────────────────────────────────────────────────────────
|
// ─── Konfiguration ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public void AttachRichTextBox(RichTextBox rtb) => _rtb = rtb;
|
/// <summary>
|
||||||
|
/// Feuert für jeden geschriebenen Eintrag (nach der Mindest-Level-Prüfung). Die Oberfläche
|
||||||
|
/// hängt sich hier ein; das Marshalling auf den UI-Thread übernimmt sie selbst, weil dieser
|
||||||
|
/// Dienst aus beliebigen Worker-Threads schreibt.
|
||||||
|
/// </summary>
|
||||||
|
public event Action<LogEntry>? EntryWritten;
|
||||||
|
|
||||||
public void SetMinLevel(AppLogLevel level) => _minLevel = level;
|
public void SetMinLevel(AppLogLevel level) => _minLevel = level;
|
||||||
|
|
||||||
@@ -51,7 +61,13 @@ public class LoggingService
|
|||||||
var entry = new LogEntry(DateTime.Now, level, module, message, ex);
|
var entry = new LogEntry(DateTime.Now, level, module, message, ex);
|
||||||
WriteToFile(entry);
|
WriteToFile(entry);
|
||||||
WriteToJsonl(entry, cid);
|
WriteToJsonl(entry, cid);
|
||||||
WriteToRtb(entry);
|
NotifySinks(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NotifySinks(LogEntry e)
|
||||||
|
{
|
||||||
|
// Eine hängende Senke darf den schreibenden Worker nicht mitreißen.
|
||||||
|
try { EntryWritten?.Invoke(e); } catch { /* Logging darf niemals abstürzen */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Datei ────────────────────────────────────────────────────────────────
|
// ─── Datei ────────────────────────────────────────────────────────────────
|
||||||
@@ -66,10 +82,10 @@ public class LoggingService
|
|||||||
var file = Path.Combine(dir, $"{e.Level}-{e.Timestamp:dd-MM-yy}.txt");
|
var file = Path.Combine(dir, $"{e.Level}-{e.Timestamp:dd-MM-yy}.txt");
|
||||||
var line = $"[{e.Timestamp:HH:mm:ss}] {e.Message}";
|
var line = $"[{e.Timestamp:HH:mm:ss}] {e.Message}";
|
||||||
if (e.Exception != null)
|
if (e.Exception != null)
|
||||||
line += $"\r\n {e.Exception}";
|
line += $"{Environment.NewLine} {e.Exception}";
|
||||||
|
|
||||||
lock (_fileLock)
|
lock (_fileLock)
|
||||||
File.AppendAllText(file, line + "\r\n");
|
File.AppendAllText(file, line + Environment.NewLine);
|
||||||
}
|
}
|
||||||
catch { /* Logging darf niemals abstürzen */ }
|
catch { /* Logging darf niemals abstürzen */ }
|
||||||
}
|
}
|
||||||
@@ -95,45 +111,18 @@ public class LoggingService
|
|||||||
catch { /* Logging darf niemals abstürzen */ }
|
catch { /* Logging darf niemals abstürzen */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── RichTextBox ──────────────────────────────────────────────────────────
|
// ─── Anzeigeformat ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private static readonly Color ColorInfo = Color.FromArgb(150, 210, 150);
|
/// <summary>
|
||||||
private static readonly Color ColorWarn = Color.FromArgb(255, 190, 60);
|
/// Einzeilige Darstellung für Log-Ansichten. Liegt hier, damit jede Oberfläche dieselbe Zeile
|
||||||
private static readonly Color ColorError = Color.FromArgb(255, 80, 80);
|
/// zeigt. <c>ToUpperInvariant</c> ist Absicht: <c>ToUpper()</c> würde unter tr-TR aus "info"
|
||||||
|
/// ein "İNFO" machen.
|
||||||
private void WriteToRtb(LogEntry e)
|
/// </summary>
|
||||||
|
public static string Format(LogEntry e)
|
||||||
{
|
{
|
||||||
if (_rtb == null) return;
|
var text = $"[{e.Timestamp:HH:mm:ss}] [{e.Level.ToString().ToUpperInvariant(),-5}] [{e.Module}] {e.Message}";
|
||||||
try
|
if (e.Exception != null)
|
||||||
{
|
text += $"{Environment.NewLine} {e.Exception.Message}";
|
||||||
var color = e.Level switch
|
return text;
|
||||||
{
|
|
||||||
AppLogLevel.Warn => ColorWarn,
|
|
||||||
AppLogLevel.Error => ColorError,
|
|
||||||
_ => ColorInfo
|
|
||||||
};
|
|
||||||
var text = $"[{e.Timestamp:HH:mm:ss}] [{e.Level.ToString().ToUpper(),-5}] [{e.Module}] {e.Message}";
|
|
||||||
if (e.Exception != null)
|
|
||||||
text += $"\r\n {e.Exception.Message}";
|
|
||||||
text += "\r\n";
|
|
||||||
|
|
||||||
if (_rtb.InvokeRequired)
|
|
||||||
_rtb.BeginInvoke(() => AppendColored(text, color));
|
|
||||||
else
|
|
||||||
AppendColored(text, color);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AppendColored(string text, Color color)
|
|
||||||
{
|
|
||||||
if (_rtb == null) return;
|
|
||||||
_rtb.SelectionStart = _rtb.TextLength;
|
|
||||||
_rtb.SelectionLength = 0;
|
|
||||||
_rtb.SelectionColor = color;
|
|
||||||
_rtb.AppendText(text);
|
|
||||||
_rtb.SelectionColor = _rtb.ForeColor;
|
|
||||||
if (_rtb.TextLength > 0)
|
|
||||||
_rtb.ScrollToCaret();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
using System.Windows.Forms;
|
|
||||||
|
|
||||||
namespace IBKRTrader.Core.Modularity;
|
namespace IBKRTrader.Core.Modularity;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Eine vom Core oder einem Modul beigesteuerte Fenster-Ansicht. Die eigentliche UI ist ein
|
/// Eine vom Core oder einem Modul beigesteuerte Fenster-Ansicht. Die Shell zeigt je View höchstens
|
||||||
/// <see cref="Form"/>, das über <see cref="CreateForm"/> erzeugt wird (mit DI-Abhängigkeiten).
|
/// eine Instanz und holt ein offenes Fenster wieder nach vorne.
|
||||||
/// Die Shell zeigt je View höchstens eine Instanz und holt ein offenes Fenster wieder nach vorne.
|
///
|
||||||
|
/// <para><b>Bewusst toolkit-neutral:</b> <see cref="CreateView"/> liefert ein <see cref="object"/>,
|
||||||
|
/// keinen konkreten Fenstertyp, und <see cref="IconKey"/> ist ein Schlüssel statt eines Bildes.
|
||||||
|
/// Dadurch trägt der Core keine UI-Abhängigkeit und bleibt plattformneutral – Voraussetzung für den
|
||||||
|
/// kopflosen Linux-Betrieb. Insbesondere hängt hier kein <c>System.Drawing.Image</c> mehr:
|
||||||
|
/// <c>System.Drawing.Common</c> ist seit .NET 7 Windows-only und wirft auf Linux. Die jeweilige
|
||||||
|
/// Shell kennt ihr Toolkit und castet – die WinForms-Shell auf <c>Form</c>, die Avalonia-Shell
|
||||||
|
/// auf <c>Window</c>.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ModuleView
|
public sealed class ModuleView
|
||||||
{
|
{
|
||||||
/// <summary>Stabile ID für Einzelinstanz-Handling (nur ein Fenster je View).</summary>
|
/// <summary>Stabile ID für Einzelinstanz-Handling (nur ein Fenster je View).</summary>
|
||||||
public string Id { get; init; } = Guid.NewGuid().ToString();
|
public string Id { get; init; } = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
/// <summary>Titel (Fallback-Fenstertitel, falls das Form keinen eigenen setzt).</summary>
|
/// <summary>Titel (Fallback-Fenstertitel, falls das Fenster keinen eigenen setzt).</summary>
|
||||||
public string Title { get; init; } = "Fenster";
|
public string Title { get; init; } = "Fenster";
|
||||||
|
|
||||||
/// <summary>Optionale Gruppierung (z. B. "Core", "CongressTrading").</summary>
|
/// <summary>Optionale Gruppierung (z. B. "Core", "CongressTrading").</summary>
|
||||||
@@ -21,17 +26,24 @@ public sealed class ModuleView
|
|||||||
/// <summary>Optionale Sortierreihenfolge in Menü/Buttons.</summary>
|
/// <summary>Optionale Sortierreihenfolge in Menü/Buttons.</summary>
|
||||||
public int Order { get; init; } = 0;
|
public int Order { get; init; } = 0;
|
||||||
|
|
||||||
/// <summary>Optionales Icon für Menü/Buttons.</summary>
|
/// <summary>
|
||||||
public System.Drawing.Image? Icon { get; set; }
|
/// Logischer Schlüssel des Symbols für Menü/Buttons (z. B. "dashboard", "logs"). Die Shell löst
|
||||||
|
/// ihn gegen ihre eigenen Bildressourcen auf. Settable, damit die Shell den von Modulen
|
||||||
|
/// registrierten Views zentral ein Symbol zuweisen kann – Module kennen die Shell-Ressourcen nicht.
|
||||||
|
/// </summary>
|
||||||
|
public string? IconKey { get; set; }
|
||||||
|
|
||||||
/// <summary>Erzeugt das anzuzeigende Fenster (frische Instanz je Öffnung).</summary>
|
/// <summary>
|
||||||
public Func<Form> CreateForm { get; init; } = () => new Form();
|
/// Erzeugt das anzuzeigende Fenster (frische Instanz je Öffnung). Rückgabetyp ist
|
||||||
|
/// <see cref="object"/> – siehe Klassen-Doku zur Toolkit-Neutralität.
|
||||||
|
/// </summary>
|
||||||
|
public Func<object> CreateView { get; init; } = () => new object();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Wird der Shell beim Start übergeben; Core und Module registrieren hier ihre Ansichten.
|
/// Wird der Shell beim Start übergeben; Core und Module registrieren hier ihre Ansichten.
|
||||||
/// Über die Navigations-Mitglieder kann JEDES Fenster (auch Modul-Fenster, die nur den Core kennen)
|
/// Über die Navigations-Mitglieder kann JEDES Fenster (auch Modul-Fenster, die nur den Core kennen)
|
||||||
/// das gemeinsame „Fenster"-Menü bauen (siehe <see cref="WindowMenu"/>).
|
/// das gemeinsame „Fenster"-Menü bauen.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IModuleUiHost
|
public interface IModuleUiHost
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ using IBKRTrader.Core.Logging;
|
|||||||
using IBKRTrader.Core.Modularity;
|
using IBKRTrader.Core.Modularity;
|
||||||
using IBKRTrader.Modules.Accounting.Persistence;
|
using IBKRTrader.Modules.Accounting.Persistence;
|
||||||
using IBKRTrader.Modules.Accounting.Services;
|
using IBKRTrader.Modules.Accounting.Services;
|
||||||
using IBKRTrader.Modules.Accounting.Ui;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@@ -47,22 +46,12 @@ public sealed class AccountingModule : IModule
|
|||||||
services.AddHostedService(sp => sp.GetRequiredService<AccountingIngestService>());
|
services.AddHostedService(sp => sp.GetRequiredService<AccountingIngestService>());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RegisterUi(IModuleUiHost host, IServiceProvider services)
|
/// <summary>
|
||||||
{
|
/// Bewusst leer: das Modulprojekt trägt keinen UI-Code mehr, damit es plattformneutral bleibt
|
||||||
host.RegisterView(new ModuleView
|
/// (kopfloser Linux-Betrieb). Das Accounting-Fenster registriert die Shell zentral in
|
||||||
{
|
/// <c>UI/ModuleViews.cs</c>; die Dienste dafür kommen aus dem DI-Container.
|
||||||
Id = "accounting.main",
|
/// </summary>
|
||||||
Title = "Accounting",
|
public void RegisterUi(IModuleUiHost host, IServiceProvider services) { }
|
||||||
Group = Name,
|
|
||||||
Order = 400,
|
|
||||||
CreateForm = () => new AccountingMainForm(
|
|
||||||
services.GetRequiredService<ILedgerRepository>(),
|
|
||||||
services.GetRequiredService<IIngestRunRepository>(),
|
|
||||||
services.GetRequiredService<AccountingReportService>(),
|
|
||||||
services.GetRequiredService<AccountingIngestService>(),
|
|
||||||
services.GetRequiredService<LoggingService>())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// DB-Schema wird extern per `dotnet ef database update` angewendet (keine Laufzeit-Migration).
|
// DB-Schema wird extern per `dotnet ef database update` angewendet (keine Laufzeit-Migration).
|
||||||
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0-windows</TargetFramework>
|
<!-- Plattformneutral: das Modul traegt keinen UI-Code mehr (Fenster liegt in der Shell,
|
||||||
|
siehe UI/ModuleViews.cs) und laeuft damit auch im kopflosen Linux-Betrieb. -->
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<!-- Modul trägt eigene WinForms-UI (Modul-Fenster) bei. -->
|
|
||||||
<UseWindowsForms>true</UseWindowsForms>
|
|
||||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ using IBKRTrader.Core.Workers;
|
|||||||
using IBKRTrader.Modules.CongressTrading.Database;
|
using IBKRTrader.Modules.CongressTrading.Database;
|
||||||
using IBKRTrader.Modules.CongressTrading.Persistence.Ef;
|
using IBKRTrader.Modules.CongressTrading.Persistence.Ef;
|
||||||
using IBKRTrader.Modules.CongressTrading.Scraper;
|
using IBKRTrader.Modules.CongressTrading.Scraper;
|
||||||
using IBKRTrader.Modules.CongressTrading.UI;
|
|
||||||
using IBKRTrader.Modules.CongressTrading.Workers;
|
using IBKRTrader.Modules.CongressTrading.Workers;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
@@ -48,21 +47,12 @@ public sealed class CongressTradingModule : IModule
|
|||||||
services.AddHostedService(sp => sp.GetRequiredService<CongressScrapeWorker>());
|
services.AddHostedService(sp => sp.GetRequiredService<CongressScrapeWorker>());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RegisterUi(IModuleUiHost host, IServiceProvider services)
|
/// <summary>
|
||||||
{
|
/// Bewusst leer: das Modulprojekt trägt keinen UI-Code mehr, damit es plattformneutral bleibt
|
||||||
host.RegisterView(new ModuleView
|
/// (kopfloser Linux-Betrieb). Das Modul-Fenster registriert die Shell zentral in
|
||||||
{
|
/// <c>UI/ModuleViews.cs</c>; die Dienste dafür kommen aus dem DI-Container.
|
||||||
Id = "congresstrading.main",
|
/// </summary>
|
||||||
Title = "Congress Trading",
|
public void RegisterUi(IModuleUiHost host, IServiceProvider services) { }
|
||||||
Group = Name,
|
|
||||||
Order = 100,
|
|
||||||
CreateForm = () => new CongressTradingForm(
|
|
||||||
services.GetRequiredService<CongressRepository>(),
|
|
||||||
services.GetRequiredService<WorkerEngine>(),
|
|
||||||
services.GetRequiredService<IPortfolioService>(),
|
|
||||||
services.GetRequiredService<LoggingService>())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// DB-Schema wird extern per `dotnet ef database update` angewendet (keine Laufzeit-Migration).
|
// DB-Schema wird extern per `dotnet ef database update` angewendet (keine Laufzeit-Migration).
|
||||||
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0-windows</TargetFramework>
|
<!-- Plattformneutral: das Modul traegt keinen UI-Code mehr (Fenster liegt in der Shell,
|
||||||
|
siehe UI/ModuleViews.cs) und laeuft damit auch im kopflosen Linux-Betrieb. -->
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<!-- Modul trägt eigene WinForms-UI (Modul-Fenster) bei. -->
|
|
||||||
<UseWindowsForms>true</UseWindowsForms>
|
|
||||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0-windows</TargetFramework>
|
<!-- Plattformneutral: das Modul traegt keinen UI-Code mehr (Fenster liegt in der Shell,
|
||||||
|
siehe UI/ModuleViews.cs) und laeuft damit auch im kopflosen Linux-Betrieb. -->
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<!-- Modul trägt eigene WinForms-UI (Modul-Fenster) bei. -->
|
|
||||||
<UseWindowsForms>true</UseWindowsForms>
|
|
||||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ using IBKRTrader.Modules.Supervisor.Agent;
|
|||||||
using IBKRTrader.Modules.Supervisor.Counterfactual;
|
using IBKRTrader.Modules.Supervisor.Counterfactual;
|
||||||
using IBKRTrader.Modules.Supervisor.Persistence;
|
using IBKRTrader.Modules.Supervisor.Persistence;
|
||||||
using IBKRTrader.Modules.Supervisor.Services;
|
using IBKRTrader.Modules.Supervisor.Services;
|
||||||
using IBKRTrader.Modules.Supervisor.Ui;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@@ -58,21 +57,12 @@ public sealed class SupervisorModule : IModule
|
|||||||
services.AddHostedService<Mcp.McpLightServer>();
|
services.AddHostedService<Mcp.McpLightServer>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RegisterUi(IModuleUiHost host, IServiceProvider services)
|
/// <summary>
|
||||||
{
|
/// Bewusst leer: das Modulprojekt trägt keinen UI-Code mehr, damit es plattformneutral bleibt
|
||||||
host.RegisterView(new ModuleView
|
/// (kopfloser Linux-Betrieb). Das Supervisor-Fenster registriert die Shell zentral in
|
||||||
{
|
/// <c>UI/ModuleViews.cs</c>; die Dienste dafür kommen aus dem DI-Container.
|
||||||
Id = "supervisor.main",
|
/// </summary>
|
||||||
Title = "Supervisor",
|
public void RegisterUi(IModuleUiHost host, IServiceProvider services) { }
|
||||||
Group = Name,
|
|
||||||
Order = 300,
|
|
||||||
CreateForm = () => new SupervisorMainForm(
|
|
||||||
services.GetRequiredService<SupervisorAgent>(),
|
|
||||||
services.GetRequiredService<DossierService>(),
|
|
||||||
services.GetRequiredService<ISupervisorReportRepository>(),
|
|
||||||
services.GetRequiredService<LoggingService>())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- net10.0-windows + WinForms, weil das Testprojekt die WinForms-Hauptassembly referenziert -->
|
<!-- Plattformneutral: die Tests decken Core und Module ab, die beide keinen UI-Code mehr
|
||||||
<TargetFramework>net10.0-windows</TargetFramework>
|
tragen. Die UI-Konstruktionsprüfung liegt jetzt beim Smoke-UI-Lauf der Shell. -->
|
||||||
<UseWindowsForms>true</UseWindowsForms>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
|
|||||||
@@ -55,17 +55,17 @@ public class CongressTradingModuleTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RegisterUi_RegistersMainView()
|
public void RegisterUi_RegistriertNichts_DamitDasModulPlattformneutralBleibt()
|
||||||
{
|
{
|
||||||
|
// Absicht, kein Versehen: würde das Modul sein Fenster selbst erzeugen, müsste es das
|
||||||
|
// UI-Toolkit referenzieren – und wäre damit nicht mehr kopflos auf Linux lauffähig.
|
||||||
|
// Das Fenster registriert die Shell zentral (App: UI/ModuleViews.cs).
|
||||||
var host = new CapturingUiHost();
|
var host = new CapturingUiHost();
|
||||||
// CreateForm wird hier NICHT aufgerufen – daher genügt ein leerer Provider.
|
|
||||||
var provider = new ServiceCollection().BuildServiceProvider();
|
var provider = new ServiceCollection().BuildServiceProvider();
|
||||||
|
|
||||||
new CongressTradingModule().RegisterUi(host, provider);
|
new CongressTradingModule().RegisterUi(host, provider);
|
||||||
|
|
||||||
host.Views.Should().ContainSingle();
|
host.Views.Should().BeEmpty();
|
||||||
host.Views[0].Id.Should().Be("congresstrading.main");
|
|
||||||
host.Views[0].Title.Should().Be("Congress Trading");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
using FluentAssertions;
|
|
||||||
using IBKRTrader.Core.Logging;
|
|
||||||
using IBKRTrader.Core.Persistence;
|
|
||||||
using IBKRTrader.Core.Persistence.Ef;
|
|
||||||
using IBKRTrader.Modules.Accounting.Persistence;
|
|
||||||
using IBKRTrader.Modules.Accounting.Services;
|
|
||||||
using IBKRTrader.Modules.Accounting.Ui;
|
|
||||||
using IBKRTrader.Modules.Supervisor.Agent;
|
|
||||||
using IBKRTrader.Modules.Supervisor.Persistence;
|
|
||||||
using IBKRTrader.Modules.Supervisor.Services;
|
|
||||||
using IBKRTrader.Modules.Supervisor.Ui;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace IBKRTrader.Tests;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Konstruiert die neuen Modul-Fenster mit In-Memory-/Stub-Abhängigkeiten – gleichwertig zum
|
|
||||||
/// Headless-Smoke-UI-Check (`--smoke-ui`), aber ohne die laufende App/DB. Forms bauen im Konstruktor
|
|
||||||
/// nur Controls (DB-Zugriff erst auf Interaktion), daher genügt Instanziierbarkeit der Services.
|
|
||||||
/// </summary>
|
|
||||||
[Trait("cat", "unit")]
|
|
||||||
public class UiConstructionTests
|
|
||||||
{
|
|
||||||
private sealed class Factory<T>(DbContextOptions<T> options) : IDbContextFactory<T> where T : DbContext
|
|
||||||
{
|
|
||||||
public T CreateDbContext() => (T)Activator.CreateInstance(typeof(T), options)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Factory<T> InMemory<T>() where T : DbContext =>
|
|
||||||
new(new DbContextOptionsBuilder<T>().UseInMemoryDatabase(Guid.NewGuid().ToString()).Options);
|
|
||||||
|
|
||||||
private sealed class NoChat : IChatCompletionClient
|
|
||||||
{
|
|
||||||
public Task<ChatResponse> CompleteAsync(string m, IReadOnlyList<ChatMessage> msgs,
|
|
||||||
IReadOnlyList<SupervisorTool> tools, CancellationToken ct) => Task.FromResult(new ChatResponse());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Exception? ConstructOnSta(Action action)
|
|
||||||
{
|
|
||||||
Exception? captured = null;
|
|
||||||
var t = new Thread(() => { try { action(); } catch (Exception ex) { captured = ex; } });
|
|
||||||
t.SetApartmentState(ApartmentState.STA);
|
|
||||||
t.Start();
|
|
||||||
t.Join();
|
|
||||||
return captured;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void AccountingMainForm_Constructs()
|
|
||||||
{
|
|
||||||
var ex = ConstructOnSta(() =>
|
|
||||||
{
|
|
||||||
var logger = new LoggingService();
|
|
||||||
var accDbf = InMemory<AccountingDbContext>();
|
|
||||||
var ledger = new EfLedgerRepository(accDbf);
|
|
||||||
var runs = new EfIngestRunRepository(accDbf);
|
|
||||||
var report = new AccountingReportService(ledger, new EfFxRateRepository(accDbf));
|
|
||||||
var ingest = new AccountingIngestService(
|
|
||||||
new NullAccountSource(), ledger, runs, new EfRawSnapshotRepository(accDbf),
|
|
||||||
new NullStatementSource(), new NullBalanceAnchorSource(), logger);
|
|
||||||
|
|
||||||
using var form = new AccountingMainForm(ledger, runs, report, ingest, logger);
|
|
||||||
form.Text.Should().Be("Accounting");
|
|
||||||
});
|
|
||||||
ex.Should().BeNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void SupervisorMainForm_Constructs()
|
|
||||||
{
|
|
||||||
var ex = ConstructOnSta(() =>
|
|
||||||
{
|
|
||||||
var logger = new LoggingService();
|
|
||||||
var coreDbf = InMemory<CoreDbContext>();
|
|
||||||
var supDbf = InMemory<SupervisorDbContext>();
|
|
||||||
IDecisionJournal journal = new EfDecisionJournal(coreDbf, logger);
|
|
||||||
IOrderEventLog orderLog = new EfOrderEventLog(coreDbf, logger);
|
|
||||||
var dossiers = new DossierService(journal, orderLog, new TradeLogReader(coreDbf));
|
|
||||||
var agent = new SupervisorAgent(new NoChat(), new SupervisorToolRegistry());
|
|
||||||
var reports = new EfSupervisorReportRepository(supDbf, logger);
|
|
||||||
|
|
||||||
using var form = new SupervisorMainForm(agent, dossiers, reports, logger);
|
|
||||||
form.Text.Should().Be("Supervisor");
|
|
||||||
});
|
|
||||||
ex.Should().BeNull();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user