fix(central-ui): Flow Debugger outcome refresh merges rows by EventId instead of replacing the graph
Found in a browser Live run on the rig: the run drew API, DB and Notify nodes from its inline rows; the 5 s pending-refresh re-read the tree through Replay before the site's cached-call telemetry (12 s) had reached central, swapped the graph wholesale, and the resolved DB node vanished (7 -> 6 steps) until a manual refresh. Polling had already stopped because nothing was Pending. MergeRefreshedGraph now unions the re-read rows into the run's known rows by EventId (fresh copy wins), stores the union back on _raw and rebuilds through FlowGraphBuilder, so a refresh only ever adds nodes. The union is exact: the inline rows are the same AuditEvent objects the site writer persists (FlowTraceCollector). Tests: new PendingRefresh_KeepsStepsTheLiveRunAlreadyHad_WhenReplayLagsBehindTelemetry (fails on the old code). Two existing fixtures were internally inconsistent and only passed under the old replace semantics: ParkedThenResolved built its Resolved outcome with the Parked raw rows, and OutboundOutcome minted fresh event/correlation ids per call, so no row could ever match across live and replay. Both now share rows per run. Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
This commit is contained in:
+23
-5
@@ -3,6 +3,7 @@ using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
using ZB.MOM.WW.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Auth;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Services;
|
||||
@@ -510,19 +511,36 @@ public partial class FlowDebugger : ComponentBase, IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the graph with the re-read tree and keeps the selection by event id. The run's own
|
||||
/// <c>_raw</c> (timing, root) and the state chip are deliberately left alone: pending outbound
|
||||
/// work is not a failed run, and the run's Started/Total are the truthful numbers.
|
||||
/// Folds the re-read tree INTO the run's known events (union by event id, re-read rows win) and
|
||||
/// rebuilds the graph from the union, keeping the selection by event id. A refresh therefore only
|
||||
/// ever adds rows: a Live run carries its site rows inline, while the same rows reach central's
|
||||
/// audit store by telemetry seconds later — a Replay read taken in between lacks them, and
|
||||
/// replacing the graph wholesale made an already-drawn node (a resolved cached DB write, on the
|
||||
/// rig 2026-09-06) vanish until the next manual refresh. The run's own <c>_raw</c> timing, root
|
||||
/// and the state chip are deliberately left alone: pending outbound work is not a failed run, and
|
||||
/// the run's Started/Total are the truthful numbers.
|
||||
/// </summary>
|
||||
private void MergeRefreshedGraph(FlowDebugOutcome outcome)
|
||||
{
|
||||
if (!outcome.Success || outcome.Graph == null || _graph == null)
|
||||
if (!outcome.Success || outcome.Graph == null || _graph == null || _raw == null)
|
||||
return;
|
||||
var selectedId = _selected?.EventId;
|
||||
_graph = outcome.Graph;
|
||||
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);
|
||||
_selected = selectedId is { } id ? _graph.Steps.FirstOrDefault(s => s.EventId == id) ?? _selected : _selected;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
var byId = new Dictionary<Guid, AuditEvent>();
|
||||
foreach (var e in known ?? []) byId[e.EventId] = e;
|
||||
foreach (var e in fresh ?? []) byId[e.EventId] = e;
|
||||
return byId.Values.ToList();
|
||||
}
|
||||
|
||||
private void CancelPendingRefresh(bool keepWatch = false)
|
||||
{
|
||||
if (!keepWatch)
|
||||
|
||||
Reference in New Issue
Block a user