# Debug View — Tabs + Hierarchy Trees Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task. **Goal:** Turn the Central UI Debug View into a tabbed page (Attributes / Alarms) where each tab is a collapsible composition tree showing every configured attribute and alarm with current status, including idle native alarm sources. **Architecture:** Reuse the existing generic `TreeView` Blazor component. A pure `DebugTreeBuilder` transforms the flat snapshot/stream lists (whose path-qualified canonical names encode the composition hierarchy) into node forests with branch-level status roll-up. A small additive change to the `AlarmStateChanged` contract carries the native source-binding canonical name so live native conditions can nest under their binding, and the site snapshot emits placeholder rows for configured-but-quiet native sources. **Tech Stack:** C#/.NET 10, Blazor Server, Akka.NET (InstanceActor, StreamRelayActor), gRPC (sitestream.proto), Newtonsoft cross-process serializer, xUnit + bUnit, Playwright. **Design doc:** `docs/plans/2026-06-17-debugview-tabs-trees-design.md` (committed 811d722). **Base:** branch `debugview-tabs-trees` off `origin/main` (670b607). --- ## Execution notes (read before starting) - **No worktrees in implementers.** This plan already runs in the `debugview-tabs-trees` worktree. Implementer subagents must NOT create their own worktree. - **Pathspec commits only.** Commit with `git commit -- ` (never `git add -A`/`-a`). Retry once on `index.lock`. - **Concurrency.** Keep ≤2–3 concurrent committers per wave; after each parallel wave, verify every task's commit is on `HEAD` (`git log --oneline -`), recover any orphan via cherry-pick. - **Targeted builds/tests per task** (`dotnet build `, `dotnet test --filter ...`). Full-solution build + docker rebuild only in the final integration task. - **Proto codegen:** `sitestream.proto` regenerates `Sitestream.cs` on build of `ZB.MOM.WW.ScadaBridge.Communication` — no manual codegen step. ## Suggested waves (for subagent-driven execution) - **Wave 1:** DV-1 (high-risk; serial spec→code review). - **Wave 2:** DV-2 ∥ DV-3 (disjoint files; both need DV-1's new fields). - **Wave 3:** DV-4 (alarm-tree builder; needs DV-3's model). - **Wave 4:** DV-5 ∥ DV-6 (DebugView page ∥ docs; disjoint). - **Wave 5:** DV-7 (integration). --- ### Task DV-1: Native-binding linkage — additive `AlarmStateChanged` contract chain **Classification:** high-risk **Estimated implement time:** ~5 min **Parallelizable with:** none **Why high-risk:** touches a cross-process message contract (Newtonsoft + gRPC proto), the native alarm actor, and both relay directions. Additive-only, but verify nothing existing breaks. **Files:** - Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Streaming/AlarmStateChanged.cs:73` (add two init properties before the closing brace) - Modify: `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/NativeAlarmActor.cs:316-334` (set `NativeSourceCanonicalName` in the `Emit` initializer) - Modify: `src/ZB.MOM.WW.ScadaBridge.Communication/Protos/sitestream.proto:86` (add fields 22 + 23 to `AlarmStateUpdate`) - Modify: `src/ZB.MOM.WW.ScadaBridge.Communication/Actors/StreamRelayActor.cs:62-78` (pack the two fields) - Modify: `src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcClient.cs:231-249` (unpack the two fields) - Test: `tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/StreamRelayActorTests.cs` (or `ProtoRoundtripTests.cs` / `SiteStreamGrpcClientTests.cs` — match where alarm round-trip is already asserted) - Test: `tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/NativeAlarmActorTests.cs` **Step 1: Write failing tests** In the Communication tests, extend the existing alarm round-trip test (find the test that packs an `AlarmStateChanged` → `AlarmStateUpdate` → back) to set `NativeSourceCanonicalName = "Motor1.MotorAlarms"` and `IsConfiguredPlaceholder = true` on the input and assert both survive the round-trip. In `NativeAlarmActorTests`, assert that an emitted `AlarmStateChanged` (the one told to the parent on a live transition) has `NativeSourceCanonicalName == _source.CanonicalName`. **Step 2: Run, expect fail** (`NativeSourceCanonicalName`/`IsConfiguredPlaceholder` don't exist yet → compile error). **Step 3: Implement** `AlarmStateChanged.cs` — add after `LimitValue` (line 73): ```csharp /// /// Canonical name of the native alarm SOURCE BINDING this condition belongs to /// (e.g. "Motor1.MotorAlarms"). Lets the Debug View nest live native conditions /// under their configured binding node. Empty for computed alarms. Additive. /// public string NativeSourceCanonicalName { get; init; } = string.Empty; /// /// True when this row is a placeholder emitted for a CONFIGURED native source /// binding that currently has no active conditions, so the Debug View tree can /// show the binding node as "no active conditions". Additive; default false. /// public bool IsConfiguredPlaceholder { get; init; } ``` `NativeAlarmActor.cs` — in the `Emit` object initializer (after `LimitValue = t.LimitValue`): ```csharp NativeSourceCanonicalName = _source.CanonicalName, ``` `sitestream.proto` — append to `message AlarmStateUpdate` (after line 86): ```proto string native_source_canonical_name = 22; // native binding canonical name; empty for computed bool is_configured_placeholder = 23; // true for a quiet-binding placeholder row ``` `StreamRelayActor.cs` — in the `AlarmStateUpdate` initializer (alongside `SourceReference`): ```csharp NativeSourceCanonicalName = msg.NativeSourceCanonicalName ?? string.Empty, IsConfiguredPlaceholder = msg.IsConfiguredPlaceholder, ``` `SiteStreamGrpcClient.cs` — in the `new AlarmStateChanged(...) { ... }` initializer (alongside `SourceReference`): ```csharp NativeSourceCanonicalName = evt.AlarmChanged.NativeSourceCanonicalName ?? string.Empty, IsConfiguredPlaceholder = evt.AlarmChanged.IsConfiguredPlaceholder, ``` **Step 4: Run tests** ``` dotnet build src/ZB.MOM.WW.ScadaBridge.Communication/ZB.MOM.WW.ScadaBridge.Communication.csproj dotnet test tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/ZB.MOM.WW.ScadaBridge.Communication.Tests.csproj --filter "FullyQualifiedName~StreamRelayActor|FullyQualifiedName~ProtoRoundtrip|FullyQualifiedName~SiteStreamGrpcClient" dotnet test tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.csproj --filter "FullyQualifiedName~NativeAlarmActor" ``` Expected: PASS. **Step 5: Commit** `git commit -- src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Streaming/AlarmStateChanged.cs src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/NativeAlarmActor.cs src/ZB.MOM.WW.ScadaBridge.Communication/Protos/sitestream.proto src/ZB.MOM.WW.ScadaBridge.Communication/Actors/StreamRelayActor.cs src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcClient.cs tests/...` --- ### Task DV-2: Site snapshot — placeholder rows for idle native source bindings **Classification:** standard **Estimated implement time:** ~4 min **Parallelizable with:** DV-3 **Depends on:** DV-1 **Files:** - Modify: `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/InstanceActor.cs:53-54` (add `_nativeAlarmKinds` dict), `:1447-1470` (record kind at native-actor creation), `:1094-1114` (`BuildAlarmStatesSnapshot` native placeholder loop) - Test: `tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/InstanceActorNativeAlarmTests.cs` **Step 1: Write failing tests** Two tests against an InstanceActor configured with a native alarm source binding: 1. `BuildAlarmStatesSnapshot_QuietNativeBinding_EmitsPlaceholder` — with a configured native binding and NO live condition for it, the debug snapshot contains exactly one alarm row for that binding canonical name with `IsConfiguredPlaceholder == true`, `State == Normal`, `NativeSourceCanonicalName == `. 2. `BuildAlarmStatesSnapshot_NativeBindingWithLiveCondition_NoPlaceholder` — after the InstanceActor receives an `AlarmStateChanged` (Kind native, `NativeSourceCanonicalName == `), the snapshot contains the live condition and **no** placeholder row for that binding. Match the existing construction pattern in `InstanceActorNativeAlarmTests.cs` (TestKit + a `FlattenedConfiguration` with `NativeAlarmSources`). Reuse the debug-snapshot ask already exercised there if present (`SubscribeDebugViewRequest` / `DebugSnapshotRequest`). **Step 2: Run, expect fail** (no placeholder emitted today). **Step 3: Implement** Add field near the other actor dictionaries (line ~54): ```csharp private readonly Dictionary _nativeAlarmKinds = new(); ``` At native-actor creation (~1447-1470), record the kind per binding using the SAME derivation `NativeAlarmActor` uses for its `_nativeKind` (read `NativeAlarmActor` ctor to see how kind is derived from the source/connection): ```csharp _nativeAlarmKinds[nativeSource.CanonicalName] = ; ``` In `BuildAlarmStatesSnapshot()`, after the existing computed fallback loop and before `return states;`: ```csharp // Native source bindings with no live condition: emit a placeholder so the // Debug View tree shows the configured binding node even when quiet. var liveBindings = _latestAlarmEvents.Values .Where(e => !string.IsNullOrEmpty(e.NativeSourceCanonicalName)) .Select(e => e.NativeSourceCanonicalName) .ToHashSet(); foreach (var binding in _nativeAlarmActors.Keys) { if (liveBindings.Contains(binding)) continue; states.Add(new AlarmStateChanged( _instanceUniqueName, binding, AlarmState.Normal, 0, DateTimeOffset.UtcNow) { Kind = _nativeAlarmKinds.GetValueOrDefault(binding, AlarmKind.NativeOpcUa), NativeSourceCanonicalName = binding, IsConfiguredPlaceholder = true }); } ``` **Step 4: Run** `dotnet test tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.csproj --filter "FullyQualifiedName~InstanceActorNativeAlarm"` → PASS. **Step 5: Commit** pathspec (`InstanceActor.cs` + the test). --- ### Task DV-3: `DebugTreeNode` model + attribute-tree builder (pure, filter, roll-up) **Classification:** standard **Estimated implement time:** ~5 min **Parallelizable with:** DV-2 **Depends on:** DV-1 (uses the new `AlarmStateChanged` fields in the shared model file) **Files:** - Create: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/DebugTreeNode.cs` - Create: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/DebugTreeBuilder.cs` - Test: `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Deployment/DebugTreeBuilderTests.cs` **Step 1: Write failing tests** (attribute tree only in this task): - Root-level attributes (`"Speed"`) become direct leaves; `"Motor1.Speed"` + `"Motor1.Temp"` nest under a `Motor1` branch; `"Motor1.Compressor.Pump"` nests two deep. - Branch roll-up: a branch with any descendant whose `Quality != "Good"` has `HasBadQuality == true`. - Filter prune: `BuildAttributeTree(attrs, "temp")` keeps only matching leaves plus their ancestor branches; empty/whitespace filter returns the full forest. - Leaf key is the full canonical name; children sorted by segment. **Step 2: Run, expect fail** (types don't exist). **Step 3: Implement** `DebugTreeNode.cs`: ```csharp namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Deployment; public sealed class DebugTreeNode { public required string Key { get; init; } // full canonical path — stable TreeView key public required string Segment { get; init; } // display label (last path segment) public List Children { get; } = new(); // Leaf payloads — exactly one is set on a leaf; both null on a pure branch. public AttributeValueChanged? Attribute { get; init; } public AlarmStateChanged? Alarm { get; init; } // computed leaf, native condition, or placeholder public bool IsNativeBinding { get; init; } // branch grouping native conditions // Roll-up (set by the builder for branch nodes). public AlarmState WorstState { get; set; } = AlarmState.Normal; public int ActiveCount { get; set; } public bool HasBadQuality { get; set; } public bool HasChildren => Children.Count > 0; } ``` `DebugTreeBuilder.cs` — implement `BuildAttributeTree(IEnumerable attrs, string? filter)`: - Optionally filter the input: keep an attribute if `AttributeName` contains `filter` (OrdinalIgnoreCase). - Split each `AttributeName` on `'.'`; walk/create branch nodes (Key = accumulated prefix), attach the attribute as a leaf at the terminal segment. - Sort children by `Segment`. - Post-order roll-up: `HasBadQuality` true if the node's own attribute is off-Good or any child has `HasBadQuality`. - (Leave `BuildAlarmTree` as a `// DV-4` stub or omit; DV-4 adds it to the same class.) **Step 4: Run** `dotnet test tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests.csproj --filter "FullyQualifiedName~DebugTreeBuilder"` → PASS. **Step 5: Commit** pathspec (the two new src files + the test). --- ### Task DV-4: Alarm-tree builder — computed leaves + native binding grouping + roll-up **Classification:** standard **Estimated implement time:** ~5 min **Parallelizable with:** none **Depends on:** DV-3 (shares `DebugTreeNode` + `DebugTreeBuilder`), DV-1 (native fields) **Files:** - Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/DebugTreeBuilder.cs` (add `BuildAlarmTree`) - Test: `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Deployment/DebugTreeBuilderTests.cs` (add alarm cases) **Step 1: Write failing tests:** - Computed alarm `"Motor1.HighTemp"` → leaf under `Motor1`. - Native condition (`Kind = NativeOpcUa`, `NativeSourceCanonicalName = "Tank1.Levels"`, `SourceReference = "Tank1.Level.HiHi"`) → nests as a condition child **under a `Tank1.Levels` binding branch** (binding placed by its canonical path under `Tank1`). Two conditions for the same binding → two children of that binding node. - Placeholder row (`IsConfiguredPlaceholder = true`, `NativeSourceCanonicalName = "Tank1.Levels"`) → the binding node renders as present with zero condition children (builder marks `IsNativeBinding` and adds no condition child). - Roll-up: a branch with any descendant `Alarm.State == Active` has `WorstState == Active` and `ActiveCount` = number of active descendants; placeholder rows never count as active. - Filter prune keeps native bindings whose name OR a condition matches. **Step 2: Run, expect fail.** **Step 3: Implement** `BuildAlarmTree`: - Partition input by `Kind`. **Computed:** leaf at the canonical path from `AlarmName`. - **Native:** group by `NativeSourceCanonicalName`. For each binding, create/walk a branch node at the binding's canonical path with `IsNativeBinding = true`. For each non-placeholder condition under it, add a child node (Key = `binding + "::" + SourceReference`, Segment = `SourceReference`, `Alarm = the event`). Placeholder rows add no child (the empty binding node renders "no active conditions" in the page). - Sort; post-order roll-up of `WorstState`/`ActiveCount` (Active dominates; count leaves/conditions with `State == Active && !IsConfiguredPlaceholder`). **Step 4: Run** `--filter "FullyQualifiedName~DebugTreeBuilder"` → PASS. **Step 5: Commit** pathspec. --- ### Task DV-5: DebugView page — tabs + two TreeViews + in-place updates **Classification:** standard **Estimated implement time:** ~5 min **Parallelizable with:** DV-6 **Depends on:** DV-4 (builder), DV-2 (placeholders flow through the snapshot at runtime; not a compile dep) **Files:** - Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/DebugView.razor` - Modify: `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Deployment/DebugViewAlarmTableTests.cs` (rework table assertions → tree assertions) **Step 1: Write/adjust failing tests** — render the connected page (reuse the harness in `DebugViewAlarmTableTests`) and assert: an Attributes tab and an Alarms tab exist (custom `nav-tabs`); switching to Alarms shows configured alarms grouped under their module branch (a `Motor1` branch containing `HighTemp`); a branch header shows the roll-up badge when a descendant alarm is Active. Preserve the dispose/stream-race coverage in `DebugViewDisposalTests.cs` / `DebugViewStreamRaceTests.cs` (those exercise CentralUI-009/021 — the marshalling is unchanged, so they should keep passing; run them, don't rewrite). **Step 2: Run, expect fail.** **Step 3: Implement** - Replace the two side-by-side `
` cards with a Bootstrap `nav-tabs` header + tab panes (custom markup, no third-party lib). Keep the status strip, site/instance selectors, live/snapshot badges, `Connect`/`Disconnect`, localStorage auto-reconnect, and site-scope checks exactly as-is. - Each tab body hosts `` with an `EmptyContent` hint for an empty instance. - `NodeContent` fragments: **attribute leaf** → name / `ValueFormatter.FormatDisplayValue` / quality badge (`GetQualityBadge`) / timestamp; **attribute branch** → segment + bad-quality indicator when `HasBadQuality`. **Alarm computed leaf / native condition** → reuse `GetAlarmStateBadge`, `GetKindBadge`, `FormatKind`, `GetAlarmLevelBadge`, `FormatLevel`, `BuildAlarmTooltip`; **native binding branch** → segment + roll-up badge (worst state + `ActiveCount`); a binding with no children renders "no active conditions"; **alarm branch** → segment + worst-state/active-count roll-up badge. - Keep `_attributeValues` / `_alarmStates` dictionaries as the latest-per-name source of truth but **remove the `MaxRows` cap**, auto-scroll, and Clear controls. Replace `FilteredAttributeValues`/`FilteredAlarmStates` with computed forests: `DebugTreeBuilder.BuildAttributeTree(_attributeValues.Values, _attrFilter)` and `DebugTreeBuilder.BuildAlarmTree(_alarmStates.Values, _alarmFilter)`. Keep the per-tab filter ``; when a filter is non-empty, call the TreeView's `ExpandAll()` via `@ref` so matches are visible. - Live-event handling: keep `HandleStreamEvent` + `SafeInvokeAsync` marshalling and `UpsertWithCap` → rename to a plain upsert (no cap). Rebuilding the forest in the computed property on each `StateHasChanged` is sufficient (pure + cheap for typical instance sizes). **Step 4: Run** `dotnet test tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests.csproj --filter "FullyQualifiedName~DebugView"` → PASS (DebugViewAlarmTable reworked; Disposal + StreamRace still green). **Step 5: Commit** pathspec. --- ### Task DV-6: Documentation **Classification:** small **Estimated implement time:** ~3 min **Parallelizable with:** DV-5 **Depends on:** DV-4 **Files:** - Modify: `docs/requirements/Component-CentralUI.md` (Debug View section — tabs + trees, branch roll-up, native binding nodes, idle-source placeholders) - Modify: `docs/requirements/Component-SiteRuntime.md` (snapshot enrichment: placeholder rows for quiet native bindings; `_nativeAlarmKinds`) - Modify: `docs/requirements/Component-Commons.md` *(if present; else note in CentralUI doc)* — streaming contract note: additive `NativeSourceCanonicalName` + `IsConfiguredPlaceholder` on `AlarmStateChanged` (and proto fields 22/23), additive-only evolution. **Steps:** Update the prose to match the shipped behaviour; no README/CLAUDE component-table change (no new component, stays at 25). Commit pathspec. (No tests.) --- ### Task DV-7: Integration — full build, docker rebuild, Playwright, smoke **Classification:** high-risk **Estimated implement time:** ~5 min (+ build/deploy wall-time) **Parallelizable with:** none **Depends on:** DV-5, DV-6, DV-2 **Files:** - Create: `tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/DebugViewTreeTests.cs` **Steps:** 1. Full-solution build: `dotnet build ZB.MOM.WW.ScadaBridge.slnx` → 0 errors. 2. Run the affected unit suites green: Communication.Tests (Grpc filter), SiteRuntime.Tests (InstanceActorNativeAlarm), CentralUI.Tests (DebugView + DebugTreeBuilder + TreeView). 3. Rebuild the local cluster: `bash docker/deploy.sh`. 4. One Playwright test (`DebugViewTreeTests`): log in (`multi-role`/`password` via `localhost:9000`), navigate to `/deployment/debug-view`, select a site + instance with composition + a configured alarm (seed via CLI in the fixture, mirroring `TemplateCrudTests`/`InstanceConfigure*` fixtures), Connect, switch to the Alarms tab, assert the configured alarm appears under its module branch and a branch roll-up badge renders. Reuse `PlaywrightFixture`. 5. Manual smoke (note results in the task comment): connect to a real instance, confirm Attributes + Alarms tabs render trees, idle native source shows a "no active conditions" binding node, a fired alarm rolls up to its branch. 6. Commit pathspec (the Playwright test). Then hand off to `finishing-a-development-branch`. --- ## Risks / watch-items - **Existing DebugView tests** (`DebugViewAlarmTableTests`) assert the flat-table DOM and WILL break — DV-5 reworks them. The concurrency tests (`DebugViewDisposalTests`, `DebugViewStreamRaceTests`) should stay green because the stream-callback marshalling is unchanged; if they fail, the merge logic diverged from the original `SafeInvokeAsync`/dispose-guard contract — fix the merge, don't weaken the tests. - **Native kind for placeholders** (DV-2): derive it the same way `NativeAlarmActor` does; if the derivation is awkward to reach from `InstanceActor`, defaulting the placeholder badge to a generic native kind is acceptable (cosmetic only) — note the choice in the commit. - **Forest rebuild on every event** (DV-5) is intentional (pure + cheap). If profiling ever shows it hot for very large instances, switch to incremental node patching — out of scope here.