3a933e3f68
Gap verified 2026-09-06: the projector and predictor already know the three outbound channels, but the live FlowTraceCollector is fed only by EmitTrace (writes/waits) and the replay query filters to ScriptTrace + the inbound row, so a run leaves every predicted outbound node as a ghost. Central method-script Database.* calls are not audited at all. Plan (11 tasks): three step kinds ApiCall/DbCall/Notify on a fourth External lane; one node per operation with the cached/notify lifecycle folded in (Pending/Parked outcomes, Lifecycle tab); sync calls in the chain, async ones on dashed branch edges; site rows ride the existing collector, replay filter widens, central DB helper gets DbOutbound rows; the page refreshes pending outcomes via the existing Replay command; spawn edges styled and Response linked from the route call (the "two parallel paths" question). Mockup: docs/plans/…-mockup.html, published as a Claude artifact. Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
329 lines
25 KiB
HTML
329 lines
25 KiB
HTML
<title>Flow Debugger External Lane</title>
|
||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap">
|
||
<style>
|
||
:root {
|
||
--ground: #f4f3ee; --surface: #fbfaf7; --surface-2: #f0efe9;
|
||
--ink: #1f231f; --ink-soft: #5d625d; --ink-faint: #8a8f89;
|
||
--rule: #dcdbd3; --rule-strong: #c7c6bd;
|
||
--accent: #2f5fd0; --active-bg: #eef2fc;
|
||
--ok: #2e7d4a; --ok-bg: #e4f2e8; --warn: #b8741a; --warn-bg: #fbf0dc; --danger: #b3362d; --danger-bg: #f9e3e0;
|
||
--sans: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
|
||
--mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace;
|
||
}
|
||
@media (prefers-color-scheme: dark) {
|
||
:root:not([data-theme="light"]) {
|
||
--ground: #1b1d1b; --surface: #242724; --surface-2: #2c2f2c;
|
||
--ink: #e8e8e1; --ink-soft: #b3b6b0; --ink-faint: #7f837e;
|
||
--rule: #383b37; --rule-strong: #4a4e49;
|
||
--accent: #7f9ff0; --active-bg: #23304d;
|
||
--ok: #7dcb95; --ok-bg: #1f3a2a; --warn: #e2b264; --warn-bg: #3f3117; --danger: #ee8b82; --danger-bg: #46231f;
|
||
}
|
||
}
|
||
:root[data-theme="dark"] {
|
||
--ground: #1b1d1b; --surface: #242724; --surface-2: #2c2f2c;
|
||
--ink: #e8e8e1; --ink-soft: #b3b6b0; --ink-faint: #7f837e;
|
||
--rule: #383b37; --rule-strong: #4a4e49;
|
||
--accent: #7f9ff0; --active-bg: #23304d;
|
||
--ok: #7dcb95; --ok-bg: #1f3a2a; --warn: #e2b264; --warn-bg: #3f3117; --danger: #ee8b82; --danger-bg: #46231f;
|
||
}
|
||
|
||
body { background: var(--ground); color: var(--ink); font-family: var(--sans); font-size: 14px; line-height: 1.45; }
|
||
.page { max-width: 1180px; margin: 0 auto; padding: 28px 24px 56px; display: flex; flex-direction: column; gap: 22px; }
|
||
h1 { font-size: 1.45rem; font-weight: 600; margin: 0; letter-spacing: -0.01em; text-wrap: balance; }
|
||
h2 { font-size: 0.95rem; font-weight: 600; margin: 0 0 8px; }
|
||
p { margin: 0; max-width: 68ch; }
|
||
.lede { color: var(--ink-soft); }
|
||
.eyebrow { font-size: 0.68rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-faint); }
|
||
.mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
||
a { color: var(--accent); }
|
||
|
||
/* ── the mocked screen ─────────────────────────────────────────────── */
|
||
.screen { background: var(--surface); border: 1px solid var(--rule); border-radius: 6px; padding: 16px 18px 18px; display: flex; flex-direction: column; gap: 12px; }
|
||
.toolbar { display: flex; flex-wrap: wrap; gap: 14px 22px; align-items: flex-end; }
|
||
.field label { display: block; font-size: 0.72rem; color: var(--ink-soft); margin-bottom: 3px; }
|
||
.seg { display: inline-flex; border: 1px solid var(--rule-strong); border-radius: 4px; overflow: hidden; }
|
||
.seg span { padding: 4px 10px; font-size: 0.8rem; color: var(--ink-soft); background: var(--surface); }
|
||
.seg span.on { background: var(--accent); color: #fff; }
|
||
.select { border: 1px solid var(--rule-strong); border-radius: 4px; padding: 4px 28px 4px 8px; font-size: 0.8rem; background: var(--surface) linear-gradient(45deg, transparent 50%, var(--ink-faint) 50%) no-repeat right 12px top 55% / 6px 6px; min-width: 190px; }
|
||
.btn { border: 1px solid var(--rule-strong); border-radius: 4px; padding: 4px 12px; font-size: 0.8rem; background: var(--surface); color: var(--ink); }
|
||
.btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
||
.spacer { flex: 1; }
|
||
|
||
.status { display: flex; flex-wrap: wrap; gap: 6px 18px; align-items: center; padding: 8px 12px; border: 1px solid var(--rule); border-radius: 4px; background: var(--surface-2); font-size: 0.8rem; }
|
||
.status .k { color: var(--ink-faint); font-size: 0.68rem; letter-spacing: 0.06em; text-transform: uppercase; margin-right: 6px; }
|
||
.chip { border: 1px solid var(--ok); color: var(--ok); border-radius: 999px; padding: 1px 10px; font-size: 0.76rem; font-weight: 500; }
|
||
.note { color: var(--warn); display: inline-flex; align-items: center; gap: 6px; }
|
||
.note .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--warn); animation: pulse 1.6s ease-in-out infinite; }
|
||
@keyframes pulse { 0%,100% { opacity: 1; box-shadow: 0 0 0 0 var(--warn-bg);} 50% { opacity: .6; box-shadow: 0 0 0 5px var(--warn-bg);} }
|
||
@media (prefers-reduced-motion: reduce) { .note .dot, .fg-node.pending .pill { animation: none !important; } }
|
||
|
||
.flow-wrap { border: 1px solid var(--rule); border-radius: 4px; background: var(--surface); overflow-x: auto; }
|
||
svg.fg { display: block; width: 1370px; height: 456px; font-family: var(--sans); }
|
||
.lane-bg.alt { fill: var(--rule); fill-opacity: .25; } .lane-bg { fill: transparent; }
|
||
.lane-rule { stroke: var(--rule); stroke-width: 1; }
|
||
.lane-label { fill: var(--ink-soft); font-size: 11px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase; }
|
||
.lane-sub { fill: var(--ink-faint); font-size: 10.5px; font-family: var(--mono); }
|
||
.fg-node { cursor: pointer; }
|
||
.fg-node rect { fill: var(--surface); stroke: var(--rule-strong); stroke-width: 1; }
|
||
.fg-node.selected rect { stroke: var(--accent); stroke-width: 2; fill: var(--active-bg); }
|
||
.body { box-sizing: border-box; width: 100%; height: 100%; padding: 6px 8px 5px; display: flex; flex-direction: column; gap: 1px; overflow: hidden; line-height: 1.2; color: var(--ink); font-family: var(--sans); }
|
||
.head { display: flex; justify-content: space-between; align-items: baseline; gap: 6px; }
|
||
.eb { color: var(--ink-faint); font-size: 9.5px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; white-space: nowrap; }
|
||
.dur { color: var(--ink-soft); font-size: 9.5px; font-family: var(--mono); white-space: nowrap; }
|
||
.title { font-size: 12px; font-weight: 600; overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||
.sub { color: var(--ink-soft); font-size: 10.5px; margin-top: auto; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.pill { margin-top: auto; align-self: flex-start; display: inline-flex; align-items: center; gap: 5px; font-size: 10px; font-weight: 500; padding: 1px 7px; border-radius: 999px; white-space: nowrap; }
|
||
.pill::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
||
.pill.ok { color: var(--ok); background: var(--ok-bg); }
|
||
.pill.pending { color: var(--warn); background: var(--warn-bg); animation: pulse 1.6s ease-in-out infinite; }
|
||
.pill.parked { color: var(--warn); background: var(--warn-bg); }
|
||
.pill.failed { color: var(--danger); background: var(--danger-bg); }
|
||
.edge { fill: none; stroke: var(--ink-faint); stroke-width: 1.5; }
|
||
.edge.boundary { stroke: var(--accent); }
|
||
.edge.async { stroke-dasharray: 5 4; }
|
||
.edge.echo { stroke-dasharray: 2 3; }
|
||
.arrow { fill: var(--ink-faint); } .arrow.accent { fill: var(--accent); }
|
||
.async-tag { fill: var(--ink-faint); font-size: 9.5px; font-family: var(--mono); }
|
||
|
||
/* ── detail panel ─────────────────────────────────────────────────── */
|
||
.detail { border: 1px solid var(--rule); border-radius: 4px; background: var(--surface); padding: 14px 16px; display: grid; grid-template-columns: 1fr 300px; gap: 18px 28px; }
|
||
.detail .hd { grid-column: 1 / -1; display: flex; align-items: flex-start; gap: 12px; }
|
||
.detail .hd h3 { margin: 2px 0 0; font-size: 1rem; font-weight: 600; }
|
||
.detail .hd .who { color: var(--ink-soft); font-size: 0.8rem; }
|
||
.tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--rule); margin-bottom: 10px; }
|
||
.tabs span { padding: 5px 12px; font-size: 0.82rem; color: var(--accent); }
|
||
.tabs span.on { color: var(--ink); border: 1px solid var(--rule); border-bottom-color: var(--surface); border-radius: 4px 4px 0 0; margin-bottom: -1px; background: var(--surface); }
|
||
table.life { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
|
||
table.life th { text-align: left; font-weight: 500; color: var(--ink-faint); font-size: 0.68rem; letter-spacing: .06em; text-transform: uppercase; padding: 4px 8px; border-bottom: 1px solid var(--rule); }
|
||
table.life td { padding: 6px 8px; border-bottom: 1px solid var(--rule); vertical-align: top; }
|
||
table.life tr:last-child td { border-bottom: 0; }
|
||
.kv { display: grid; grid-template-columns: 110px 1fr; gap: 6px 10px; font-size: 0.8rem; align-items: baseline; }
|
||
.kv .k { font-weight: 500; color: var(--ink-soft); }
|
||
.kv .v { font-family: var(--mono); font-size: 0.78rem; }
|
||
.btnrow { display: flex; gap: 8px; margin-top: 12px; }
|
||
.btn.sm { font-size: 0.74rem; padding: 3px 9px; }
|
||
|
||
/* ── explanatory sections ──────────────────────────────────────────── */
|
||
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px 32px; }
|
||
@media (max-width: 860px) { .grid2, .detail { grid-template-columns: 1fr; } }
|
||
.legend { display: grid; grid-template-columns: 34px 1fr; gap: 8px 12px; align-items: center; font-size: 0.82rem; }
|
||
.legend svg { display: block; }
|
||
.legend .pill { margin: 0; font-size: 11px; }
|
||
table.rows { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
|
||
table.rows th { text-align: left; font-weight: 500; color: var(--ink-faint); font-size: 0.68rem; letter-spacing: .06em; text-transform: uppercase; padding: 4px 8px 6px; border-bottom: 1px solid var(--rule-strong); }
|
||
table.rows td { padding: 6px 8px; border-bottom: 1px solid var(--rule); vertical-align: top; }
|
||
table.rows code { font-family: var(--mono); font-size: 0.76rem; }
|
||
.table-wrap { overflow-x: auto; }
|
||
ol.plan { margin: 0; padding-left: 22px; display: grid; gap: 5px; font-size: 0.84rem; }
|
||
ol.plan b { font-weight: 600; }
|
||
.callout { border-left: 3px solid var(--accent); background: var(--surface-2); padding: 10px 14px; font-size: 0.84rem; border-radius: 0 4px 4px 0; }
|
||
</style>
|
||
|
||
<div class="page">
|
||
<header>
|
||
<div class="eyebrow">Flow Debugger · design addendum · 2026-09-06</div>
|
||
<h1>Outbound calls on a fourth lane</h1>
|
||
<p class="lede">What the swimlane looks like once <code class="mono">ExternalSystem</code>, <code class="mono">Database</code> and <code class="mono">Notify</code> calls become nodes. The flow below is the rig demo the plan seeds (<code class="mono">FlowOutboundDemo</code> → <code class="mono">DelmiaSim01.OutboundDemo</code>): one synchronous REST call, one cached database write, one notification.</p>
|
||
</header>
|
||
|
||
<!-- ══ the mocked page ══ -->
|
||
<section class="screen" aria-label="Flow Debugger mockup">
|
||
<div class="toolbar">
|
||
<div class="field"><label>Entry point</label><span class="seg"><span class="on">Inbound API method</span><span>Value-change trigger</span></span></div>
|
||
<div class="field"><label>Method</label><span class="select">FlowOutboundDemo</span></div>
|
||
<div class="field"><label>Mode</label><span class="seg"><span class="on">Live</span><span>Replay</span></span></div>
|
||
<div class="spacer"></div>
|
||
<span class="btn">Refresh outcomes</span>
|
||
<span class="btn primary">Run</span>
|
||
</div>
|
||
|
||
<div class="status">
|
||
<span class="chip">Completed</span>
|
||
<span><span class="k">Started</span><span class="mono">05:31:02.118Z</span></span>
|
||
<span><span class="k">Total</span><span class="mono">371 ms</span></span>
|
||
<span><span class="k">Steps</span><span class="mono">9</span></span>
|
||
<span><span class="k">Boundaries</span><span class="mono">2</span></span>
|
||
<span><span class="k">Writes</span><span class="mono">2</span></span>
|
||
<span><span class="k">Pending</span><span class="mono">1</span></span>
|
||
<span class="note"><span class="dot"></span>outcomes still arriving — refreshing every 5 s (1:40 left)</span>
|
||
</div>
|
||
|
||
<div class="flow-wrap">
|
||
<svg class="fg" id="fg" viewBox="0 0 1370 456" role="img" aria-label="Flow diagram: 9 steps on four lanes, 2 boundary crossings, 1 pending outbound call">
|
||
<defs>
|
||
<marker id="ar" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" class="arrow"/></marker>
|
||
<marker id="ar-acc" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" class="arrow accent"/></marker>
|
||
</defs>
|
||
</svg>
|
||
</div>
|
||
|
||
<!-- detail panel: the cached DB write is selected -->
|
||
<div class="detail">
|
||
<div class="hd">
|
||
<div>
|
||
<div class="eyebrow">DbCall · External</div>
|
||
<h3>RigDb · CachedWrite</h3>
|
||
<div class="who">DelmiaSim01 · OutboundDemo</div>
|
||
</div>
|
||
<div class="spacer"></div>
|
||
<span class="pill ok">Delivered</span>
|
||
</div>
|
||
<div>
|
||
<div class="tabs"><span>SQL + parameters</span><span>Result</span><span class="on">Lifecycle</span><span>Timing</span></div>
|
||
<table class="life">
|
||
<thead><tr><th>Time</th><th>Row</th><th>Status</th><th>Node</th><th>Error</th></tr></thead>
|
||
<tbody>
|
||
<tr><td class="mono">05:31:02.301</td><td>CachedSubmit</td><td>Submitted</td><td class="mono">site-a / node-a</td><td>—</td></tr>
|
||
<tr><td class="mono">+0.9 s</td><td>DbWriteCached</td><td>Forwarded</td><td class="mono">node-a</td><td>—</td></tr>
|
||
<tr><td class="mono">+4.1 s</td><td>CachedResolve</td><td>Delivered</td><td class="mono">node-a</td><td>—</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div>
|
||
<div class="eyebrow" style="margin-bottom:8px">Where this ran</div>
|
||
<div class="kv">
|
||
<span class="k">Site / node</span><span class="v">site-a / node-a</span>
|
||
<span class="k">Instance</span><span class="v">DelmiaSim01</span>
|
||
<span class="k">Connection</span><span class="v">RigDb</span>
|
||
<span class="k">Operation</span><span class="v">7c2e91d4…</span>
|
||
<span class="k">Delivered by</span><span class="v">node-a · +4.1 s</span>
|
||
</div>
|
||
<div class="btnrow"><span class="btn sm">Open in Debug View</span><span class="btn sm">Audit rows (3)</span></div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ══ legend + node anatomy ══ -->
|
||
<section class="grid2">
|
||
<div>
|
||
<h2>Connectors and pills</h2>
|
||
<div class="legend">
|
||
<svg width="34" height="12"><path d="M0 6H30" class="edge" marker-end="url(#ar)"/></svg><span>Executed edge — the run continued here.</span>
|
||
<svg width="34" height="12"><path d="M0 6H30" class="edge boundary" marker-end="url(#ar-acc)"/></svg><span>Crossed the central ↔ site gRPC boundary.</span>
|
||
<svg width="34" height="12"><path d="M0 6H30" class="edge async" marker-end="url(#ar)"/></svg><span><b>New.</b> Async branch — <code class="mono">CachedCall</code> / <code class="mono">CachedWrite</code> / <code class="mono">Notify.Send</code> returned at once; the run carried on without it.</span>
|
||
<svg width="34" height="12"><path d="M0 6H30" class="edge echo" marker-end="url(#ar)"/></svg><span>Inferred device echo (a write that satisfied a wait).</span>
|
||
<svg width="34" height="14"><path d="M0 7H26" class="edge" /><path d="M20 1l-4 6h5l-3 6 7-8h-5l3-4z" class="arrow"/></svg><span><b>New.</b> Trigger spawn — this write fired a value-change script.</span>
|
||
<span class="pill ok">Ok</span><span>Sync call answered, or cached operation delivered.</span>
|
||
<span class="pill pending">Pending</span><span>Submitted / forwarded, not yet resolved. The page keeps refreshing for up to two minutes after a Live run.</span>
|
||
<span class="pill parked">Parked</span><span>Retries exhausted or permanent failure downstream; retry or discard from Site Calls / Notifications.</span>
|
||
<span class="pill failed">Failed</span><span>A sync call failed and the script received the error.</span>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<h2>What changed in the graph</h2>
|
||
<div class="callout">
|
||
<p style="max-width:none"><b>External is a lane, not a caller-side node.</b> Like Tags, it holds actors outside ScadaBridge — a REST endpoint, a database, the mail relay — so a hop that leaves the system reads as one. Its gutter lists the targets touched.</p>
|
||
<p style="max-width:none;margin-top:8px"><b>One node per operation.</b> A cached call's four audit rows (Submitted → Forwarded → Attempted → Delivered) fold into one node; the pill is the latest status and the Lifecycle tab is the history. A notification folds the site's <code class="mono">NotifySend</code> with central's <code class="mono">NotifyDeliver</code> rows.</p>
|
||
<p style="max-width:none;margin-top:8px"><b>Sync calls sit in the chain; async ones branch.</b> The script blocks on <code class="mono">ExternalSystem.Call</code>, so the chain runs through it (write → API call → write). It does not block on the cached write or the notification, so those hang off the script run on a dashed edge and the chain skips them.</p>
|
||
<p style="max-width:none;margin-top:8px"><b>The Response now starts from the route call</b>, not from whatever step happened last — a cascaded write on another instance is not what answered the HTTP request.</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ══ rows → nodes ══ -->
|
||
<section>
|
||
<h2>How audit rows become nodes</h2>
|
||
<div class="table-wrap">
|
||
<table class="rows">
|
||
<thead><tr><th>Script call</th><th>Audit rows already written</th><th>Node</th><th>Live inline</th><th>Arrives later</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>ExternalSystem.Call</code></td><td><code>ApiOutbound / ApiCall</code> Delivered|Failed, request + response, duration</td><td>API CALL · in chain</td><td>whole node</td><td>—</td></tr>
|
||
<tr><td><code>ExternalSystem.CachedCall</code></td><td><code>CachedSubmit</code> → <code>ApiCallCached</code> Forwarded / Attempted ×n → <code>CachedResolve</code></td><td>API CALL · async branch</td><td>Submitted</td><td>Forwarded, attempts, Delivered / Parked</td></tr>
|
||
<tr><td><code>Database.Query / Execute</code></td><td><code>DbOutbound / DbWrite</code>, <code>Extra {op, rows…}</code></td><td>DATABASE · in chain</td><td>whole node</td><td>—</td></tr>
|
||
<tr><td><code>Database.CachedWrite</code></td><td><code>CachedSubmit</code> → <code>DbWriteCached</code> → <code>CachedResolve</code></td><td>DATABASE · async branch</td><td>Submitted</td><td>the rest</td></tr>
|
||
<tr><td><code>Notify.Send</code></td><td>site <code>NotifySend</code> Submitted → central <code>NotifyDeliver</code> Attempted / Delivered / Parked</td><td>NOTIFY · async branch</td><td>Submitted</td><td>central outbox rows</td></tr>
|
||
<tr><td>central method script <code>Database.*</code></td><td><b>none today</b> → gets <code>DbOutbound / DbWrite</code> (closes an audit gap)</td><td>DATABASE · in chain, Central-side caller</td><td>whole node</td><td>—</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ══ plan at a glance ══ -->
|
||
<section class="grid2">
|
||
<div>
|
||
<h2>Plan at a glance</h2>
|
||
<ol class="plan" start="0">
|
||
<li><b>Commons types</b> — kinds <code class="mono">ApiCall / DbCall / Notify</code>, lane <code class="mono">External</code>, outcomes <code class="mono">Pending / Parked</code>, <code class="mono">FlowLifecycleRow</code>.</li>
|
||
<li><b>Projector</b> maps the three outbound channels onto the External lane.</li>
|
||
<li><b>Graph builder</b> folds lifecycles into one node, async branch edges, spawn edges, Response from the route call.</li>
|
||
<li><b>Site runtime</b> routes outbound rows through the existing trace collector.</li>
|
||
<li><b>Central</b> audits method-script <code class="mono">Database.*</code> and feeds the trace sink.</li>
|
||
<li><b>Replay</b> filter widens to the three channels; site pull verified.</li>
|
||
<li><b>Predictor</b> emits the three kinds with async edges.</li>
|
||
<li><b>FlowGraphView</b> — fourth lane, pills, async/spawn connectors.</li>
|
||
<li><b>Step detail</b> — Lifecycle tab, request/response labels.</li>
|
||
<li><b>Page</b> — refresh pending outcomes after a Live run.</li>
|
||
<li><b>CLI, specs, rig demo</b> + verification step I.</li>
|
||
</ol>
|
||
</div>
|
||
<div>
|
||
<h2>Not in this slice</h2>
|
||
<p>Retry / Discard on a parked node (Site Calls and the Notification report own those actions), deep links by operation id into those pages, and per-attempt request diffs. Each is a register row, not a blocker.</p>
|
||
<p style="margin-top:10px">Repo plan: <code class="mono">docs/plans/2026-09-06-flow-debugger-outbound-calls.md</code>.</p>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
|
||
<script>
|
||
(function () {
|
||
const LH = 110, NW = 150, NH = 84, X0 = 160, PITCH = 170, PAD = 16;
|
||
const lanes = [
|
||
{ name: 'Central', sub: '' },
|
||
{ name: 'Site', sub: 'site-a · DelmiaSim01' },
|
||
{ name: 'Tags', sub: 'DelmiaSim01' },
|
||
{ name: 'External', sub: 'RigRest · RigDb · FlowDemo' },
|
||
];
|
||
// lane, column, eyebrow, duration, title, subtitle | pill
|
||
const nodes = {
|
||
req: { lane: 0, col: 0, eb: 'Request', dur: '13 ms', title: 'FlowOutboundDemo', sub: 'multi-role' },
|
||
route: { lane: 0, col: 1, eb: 'Route call', dur: '358 ms', title: 'DelmiaSim01.OutboundDemo', sub: '→ 9b1c02aa' },
|
||
run: { lane: 1, col: 2, eb: 'Script run', dur: '341 ms', title: 'OutboundDemo', sub: 'DelmiaSim01 · call' },
|
||
w1: { lane: 2, col: 3, eb: 'Write', dur: '0 ms', title: 'RecipeProcessedFlag', sub: 'RecipeProcessedFlag = True' },
|
||
api: { lane: 3, col: 4, eb: 'API call', dur: '212 ms', title: 'RigRest.AckRecipe', pill: ['ok', 'Ok · 200 · sync'] },
|
||
w2: { lane: 2, col: 5, eb: 'Write', dur: '0 ms', title: 'LastAckAt', sub: 'LastAckAt = 05:31:02.5' },
|
||
db: { lane: 3, col: 3, eb: 'Database', dur: '+4.1 s', title: 'RigDb · CachedWrite', pill: ['ok', 'Delivered · cached'], selected: true },
|
||
ntf: { lane: 3, col: 5, eb: 'Notify', dur: '', title: 'FlowDemo', pill: ['pending', 'Pending · submitted'] },
|
||
resp: { lane: 0, col: 6, eb: 'Response', dur: '', title: 'Response', sub: '200' },
|
||
};
|
||
const edges = [
|
||
['req', 'route', ''], ['route', 'run', 'boundary'], ['run', 'w1', ''], ['w1', 'api', ''], ['api', 'w2', ''],
|
||
['run', 'db', 'async'], ['run', 'ntf', 'async'], ['route', 'resp', ''],
|
||
];
|
||
const svg = document.getElementById('fg');
|
||
const NS = 'http://www.w3.org/2000/svg';
|
||
const el = (t, a, parent) => { const e = document.createElementNS(NS, t); for (const k in a) e.setAttribute(k, a[k]); (parent || svg).appendChild(e); return e; };
|
||
const W = 1370;
|
||
lanes.forEach((l, i) => {
|
||
el('rect', { x: 0, y: i * LH, width: W, height: LH, class: 'lane-bg' + (i % 2 ? ' alt' : '') });
|
||
el('line', { x1: 0, y1: i * LH, x2: W, y2: i * LH, class: 'lane-rule' });
|
||
el('text', { x: 12, y: i * LH + 24, class: 'lane-label' }).textContent = l.name;
|
||
if (l.sub) el('text', { x: 12, y: i * LH + 40, class: 'lane-sub' }).textContent = l.sub;
|
||
});
|
||
for (const k in nodes) { const n = nodes[k]; n.x = X0 + n.col * PITCH; n.y = PAD + n.lane * LH; }
|
||
const path = (a, b) => {
|
||
const x1 = a.x + NW, y1 = a.y + NH / 2, x2 = b.x, y2 = b.y + NH / 2;
|
||
if (Math.abs(y1 - y2) < .01) return `M ${x1} ${y1} H ${x2}`;
|
||
const mid = x1 + (x2 - x1) / 2;
|
||
return `M ${x1} ${y1} H ${mid} V ${y2} H ${x2}`;
|
||
};
|
||
// Async branches leave the source's bottom edge, run along the strip above the target
|
||
// lane's nodes and drop into the target's top — so they never cross a node in between.
|
||
const asyncPath = (a, b) => `M ${a.x + NW / 2} ${a.y + NH} V ${b.y - 8} H ${b.x + 16} V ${b.y}`;
|
||
edges.forEach(([f, t, cls]) => {
|
||
const d = cls === 'async' ? asyncPath(nodes[f], nodes[t]) : path(nodes[f], nodes[t]);
|
||
el('path', { d, class: 'edge ' + cls, 'marker-end': cls === 'boundary' ? 'url(#ar-acc)' : 'url(#ar)' });
|
||
if (cls === 'async') el('text', { x: nodes[t].x + 22, y: nodes[t].y - 11, class: 'async-tag' }).textContent = 'async';
|
||
});
|
||
for (const k in nodes) {
|
||
const n = nodes[k];
|
||
const g = el('g', { class: 'fg-node' + (n.selected ? ' selected' : '') + (n.pill && n.pill[0] === 'pending' ? ' pending' : ''), transform: `translate(${n.x} ${n.y})`, role: 'button', tabindex: 0 });
|
||
el('rect', { x: 0, y: 0, width: NW, height: NH, rx: 4, ry: 4 }, g);
|
||
const fo = el('foreignObject', { x: 0, y: 0, width: NW, height: NH }, g);
|
||
const body = document.createElement('div'); body.className = 'body'; body.title = n.title + (n.sub ? ' — ' + n.sub : '');
|
||
const bottom = n.pill ? `<span class="pill ${n.pill[0]}">${n.pill[1]}</span>` : `<div class="sub">${n.sub || ''}</div>`;
|
||
body.innerHTML = `<div class="head"><span class="eb">${n.eb}</span><span class="dur">${n.dur}</span></div><div class="title">${n.title.replace(/([a-z])([A-Z])/g, '$1$2').replace(/\./g, '.')}</div>${bottom}`;
|
||
fo.appendChild(body);
|
||
}
|
||
})();
|
||
</script>
|