Merge wt/step-logs-t13: Task 13 page strip counts, truncation notes, selection hookup
This commit is contained in:
@@ -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">inline log lines capped — refresh outcomes pulls the rest</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>
|
||||
|
||||
+51
-2
@@ -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,11 +685,22 @@ 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);
|
||||
// D7: the inline copy's cap is only worth reporting while the page still holds that
|
||||
// incomplete copy. A refresh MERGES rows, so the moment it brings back even one ScriptLog
|
||||
// row the run did not already have, the page is no longer short of lines and the flag
|
||||
// clears. Until then it stays: the site's rows reach central's audit store by telemetry
|
||||
// 5-30 s later, so a refresh that adds nothing proves nothing.
|
||||
var logsTruncated = (_raw.LogsTruncated || outcome.Raw?.LogsTruncated == true)
|
||||
&& CountLogRows(merged) <= CountLogRows(_raw.Events);
|
||||
_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;
|
||||
}
|
||||
|
||||
/// <summary>Number of <c>ScriptTrace.ScriptLog</c> rows in <paramref name="events"/> — the log lines a refresh can add to.</summary>
|
||||
private static int CountLogRows(IReadOnlyList<AuditEvent>? events)
|
||||
=> events?.Count(FlowStepProjector.IsLogLine) ?? 0;
|
||||
|
||||
/// <summary>Union of two audit-row lists by <see cref="AuditEvent.EventId"/>; a row present in both takes the fresh copy.</summary>
|
||||
internal static IReadOnlyList<AuditEvent> MergeEvents(IReadOnlyList<AuditEvent>? known, IReadOnlyList<AuditEvent>? fresh)
|
||||
{
|
||||
|
||||
@@ -537,8 +537,8 @@ public sealed class FlowDebugService : IFlowDebugService
|
||||
if (result == null)
|
||||
return FlowDebugOutcome.Fail("The server returned no result.", "BAD_RESULT");
|
||||
|
||||
var graph = FlowGraphBuilder.Build(result.Events ?? [], result.Truncated);
|
||||
return new FlowDebugOutcome(graph, result, null, null);
|
||||
var graph = FlowGraphBuilder.Build(result.Events ?? [], result.Truncated, result.LogsTruncated);
|
||||
return new FlowDebugOutcome(graph, result, null, null, result.LogsTruncated);
|
||||
}
|
||||
case ManagementUnauthorized unauthorized:
|
||||
return FlowDebugOutcome.Fail(unauthorized.Message, "UNAUTHORIZED");
|
||||
|
||||
@@ -15,7 +15,14 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Services;
|
||||
/// <param name="Raw">The server's <see cref="FlowDebugRunResult"/>; null when no result came back.</param>
|
||||
/// <param name="Error">Human-readable failure, null on success.</param>
|
||||
/// <param name="ErrorCode">Machine-readable failure code (<c>STANDBY_NODE</c>, <c>NO_TRIGGER_WITHIN_WINDOW</c>, <c>UNAUTHORIZED</c>, <c>SERVICE_UNAVAILABLE</c>, …), null on success.</param>
|
||||
public sealed record FlowDebugOutcome(FlowGraph? Graph, FlowDebugRunResult? Raw, string? Error, string? ErrorCode)
|
||||
/// <param name="LogsTruncated">
|
||||
/// Step logs (design 2026-09-09 D7): true when the inline trace collector dropped low-priority
|
||||
/// <c>ScriptLog</c> rows to protect room for the step rows — the run's lines are incomplete inline,
|
||||
/// while Replay (which reads the never-truncated audit store) is complete. Mirrors
|
||||
/// <see cref="FlowDebugRunResult.LogsTruncated"/> so the page can note it without unwrapping the raw result.
|
||||
/// </param>
|
||||
public sealed record FlowDebugOutcome(
|
||||
FlowGraph? Graph, FlowDebugRunResult? Raw, string? Error, string? ErrorCode, bool LogsTruncated = false)
|
||||
{
|
||||
/// <summary>True when a graph was built (the run may still have finished incomplete — see <see cref="Raw"/>).</summary>
|
||||
public bool Success => Graph != null && Error == null;
|
||||
|
||||
Reference in New Issue
Block a user