diff --git a/Ui/Views/TerminalView.Designer.cs b/Ui/Views/TerminalView.Designer.cs
index b98cc96..b854f6b 100644
--- a/Ui/Views/TerminalView.Designer.cs
+++ b/Ui/Views/TerminalView.Designer.cs
@@ -19,6 +19,8 @@ namespace PolyTraderSharp.Ui.Views
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
+ this.tabControlTerminal = new System.Windows.Forms.TabControl();
+ this.tabLive = new System.Windows.Forms.TabPage();
this.pnlTop = new System.Windows.Forms.Panel();
this.btnAutoscroll = new System.Windows.Forms.Button();
this.cbLogLevel = new System.Windows.Forms.ComboBox();
@@ -30,19 +32,64 @@ namespace PolyTraderSharp.Ui.Views
this.miCopyAll = new System.Windows.Forms.ToolStripMenuItem();
this.miSep1 = new System.Windows.Forms.ToolStripSeparator();
this.miClear = new System.Windows.Forms.ToolStripMenuItem();
+ this.tabViewer = new System.Windows.Forms.TabPage();
+ this.dgvLogs = new System.Windows.Forms.DataGridView();
+ this.colLogTime = new System.Windows.Forms.DataGridViewTextBoxColumn();
+ this.colLogLevel = new System.Windows.Forms.DataGridViewTextBoxColumn();
+ this.colLogCid = new System.Windows.Forms.DataGridViewTextBoxColumn();
+ this.colLogMsg = new System.Windows.Forms.DataGridViewTextBoxColumn();
+ this.pnlViewerTop = new System.Windows.Forms.Panel();
+ this.lblVDatum = new System.Windows.Forms.Label();
+ this.dtViewerDate = new System.Windows.Forms.DateTimePicker();
+ this.lblVLevel = new System.Windows.Forms.Label();
+ this.cbViewerLevel = new System.Windows.Forms.ComboBox();
+ this.lblVCid = new System.Windows.Forms.Label();
+ this.tbViewerCid = new System.Windows.Forms.TextBox();
+ this.lblVText = new System.Windows.Forms.Label();
+ this.tbViewerText = new System.Windows.Forms.TextBox();
+ this.btnViewerLoad = new System.Windows.Forms.Button();
+ this.lblViewerStatus = new System.Windows.Forms.Label();
+ this.tabControlTerminal.SuspendLayout();
+ this.tabLive.SuspendLayout();
this.pnlTop.SuspendLayout();
this.contextMenuTerminal.SuspendLayout();
+ this.tabViewer.SuspendLayout();
+ ((System.ComponentModel.ISupportInitialize)(this.dgvLogs)).BeginInit();
+ this.pnlViewerTop.SuspendLayout();
this.SuspendLayout();
//
+ // tabControlTerminal
+ //
+ this.tabControlTerminal.Controls.Add(this.tabLive);
+ this.tabControlTerminal.Controls.Add(this.tabViewer);
+ this.tabControlTerminal.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.tabControlTerminal.Location = new System.Drawing.Point(0, 0);
+ this.tabControlTerminal.Name = "tabControlTerminal";
+ this.tabControlTerminal.SelectedIndex = 0;
+ this.tabControlTerminal.Size = new System.Drawing.Size(1000, 650);
+ this.tabControlTerminal.TabIndex = 0;
+ //
+ // tabLive
+ //
+ this.tabLive.Controls.Add(this.rtbTerminal);
+ this.tabLive.Controls.Add(this.pnlTop);
+ this.tabLive.Location = new System.Drawing.Point(4, 24);
+ this.tabLive.Name = "tabLive";
+ this.tabLive.Padding = new System.Windows.Forms.Padding(3);
+ this.tabLive.Size = new System.Drawing.Size(992, 622);
+ this.tabLive.TabIndex = 0;
+ this.tabLive.Text = "Live";
+ this.tabLive.UseVisualStyleBackColor = true;
+ //
// pnlTop
//
this.pnlTop.Controls.Add(this.btnAutoscroll);
this.pnlTop.Controls.Add(this.cbLogLevel);
this.pnlTop.Controls.Add(this.lblFilter);
this.pnlTop.Dock = System.Windows.Forms.DockStyle.Top;
- this.pnlTop.Location = new System.Drawing.Point(0, 0);
+ this.pnlTop.Location = new System.Drawing.Point(3, 3);
this.pnlTop.Name = "pnlTop";
- this.pnlTop.Size = new System.Drawing.Size(900, 36);
+ this.pnlTop.Size = new System.Drawing.Size(986, 36);
this.pnlTop.TabIndex = 0;
//
// btnAutoscroll
@@ -83,15 +130,15 @@ namespace PolyTraderSharp.Ui.Views
// rtbTerminal
//
this.rtbTerminal.BackColor = System.Drawing.Color.Black;
+ this.rtbTerminal.ContextMenuStrip = this.contextMenuTerminal;
this.rtbTerminal.Dock = System.Windows.Forms.DockStyle.Fill;
this.rtbTerminal.ForeColor = System.Drawing.Color.White;
- this.rtbTerminal.Location = new System.Drawing.Point(0, 36);
+ this.rtbTerminal.Location = new System.Drawing.Point(3, 39);
this.rtbTerminal.Name = "rtbTerminal";
this.rtbTerminal.ReadOnly = true;
- this.rtbTerminal.Size = new System.Drawing.Size(900, 464);
+ this.rtbTerminal.Size = new System.Drawing.Size(986, 580);
this.rtbTerminal.TabIndex = 1;
this.rtbTerminal.Text = "";
- this.rtbTerminal.ContextMenuStrip = this.contextMenuTerminal;
//
// contextMenuTerminal
//
@@ -129,23 +176,206 @@ namespace PolyTraderSharp.Ui.Views
this.miClear.Name = "miClear";
this.miClear.Text = "Terminal leeren";
//
+ // tabViewer
+ //
+ this.tabViewer.Controls.Add(this.dgvLogs);
+ this.tabViewer.Controls.Add(this.pnlViewerTop);
+ this.tabViewer.Location = new System.Drawing.Point(4, 24);
+ this.tabViewer.Name = "tabViewer";
+ this.tabViewer.Padding = new System.Windows.Forms.Padding(3);
+ this.tabViewer.Size = new System.Drawing.Size(992, 622);
+ this.tabViewer.TabIndex = 1;
+ this.tabViewer.Text = "Log Viewer";
+ this.tabViewer.UseVisualStyleBackColor = true;
+ //
+ // dgvLogs
+ //
+ this.dgvLogs.AllowUserToAddRows = false;
+ this.dgvLogs.AllowUserToDeleteRows = false;
+ this.dgvLogs.AutoGenerateColumns = false;
+ this.dgvLogs.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
+ this.dgvLogs.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
+ this.colLogTime,
+ this.colLogLevel,
+ this.colLogCid,
+ this.colLogMsg});
+ this.dgvLogs.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.dgvLogs.Location = new System.Drawing.Point(3, 41);
+ this.dgvLogs.Name = "dgvLogs";
+ this.dgvLogs.ReadOnly = true;
+ this.dgvLogs.RowHeadersVisible = false;
+ this.dgvLogs.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect;
+ this.dgvLogs.Size = new System.Drawing.Size(986, 578);
+ this.dgvLogs.TabIndex = 1;
+ //
+ // colLogTime
+ //
+ this.colLogTime.DataPropertyName = "Time";
+ this.colLogTime.HeaderText = "Zeit";
+ this.colLogTime.Name = "colLogTime";
+ this.colLogTime.ReadOnly = true;
+ this.colLogTime.Width = 140;
+ //
+ // colLogLevel
+ //
+ this.colLogLevel.DataPropertyName = "Level";
+ this.colLogLevel.HeaderText = "Level";
+ this.colLogLevel.Name = "colLogLevel";
+ this.colLogLevel.ReadOnly = true;
+ this.colLogLevel.Width = 110;
+ //
+ // colLogCid
+ //
+ this.colLogCid.DataPropertyName = "Cid";
+ this.colLogCid.HeaderText = "CorrelationId";
+ this.colLogCid.Name = "colLogCid";
+ this.colLogCid.ReadOnly = true;
+ this.colLogCid.Width = 130;
+ //
+ // colLogMsg
+ //
+ this.colLogMsg.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
+ this.colLogMsg.DataPropertyName = "Message";
+ this.colLogMsg.HeaderText = "Nachricht";
+ this.colLogMsg.Name = "colLogMsg";
+ this.colLogMsg.ReadOnly = true;
+ //
+ // pnlViewerTop
+ //
+ this.pnlViewerTop.Controls.Add(this.lblViewerStatus);
+ this.pnlViewerTop.Controls.Add(this.btnViewerLoad);
+ this.pnlViewerTop.Controls.Add(this.tbViewerText);
+ this.pnlViewerTop.Controls.Add(this.lblVText);
+ this.pnlViewerTop.Controls.Add(this.tbViewerCid);
+ this.pnlViewerTop.Controls.Add(this.lblVCid);
+ this.pnlViewerTop.Controls.Add(this.cbViewerLevel);
+ this.pnlViewerTop.Controls.Add(this.lblVLevel);
+ this.pnlViewerTop.Controls.Add(this.dtViewerDate);
+ this.pnlViewerTop.Controls.Add(this.lblVDatum);
+ this.pnlViewerTop.Dock = System.Windows.Forms.DockStyle.Top;
+ this.pnlViewerTop.Location = new System.Drawing.Point(3, 3);
+ this.pnlViewerTop.Name = "pnlViewerTop";
+ this.pnlViewerTop.Size = new System.Drawing.Size(986, 38);
+ this.pnlViewerTop.TabIndex = 0;
+ //
+ // lblVDatum
+ //
+ this.lblVDatum.AutoSize = true;
+ this.lblVDatum.Location = new System.Drawing.Point(8, 11);
+ this.lblVDatum.Name = "lblVDatum";
+ this.lblVDatum.Size = new System.Drawing.Size(46, 15);
+ this.lblVDatum.TabIndex = 0;
+ this.lblVDatum.Text = "Datum:";
+ //
+ // dtViewerDate
+ //
+ this.dtViewerDate.Format = System.Windows.Forms.DateTimePickerFormat.Short;
+ this.dtViewerDate.Location = new System.Drawing.Point(58, 7);
+ this.dtViewerDate.Name = "dtViewerDate";
+ this.dtViewerDate.Size = new System.Drawing.Size(110, 23);
+ this.dtViewerDate.TabIndex = 1;
+ //
+ // lblVLevel
+ //
+ this.lblVLevel.AutoSize = true;
+ this.lblVLevel.Location = new System.Drawing.Point(180, 11);
+ this.lblVLevel.Name = "lblVLevel";
+ this.lblVLevel.Size = new System.Drawing.Size(38, 15);
+ this.lblVLevel.TabIndex = 2;
+ this.lblVLevel.Text = "Level:";
+ //
+ // cbViewerLevel
+ //
+ this.cbViewerLevel.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+ this.cbViewerLevel.Items.AddRange(new object[] {
+ "Alle",
+ "Debug",
+ "Info",
+ "Warning",
+ "Error",
+ "Trade",
+ "TradeReasoning"});
+ this.cbViewerLevel.Location = new System.Drawing.Point(222, 7);
+ this.cbViewerLevel.Name = "cbViewerLevel";
+ this.cbViewerLevel.Size = new System.Drawing.Size(140, 23);
+ this.cbViewerLevel.TabIndex = 3;
+ //
+ // lblVCid
+ //
+ this.lblVCid.AutoSize = true;
+ this.lblVCid.Location = new System.Drawing.Point(374, 11);
+ this.lblVCid.Name = "lblVCid";
+ this.lblVCid.Size = new System.Drawing.Size(29, 15);
+ this.lblVCid.TabIndex = 4;
+ this.lblVCid.Text = "CID:";
+ //
+ // tbViewerCid
+ //
+ this.tbViewerCid.Location = new System.Drawing.Point(408, 7);
+ this.tbViewerCid.Name = "tbViewerCid";
+ this.tbViewerCid.PlaceholderText = "SignalId …";
+ this.tbViewerCid.Size = new System.Drawing.Size(150, 23);
+ this.tbViewerCid.TabIndex = 5;
+ //
+ // lblVText
+ //
+ this.lblVText.AutoSize = true;
+ this.lblVText.Location = new System.Drawing.Point(570, 11);
+ this.lblVText.Name = "lblVText";
+ this.lblVText.Size = new System.Drawing.Size(31, 15);
+ this.lblVText.TabIndex = 6;
+ this.lblVText.Text = "Text:";
+ //
+ // tbViewerText
+ //
+ this.tbViewerText.Location = new System.Drawing.Point(606, 7);
+ this.tbViewerText.Name = "tbViewerText";
+ this.tbViewerText.PlaceholderText = "Suchtext …";
+ this.tbViewerText.Size = new System.Drawing.Size(180, 23);
+ this.tbViewerText.TabIndex = 7;
+ //
+ // btnViewerLoad
+ //
+ this.btnViewerLoad.Location = new System.Drawing.Point(796, 6);
+ this.btnViewerLoad.Name = "btnViewerLoad";
+ this.btnViewerLoad.Size = new System.Drawing.Size(90, 26);
+ this.btnViewerLoad.TabIndex = 8;
+ this.btnViewerLoad.Text = "Laden";
+ this.btnViewerLoad.UseVisualStyleBackColor = true;
+ //
+ // lblViewerStatus
+ //
+ this.lblViewerStatus.AutoSize = true;
+ this.lblViewerStatus.Location = new System.Drawing.Point(895, 11);
+ this.lblViewerStatus.Name = "lblViewerStatus";
+ this.lblViewerStatus.Size = new System.Drawing.Size(23, 15);
+ this.lblViewerStatus.TabIndex = 9;
+ this.lblViewerStatus.Text = "—";
+ //
// TerminalView
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
- this.Controls.Add(this.rtbTerminal);
- this.Controls.Add(this.pnlTop);
+ this.Controls.Add(this.tabControlTerminal);
this.ClientSize = new System.Drawing.Size(1000, 650);
this.Name = "TerminalView";
this.Text = "Terminal / Logs";
+ this.tabControlTerminal.ResumeLayout(false);
+ this.tabLive.ResumeLayout(false);
this.pnlTop.ResumeLayout(false);
this.pnlTop.PerformLayout();
this.contextMenuTerminal.ResumeLayout(false);
+ this.tabViewer.ResumeLayout(false);
+ ((System.ComponentModel.ISupportInitialize)(this.dgvLogs)).EndInit();
+ this.pnlViewerTop.ResumeLayout(false);
+ this.pnlViewerTop.PerformLayout();
this.ResumeLayout(false);
}
#endregion
+ private System.Windows.Forms.TabControl tabControlTerminal;
+ private System.Windows.Forms.TabPage tabLive;
private System.Windows.Forms.Panel pnlTop;
private System.Windows.Forms.Label lblFilter;
private System.Windows.Forms.ComboBox cbLogLevel;
@@ -157,5 +387,22 @@ namespace PolyTraderSharp.Ui.Views
private System.Windows.Forms.ToolStripMenuItem miCopyAll;
private System.Windows.Forms.ToolStripSeparator miSep1;
private System.Windows.Forms.ToolStripMenuItem miClear;
+ private System.Windows.Forms.TabPage tabViewer;
+ private System.Windows.Forms.Panel pnlViewerTop;
+ private System.Windows.Forms.Label lblVDatum;
+ private System.Windows.Forms.DateTimePicker dtViewerDate;
+ private System.Windows.Forms.Label lblVLevel;
+ private System.Windows.Forms.ComboBox cbViewerLevel;
+ private System.Windows.Forms.Label lblVCid;
+ private System.Windows.Forms.TextBox tbViewerCid;
+ private System.Windows.Forms.Label lblVText;
+ private System.Windows.Forms.TextBox tbViewerText;
+ private System.Windows.Forms.Button btnViewerLoad;
+ private System.Windows.Forms.Label lblViewerStatus;
+ private System.Windows.Forms.DataGridView dgvLogs;
+ private System.Windows.Forms.DataGridViewTextBoxColumn colLogTime;
+ private System.Windows.Forms.DataGridViewTextBoxColumn colLogLevel;
+ private System.Windows.Forms.DataGridViewTextBoxColumn colLogCid;
+ private System.Windows.Forms.DataGridViewTextBoxColumn colLogMsg;
}
}
diff --git a/Ui/Views/TerminalView.cs b/Ui/Views/TerminalView.cs
index 7cda319..d72ef8b 100644
--- a/Ui/Views/TerminalView.cs
+++ b/Ui/Views/TerminalView.cs
@@ -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 =====
+
+ /// Anzeige-Zeile des Log Viewers (Bindung über DataPropertyName).
+ 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();
+ 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();
+ 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(rows);
+ lblViewerStatus.Text = $"{rows.Count} Einträge.";
+ }
+ catch (Exception ex)
+ {
+ lblViewerStatus.Text = $"Fehler: {ex.Message}";
+ }
}
private void CopyAllToClipboard()
diff --git a/src/PolyTrader.Core/Services/TerminalLogger.cs b/src/PolyTrader.Core/Services/TerminalLogger.cs
index a7fbed8..6aa5588 100644
--- a/src/PolyTrader.Core/Services/TerminalLogger.cs
+++ b/src/PolyTrader.Core/Services/TerminalLogger.cs
@@ -47,6 +47,32 @@ namespace PolyTraderSharp.Services
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
});
}
+
+ /// Geparste JSONL-Zeile für den Log Viewer.
+ public sealed record ParsedLogLine(string Time, string Level, string Cid, string Message);
+
+ ///
+ /// Parst eine JSONL-Zeile (Gegenstück zu ). Liefert null bei leeren/
+ /// beschädigten Zeilen (der Viewer überspringt sie, statt zu brechen).
+ ///
+ public static ParsedLogLine? ParseLine(string line)
+ {
+ if (string.IsNullOrWhiteSpace(line)) return null;
+ try
+ {
+ using var doc = System.Text.Json.JsonDocument.Parse(line);
+ var root = doc.RootElement;
+ string ts = root.TryGetProperty("ts", out var t) ? t.GetString() ?? "" : "";
+ string level = root.TryGetProperty("level", out var l) ? l.GetString() ?? "" : "";
+ string cid = root.TryGetProperty("cid", out var c) ? c.GetString() ?? "" : "";
+ string msg = root.TryGetProperty("msg", out var m) ? m.GetString() ?? "" : "";
+ return new ParsedLogLine(ts, level, cid, msg);
+ }
+ catch (System.Text.Json.JsonException)
+ {
+ return null;
+ }
+ }
}
public class TerminalLogger
diff --git a/tests/PolyTrader.Tests/AnalysisJournalTests.cs b/tests/PolyTrader.Tests/AnalysisJournalTests.cs
index 1c83d78..656df28 100644
--- a/tests/PolyTrader.Tests/AnalysisJournalTests.cs
+++ b/tests/PolyTrader.Tests/AnalysisJournalTests.cs
@@ -37,6 +37,28 @@ namespace PolyTrader.Tests
Assert.False(doc.RootElement.TryGetProperty("cid", out _));
}
+ [Fact]
+ public void LogJson_parse_roundtrips_format()
+ {
+ var e = new LogMessageEventArgs("Nachricht äöü", LogLevel.Warning, "sig-9");
+ var p = LogJson.ParseLine(LogJson.Format(e));
+
+ Assert.NotNull(p);
+ Assert.Equal("Warning", p!.Level);
+ Assert.Equal("sig-9", p.Cid);
+ Assert.Equal("Nachricht äöü", p.Message);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(" ")]
+ [InlineData("kein json")]
+ [InlineData("{kaputt")]
+ public void LogJson_parse_returns_null_for_broken_lines(string line)
+ {
+ Assert.Null(LogJson.ParseLine(line));
+ }
+
// ----- Repos (EF InMemory) -----
private static InMemoryContextFactory Factory() => new(o => new CoreDbContext(o));