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.
This commit is contained in:
@@ -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<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.
|
||||||
Reference in New Issue
Block a user