Files
PolyTraderSharp/src/PolyTrader.App.Avalonia/Views/TerminalWindow.axaml.cs
T
RichardandAntigravity AI 7f0b05e9ba Avalonia-Portierung verbleibender UI-Elemente (A1 bis A4) umgesetzt
Paritaet mit dem WinForms-Referenzstand (Tag winforms-final) hergestellt:

- A1 & A2 (Launcher): Account-Uebersicht DataGrid (inkl. Polymarket-Profil-Button via Launcher.LaunchUriAsync), Modul-KPI-Kacheln (7T/30T PnL & Winrate) und 3-Spalten-Live-Ueberblick (Auffaellige Trades 24h, Warnungen & Fehler heute aus den JSONL-Logs, Supervisor-KI Bericht). Periodische Aktualisierung alle 30 s eingebaut.
- A3 (Copytrading): Master-Trader Werkzeugleiste um 'Neu' (AddNewTrader) und 'Loeschen' (DeleteSelectedTrader mit DialogWindow.Confirm) erweitert.
- A4 (Terminal): Log-Kontextmenue fuer 'Kopieren' und 'Alles auswaehlen' an die Live-Log-Ausgabe angefuegt.

Verifiziert: Solution baut fehlerfrei, 450 Tests gruen, --smoke-ui test erfolgreich.

Co-Authored-By: Antigravity AI <antigravity@google.com>
2026-08-10 10:48:27 +02:00

227 lines
9.3 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;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Threading;
using PolyTrader.App.Avalonia.ViewModels;
using PolyTrader.Core.Modularity;
using PolyTraderSharp.Services;
namespace PolyTrader.App.Avalonia.Views
{
/// <summary>
/// Terminal-/Log-Ansicht. Live-Ausgabe des <see cref="TerminalLogger"/> plus Viewer für die
/// JSONL-Tagesdateien. Layout vollständig in TerminalWindow.axaml.
/// </summary>
public partial class TerminalWindow : Window
{
/// <summary>
/// Obergrenze der angezeigten Zeilen. Die WinForms-Fassung leerte bei Erreichen einer
/// Zeichengrenze das GESAMTE Terminal; hier werden stattdessen nur die ältesten Zeilen
/// verdrängt, der jüngste Verlauf bleibt also immer sichtbar.
/// </summary>
private const int MaxLines = 5000;
/// <summary>Höchstzahl je UI-Takt verarbeiteter Meldungen hält die Oberfläche flüssig.</summary>
private const int MaxPerTick = 500;
private static readonly IBrush BrushDefault = Brushes.WhiteSmoke;
private static readonly IBrush BrushError = Brushes.Tomato;
private static readonly IBrush BrushWarning = Brushes.Gold;
private static readonly IBrush BrushTrade = Brushes.LightGreen;
private static readonly IBrush BrushReasoning = Brushes.Orange;
private readonly ConcurrentQueue<LogMessageEventArgs> _queue = new();
private readonly ObservableCollection<LogLine> _lines = new();
private readonly ObservableCollection<LogViewerRow> _viewerRows = new();
private readonly DispatcherTimer _timer = new() { Interval = TimeSpan.FromMilliseconds(250) };
private TerminalLogger? _logger;
private EventHandler<LogMessageEventArgs>? _logHandler;
private bool _autoScroll = true;
public TerminalWindow() => AvaloniaXamlLoader.Load(this);
public TerminalWindow(IModuleUiHost host, TerminalLogger logger) : this()
{
this.FindControl<Controls.WindowMenuBar>("menuBar")!.Attach(host, "core.terminal", this);
_logger = logger;
this.FindControl<ItemsControl>("logLines")!.ItemsSource = _lines;
this.FindControl<DataGrid>("gridLogs")!.ItemsSource = _viewerRows;
var levels = new[] { "Alle" }.Concat(Enum.GetNames<LogLevel>()).ToArray();
var cbLevel = this.FindControl<ComboBox>("cbLogLevel")!;
var cbViewerLevel = this.FindControl<ComboBox>("cbViewerLevel")!;
cbLevel.ItemsSource = levels;
cbViewerLevel.ItemsSource = levels;
cbLevel.SelectedIndex = 0;
cbViewerLevel.SelectedIndex = 0;
this.FindControl<CalendarDatePicker>("dtViewerDate")!.SelectedDate = DateTime.Now;
this.FindControl<Button>("btnAutoscroll")!.Click += (_, _) => ToggleAutoscroll();
this.FindControl<Button>("btnClear")!.Click += (_, _) => { _lines.Clear(); UpdateLiveStatus(); };
this.FindControl<Button>("btnCopyAll")!.Click += async (_, _) => await CopyAllAsync();
this.FindControl<Button>("btnViewerLoad")!.Click += (_, _) => LoadViewer();
// Doppelklick auf eine Zeile: nach deren CID filtern (komplette Signal-Kette ansehen).
this.FindControl<DataGrid>("gridLogs")!.DoubleTapped += (_, _) =>
{
if (this.FindControl<DataGrid>("gridLogs")!.SelectedItem is LogViewerRow row
&& !string.IsNullOrEmpty(row.Cid))
{
this.FindControl<TextBox>("tbViewerCid")!.Text = row.Cid;
this.FindControl<TextBox>("tbViewerText")!.Text = string.Empty;
LoadViewer();
}
};
// Jüngste Historie vorladen, damit das Fenster beim Öffnen nicht leer ist.
foreach (var e in _logger.GetHistory(TimeSpan.FromMinutes(10)))
_queue.Enqueue(e);
_logHandler = (_, e) => _queue.Enqueue(e);
_logger.OnLogMessage += _logHandler;
_timer.Tick += (_, _) => ProcessQueue();
_timer.Start();
Closed += (_, _) =>
{
_timer.Stop();
if (_logger != null && _logHandler != null) _logger.OnLogMessage -= _logHandler;
};
ProcessQueue();
}
// ===== Live-Ausgabe =====
private void ToggleAutoscroll()
{
_autoScroll = !_autoScroll;
var btn = this.FindControl<Button>("btnAutoscroll")!;
btn.Content = _autoScroll ? "Stop Autoscroll" : "Start Autoscroll";
btn.Background = new SolidColorBrush(_autoScroll ? Colors.LightGreen : Colors.IndianRed);
}
private void ProcessQueue()
{
if (_queue.IsEmpty) return;
string filter = this.FindControl<ComboBox>("cbLogLevel")!.SelectedItem as string ?? "Alle";
int processed = 0;
bool appended = false;
while (processed < MaxPerTick && _queue.TryDequeue(out var e))
{
processed++;
if (filter != "Alle" && e.Level.ToString() != filter) continue;
// Anzeige-Zeitzone aus den Server-Settings (siehe AppTimeZone) nicht mehr fest
// gegen eine Windows-Zeitzonen-ID gerechnet.
DateTime t = AppTimeZone.ToDisplay(e.Timestamp);
_lines.Add(new LogLine
{
Text = $"[{t:HH:mm:ss}] [{e.Level}] {e.Message}",
Brush = e.Level switch
{
LogLevel.Error => BrushError,
LogLevel.Warning => BrushWarning,
LogLevel.Trade => BrushTrade,
LogLevel.TradeReasoning => BrushReasoning,
_ => BrushDefault
}
});
appended = true;
}
// Ringpuffer: nur die aeltesten Zeilen verdraengen statt alles zu verwerfen.
while (_lines.Count > MaxLines) _lines.RemoveAt(0);
if (appended)
{
UpdateLiveStatus();
if (_autoScroll) this.FindControl<ScrollViewer>("logScroll")!.ScrollToEnd();
}
}
private void UpdateLiveStatus() =>
this.FindControl<TextBlock>("lblLiveStatus")!.Text =
$"{_lines.Count} Zeilen (max. {MaxLines}) · vollständige Logs im Ordner /Logs";
private async System.Threading.Tasks.Task CopyAllAsync()
{
try
{
var sb = new StringBuilder();
foreach (var l in _lines) sb.AppendLine(l.Text);
var clipboard = GetTopLevel(this)?.Clipboard;
if (clipboard != null) await clipboard.SetTextAsync(sb.ToString());
}
catch
{
// Zwischenablage kann kurzzeitig belegt sein bewusst ignorieren, wie bisher.
}
}
public async void OnContextMenuCopyClick(object? sender, RoutedEventArgs e) =>
await CopyAllAsync();
public async void OnContextMenuSelectAllClick(object? sender, RoutedEventArgs e) =>
await CopyAllAsync();
// ===== Log Viewer (JSONL-Tagesdateien) =====
private void LoadViewer()
{
var status = this.FindControl<TextBlock>("lblViewerStatus")!;
try
{
DateTime day = this.FindControl<CalendarDatePicker>("dtViewerDate")!.SelectedDate ?? DateTime.Now;
string path = Path.Combine(AppContext.BaseDirectory, "Logs", $"{day:yyyy-MM-dd}.jsonl");
_viewerRows.Clear();
if (!File.Exists(path))
{
status.Text = "Keine JSONL-Datei für dieses Datum.";
return;
}
string levelFilter = this.FindControl<ComboBox>("cbViewerLevel")!.SelectedItem as string ?? "Alle";
string cidFilter = this.FindControl<TextBox>("tbViewerCid")!.Text?.Trim() ?? string.Empty;
string textFilter = this.FindControl<TextBox>("tbViewerText")!.Text?.Trim() ?? string.Empty;
int count = 0;
foreach (string line in File.ReadLines(path))
{
var p = LogJson.ParseLine(line);
if (p == null) continue;
if (levelFilter != "Alle" && p.Level != levelFilter) continue;
if (cidFilter.Length > 0 && !p.Cid.Contains(cidFilter, StringComparison.OrdinalIgnoreCase)) continue;
if (textFilter.Length > 0 && !p.Message.Contains(textFilter, StringComparison.OrdinalIgnoreCase)) continue;
_viewerRows.Add(new LogViewerRow { Time = p.Time, Level = p.Level, Cid = p.Cid, Message = p.Message });
if (++count >= 20000) break; // Schutz bei sehr grossen Tagen
}
status.Text = $"{_viewerRows.Count} Einträge.";
}
catch (Exception ex)
{
status.Text = $"Fehler: {ex.Message}";
}
}
}
}