From 811d72255c35807d90176f326bcaa1875fe83720 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 17 Jun 2026 14:06:08 -0400 Subject: [PATCH] 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-debugview-tabs-trees-design.md | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 docs/plans/2026-06-17-debugview-tabs-trees-design.md diff --git a/docs/plans/2026-06-17-debugview-tabs-trees-design.md b/docs/plans/2026-06-17-debugview-tabs-trees-design.md new file mode 100644 index 00000000..f690567f --- /dev/null +++ b/docs/plans/2026-06-17-debugview-tabs-trees-design.md @@ -0,0 +1,98 @@ +# 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` 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 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` 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.