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:
@@ -0,0 +1,188 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using IBKRTrader.Core.Analytics;
|
||||
using IBKRTrader.Core.Logging;
|
||||
using IBKRTrader.Modules.Supervisor.Agent;
|
||||
using IBKRTrader.Modules.Supervisor.Persistence;
|
||||
using IBKRTrader.Modules.Supervisor.Services;
|
||||
|
||||
namespace IBKRTrader.UI.Views.Modules;
|
||||
|
||||
/// <summary>
|
||||
/// Fenster des Supervisor-Moduls: Analyse (Chat mit dem Agenten, Tool-Aufrufe sichtbar), Dossier-Browser,
|
||||
/// Berichte, Settings. Read-only. DB-/Agent-Zugriffe laufen NUR auf Nutzer-Interaktion (Smoke-UI-sicher).
|
||||
/// </summary>
|
||||
public sealed class SupervisorMainForm : Form
|
||||
{
|
||||
private readonly SupervisorAgent _agent;
|
||||
private readonly DossierService _dossiers;
|
||||
private readonly ISupervisorReportRepository _reports;
|
||||
private readonly LoggingService _logger;
|
||||
|
||||
private readonly ComboBox _profile = new() { DropDownStyle = ComboBoxStyle.DropDownList, Width = 160 };
|
||||
private readonly TextBox _question = new() { Dock = DockStyle.Fill, Multiline = true, Height = 60 };
|
||||
private readonly RichTextBox _answer = new() { Dock = DockStyle.Fill, ReadOnly = true, Font = new Font("Consolas", 9f) };
|
||||
private readonly Button _ask = new() { Text = "Fragen", Width = 100 };
|
||||
|
||||
private readonly DataGridView _signals = new() { Dock = DockStyle.Left, Width = 360, ReadOnly = true, AllowUserToAddRows = false, RowHeadersVisible = false, AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill };
|
||||
private readonly RichTextBox _dossier = new() { Dock = DockStyle.Fill, ReadOnly = true, Font = new Font("Consolas", 9f) };
|
||||
private readonly DataGridView _reportsGrid = new() { Dock = DockStyle.Fill, ReadOnly = true, AllowUserToAddRows = false, RowHeadersVisible = false, AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill };
|
||||
|
||||
public SupervisorMainForm(
|
||||
SupervisorAgent agent, DossierService dossiers, ISupervisorReportRepository reports, LoggingService logger)
|
||||
{
|
||||
_agent = agent;
|
||||
_dossiers = dossiers;
|
||||
_reports = reports;
|
||||
_logger = logger;
|
||||
|
||||
Text = "Supervisor";
|
||||
Width = 1080;
|
||||
Height = 720;
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
MinimumSize = new Size(800, 520);
|
||||
|
||||
foreach (var p in SupervisorProfiles.All) _profile.Items.Add(p.Name);
|
||||
_profile.SelectedIndex = 0;
|
||||
|
||||
BuildLayout();
|
||||
}
|
||||
|
||||
private void BuildLayout()
|
||||
{
|
||||
var tabs = new TabControl { Dock = DockStyle.Fill };
|
||||
|
||||
// ── Tab: Analyse ──
|
||||
var tabChat = new TabPage("Analyse");
|
||||
var top = new FlowLayoutPanel { Dock = DockStyle.Top, Height = 36, Padding = new Padding(8, 6, 8, 0) };
|
||||
top.Controls.Add(new Label { Text = "Profil", AutoSize = true, Margin = new Padding(0, 8, 4, 0) });
|
||||
top.Controls.Add(_profile);
|
||||
_ask.Click += async (_, _) => await AskAsync();
|
||||
var qPanel = new Panel { Dock = DockStyle.Top, Height = 70, Padding = new Padding(8, 2, 8, 4) };
|
||||
qPanel.Controls.Add(_question);
|
||||
var askPanel = new Panel { Dock = DockStyle.Top, Height = 34, Padding = new Padding(8, 0, 8, 0) };
|
||||
askPanel.Controls.Add(_ask);
|
||||
var answerPanel = new Panel { Dock = DockStyle.Fill, Padding = new Padding(8) };
|
||||
answerPanel.Controls.Add(_answer);
|
||||
tabChat.Controls.Add(answerPanel);
|
||||
tabChat.Controls.Add(askPanel);
|
||||
tabChat.Controls.Add(qPanel);
|
||||
tabChat.Controls.Add(top);
|
||||
|
||||
// ── Tab: Dossier-Browser ──
|
||||
var tabDossier = new TabPage("Dossier-Browser");
|
||||
_signals.SelectionChanged += (_, _) => ShowSelectedDossier();
|
||||
var refreshSignals = new Button { Text = "Signale laden", Dock = DockStyle.Top, Height = 28 };
|
||||
refreshSignals.Click += (_, _) => LoadSignals();
|
||||
var left = new Panel { Dock = DockStyle.Left, Width = 360 };
|
||||
left.Controls.Add(_signals);
|
||||
left.Controls.Add(refreshSignals);
|
||||
var dossierPanel = new Panel { Dock = DockStyle.Fill, Padding = new Padding(8) };
|
||||
dossierPanel.Controls.Add(_dossier);
|
||||
tabDossier.Controls.Add(dossierPanel);
|
||||
tabDossier.Controls.Add(left);
|
||||
|
||||
// ── Tab: Berichte ──
|
||||
var tabReports = new TabPage("Berichte");
|
||||
var refreshReports = new Button { Text = "Berichte laden", Dock = DockStyle.Top, Height = 28 };
|
||||
refreshReports.Click += (_, _) => LoadReports();
|
||||
tabReports.Controls.Add(_reportsGrid);
|
||||
tabReports.Controls.Add(refreshReports);
|
||||
|
||||
// ── Tab: Settings (Info) ──
|
||||
var tabSettings = new TabPage("Settings");
|
||||
tabSettings.Controls.Add(new Label
|
||||
{
|
||||
Dock = DockStyle.Fill, Padding = new Padding(16),
|
||||
Text =
|
||||
"Supervisor – read-only Analyse/Forensik über alle Module.\n\n" +
|
||||
"OpenRouter-Key: env IBKRTRADER_OPENROUTER_KEY oder Datei 'openrouter.key' (gitignored).\n" +
|
||||
$" Status: {(string.IsNullOrEmpty(OpenRouterClient.DefaultApiKeyProvider()) ? "NICHT gesetzt – Chat nicht verfügbar" : "gesetzt")}\n\n" +
|
||||
"Tagesbericht (opt-in): env IBKRTRADER_SUPERVISOR_DAILY = Stunde 0–23.\n" +
|
||||
"MCP-Light (opt-in): env IBKRTRADER_MCP_PORT = Port (bindet nur 127.0.0.1).\n\n" +
|
||||
"Sicherheit: OpenRouter ist ein bewusst freigegebener externer Datenempfänger. Es werden nur\n" +
|
||||
"Analyse-Daten der Tools gesendet, niemals Secrets. Kein Tool kann handeln oder schreiben."
|
||||
});
|
||||
|
||||
tabs.TabPages.AddRange(new[] { tabChat, tabDossier, tabReports, tabSettings });
|
||||
Controls.Add(tabs);
|
||||
}
|
||||
|
||||
// ── Analyse ──
|
||||
|
||||
private async Task AskAsync()
|
||||
{
|
||||
var question = _question.Text.Trim();
|
||||
if (string.IsNullOrEmpty(question)) return;
|
||||
|
||||
_ask.Enabled = false;
|
||||
_answer.Clear();
|
||||
var profile = SupervisorProfiles.ByName(_profile.Text);
|
||||
var progress = new Progress<string>(s => AppendLine(s));
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _agent.AskAsync(question, profile: profile, progress: progress);
|
||||
AppendLine("");
|
||||
AppendLine("─── Antwort ───");
|
||||
AppendLine(result.Answer);
|
||||
|
||||
_reports.Insert(new SupervisorReport
|
||||
{
|
||||
Profile = profile.Name,
|
||||
Model = SupervisorAgent.DefaultModel,
|
||||
Question = question,
|
||||
Answer = result.Answer,
|
||||
ToolCallsJson = JsonSerializer.Serialize(result.ToolInvocations.Select(i => new { i.Tool, i.Arguments })),
|
||||
ToolCallCount = result.ToolInvocations.Count,
|
||||
PromptTokens = result.PromptTokens,
|
||||
CompletionTokens = result.CompletionTokens
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppendLine("");
|
||||
AppendLine($"FEHLER: {ex.Message}");
|
||||
_logger.Warn("Supervisor", $"Analyse fehlgeschlagen: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_ask.Enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void AppendLine(string text)
|
||||
{
|
||||
if (_answer.InvokeRequired) { _answer.BeginInvoke(() => AppendLine(text)); return; }
|
||||
_answer.AppendText(text + "\n");
|
||||
_answer.ScrollToCaret();
|
||||
}
|
||||
|
||||
// ── Dossier ──
|
||||
|
||||
private void LoadSignals()
|
||||
{
|
||||
try { _signals.DataSource = _dossiers.RecentSignals(200); }
|
||||
catch (Exception ex) { _logger.Warn("Supervisor", $"Signale laden fehlgeschlagen: {ex.Message}"); }
|
||||
}
|
||||
|
||||
private void ShowSelectedDossier()
|
||||
{
|
||||
if (_signals.CurrentRow?.DataBoundItem is not SignalSummary s) return;
|
||||
try { _dossier.Text = DossierBuilder.ToMarkdown(_dossiers.BuildForSignal(s.SignalId)); }
|
||||
catch (Exception ex) { _dossier.Text = $"FEHLER: {ex.Message}"; }
|
||||
}
|
||||
|
||||
// ── Berichte ──
|
||||
|
||||
private void LoadReports()
|
||||
{
|
||||
try
|
||||
{
|
||||
_reportsGrid.DataSource = _reports.GetRecent(100)
|
||||
.Select(r => new { r.CreatedAt, r.Profile, r.Model, r.Question, r.ToolCallCount, r.PromptTokens, r.CompletionTokens })
|
||||
.ToList();
|
||||
}
|
||||
catch (Exception ex) { _logger.Warn("Supervisor", $"Berichte laden fehlgeschlagen: {ex.Message}"); }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user