Files
IBKRTrader/UI/Views/Modules/SupervisorMainForm.cs
T
RichardandClaude Opus 5 123f38ab6f 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>
2026-08-06 22:38:01 +02:00

189 lines
8.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 023.\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}"); }
}
}