1045e7966d
7 tasks (DV-1..DV-7): additive AlarmStateChanged native-binding contract chain, site snapshot native placeholders, DebugTreeNode + pure builder (attribute + alarm trees with roll-up/filter), DebugView tabs reusing TreeView<TItem>, docs, and integration (build + docker + Playwright).
337 lines
22 KiB
Markdown
337 lines
22 KiB
Markdown
# 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.
|