Files
ScadaBridge/docs/plans/2026-06-17-debugview-tabs-trees-design.md
T
Joseph Doherty 811d72255c docs(plans): design for Debug View tabs + hierarchy trees
Tabbed Attributes/Alarms view with collapsible composition trees derived
from path-qualified canonical names; all configured alarms (computed +
native) shown with current status; branch-level status roll-up; native
source bindings as nodes with conditions nested. Site snapshot enriched
with placeholder rows for idle native sources via an additive
IsConfiguredPlaceholder field on AlarmStateChanged.
2026-06-17 14:06:08 -04:00

11 KiB

Debug View — Tabs + Hierarchy Trees Design

Date: 2026-06-17 Status: Approved (brainstorming) — ready for implementation planning Component: #9 Central UI (Debug View page) + #3 Site Runtime (snapshot enrichment) + #16 Commons (Streaming message contract) Base: branch debugview-tabs-trees off origin/main (670b607)

Goal

Restructure the Central UI Debug View page (/deployment/debug-view) from two side-by-side, change-driven flat tables into a tabbed layout — an Attributes tab and an Alarms tab — where each tab presents the instance's data as a collapsible hierarchy tree. Every configured alarm (computed and native) is shown with its current status, even when Normal/quiet. The tree hierarchy is derived from the instance's composition.

Background — how the page works today

(Findings from code inspection; file:line references are against origin/main 670b607.)

  • Per-instance, change-driven. The page connects to one selected instance via DebugStreamService.StartStreamAsync (src/ZB.MOM.WW.ScadaBridge.Communication/DebugStreamService.cs). It receives an initial snapshot (DebugViewSnapshot, via ClusterClient ask) plus live events (AttributeValueChanged / AlarmStateChanged, via gRPC stream). UI: src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/DebugView.razor.
  • Two flat tables. Attributes (left) and Alarm States (right), each a flat table filtered by name, capped at 200 rows, with auto-scroll and a "Clear" button — a change-feed model. Latest value per name held in Dictionary<string, …> keyed by AttributeName / AlarmName.
  • No runtime child instances. Composition is flattened at deploy time (src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/FlatteningService.cs). Composed members carry path-qualified canonical names of the form [ModuleInstanceName].[MemberName] (and nest, e.g. Motor1.Compressor.Pump). The hierarchy is fully recoverable by splitting these names on ..
  • Attributes are already "all configured." The site seeds InstanceActor._attributes from FlattenedConfiguration defaults + static overrides, so the snapshot already carries every configured attribute (not just changed ones).
  • Computed alarms are already "all configured." InstanceActor.BuildAlarmStatesSnapshot() (src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/InstanceActor.cs) iterates _alarmActors.Keys and emits a bare Normal row for every computed alarm that hasn't fired.
  • The one gap: idle native sources. Native alarm source bindings (_nativeAlarmActors, configured via ResolvedNativeAlarmSource) produce a row only once a live condition arrives. A configured-but-quiet native source currently shows nothing.
  • Message shapes.
    • AttributeValueChanged(InstanceUniqueName, AttributePath, AttributeName, object? Value, string Quality, DateTimeOffset Timestamp).
    • AlarmStateChanged(InstanceUniqueName, AlarmName, AlarmState State, int Priority, DateTimeOffset Timestamp) plus init-only enrichment: Level, Message, Kind (Computed/NativeOpcUa/NativeMxAccess), Condition (AlarmConditionState), and native-only SourceReference, AlarmTypeName, Category, OperatorUser, OperatorComment, OriginalRaiseTime, CurrentValue, LimitValue.
    • Neither carries a separate parent/child/path field beyond the path-qualified name — the canonical name is the locator.

Confirmed decisions

  1. Hierarchy from canonical names. The tree is a presentation of the flattened names already in the snapshot: instance = root; root-level attributes/alarms are direct leaves; each composed module (nested as deep as composition goes) is a collapsible branch. No new "child instance" concept.
  2. Extend the site snapshot for native too. A small site-side change emits a placeholder row for each configured native alarm source that is currently quiet, so every configured alarm/source appears as a tree node from the moment you connect.
  3. Roll up status to branch headers. A collapsed module branch shows an aggregate badge — on the Alarms tree: worst descendant alarm state + active count; on the Attributes tree: a bad/uncertain-quality indicator if any descendant is off-Good.
  4. Native binding = node, conditions nest under it. A native source binding is a tree node (placed by its canonical name); its current live conditions appear as child rows; a quiet binding shows a single "no active conditions" row; the binding header rolls up the worst condition.
  5. Replace the chronological change-feed. The cap-200 / auto-scroll / Clear semantics are dropped in favour of a stable tree that updates values in place. A per-tab filter box is retained (prunes the tree to matching leaves + their ancestor branches, auto-expanding matches).

Approach — hybrid reusable tree

A single recursive TreeView shell component owns the shared, harder parts — expand/collapse, branch roll-up, filter pruning — and accepts a per-tab leaf render fragment for the bespoke content (attribute leaf vs. alarm leaf). The flat-list → node-forest transform is a pure static helper so the gnarly logic is unit-testable without rendering.

Alternatives rejected:

  • Two fully-bespoke trees — duplicates expand/filter/roll-up logic across both tabs.
  • Single indented table faking a tree — cannot collapse or roll up; fails decisions 3 and 5.

Design

1. Page restructure (DebugView.razor)

  • The two side-by-side cards become a tabbed layout — custom Bootstrap nav-tabs (no third-party component libs, per project rule) — with an Attributes tab and an Alarms tab, each hosting a TreeView.
  • Connection strip, site/instance selectors, live/snapshot badges, auto-reconnect-from-localStorage, and site-scope checks are unchanged.
  • Drop cap-200 / auto-scroll / Clear. Keep a per-tab filter box.
  • Values update in place on live events; the tree structure is stable for the life of the connection.

2. Tree model & building (pure helper + TreeView.razor)

  • DebugTreeNode { string Path; string Segment; List<DebugTreeNode> Children; LeafPayload? Leaf; }Path is the full canonical prefix (stable key for expand-state + diffing), Segment the display label.
  • Build: split each item's CanonicalName on .; walk/create branch nodes; attach the leaf at the terminal segment. Children sorted by name; root = the instance.
  • Attributes tab: leaf payload = AttributeValueChanged → value / quality badge / timestamp (today's columns rendered as a leaf row).
  • Alarms tab:
    • Computed alarm → leaf (state / kind / severity / level), reusing today's badge helpers (GetAlarmStateBadge, GetAlarmLevelBadge, GetKindBadge, FormatKind, FormatLevel, BuildAlarmTooltip).
    • Native source binding → branch node (placed by canonical name) whose live conditions are child rows keyed by SourceReference; a quiet binding (placeholder row) renders "no active conditions".
  • Roll-up (computed during build, stored on the branch node):
    • Alarm branch → worst descendant AlarmState (Active dominates) + active-count badge.
    • Attribute branch → bad/uncertain-quality indicator if any descendant attribute is off-Good.
  • Expand state: the page holds a HashSet<string> of expanded Paths; default-expand the root + first level; an active filter auto-expands matching paths. State survives live re-renders (keyed by Path).
  • Threading: the build + in-place merge run on the render thread via the existing SafeInvokeAsync marshalling (preserves CentralUI-009 dispose guard and CentralUI-021 dictionary single-thread access).

3. Site-side snapshot enrichment (native placeholders)

  • Extend InstanceActor.BuildAlarmStatesSnapshot(): after the existing computed-alarm fallback loop, iterate the configured native source bindings (_nativeAlarmActors.Keys) and, for any binding with no live condition currently represented, emit a placeholder AlarmStateChanged (native Kind, State = Normal).
  • Add an additive init-only field to AlarmStateChanged: bool IsConfiguredPlaceholder { get; init; } = false; — additive-only message-contract evolution; default false keeps every existing positional constructor call and serialized wire frame valid (Newtonsoft cross-process serializer). The UI uses it to render "binding present, no active conditions" distinctly from a real Normal condition.
  • Detail to verify during planning: whether native live events key by binding canonical name or per-condition (AlarmName vs SourceReference). This decides exactly how a placeholder is suppressed once a live condition arrives, and how the frontend replaces a placeholder node with condition child-rows. Inspect InstanceActor native-event handling + NativeAlarmActor emission before writing the enrichment.

4. Data flow / real-time

  • Transport unchanged: snapshot via ClusterClient ask, live events via gRPC. The frontend merges each event into the attribute/alarm forests in place. When the last condition for a native binding clears, the binding reverts to its "no active conditions" presentation (zero condition child-rows).

5. Error handling

  • Disconnect / terminated / instance-not-found paths unchanged.
  • Empty instance (no attributes/alarms) → tree shows just the root node with an empty-state hint.

Testing

  • Site Runtime (tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests): BuildAlarmStatesSnapshot emits a placeholder for a configured-but-quiet native source binding, and does not duplicate/placeholder a binding that already has a live condition.
  • Central UI (tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests): pure tree-build helper tests — nesting from canonical names, worst-state roll-up, filter pruning to matches + ancestors; plus a leaf-render smoke test for each tab.
  • Playwright (one): connect to an instance → switch to the Alarms tab → assert configured alarms are grouped under their module branch → expand a branch → assert the roll-up badge is visible.

Docs to update (with the implementation)

  • docs/requirements/Component-CentralUI.md — Debug View section (tabs + trees, roll-up, native placeholders).
  • docs/requirements/Component-SiteRuntime.md — snapshot enrichment for idle native sources.
  • Streaming message-contract note for the additive IsConfiguredPlaceholder field (additive-only evolution).
  • No new component → README.md / CLAUDE.md component table unchanged (stays at 25).

Out of scope / non-goals

  • No new transport, no per-site (multi-instance) debug session — still one instance per connection.
  • No chronological change-feed / history (replaced by the live tree). KPI/history trends are a separate effort (M6).
  • No write-back / alarm acknowledgement from Debug View — it remains read-only.