L6: IBKRTrader.App.Avalonia -> IBKRTrader.App; letzte WinForms-Spuren raus
Build & Test / build (ubuntu-latest) (push) Canceled after 0s
Build & Test / build (windows-latest) (push) Canceled after 0s

Das Suffix ".Avalonia" gab es nur, weil daneben ein WinForms-IBKRTrader.App
stand. Das ist seit L5 weg, also faellt auch das Suffix. Git erkennt alle
Dateien als Umbenennung; Assembly, Wurzel-Namensraum und die
avares://-Ressourcen-URI sind mitgezogen.

Nebeneffekt der Umbenennung: die global::Avalonia-Qualifizierungen entfallen.
Sie waren noetig, weil der Namensraum IBKRTrader.App.Avalonia das
Avalonia-Paket verdeckt hat - ein Ueberbleibsel genau der Namensgebung, die
jetzt weg ist.

Inhaltlich falsch gewordene Aussagen berichtigt - das waren die eigentlichen
Ueberbleibsel, nicht die Kommentare:
- .agents/rules/grundregeln.md schrieb weiterhin "C# .NET 10 WinForms",
  RichTextBox-Logging, LauncherForm und PropertyGrid vor. Das ist die Regel,
  nach der kuenftig gearbeitet wird - sie haette die Portierung Stueck fuer
  Stueck rueckgaengig gemacht. Jetzt: Avalonia, keine Plattform-Suffixe, die
  11er-Pinnung mit Begruendung, dazu die beiden Regeln, die uns in L1b am
  meisten gekostet haben (UTC persistieren + AppTimeZone statt DateTime.Now;
  jede Formatierung mit ausdruecklichem IFormatProvider).
- Core: LogEntry ("wird in RichTextBox geschrieben"), IWorker/WorkerEngine/
  WorkerInfo ("DataGridView-Zeile"/"-Binding"), ModuleView ("die
  WinForms-Shell castet auf Form").
- Doku: ARCHITECTURE (Modul-Ui-Ordner, "designbare Forms mit Initialize"),
  KONZEPT-Modul-Accounting ("UI (WinForms, ein Fenster mit Tabs)").

BEWUSST STEHEN GEBLIEBEN sind die Kommentare, die WinForms nur als
Begruendung nennen - warum LoggingService ein Ereignis hat statt einer
RichTextBox, warum ModuleView Func<object> liefert, warum es benannte
Record-Zeilentypen gibt, warum die Einstellungsmaske aus Attributen entsteht.
Das ist die Herleitung des heutigen Entwurfs; ohne sie sieht spaeter jede
dieser Stellen nach Umstaendlichkeit ohne Grund aus.

KONZEPT-Linux-Portierung.md bekommt einen Statusvermerk: umgesetzt, die
Pfadangaben im Fundstellenverzeichnis beziehen sich auf den alten Aufbau.
Zwei Abweichungen von der Schaetzung sind dort festgehalten - der geringere
Aufwand dank der PolytraderSharp-Vorlage, und dass die dort empfohlene
InvariantGlobalization ein Fehler gewesen waere.

Verifiziert: Build 0 Fehler/0 Warnungen, 193 Tests gruen, Smoke-UI
konstruiert alle 7 Ansichten + Launcher + Dialog, Daemon-Prueflauf OK,
publish -r linux-x64 fuer beide Einstiegspunkte fehlerfrei.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-08-07 21:16:48 +02:00
co-authored by Claude Opus 5
parent d86a083438
commit c176b05ea1
45 changed files with 139 additions and 98 deletions
@@ -0,0 +1,197 @@
using System.Text.Json;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using IBKRTrader.App.Shell;
using IBKRTrader.App.ViewModels;
using IBKRTrader.Core.Analytics;
using IBKRTrader.Core.Logging;
using IBKRTrader.Core.Modularity;
using IBKRTrader.Core.Time;
using IBKRTrader.Modules.Supervisor.Agent;
using IBKRTrader.Modules.Supervisor.Persistence;
using IBKRTrader.Modules.Supervisor.Services;
namespace IBKRTrader.App.Views.Modules;
/// <summary>
/// Fenster des Supervisor-Moduls: Analyse (Chat mit dem Agenten, Tool-Aufrufe sichtbar),
/// Dossier-Browser, Berichte und Hinweise. Strikt read-only – kein Tool kann handeln oder schreiben.
///
/// <para>DB- und Agent-Zugriffe laufen NUR auf Nutzerinteraktion, nie im Konstruktor.</para>
/// </summary>
public partial class SupervisorWindow : Window
{
private readonly SupervisorAgent _agent;
private readonly DossierService _dossiers;
private readonly ISupervisorReportRepository _reports;
private readonly LoggingService _logger;
public SupervisorWindow(IModuleUiHost uiHost,
SupervisorAgent agent,
DossierService dossiers,
ISupervisorReportRepository reports,
LoggingService logger)
{
_agent = agent;
_dossiers = dossiers;
_reports = reports;
_logger = logger;
InitializeComponent();
WindowMenu.Wire(this.FindControl<Menu>("WindowMenuBar")!, uiHost, "supervisor.main");
var profiles = this.FindControl<ComboBox>("ProfileBox")!;
profiles.ItemsSource = SupervisorProfiles.All.Select(p => p.Name).ToList();
profiles.SelectedIndex = 0;
this.FindControl<Button>("AskButton")!.Click += async (_, _) => await AskAsync();
this.FindControl<Button>("LoadSignalsButton")!.Click += (_, _) => LoadSignals();
this.FindControl<Button>("LoadReportsButton")!.Click += (_, _) => LoadReports();
this.FindControl<DataGrid>("SignalsGrid")!.SelectionChanged += (_, _) => ShowSelectedDossier();
this.FindControl<SelectableTextBlock>("InfoText")!.Text = BuildInfoText();
SetStatus("Bereit.");
}
private void InitializeComponent() => AvaloniaXamlLoader.Load(this);
// ── Analyse ──────────────────────────────────────────────────────────────
private async Task AskAsync()
{
var questionBox = this.FindControl<TextBox>("QuestionBox")!;
var question = (questionBox.Text ?? "").Trim();
if (question.Length == 0) return;
var askButton = this.FindControl<Button>("AskButton")!;
var answer = this.FindControl<SelectableTextBlock>("AnswerText")!;
askButton.IsEnabled = false;
answer.Text = "";
var profile = SupervisorProfiles.ByName((string?)this.FindControl<ComboBox>("ProfileBox")!.SelectedItem ?? "");
// Der Agent meldet Tool-Aufrufe im Verlauf – die sollen live sichtbar sein, nicht erst
// am Ende. Progress<T> meldet auf dem erfassten Kontext; der Dispatcher-Wechsel bleibt
// trotzdem stehen, weil der Agent aus einem Worker-Thread berichten kann.
var progress = new Progress<string>(AppendLine);
try
{
SetStatus("Analyse läuft …");
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
});
SetStatus($"Analyse abgeschlossen ({result.ToolInvocations.Count} Tool-Aufrufe).");
}
catch (Exception ex)
{
AppendLine("");
AppendLine($"FEHLER: {ex.Message}");
_logger.Warn("Supervisor", $"Analyse fehlgeschlagen: {ex.Message}");
SetStatus($"Analyse fehlgeschlagen: {ex.Message}");
}
finally
{
askButton.IsEnabled = true;
}
}
private void AppendLine(string text)
{
if (!Dispatcher.UIThread.CheckAccess())
{
Dispatcher.UIThread.Post(() => AppendLine(text));
return;
}
var block = this.FindControl<SelectableTextBlock>("AnswerText")!;
block.Text += text + Environment.NewLine;
this.FindControl<ScrollViewer>("AnswerScroller")!.ScrollToEnd();
}
// ── Dossier ──────────────────────────────────────────────────────────────
private void LoadSignals()
{
try
{
var signals = _dossiers.RecentSignals(200);
this.FindControl<DataGrid>("SignalsGrid")!.ItemsSource = signals;
SetStatus($"{signals.Count} Signale geladen.");
}
catch (Exception ex)
{
_logger.Warn("Supervisor", $"Signale laden fehlgeschlagen: {ex.Message}");
SetStatus($"Signale laden fehlgeschlagen: {ex.Message}");
}
}
private void ShowSelectedDossier()
{
if (this.FindControl<DataGrid>("SignalsGrid")!.SelectedItem is not SignalSummary s) return;
var target = this.FindControl<SelectableTextBlock>("DossierText")!;
try { target.Text = DossierBuilder.ToMarkdown(_dossiers.BuildForSignal(s.SignalId)); }
catch (Exception ex) { target.Text = $"FEHLER: {ex.Message}"; }
}
// ── Berichte ─────────────────────────────────────────────────────────────
private void LoadReports()
{
try
{
var rows = _reports.GetRecent(100)
.Select(r => new SupervisorReportRow(
r.CreatedAt, r.Profile, r.Model, r.Question, r.ToolCallCount,
$"{r.PromptTokens} / {r.CompletionTokens}"))
.ToList();
this.FindControl<DataGrid>("ReportsGrid")!.ItemsSource = rows;
SetStatus($"{rows.Count} Berichte geladen.");
}
catch (Exception ex)
{
_logger.Warn("Supervisor", $"Berichte laden fehlgeschlagen: {ex.Message}");
SetStatus($"Berichte laden fehlgeschlagen: {ex.Message}");
}
}
// ── Hinweise ─────────────────────────────────────────────────────────────
private static string BuildInfoText()
{
var keySet = !string.IsNullOrEmpty(OpenRouterClient.DefaultApiKeyProvider());
return
"Supervisor – read-only Analyse und Forensik über alle Module." + Environment.NewLine + Environment.NewLine +
"OpenRouter-Key: env IBKRTRADER_OPENROUTER_KEY oder Datei 'openrouter.key' (gitignored)." + Environment.NewLine +
$" Status: {(keySet ? "gesetzt" : "NICHT gesetzt – Analyse nicht verfügbar")}" + Environment.NewLine + Environment.NewLine +
"Tagesbericht (opt-in): env IBKRTRADER_SUPERVISOR_DAILY = Stunde 0–23." + Environment.NewLine +
$" Die Stunde gilt in der Betriebszeitzone dieser Instanz ({AppTimeZone.CurrentId})." + Environment.NewLine +
"MCP-Light (opt-in): env IBKRTRADER_MCP_PORT = Port (bindet nur 127.0.0.1)." + Environment.NewLine + Environment.NewLine +
"Sicherheit: OpenRouter ist ein bewusst freigegebener externer Datenempfänger. Gesendet werden " +
"nur Analyse-Daten der Tools, niemals Secrets. Kein Tool kann handeln oder schreiben.";
}
private void SetStatus(string text) =>
this.FindControl<TextBlock>("StatusText")!.Text = $"{text} | {AppTimeZone.Now:HH:mm:ss}";
}