feat(central-ui): Flow Debugger status strip log counts + inline-truncation notes; detail cross-step selection

This commit is contained in:
Joseph Doherty
2026-09-09 12:38:35 -04:00
parent 7d76bf96d5
commit 032598607e
6 changed files with 250 additions and 8 deletions
@@ -166,6 +166,12 @@
<span><span class="fd-label">Steps</span><span class="fd-mono" data-test="flow-step-count">@_graph.Steps.Count</span></span>
<span><span class="fd-label">Boundaries</span><span class="fd-mono">@_graph.Edges.Count(e => e.CrossesBoundary)</span></span>
<span><span class="fd-label">Writes</span><span class="fd-mono">@_graph.Steps.Count(s => s.Kind == FlowStepKind.AttributeWrite)</span></span>
@* Step logs (design 2026-09-09 D5): the run's line counts by level — a Log.Error line is
NOT a failed step, so it never moves the state chip. *@
@if (LogCounts is { Total: > 0 })
{
<span><span class="fd-label">Log</span><span class="fd-mono" data-test="flow-log-counts">@LogSummary</span></span>
}
@if (PendingCount > 0)
{
<span><span class="fd-label">Pending</span><span class="fd-mono" data-test="flow-pending-count">@PendingCount</span></span>
@@ -191,6 +197,14 @@
{
<span class="badge bg-warning text-dark">event list truncated</span>
}
@if (_graph?.LogsTruncated == true)
{
<span class="badge bg-warning text-dark" data-test="flow-logs-truncated">log lines truncated inline — Replay is complete</span>
}
@if (_graph?.UnattachedLogLines > 0)
{
<span class="text-muted small" data-test="flow-unattached-logs">@_graph.UnattachedLogLines log line(s) could not be placed on a step</span>
}
</div>
<div class="row g-3">
@@ -284,7 +298,7 @@
SiteDbId="@(_entry == EntryKind.Script && _selectedSiteId > 0 ? _selectedSiteId : null)"
InstanceDbId="@(_entry == EntryKind.Script && _selectedInstanceId > 0 ? _selectedInstanceId : null)"
OnRetry="RetryCallback" OnDiscard="DiscardCallback"
ActionInProgress="_actionInProgress" />
ActionInProgress="_actionInProgress" OnSelectStep="OnSelectStep" />
</div>
</div>
</div>
@@ -183,6 +183,44 @@ public partial class FlowDebugger : ComponentBase, IDisposable
private int PendingCount => _graph?.Steps.Count(s => s.Outcome == FlowStepOutcome.Pending) ?? 0;
/// <summary>
/// Step logs (design 2026-09-09 D5): the run's log lines summed by level over every step that
/// carries any. <c>error</c> and <c>warn</c> are counted by name (ordinal, case-insensitive —
/// the level rides the audit row as free text); everything else, <c>debug</c> included, is
/// "info", so the three counts always add up to <c>Total</c>. Lines the builder could not place
/// on a step are NOT here — they are reported separately as
/// <see cref="FlowGraph.UnattachedLogLines"/>.
/// </summary>
internal static (int Errors, int Warnings, int Info, int Total) CountLogs(FlowGraph? graph)
{
if (graph is null)
return (0, 0, 0, 0);
var errors = 0;
var warnings = 0;
var info = 0;
foreach (var line in graph.Steps.SelectMany(s => s.Logs ?? []))
{
if (string.Equals(line.Level, "error", StringComparison.OrdinalIgnoreCase)) errors++;
else if (string.Equals(line.Level, "warn", StringComparison.OrdinalIgnoreCase)) warnings++;
else info++;
}
return (errors, warnings, info, errors + warnings + info);
}
private (int Errors, int Warnings, int Info, int Total) LogCounts => CountLogs(_graph);
/// <summary>Status-strip text for <see cref="LogCounts"/>.</summary>
private string LogSummary
{
get
{
var counts = LogCounts;
return $"{counts.Errors} errors · {counts.Warnings} warnings · {counts.Info} info";
}
}
/// <summary>Pending outbound work, or an acted-on node that is still Parked, keeps the refresh loop alive.</summary>
private bool ShouldKeepPolling =>
PendingCount > 0
@@ -647,8 +685,11 @@ public partial class FlowDebugger : ComponentBase, IDisposable
var selectedId = _selected?.EventId;
var merged = MergeEvents(_raw.Events, outcome.Raw?.Events);
var truncated = _raw.Truncated || outcome.Raw?.Truncated == true;
_raw = _raw with { Events = merged, Truncated = truncated };
_graph = FlowGraphBuilder.Build(merged, truncated);
// Truncation is sticky across a refresh, exactly as the event-list flag is: the Live run's
// inline copy stays the incomplete one whatever the re-read brought back (D7).
var logsTruncated = _raw.LogsTruncated || outcome.Raw?.LogsTruncated == true;
_raw = _raw with { Events = merged, Truncated = truncated, LogsTruncated = logsTruncated };
_graph = FlowGraphBuilder.Build(merged, truncated, logsTruncated);
_selected = selectedId is { } id ? _graph.Steps.FirstOrDefault(s => s.EventId == id) ?? _selected : _selected;
}