Supervisor S-0c: Log Viewer im Terminal (JSONL menschenlesbar, CID-Forensik)

Terminal-Fenster jetzt mit TabControl (via Designer): Tab 'Live' = bisherige Anzeige,
Tab 'Log Viewer' = JSONL-Tagesdateien menschenlesbar aufbereitet:
- Filter: Datum (DateTimePicker), Level, CorrelationId (SignalId), Suchtext.
- Doppelklick auf eine CID laedt die komplette Signal-Kette ('zeig mir alles zu diesem Signal').
- LogJson.ParseLine (pur, Gegenstueck zu Format): tolerant gegen kaputte Zeilen (null statt Crash);
  Viewer ueberspringt sie. UI-Schutz bei sehr grossen Tagen (20k-Limit).

Tests: +5 (Parse-Roundtrip, kaputte Zeilen). Build 0 Fehler, 341 Tests gruen, --smoke-ui komplett gruen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-07-16 18:52:15 +02:00
co-authored by Claude Opus 4.8
parent bf3e012f8b
commit ca00977ea3
4 changed files with 369 additions and 7 deletions
+67
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Windows.Forms;
using PolyTraderSharp.Services;
@@ -33,6 +34,72 @@ namespace PolyTraderSharp.Ui.Views
miSelectAll.Click += (_, _) => rtbTerminal.SelectAll();
miCopyAll.Click += (_, _) => CopyAllToClipboard();
miClear.Click += (_, _) => rtbTerminal.Clear();
// Log Viewer (S-0): JSONL-Tagesdateien menschenlesbar, filterbar nach Level/CID/Text.
if (cbViewerLevel.Items.Count > 0) cbViewerLevel.SelectedIndex = 0;
btnViewerLoad.Click += (_, _) => LoadViewer();
tbViewerCid.KeyDown += (_, e) => { if (e.KeyCode == Keys.Enter) { LoadViewer(); e.SuppressKeyPress = true; } };
tbViewerText.KeyDown += (_, e) => { if (e.KeyCode == Keys.Enter) { LoadViewer(); e.SuppressKeyPress = true; } };
// Doppelklick auf eine CID: komplette Signal-Kette anzeigen.
dgvLogs.CellDoubleClick += (_, e) =>
{
if (e.RowIndex < 0) return;
if (dgvLogs.Rows[e.RowIndex].DataBoundItem is LogViewerRow row && !string.IsNullOrEmpty(row.Cid))
{
tbViewerCid.Text = row.Cid;
tbViewerText.Text = "";
LoadViewer();
}
};
}
// ===== Log Viewer =====
/// <summary>Anzeige-Zeile des Log Viewers (Bindung über DataPropertyName).</summary>
private sealed class LogViewerRow
{
public string Time { get; init; } = "";
public string Level { get; init; } = "";
public string Cid { get; init; } = "";
public string Message { get; init; } = "";
}
private void LoadViewer()
{
try
{
string path = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs",
$"{dtViewerDate.Value:yyyy-MM-dd}.jsonl");
if (!System.IO.File.Exists(path))
{
dgvLogs.DataSource = new System.ComponentModel.BindingList<LogViewerRow>();
lblViewerStatus.Text = "Keine JSONL-Datei für dieses Datum.";
return;
}
string levelFilter = cbViewerLevel.SelectedItem?.ToString() ?? "Alle";
string cidFilter = tbViewerCid.Text.Trim();
string textFilter = tbViewerText.Text.Trim();
var rows = new List<LogViewerRow>();
foreach (string line in System.IO.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;
rows.Add(new LogViewerRow { Time = p.Time, Level = p.Level, Cid = p.Cid, Message = p.Message });
if (rows.Count >= 20000) break; // UI-Schutz bei sehr großen Tagen
}
dgvLogs.DataSource = new System.ComponentModel.BindingList<LogViewerRow>(rows);
lblViewerStatus.Text = $"{rows.Count} Einträge.";
}
catch (Exception ex)
{
lblViewerStatus.Text = $"Fehler: {ex.Message}";
}
}
private void CopyAllToClipboard()