@page "/deployment/flow-debugger" @using ZB.MOM.WW.ScadaBridge.Security @using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Deployment @using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared @using ZB.MOM.WW.ScadaBridge.Commons.Types.Debugging @* arch-review F3: opening the page is a Replay-capable read (Deployer + the OperationalAudit roles) so the Audit Log / Execution Chain drill-ins work for an auditor. Every Live-run affordance below is additionally gated on _canLive (the Deployer role). *@ @attribute [Authorize(Policy = AuthorizationPolicies.RequireFlowDebugger)] Flow Debugger

Flow Debugger

@* ── Toolbar ─────────────────────────────────────────────────────────── *@
@* The entry point only selects what a Live run starts from, so it is Deployer-only. *@ @if (_canLive) {
} @if (_mode == FlowMode.Live) { @if (_entry == EntryKind.Method) {
} else {
@* Wider than the site select beside it: the field carries a whole instance name, not a short site name. The script-entry branch still sums to 8 of 12 (site 2 + instance 3 + script 3) alongside the auto-width toolbar columns. *@
@* A site can hold hundreds of instances, so this is the shared area-tree combobox, not a flat list (design 2026-09-08). _areas/_instances are loaded once per site — the picker treats a new list as a site change. *@
} } else {
} @* Without Deployer there is only Replay, so the toggle would be a single dead button. *@ @if (_canLive) {
}
@if (_mode == FlowMode.Live) { } else { }
@if (!_canLive) {
Replay only — rebuilding a past execution from the audit log. Starting a live run requires the Deployer role.
} @* ── Inline alerts ───────────────────────────────────────────────────── *@ @if (_error != null) { } @* ── Status strip ─────────────────────────────────────────────────────── *@
@StateLabel @if (_runNumber > 0) { Run#@_runNumber } @if (_raw != null) { Started@_raw.StartedAtUtc.ToUniversalTime().ToString("HH:mm:ss.fff")Z Total@((int)(_raw.FinishedAtUtc - _raw.StartedAtUtc).TotalMilliseconds) ms } @if (_graph != null) { Steps@_graph.Steps.Count Boundaries@_graph.Edges.Count(e => e.CrossesBoundary) Writes@_graph.Steps.Count(s => s.Kind == FlowStepKind.AttributeWrite) @if (PendingCount > 0) { Pending@PendingCount } } @if (_raw != null) { } @if (_refreshing) { outcomes still arriving — refreshing } @if (_raw != null) { Root @_raw.RootExecutionId } @if (_graph?.Truncated == true) { event list truncated }
@* ── Inputs (Live only) ────────────────────────────────────────────── *@ @if (_mode == FlowMode.Live && (_entry == EntryKind.Script ? SelectedScript != null : _inputFields.Count > 0)) {
Inputs
@if (_entry == EntryKind.Method) { @foreach (var f in _inputFields) { var field = f;
} } else if (SelectedScript is { } script) { @* A write kind is fired by writing its own trigger attribute — the operator supplies only the value. Every other kind is called directly with the parameters its definitions declare. *@ @if (script.IsWriteKind) {
@script.TriggerType trigger — writing the attribute fires the script.
} else if (string.IsNullOrWhiteSpace(script.ParameterDefinitions)) {
This script takes no parameters.
} else {
@* Keyed on the script so a change of script REMOUNTS the form: its raw text and parse errors are per-instance state that must not survive (and two scripts may even declare byte-identical definitions, which would otherwise skip the reparse and its ValidChanged entirely). *@
} }
} @* ── Flow panel ────────────────────────────────────────────────────── *@
@if (_graph == null && _predicted != null) { Predicted from script syntax — dashed nodes depend on inputs or live site state. } else if (SiteNotTraced) { Site not traced — the site returned no trace rows for the routed call (older site build or an untraced call). }
@* ── Detail panel ──────────────────────────────────────────────────── *@
@* Retry / Discard relay a command to the site — only wired for a Deployer, so the buttons never render for an auditor (arch-review F3). *@