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.
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 byAttributeName/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._attributesfromFlattenedConfigurationdefaults + 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.Keysand emits a bareNormalrow for every computed alarm that hasn't fired. - The one gap: idle native sources. Native alarm source bindings (
_nativeAlarmActors, configured viaResolvedNativeAlarmSource) 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-onlySourceReference,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
- 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.
- 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.
- 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.
- 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.
- 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 aTreeView. - 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; }—Pathis the full canonical prefix (stable key for expand-state + diffing),Segmentthe display label.- Build: split each item's
CanonicalNameon.; 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".
- Computed alarm → leaf (state / kind / severity / level), reusing today's badge helpers (
- 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.
- Alarm branch → worst descendant
- Expand state: the page holds a
HashSet<string>of expandedPaths; default-expand the root + first level; an active filter auto-expands matching paths. State survives live re-renders (keyed byPath). - Threading: the build + in-place merge run on the render thread via the existing
SafeInvokeAsyncmarshalling (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 placeholderAlarmStateChanged(nativeKind,State = Normal). - Add an additive init-only field to
AlarmStateChanged:bool IsConfiguredPlaceholder { get; init; } = false;— additive-only message-contract evolution; defaultfalsekeeps 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 (
AlarmNamevsSourceReference). 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. InspectInstanceActornative-event handling +NativeAlarmActoremission 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):BuildAlarmStatesSnapshotemits 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
IsConfiguredPlaceholderfield (additive-only evolution). - No new component →
README.md/CLAUDE.mdcomponent 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.