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));