811d72255c
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.
99 lines
11 KiB
Markdown
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.
|