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>
189 lines
8.2 KiB
C#
189 lines
8.2 KiB
C#
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}"); }
|
||
}
|
||
}
|