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))
|
||||
{
|
||||
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,
|
||||
ImageScaling = ToolStripItemImageScaling.None,
|
||||
|
||||
+9
-28
@@ -84,7 +84,8 @@ internal static class Program
|
||||
RegisterCoreViews(uiHost, AppHost.Services);
|
||||
foreach (var module in modules)
|
||||
module.RegisterUi(uiHost, AppHost.Services);
|
||||
AssignViewIcons(uiHost);
|
||||
ModuleViews.Register(uiHost, AppHost.Services);
|
||||
ViewIcons.AssignDefaults(uiHost);
|
||||
|
||||
Application.Run(AppHost.Services.GetRequiredService<LauncherForm>());
|
||||
AppHost.StopAsync().GetAwaiter().GetResult();
|
||||
@@ -163,35 +164,13 @@ internal static class Program
|
||||
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>
|
||||
private static void RegisterCoreViews(IModuleUiHost uiHost, IServiceProvider sp)
|
||||
{
|
||||
uiHost.RegisterView(new ModuleView
|
||||
{
|
||||
Id = "core.dashboard", Title = "Dashboard", Group = "Core", Order = 5,
|
||||
CreateForm = () => new DashboardView(
|
||||
CreateView = () => new DashboardView(
|
||||
sp.GetRequiredService<DashboardService>(),
|
||||
sp.GetRequiredService<SettingsService>(),
|
||||
sp.GetServices<IModule>(),
|
||||
@@ -201,17 +180,17 @@ internal static class Program
|
||||
uiHost.RegisterView(new ModuleView
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
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);
|
||||
foreach (var module in modules)
|
||||
module.RegisterUi(uiHost, host.Services);
|
||||
ModuleViews.Register(uiHost, host.Services);
|
||||
ViewIcons.AssignDefaults(uiHost);
|
||||
|
||||
var failures = 0;
|
||||
Console.WriteLine("=== Smoke-UI: View-Konstruktion ===");
|
||||
foreach (var view in uiHost.Views)
|
||||
{
|
||||
try { using var form = view.CreateForm(); Console.WriteLine($"[OK] {view.Id} ({view.Title})"); }
|
||||
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}"); }
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,19 @@ using IBKRTrader.Core.Logging;
|
||||
namespace IBKRTrader.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Steuert das Log-Panel (RichTextBox im Logs-Tab).
|
||||
/// Bietet Clear- und Filter-Funktionalität.
|
||||
/// Bindet eine <see cref="RichTextBox"/> an den <see cref="LoggingService"/> und färbt die Zeilen
|
||||
/// 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>
|
||||
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 LoggingService _logger;
|
||||
|
||||
@@ -16,15 +24,53 @@ public class LogPanelController
|
||||
_rtb = rtb;
|
||||
_logger = logger;
|
||||
|
||||
// Logging-Service mit RichTextBox verbinden
|
||||
_logger.AttachRichTextBox(rtb);
|
||||
|
||||
// Hintergrund der RTB auf dunkles Theme setzen
|
||||
// Dunkles Theme fürs Log-Panel.
|
||||
_rtb.BackColor = Color.FromArgb(20, 20, 30);
|
||||
_rtb.ForeColor = Color.FromArgb(200, 200, 200);
|
||||
_rtb.Font = new Font("Consolas", 9f);
|
||||
_rtb.ReadOnly = true;
|
||||
_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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
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)
|
||||
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.Services;
|
||||
|
||||
namespace IBKRTrader.Modules.Accounting.Ui;
|
||||
namespace IBKRTrader.UI.Views.Modules;
|
||||
|
||||
/// <summary>
|
||||
/// Fenster des Accounting-Moduls: Übersicht/BWA, Ledger, Steuer (Platzhalter), Abrechnung/Export,
|
||||
+2
-1
@@ -1,9 +1,10 @@
|
||||
using IBKRTrader.Core.Logging;
|
||||
using IBKRTrader.Core.Trading;
|
||||
using IBKRTrader.Core.Workers;
|
||||
using IBKRTrader.Modules.CongressTrading;
|
||||
using IBKRTrader.Modules.CongressTrading.Database;
|
||||
|
||||
namespace IBKRTrader.Modules.CongressTrading.UI;
|
||||
namespace IBKRTrader.UI.Views.Modules;
|
||||
|
||||
/// <summary>
|
||||
/// 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.Services;
|
||||
|
||||
namespace IBKRTrader.Modules.Supervisor.Ui;
|
||||
namespace IBKRTrader.UI.Views.Modules;
|
||||
|
||||
/// <summary>
|
||||
/// Fenster des Supervisor-Moduls: Analyse (Chat mit dem Agenten, Tool-Aufrufe sichtbar), Dossier-Browser,
|
||||
@@ -1,12 +1,17 @@
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
using IBKRTrader.Core.Modularity;
|
||||
|
||||
namespace IBKRTrader.Core.Modularity;
|
||||
namespace IBKRTrader.UI;
|
||||
|
||||
/// <summary>
|
||||
/// 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"/>
|
||||
/// nutzt, funktioniert es auch aus Modul-Fenstern (die die App nicht kennen).
|
||||
/// Fenstern (Launcher + Core + Module) erlaubt. Es nutzt nur den Core-Contract
|
||||
/// <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>
|
||||
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))
|
||||
{
|
||||
bool isCurrent = view.Id == currentViewId;
|
||||
var icon = ViewIcons.Resolve(view.IconKey);
|
||||
var item = new ToolStripMenuItem(view.Title)
|
||||
{
|
||||
Image = view.Icon,
|
||||
Image = icon,
|
||||
ImageScaling = ToolStripItemImageScaling.SizeToFit,
|
||||
DisplayStyle = view.Icon != null
|
||||
DisplayStyle = icon != null
|
||||
? ToolStripItemDisplayStyle.ImageAndText
|
||||
: ToolStripItemDisplayStyle.Text,
|
||||
Checked = isCurrent || host.IsOpen(view.Id)
|
||||
@@ -1,12 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -4,22 +4,32 @@ namespace IBKRTrader.Core.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Thread-sicherer Logging-Service.
|
||||
/// – Schreibt farbig in die RichTextBox (UI-Thread-safe via BeginInvoke)
|
||||
/// – Schreibt in Logs\{Module}\{Level}-dd-MM-yy.txt
|
||||
/// – Schreibt in Logs\{Module}\{Level}-dd-MM-yy.txt sowie strukturiert nach Logs\{Datum}.jsonl
|
||||
/// – 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>
|
||||
public class LoggingService
|
||||
{
|
||||
private RichTextBox? _rtb;
|
||||
private AppLogLevel _minLevel = AppLogLevel.Info;
|
||||
private readonly object _fileLock = new();
|
||||
private readonly object _jsonlLock = new();
|
||||
|
||||
private static readonly string LogBaseDir =
|
||||
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
|
||||
Path.Combine(AppContext.BaseDirectory, "Logs");
|
||||
|
||||
// ─── 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;
|
||||
|
||||
@@ -51,7 +61,13 @@ public class LoggingService
|
||||
var entry = new LogEntry(DateTime.Now, level, module, message, ex);
|
||||
WriteToFile(entry);
|
||||
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 ────────────────────────────────────────────────────────────────
|
||||
@@ -66,10 +82,10 @@ public class LoggingService
|
||||
var file = Path.Combine(dir, $"{e.Level}-{e.Timestamp:dd-MM-yy}.txt");
|
||||
var line = $"[{e.Timestamp:HH:mm:ss}] {e.Message}";
|
||||
if (e.Exception != null)
|
||||
line += $"\r\n {e.Exception}";
|
||||
line += $"{Environment.NewLine} {e.Exception}";
|
||||
|
||||
lock (_fileLock)
|
||||
File.AppendAllText(file, line + "\r\n");
|
||||
File.AppendAllText(file, line + Environment.NewLine);
|
||||
}
|
||||
catch { /* Logging darf niemals abstürzen */ }
|
||||
}
|
||||
@@ -95,45 +111,18 @@ public class LoggingService
|
||||
catch { /* Logging darf niemals abstürzen */ }
|
||||
}
|
||||
|
||||
// ─── RichTextBox ──────────────────────────────────────────────────────────
|
||||
// ─── Anzeigeformat ────────────────────────────────────────────────────────
|
||||
|
||||
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 void WriteToRtb(LogEntry e)
|
||||
/// <summary>
|
||||
/// Einzeilige Darstellung für Log-Ansichten. Liegt hier, damit jede Oberfläche dieselbe Zeile
|
||||
/// zeigt. <c>ToUpperInvariant</c> ist Absicht: <c>ToUpper()</c> würde unter tr-TR aus "info"
|
||||
/// ein "İNFO" machen.
|
||||
/// </summary>
|
||||
public static string Format(LogEntry e)
|
||||
{
|
||||
if (_rtb == null) return;
|
||||
try
|
||||
{
|
||||
var color = e.Level switch
|
||||
{
|
||||
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();
|
||||
var text = $"[{e.Timestamp:HH:mm:ss}] [{e.Level.ToString().ToUpperInvariant(),-5}] [{e.Module}] {e.Message}";
|
||||
if (e.Exception != null)
|
||||
text += $"{Environment.NewLine} {e.Exception.Message}";
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace IBKRTrader.Core.Modularity;
|
||||
|
||||
/// <summary>
|
||||
/// Eine vom Core oder einem Modul beigesteuerte Fenster-Ansicht. Die eigentliche UI ist ein
|
||||
/// <see cref="Form"/>, das über <see cref="CreateForm"/> erzeugt wird (mit DI-Abhängigkeiten).
|
||||
/// Die Shell zeigt je View höchstens eine Instanz und holt ein offenes Fenster wieder nach vorne.
|
||||
/// Eine vom Core oder einem Modul beigesteuerte Fenster-Ansicht. 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>
|
||||
public sealed class ModuleView
|
||||
{
|
||||
/// <summary>Stabile ID für Einzelinstanz-Handling (nur ein Fenster je View).</summary>
|
||||
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";
|
||||
|
||||
/// <summary>Optionale Gruppierung (z. B. "Core", "CongressTrading").</summary>
|
||||
@@ -21,17 +26,24 @@ public sealed class ModuleView
|
||||
/// <summary>Optionale Sortierreihenfolge in Menü/Buttons.</summary>
|
||||
public int Order { get; init; } = 0;
|
||||
|
||||
/// <summary>Optionales Icon für Menü/Buttons.</summary>
|
||||
public System.Drawing.Image? Icon { get; set; }
|
||||
/// <summary>
|
||||
/// 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>
|
||||
public Func<Form> CreateForm { get; init; } = () => new Form();
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// 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)
|
||||
/// das gemeinsame „Fenster"-Menü bauen (siehe <see cref="WindowMenu"/>).
|
||||
/// das gemeinsame „Fenster"-Menü bauen.
|
||||
/// </summary>
|
||||
public interface IModuleUiHost
|
||||
{
|
||||
|
||||
@@ -4,7 +4,6 @@ using IBKRTrader.Core.Logging;
|
||||
using IBKRTrader.Core.Modularity;
|
||||
using IBKRTrader.Modules.Accounting.Persistence;
|
||||
using IBKRTrader.Modules.Accounting.Services;
|
||||
using IBKRTrader.Modules.Accounting.Ui;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -47,22 +46,12 @@ public sealed class AccountingModule : IModule
|
||||
services.AddHostedService(sp => sp.GetRequiredService<AccountingIngestService>());
|
||||
}
|
||||
|
||||
public void RegisterUi(IModuleUiHost host, IServiceProvider services)
|
||||
{
|
||||
host.RegisterView(new ModuleView
|
||||
{
|
||||
Id = "accounting.main",
|
||||
Title = "Accounting",
|
||||
Group = Name,
|
||||
Order = 400,
|
||||
CreateForm = () => new AccountingMainForm(
|
||||
services.GetRequiredService<ILedgerRepository>(),
|
||||
services.GetRequiredService<IIngestRunRepository>(),
|
||||
services.GetRequiredService<AccountingReportService>(),
|
||||
services.GetRequiredService<AccountingIngestService>(),
|
||||
services.GetRequiredService<LoggingService>())
|
||||
});
|
||||
}
|
||||
/// <summary>
|
||||
/// Bewusst leer: das Modulprojekt trägt keinen UI-Code mehr, damit es plattformneutral bleibt
|
||||
/// (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.
|
||||
/// </summary>
|
||||
public void RegisterUi(IModuleUiHost host, IServiceProvider services) { }
|
||||
|
||||
// DB-Schema wird extern per `dotnet ef database update` angewendet (keine Laufzeit-Migration).
|
||||
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<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>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- Modul trägt eigene WinForms-UI (Modul-Fenster) bei. -->
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ using IBKRTrader.Core.Workers;
|
||||
using IBKRTrader.Modules.CongressTrading.Database;
|
||||
using IBKRTrader.Modules.CongressTrading.Persistence.Ef;
|
||||
using IBKRTrader.Modules.CongressTrading.Scraper;
|
||||
using IBKRTrader.Modules.CongressTrading.UI;
|
||||
using IBKRTrader.Modules.CongressTrading.Workers;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@@ -48,21 +47,12 @@ public sealed class CongressTradingModule : IModule
|
||||
services.AddHostedService(sp => sp.GetRequiredService<CongressScrapeWorker>());
|
||||
}
|
||||
|
||||
public void RegisterUi(IModuleUiHost host, IServiceProvider services)
|
||||
{
|
||||
host.RegisterView(new ModuleView
|
||||
{
|
||||
Id = "congresstrading.main",
|
||||
Title = "Congress Trading",
|
||||
Group = Name,
|
||||
Order = 100,
|
||||
CreateForm = () => new CongressTradingForm(
|
||||
services.GetRequiredService<CongressRepository>(),
|
||||
services.GetRequiredService<WorkerEngine>(),
|
||||
services.GetRequiredService<IPortfolioService>(),
|
||||
services.GetRequiredService<LoggingService>())
|
||||
});
|
||||
}
|
||||
/// <summary>
|
||||
/// Bewusst leer: das Modulprojekt trägt keinen UI-Code mehr, damit es plattformneutral bleibt
|
||||
/// (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.
|
||||
/// </summary>
|
||||
public void RegisterUi(IModuleUiHost host, IServiceProvider services) { }
|
||||
|
||||
// DB-Schema wird extern per `dotnet ef database update` angewendet (keine Laufzeit-Migration).
|
||||
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<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>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- Modul trägt eigene WinForms-UI (Modul-Fenster) bei. -->
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<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>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- Modul trägt eigene WinForms-UI (Modul-Fenster) bei. -->
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ using IBKRTrader.Modules.Supervisor.Agent;
|
||||
using IBKRTrader.Modules.Supervisor.Counterfactual;
|
||||
using IBKRTrader.Modules.Supervisor.Persistence;
|
||||
using IBKRTrader.Modules.Supervisor.Services;
|
||||
using IBKRTrader.Modules.Supervisor.Ui;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -58,21 +57,12 @@ public sealed class SupervisorModule : IModule
|
||||
services.AddHostedService<Mcp.McpLightServer>();
|
||||
}
|
||||
|
||||
public void RegisterUi(IModuleUiHost host, IServiceProvider services)
|
||||
{
|
||||
host.RegisterView(new ModuleView
|
||||
{
|
||||
Id = "supervisor.main",
|
||||
Title = "Supervisor",
|
||||
Group = Name,
|
||||
Order = 300,
|
||||
CreateForm = () => new SupervisorMainForm(
|
||||
services.GetRequiredService<SupervisorAgent>(),
|
||||
services.GetRequiredService<DossierService>(),
|
||||
services.GetRequiredService<ISupervisorReportRepository>(),
|
||||
services.GetRequiredService<LoggingService>())
|
||||
});
|
||||
}
|
||||
/// <summary>
|
||||
/// Bewusst leer: das Modulprojekt trägt keinen UI-Code mehr, damit es plattformneutral bleibt
|
||||
/// (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.
|
||||
/// </summary>
|
||||
public void RegisterUi(IModuleUiHost host, IServiceProvider services) { }
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- net10.0-windows + WinForms, weil das Testprojekt die WinForms-Hauptassembly referenziert -->
|
||||
<TargetFramework>net10.0-windows</TargetFramework>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<!-- Plattformneutral: die Tests decken Core und Module ab, die beide keinen UI-Code mehr
|
||||
tragen. Die UI-Konstruktionsprüfung liegt jetzt beim Smoke-UI-Lauf der Shell. -->
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
|
||||
@@ -55,17 +55,17 @@ public class CongressTradingModuleTests
|
||||
}
|
||||
|
||||
[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();
|
||||
// CreateForm wird hier NICHT aufgerufen – daher genügt ein leerer Provider.
|
||||
var provider = new ServiceCollection().BuildServiceProvider();
|
||||
|
||||
new CongressTradingModule().RegisterUi(host, provider);
|
||||
|
||||
host.Views.Should().ContainSingle();
|
||||
host.Views[0].Id.Should().Be("congresstrading.main");
|
||||
host.Views[0].Title.Should().Be("Congress Trading");
|
||||
host.Views.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[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