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

99 lines
11 KiB
Markdown

# 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 `Path`s; 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.