From 0780f72101ff3a17ab47db1ecda0acfcb4037d84 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Fri, 8 May 2026 17:21:33 +0300 Subject: [PATCH 1/3] feat(compare): Improve compare UI with accordion sections --- .../Resources/css/debugprobe.css | 54 ++++++++++++ .../Resources/html/details.html | 8 +- .../Resources/js/debugprobe-compare.js | 84 ++++++++++++++----- 3 files changed, 122 insertions(+), 24 deletions(-) diff --git a/DebugProbe.AspNetCore/Resources/css/debugprobe.css b/DebugProbe.AspNetCore/Resources/css/debugprobe.css index 246ad4c..b2f0581 100644 --- a/DebugProbe.AspNetCore/Resources/css/debugprobe.css +++ b/DebugProbe.AspNetCore/Resources/css/debugprobe.css @@ -124,6 +124,9 @@ pre { .json-compare > div { flex: 1; min-width: 0; + padding: 10px; + border-bottom: 1px solid #eee; + text-align: left; } /* ========================= @@ -194,3 +197,54 @@ pre { color: #e74c3c; font-weight: bold; } + + +/* ========================= + Accordion +========================= */ + +.accordion-section { + background: #fff; + border-radius: 8px; + margin-bottom: 14px; + overflow: hidden; + border: 1px solid #e9e9e9; +} + +.accordion-header { + /*min-height: 56px;*/ + display: flex; + justify-content: space-between; + align-items: center; + padding: 14px 10px; + cursor: pointer; + user-select: none; + background: #fafafa; + border-bottom: 1px solid #eee; +} + + .accordion-header:hover { + background: #f3f3f3; + } + +.accordion-title { + font-size: 16px; + font-weight: 600; +} + +.accordion-meta { + font-size: 22px; + color: #777; + font-weight: 300; + width: 24px; + text-align: center; + flex-shrink: 0; +} + +.accordion-body { + display: none; +} + + .accordion-body.open { + display: block; + } \ No newline at end of file diff --git a/DebugProbe.AspNetCore/Resources/html/details.html b/DebugProbe.AspNetCore/Resources/html/details.html index 6f8210c..302a11e 100644 --- a/DebugProbe.AspNetCore/Resources/html/details.html +++ b/DebugProbe.AspNetCore/Resources/html/details.html @@ -31,9 +31,11 @@

Response Body

Compare

- - - +
+ + + +
diff --git a/DebugProbe.AspNetCore/Resources/js/debugprobe-compare.js b/DebugProbe.AspNetCore/Resources/js/debugprobe-compare.js index 825c344..f0d1d0e 100644 --- a/DebugProbe.AspNetCore/Resources/js/debugprobe-compare.js +++ b/DebugProbe.AspNetCore/Resources/js/debugprobe-compare.js @@ -1,4 +1,4 @@ -window.runCompare = async function () { +window.runCompare = async function () { const id = window.location.pathname.split('/').pop(); const base = document.getElementById('baseUrl').value.trim(); const remoteId = document.getElementById('compareId').value.trim(); @@ -32,24 +32,33 @@ function setCompareResult(html) { function renderCompare(result) { return [ - renderSection('Environment', [ - { field: 'Environment', local: result.environment?.local, remote: result.environment?.remote }, - { field: 'Culture', local: result.culture?.local, remote: result.culture?.remote } - ]), - renderSection('Overview', [ - { field: 'Method', local: result.method?.local, remote: result.method?.remote }, - { field: 'Path', local: result.path?.local, remote: result.path?.remote }, - { field: 'Status', local: result.status?.local, remote: result.status?.remote }, - { field: 'Request Time', local: result.requestTime?.local, remote: result.requestTime?.remote } - ]), - '

Request

', - renderSideBySideJson(result.requestBody), - '

Response

', - renderSideBySideJson(result.responseBody) + renderAccordionSection( + 'Environment', + renderSectionRows([ + { field: 'Environment', local: result.environment?.local, remote: result.environment?.remote }, + { field: 'Culture', local: result.culture?.local, remote: result.culture?.remote } + ]) + ), + + renderAccordionSection( + 'Overview', + renderSectionRows([ + { field: 'Method', local: result.method?.local, remote: result.method?.remote }, + { field: 'Path', local: result.path?.local, remote: result.path?.remote }, + { field: 'Status', local: result.status?.local, remote: result.status?.remote }, + { field: 'Request Time', local: result.requestTime?.local, remote: result.requestTime?.remote } + ]), + true // expanded by default + ), + + renderAccordionSection('Request', renderSideBySideJson(result.requestBody)), + + renderAccordionSection('Response', renderSideBySideJson(result.responseBody) + ) ].join(''); } -function renderSection(title, rows) { +function renderSectionRows(rows) { const body = rows.map(row => { const changed = row.local !== row.remote; const rowStyle = changed ? ' style="background:rgba(255,200,0,0.12)"' : ''; @@ -62,13 +71,17 @@ function renderSection(title, rows) { `; }).join(''); - return `

${escapeHtml(title)}

- - + return ` +
FieldLocalRemote
+ + + + + ${body} -
FieldLocalRemote
`; + + `; } - function renderSideBySideJson(data) { const localJson = data?.local || ''; const remoteJson = data?.remote || ''; @@ -86,6 +99,34 @@ function renderSideBySideJson(data) { `; } +function renderAccordionSection(title, content, expanded = false) { + return ` +
+
+
${escapeHtml(title)}
+ +
+ ${expanded ? '-' : '+'} +
+
+ +
+ ${content} +
+
+ `; +} + +function toggleAccordion(header) { + const body = header.nextElementSibling; + const meta = header.querySelector('.accordion-meta'); + + body.classList.toggle('open'); + + meta.textContent = + body.classList.contains('open') ? '-' : '+'; +} + function compareJsonBodies(localJson, remoteJson) { const local = parseJson(localJson); const remote = parseJson(remoteJson); @@ -452,3 +493,4 @@ function escapeHtml(value) { .replace(/"/g, '"') .replace(/'/g, '''); } + From 343dfb291faeaee5c987f28d7691c9e291df54ac Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Fri, 8 May 2026 18:05:48 +0300 Subject: [PATCH 2/3] feat: Improve details and environment diagnostics UI --- .../Internal/HtmlRenderer.cs | 9 +++ .../Middleware/DebugProbeMiddleware.cs | 36 ++++++++-- DebugProbe.AspNetCore/Models/DebugEntry.cs | 16 +++-- .../Resources/css/debugprobe.css | 48 +++++++++++++ .../Resources/html/details.html | 71 +++++++++++++++++-- 5 files changed, 162 insertions(+), 18 deletions(-) diff --git a/DebugProbe.AspNetCore/Internal/HtmlRenderer.cs b/DebugProbe.AspNetCore/Internal/HtmlRenderer.cs index a833b1c..f28eb4f 100644 --- a/DebugProbe.AspNetCore/Internal/HtmlRenderer.cs +++ b/DebugProbe.AspNetCore/Internal/HtmlRenderer.cs @@ -53,10 +53,19 @@ public static string RenderDetailsPage(DebugEntry x, string req, string res) .Replace("{{path}}", Encode(pathWithQuery)) .Replace("{{status}}", x.StatusCode.ToString()) .Replace("{{tradeId}}", x.Id.ToString()) + .Replace("{{time}}", x.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff")) .Replace("{{local}}", x.Timestamp.ToLocalTime().ToString("HH:mm:ss")) + .Replace("{{env}}", Encode(x.Environment)) .Replace("{{culture}}", Encode(x.Culture)) + + .Replace("{{machineName}}", Encode(x.MachineName)) + .Replace("{{timeZone}}", Encode(x.TimeZone)) + .Replace("{{decimalSeparator}}", Encode(x.DecimalSeparator)) + .Replace("{{dateFormat}}", x.DateFormat ?? "") + .Replace("{{assemblyVersion}}", Encode(x.AssemblyVersion)) + .Replace("{{requestUrl}}", Encode(string.IsNullOrEmpty(x.RequestUrl) ? "(empty)" : x.RequestUrl)) .Replace("{{request}}", Encode(string.IsNullOrEmpty(req) ? "(empty)" : req)) .Replace("{{response}}", Encode(string.IsNullOrEmpty(res) ? "(empty)" : res)) diff --git a/DebugProbe.AspNetCore/Middleware/DebugProbeMiddleware.cs b/DebugProbe.AspNetCore/Middleware/DebugProbeMiddleware.cs index 6d89ff8..eb6cf09 100644 --- a/DebugProbe.AspNetCore/Middleware/DebugProbeMiddleware.cs +++ b/DebugProbe.AspNetCore/Middleware/DebugProbeMiddleware.cs @@ -1,8 +1,10 @@ using System.Globalization; +using System.Reflection; using DebugProbe.AspNetCore.Internal; using DebugProbe.AspNetCore.Models; using DebugProbe.AspNetCore.Storage; using Microsoft.AspNetCore.Http; +using Microsoft.VisualBasic; namespace DebugProbe.AspNetCore.Middleware; @@ -47,27 +49,47 @@ public async Task Invoke(HttpContext context, DebugEntryStore store) ms.Position = 0; await ms.CopyToAsync(originalBody); + var shortDatePattern = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern; + var index = shortDatePattern.LastIndexOf('y'); + var dataFormat = index >= 0 ? shortDatePattern[..(index + 1)] : shortDatePattern; + store.Add(new DebugEntry { Id = Guid.NewGuid().ToString(), - Path = context.Request.Path, + // Environment + Environment = EnvironmentUtils.TryGetEnvironment(), + MachineName = Environment.MachineName, + AssemblyVersion = Assembly.GetEntryAssembly()?.GetName().Version?.ToString(), + TimeZone = TimeZoneInfo.Local.DisplayName, + Culture = CultureInfo.CurrentCulture.Name, + DecimalSeparator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator, + DateFormat = dataFormat, + + // Overview Method = context.Request.Method, - StatusCode = context.Response.StatusCode, + Path = context.Request.Path, Query = context.Request.QueryString.ToString(), + StatusCode = context.Response.StatusCode, + RequestTimeUtc = DateTime.UtcNow, + // Request RequestUrl = $"{context.Request.Scheme}://{context.Request.Host}" + $"{context.Request.Path}{context.Request.QueryString}", - RequestBody = Trim(requestBody), + + + // Response ResponseBody = Trim(responseBody), + + // Headers Headers = context.Request.Headers.ToDictionary(x => x.Key, x => x.Value.ToString()), - Timestamp = DateTime.UtcNow, - Environment = EnvironmentUtils.TryGetEnvironment(), - Culture = CultureInfo.CurrentCulture.Name, - RequestTimeUtc = DateTime.UtcNow, + + // Other + Timestamp = DateTime.UtcNow, + }); } diff --git a/DebugProbe.AspNetCore/Models/DebugEntry.cs b/DebugProbe.AspNetCore/Models/DebugEntry.cs index f968d43..bc3f6e8 100644 --- a/DebugProbe.AspNetCore/Models/DebugEntry.cs +++ b/DebugProbe.AspNetCore/Models/DebugEntry.cs @@ -10,20 +10,26 @@ public class DebugEntry public string? Query { get; set; } public string? RequestUrl { get; set; } public string RequestBody { get; set; } = default!; - public DateTime RequestTimeUtc { get; set; } - + public DateTimeOffset RequestTimeUtc { get; set; } // Response public int StatusCode { get; set; } public string ResponseBody { get; set; } = default!; - - // Context + // Environment public string Environment { get; set; } = default!; public string Culture { get; set; } = default!; + public string? UiCulture { get; set; } + + public string? MachineName { get; set; } + public string? AssemblyVersion { get; set; } + public string? TimeZone { get; set; } + public string? DecimalSeparator { get; set; } + public string? DateFormat { get; set; } // Headers public Dictionary Headers { get; set; } = new(); - public DateTime Timestamp { get; set; } + // Metadata + public DateTimeOffset Timestamp { get; set; } } \ No newline at end of file diff --git a/DebugProbe.AspNetCore/Resources/css/debugprobe.css b/DebugProbe.AspNetCore/Resources/css/debugprobe.css index b2f0581..9256076 100644 --- a/DebugProbe.AspNetCore/Resources/css/debugprobe.css +++ b/DebugProbe.AspNetCore/Resources/css/debugprobe.css @@ -70,6 +70,54 @@ a { opacity: 0.9; } +/* ========================= + Details +========================= */ + +.details-grid { + align-items: start; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); + gap: 16px; + margin-bottom: 24px; +} + +.details-card { + background: #fff; + border: 1px solid #e9e9e9; + border-radius: 8px; + overflow: hidden; +} + +.details-card-title { + padding: 14px 16px; + font-size: 16px; + font-weight: 600; + border-bottom: 1px solid #eee; + background: #fafafa; +} + +.details-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 16px; + border-bottom: 1px solid #f3f3f3; +} + + .details-item:last-child { + border-bottom: none; + } + + .details-item span { + color: #666; + } + + .details-item strong { + font-weight: 600; + font-size: 15px; + } + /* ========================= Table ========================= */ diff --git a/DebugProbe.AspNetCore/Resources/html/details.html b/DebugProbe.AspNetCore/Resources/html/details.html index 302a11e..53123f2 100644 --- a/DebugProbe.AspNetCore/Resources/html/details.html +++ b/DebugProbe.AspNetCore/Resources/html/details.html @@ -4,12 +4,71 @@

{{method}} {{path}}

-

Status: {{status}}

-

TraceId: {{tradeId}}

-

Time: {{time}}

-

Local: {{local}}

-

Env: {{env}}

-

Culture: {{culture}}

+
+ +
+
Overview
+ +
+ Status + {{status}} +
+ +
+ TraceId + {{tradeId}} +
+ +
+ Time + {{time}} +
+ +
+ Local + {{local}} +
+
+ +
+
Environment
+ +
+ Environment + {{env}} +
+ +
+ Culture + {{culture}} +
+ +
+ Machine + {{machineName}} +
+ +
+ TimeZone + {{timeZone}} +
+ +
+ Decimal + {{decimalSeparator}} +
+ +
+ Date Format + {{dateFormat}} +
+ +
+ Version + {{assemblyVersion}} +
+
+

Request URL

From fee6900ae20cbdc2905f8365e1881620a9198c15 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Fri, 8 May 2026 18:39:38 +0300 Subject: [PATCH 3/3] feat: Improve overview diagnostics metadata --- DebugProbe.AspNetCore/Internal/HtmlRenderer.cs | 8 ++++++++ .../Middleware/DebugProbeMiddleware.cs | 11 +++++++++-- DebugProbe.AspNetCore/Models/DebugEntry.cs | 3 +++ DebugProbe.AspNetCore/Resources/html/details.html | 15 +++++++++++++++ 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/DebugProbe.AspNetCore/Internal/HtmlRenderer.cs b/DebugProbe.AspNetCore/Internal/HtmlRenderer.cs index f28eb4f..d155d9b 100644 --- a/DebugProbe.AspNetCore/Internal/HtmlRenderer.cs +++ b/DebugProbe.AspNetCore/Internal/HtmlRenderer.cs @@ -57,6 +57,10 @@ public static string RenderDetailsPage(DebugEntry x, string req, string res) .Replace("{{time}}", x.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff")) .Replace("{{local}}", x.Timestamp.ToLocalTime().ToString("HH:mm:ss")) + .Replace("{{durationMs}}", x.DurationMs.ToString()) + .Replace("{{requestSize}}", x.RequestSize.ToString()) + .Replace("{{responseSize}}", x.ResponseSize.ToString()) + .Replace("{{env}}", Encode(x.Environment)) .Replace("{{culture}}", Encode(x.Culture)) @@ -66,6 +70,10 @@ public static string RenderDetailsPage(DebugEntry x, string req, string res) .Replace("{{dateFormat}}", x.DateFormat ?? "") .Replace("{{assemblyVersion}}", Encode(x.AssemblyVersion)) + + + + .Replace("{{requestUrl}}", Encode(string.IsNullOrEmpty(x.RequestUrl) ? "(empty)" : x.RequestUrl)) .Replace("{{request}}", Encode(string.IsNullOrEmpty(req) ? "(empty)" : req)) .Replace("{{response}}", Encode(string.IsNullOrEmpty(res) ? "(empty)" : res)) diff --git a/DebugProbe.AspNetCore/Middleware/DebugProbeMiddleware.cs b/DebugProbe.AspNetCore/Middleware/DebugProbeMiddleware.cs index eb6cf09..745ad2e 100644 --- a/DebugProbe.AspNetCore/Middleware/DebugProbeMiddleware.cs +++ b/DebugProbe.AspNetCore/Middleware/DebugProbeMiddleware.cs @@ -1,10 +1,11 @@ -using System.Globalization; +using System.Diagnostics; +using System.Globalization; using System.Reflection; +using System.Text; using DebugProbe.AspNetCore.Internal; using DebugProbe.AspNetCore.Models; using DebugProbe.AspNetCore.Storage; using Microsoft.AspNetCore.Http; -using Microsoft.VisualBasic; namespace DebugProbe.AspNetCore.Middleware; @@ -41,8 +42,11 @@ public async Task Invoke(HttpContext context, DebugEntryStore store) using var ms = new MemoryStream(); context.Response.Body = ms; + var started = Stopwatch.StartNew(); + await _next(context); + started.Stop(); ms.Position = 0; var responseBody = await new StreamReader(ms).ReadToEndAsync(); @@ -72,6 +76,9 @@ public async Task Invoke(HttpContext context, DebugEntryStore store) Query = context.Request.QueryString.ToString(), StatusCode = context.Response.StatusCode, RequestTimeUtc = DateTime.UtcNow, + DurationMs = started.ElapsedMilliseconds, + RequestSize = Encoding.UTF8.GetByteCount(requestBody), + ResponseSize = Encoding.UTF8.GetByteCount(responseBody), // Request RequestUrl = $"{context.Request.Scheme}://{context.Request.Host}" + diff --git a/DebugProbe.AspNetCore/Models/DebugEntry.cs b/DebugProbe.AspNetCore/Models/DebugEntry.cs index bc3f6e8..05d18c8 100644 --- a/DebugProbe.AspNetCore/Models/DebugEntry.cs +++ b/DebugProbe.AspNetCore/Models/DebugEntry.cs @@ -11,9 +11,12 @@ public class DebugEntry public string? RequestUrl { get; set; } public string RequestBody { get; set; } = default!; public DateTimeOffset RequestTimeUtc { get; set; } + public long RequestSize { get; set; } + public long DurationMs { get; set; } // Response public int StatusCode { get; set; } + public long ResponseSize { get; set; } public string ResponseBody { get; set; } = default!; // Environment diff --git a/DebugProbe.AspNetCore/Resources/html/details.html b/DebugProbe.AspNetCore/Resources/html/details.html index 53123f2..04dc444 100644 --- a/DebugProbe.AspNetCore/Resources/html/details.html +++ b/DebugProbe.AspNetCore/Resources/html/details.html @@ -28,6 +28,21 @@

{{method}} {{path}}

Local {{local}}
+ +
+ Duration + {{durationMs}} ms +
+ +
+ Request Size + {{requestSize}} B +
+ +
+ Response Size + {{responseSize}} B +