feat(central-ui): FlowGraphView External lane, outbound status pills, async/spawn connectors

Four lanes (LaneCount 4; External sublabel = distinct targets, three then +n);
ApiCall/DbCall/Notify nodes carry a status pill (Ok/Pending/Parked/Failed —
pending pulses, reduced-motion aware) plus a sync/cached/notify mode; folded
lifecycles show '+4.1 s' submit→terminal wall time. Async edges are dashed
and routed through the inter-lane strip with a gutter tag; spawn edges use a
bolt marker. Predicted async edges (Task 6's PredictedEdge.Async) render the
same way. de-DE height pinned at 319.2 = (4×110+16)×0.7.

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
This commit is contained in:
Joseph Doherty
2026-09-06 01:57:47 -04:00
parent ec9011429c
commit 56154fd1d2
5 changed files with 399 additions and 28 deletions
@@ -2,9 +2,10 @@
@using ZB.MOM.WW.ScadaBridge.Commons.Types.Debugging
@using ZB.MOM.WW.ScadaBridge.ScriptAnalysis
@* Flow Debugger swimlane (design 2026-09-05 §4). Dependency-free inline SVG:
three fixed lanes, one node per step, orthogonal connectors. All coordinate
math lives in the code-behind (InvariantCulture throughout). *@
@* Flow Debugger swimlane (design 2026-09-05 §4; External lane, status pills and async/spawn
connectors per the 2026-09-06 outbound-calls plan). Dependency-free inline SVG: four fixed
lanes, one node per step, orthogonal connectors. All coordinate math lives in the
code-behind (InvariantCulture throughout). *@
<div class="fg-wrap" data-test="flow-graph">
<svg width="@RenderedWidth" height="@RenderedHeight" viewBox="@ViewBox"
@@ -16,10 +17,14 @@
<marker id="fg-arrow-accent" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" class="fg-arrowhead accent" />
</marker>
@* Trigger spawn (plan D9): a small bolt in place of the arrowhead — this write fired a value-change script. *@
<marker id="fg-arrow-spawn" viewBox="0 0 12 14" refX="10" refY="7" markerWidth="12" markerHeight="14" orient="auto-start-reverse">
<path d="M 5 1 l -4 6 h 5 l -3 6 l 7 -8 h -5 l 3 -4 z" class="fg-arrowhead spawn" />
</marker>
</defs>
@* Lanes *@
@for (var lane = 0; lane < 3; lane++)
@for (var lane = 0; lane < LaneCount; lane++)
{
var i = lane;
<g class="fg-lane" data-lane="@LaneName(i)">
@@ -47,6 +52,11 @@
@foreach (var e in _layout.Edges)
{
<path d="@e.Path" class="@EdgeClass(e)" marker-end="@MarkerFor(e)" data-test="flow-edge" data-from="@e.From" data-to="@e.To" />
@if (AsyncTagFor(e) is { } tag)
{
@* Names the branch where it lands: the run carried on without waiting for this. *@
@SvgText(tag.X, tag.Y, "fg-edge-tag", "async", dataTest: "flow-edge-tag")
}
}
@* Nodes *@
@@ -69,7 +79,18 @@
<span class="fg-duration">@Duration(node)</span>
</div>
<div class="fg-title" data-test="flow-node-title">@WrapHints(node.DisplayTitle)</div>
<div class="fg-subtitle" data-test="flow-node-subtitle">@node.DisplaySubtitle</div>
@if (HasPill(node))
{
@* Outbound kinds: the folded outcome as a pill, then the call mode (node anatomy: "● Ok · sync"). *@
<div class="fg-status" data-test="flow-node-status">
<span class="@PillClass(node.Step!.Outcome)" data-test="flow-node-pill">@PillText(node.Step.Outcome)</span>
<span class="fg-mode">· @OutboundMode(node.Kind, node.Subtitle)</span>
</div>
}
else
{
<div class="fg-subtitle" data-test="flow-node-subtitle">@node.DisplaySubtitle</div>
}
</div>
</foreignObject>
</g>