# 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<TItem>` 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 -- <explicit paths>` (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 -<n>`), recover any orphan via cherry-pick.
- **Targeted builds/tests per task** (`dotnet build <project.csproj>`, `dotnet test <testproject.csproj> --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
/// <summary>
/// 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.
/// </summary>
public string NativeSourceCanonicalName { get; init; } = string.Empty;

/// <summary>
/// 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.
/// </summary>
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 == <binding>`.
2. `BuildAlarmStatesSnapshot_NativeBindingWithLiveCondition_NoPlaceholder` — after the InstanceActor receives an `AlarmStateChanged` (Kind native, `NativeSourceCanonicalName == <binding>`), 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<string, AlarmKind> _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] = <kind derived as NativeAlarmActor does>;
```
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<DebugTreeNode> 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<AttributeValueChanged> 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 `<div class="col-md-*">` 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 `<TreeView TItem="DebugTreeNode" Items="..." ChildrenSelector="n => n.Children" HasChildrenSelector="n => n.HasChildren" KeySelector="n => n.Key" NodeContent="..." StorageKey="debugview.attrTree"/...>` 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 `<input>`; 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.
