Bite-sized per-task plan for T22-T26, T28, T30-T32 + CLI cached-call retry/discard. ManagementActor.cs/ValidationService.cs/shared-razor serialization points enumerated; blockedBy deps + Parallelizable-with sets; integration trace + EF model-drift + shared fixture re-run checklist per integration-catches-cross-cutting-gaps.
33 KiB
M9 — Templates & Authoring Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development to implement this plan task-by-task.
Goal: Deliver the in-scope authoring backlog — searchable/reorderable template tree, move + live-status for data connections, multi-level inheritance authoring + staleness banner, opt-in strict trigger analysis, schema-driven value entry ($ref library + nested forms + Monaco hover), and CLI cached-call Retry/Discard.
Architecture: Mostly Central UI (Blazor Server) layered on existing seams, a few new ManagementActor commands + handlers, one new EF entity (SharedSchema) + migration, and a read-only template-inheritance resolve service. Every cross-cluster command is traced end-to-end at integration. No new NuGet packages (System.Text.Json only).
Tech Stack: .NET 10, C#, Akka.NET (ManagementActor), EF Core (ConfigurationDatabase), Blazor Server + Bootstrap (CentralUI), xUnit + bUnit + NSubstitute (tests), System.CommandLine (CLI).
Design doc: docs/plans/2026-06-18-m9-templates-authoring-design.md (decisions D1–D7). Branch: worktree-m9-templates-authoring off origin/main @ 72aec3b4.
Execution rules (read before dispatching)
- Worktree: all work happens in this worktree. Implementer subagents do NOT create their own worktrees.
- Commits: pathspec form only —
git commit -m "msg" -- <explicit paths>(-mBEFORE--). New files:git add <explicit path>(targeted; nevergit add -A/-a/.). Retry once onindex.lock. - Concurrency: ≤2–3 concurrent committers. After each wave, verify every wave commit is on
HEAD(git log --oneline); recover any orphan via cherry-pick. - Shared-file serialization points (NEVER dispatch two of these concurrently):
ManagementService/ManagementActor.cs+Commons/Messages/Management/ManagementCommandRegistry.cs→ T23a, T24a, T32c, T26a (theParallelizable withlists already exclude each other — honor it).TemplateEngine/Validation/ValidationService.cs→ T28a, T32b.Components/Pages/Design/Templates.razor→ T22, T23b (T23b is blocked by both).Components/Pages/Design/DataConnections.razor→ T25, T24b (T24b blocked by both).Components/Pages/Design/TemplateEdit.razor→ T28b, T26b (different waves; do not run together).Components/Shared/ParameterValueForm.razor/ value-entry surface → T30, T31 (T31 blocked by T30).
- Builds/tests: targeted per task — build only the touched project(s), run the filtered test class. Full-solution build + docker rebuild + Playwright + live smoke happen ONLY in the integration task (M9-INT).
- Do not trust filtered/
--no-buildgreen from a subagent; the integration task runs an unfiltered build of every touched project.
Wave / dependency overview
| Wave | Tasks (parallel within wave unless shared-file) |
|---|---|
| 1 | T22 ‖ CLI ‖ T28a → T28b |
| 2 | T23a → T23b ‖ T25 |
| 3 | (T23a✓ then) T24a ‖ T32a → T32b |
| 4 | T32c ‖ T30 ‖ T31 ‖ T26a → T26b ; T24b |
| 5 | INT |
blockedBy (logical deps): T28b←T28a · T23b←T22,T23a · T24b←T24a,T25 · T32b←T32a · T32c←T32a,T32b · T30←T32b · T31←T32b,T30 · T26b←T26a · INT←all. ManagementActor tasks (T23a,T24a,T32c,T26a) are serialized by Parallelizable with omission, not blockedBy.
Wave 1
Task T22: Template tree search box
Classification: small Estimated implement time: ~3 min Parallelizable with: CLI, T28a, T32a (NOT T23b — same file)
Files:
- Modify:
src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/Templates.razor - Test:
tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/(bUnit — add a Templates search test; mirror an existingTemplates/DataConnectionsrender test)
Context: TemplateFolderTree.razor already implements recursive substring filtering + ancestor auto-expand via its Filter parameter (ApplyFilter/CopyMatching, invoked in OnParametersSet). Templates.razor uses TemplateFolderTree but never sets Filter and has no search input. DataConnections.razor is the reference for a search box. This is UI wiring only — no service/entity/command change.
Step 1: Write the failing bUnit test — render Templates with a folder/template set, type into the search input, assert the tree shows only matching nodes (and ancestors), and that clearing restores the full tree. Reference an existing CentralUI.Tests fixture for service-substitute setup (the page injects template/folder query services — register NSubstitute fakes returning a small tree).
Step 2: Run it, expect FAIL (no search input / Filter not bound).
dotnet test tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests.csproj --filter "FullyQualifiedName~Templates"
Step 3: Implement — add a _searchText field + a search <input> (@bind="_searchText" @bind:event="oninput") above the tree, with a clear (✕) affordance; pass Filter="_searchText" to <TemplateFolderTree>. Match the styling of the DataConnections.razor search box.
Step 4: Run test, expect PASS.
Step 5: Commit — git commit -m "feat(m9/T22): template tree search box (wire TemplateFolderTree.Filter)" -- src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/Templates.razor tests/...
Acceptance: search filters the template tree with auto-expanded ancestors; clearing restores full tree + prior expansion; no backend change.
Task CLI: cached-call Retry/Discard command group
Classification: small Estimated implement time: ~4 min Parallelizable with: T22, T28a, T28b, T23a, T25, T32a (CLI-isolated)
Files:
- Create:
src/ZB.MOM.WW.ScadaBridge.CLI/Commands/CachedCallCommands.cs - Modify:
src/ZB.MOM.WW.ScadaBridge.CLI/Program.cs(register the new command group) - Modify:
src/ZB.MOM.WW.ScadaBridge.CLI/README.md,docs/requirements/Component-CLI.md - Test:
tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/(mirrorNotificationCommands/SiteCommandscommand-construction tests)
Context: Backend relay already exists and is Deployer-gated: RetryParkedMessageCommand / DiscardParkedMessageCommand in Commons/Messages/Management/RemoteQueryCommands.cs, dispatched at ManagementActor.cs:220,380. The CLI sends commands via CommandHelpers.ExecuteCommandAsync → ManagementHttpClient.SendCommandAsync, resolving the command name via ManagementCommandRegistry.GetCommandName. First verify RetryParkedMessageCommand/DiscardParkedMessageCommand are present in ManagementCommandRegistry.cs — if not, that registration is part of this task (and flag it for the M9-INT trace).
Step 1: Write the failing test — construct cached-call retry --site-id S1 --tracked-operation-id <guid> and assert it maps to RetryParkedMessageCommand(siteIdentifier, messageId); same for discard → DiscardParkedMessageCommand. Model on the existing NotificationCommands test.
Step 2: Run, expect FAIL (command group absent).
dotnet test tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/ZB.MOM.WW.ScadaBridge.CLI.Tests.csproj --filter "FullyQualifiedName~CachedCall"
Step 3: Implement — CachedCallCommands.Build() returning a cached-call command with retry + discard subcommands (options --site-id, --tracked-operation-id), each calling CommandHelpers.ExecuteCommandAsync(...). Register in Program.cs. Verify/add registry mappings. Document in both CLI docs.
Step 4: Run test, expect PASS.
Step 5: Commit (pathspec, including the new file via git add).
Acceptance: cached-call retry|discard resolves + sends the existing Deployer-gated commands; registry maps both names; docs updated.
Task T28a: Strict expression-trigger analysis kind — backend
Classification: small Estimated implement time: ~5 min Parallelizable with: T22, CLI, T32a (NOT T32b — ValidationService shared)
Files:
- Modify: trigger config carrier — confirm whether the kind rides the existing
TriggerConfigurationJSON (preferred — additive, no migration) onCommons/Entities/Templates/TemplateAlarm.cs/TemplateScript.cs, or needs a dedicated field. Default: carry it in the trigger-config JSON to avoid a migration. - Modify:
src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Validation/ValidationService.cs(CheckExpressionTrigger, ~line 263–401) - Test:
tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/
Context (D7): Expression triggers ALREADY get a real Roslyn compile + forbidden-API + undefined-attribute analysis that blocks deploy (M2/M3). Blank-expression and similar are currently advisory (warnings). First confirm the exact current advisory set by reading CheckExpressionTrigger. T28a adds an AnalysisKind (default Advisory = today's behavior exactly; Strict = promote those advisory findings to deploy-blocking errors). Keep it minimal — do NOT re-implement analysis.
Step 1: Write failing tests — (a) Advisory kind on a trigger with a blank/ambiguous expression → validation passes with a warning (current behavior preserved); (b) Strict kind on the same → validation FAILS (deploy-blocking error). Use the existing ValidationService test fixtures for a template + expression trigger.
Step 2: Run, expect FAIL (no kind; both currently warn).
dotnet test tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests.csproj --filter "FullyQualifiedName~Trigger"
Step 3: Implement — read AnalysisKind from the trigger config (default Advisory); in CheckExpressionTrigger, when Strict, add the currently-advisory findings to the error list instead of the warning list. Additive; Advisory path byte-for-byte unchanged.
Step 4: Run, expect PASS (+ existing ValidationService tests still green).
Step 5: Commit.
Acceptance: Advisory preserves current pass/warn behavior; Strict escalates advisory findings to deploy-blocking; no migration; analysis logic unchanged.
Task T28b: Strict trigger-kind — UI selector + CLI flag
Classification: small Estimated implement time: ~4 min Parallelizable with: T25, T23a (NOT T26b — TemplateEdit shared; different wave anyway) blockedBy: T28a
Files:
- Modify: the alarm/script trigger editor UI — locate the trigger-config editor in/under
src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/TemplateEdit.razor(or its trigger sub-component) - Modify:
src/ZB.MOM.WW.ScadaBridge.CLI/Commands/TemplateCommands.cs(add--trigger-kind/--strictto the relevant alarm/script trigger sub-command) - Test: CentralUI.Tests (bUnit selector) + CLI.Tests (flag → command field)
Step 1: Write failing tests — selector defaults to Advisory and round-trips Strict into the trigger config; CLI --strict sets the kind on the create/update command.
Step 2: Run, expect FAIL.
Step 3: Implement — a small Advisory|Strict <select> in the trigger editor bound into the trigger config; CLI flag wired into the existing trigger command.
Step 4: Run, expect PASS.
Step 5: Commit.
Acceptance: authors can set the kind in the UI and CLI; default Advisory; value persists through the trigger config consumed by T28a.
Wave 2
Task T23a: Folder sibling reorder — service + command + handler
Classification: standard Estimated implement time: ~5 min Parallelizable with: T25 (NOT T24a/T32c/T26a — ManagementActor.cs shared)
Files:
- Modify:
src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Services/TemplateFolderService.cs - Modify:
src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/TemplateFolderCommands.cs - Modify:
src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs - Modify:
src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/ManagementCommandRegistry.cs - Test:
tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Services/TemplateFolderServiceTests.cs
Context: TemplateFolder.SortOrder (TemplateFolder.cs:12) already exists; TemplateFolderService has Create/Rename/Move/Delete (Designer-gated, audited) but no reorder. Match the existing command/handler/registry pattern exactly (see the sibling folder commands).
Step 1: Write failing tests — ReorderFolderAsync(folderId, direction, user) swaps SortOrder with the adjacent sibling under the same parent; no-op at the top/bottom; reorder of 3+ siblings produces the expected order; audit row written.
Step 2: Run, expect FAIL.
dotnet test tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests.csproj --filter "FullyQualifiedName~TemplateFolderService"
Step 3: Implement — ReorderFolderAsync (swap SortOrder among same-parent siblings, ordered by SortOrder then Name); ReorderTemplateFolderCommand(FolderId, Direction) (Up/Down); ManagementActor handler (Designer-gated, mirror Move handler); register in ManagementCommandRegistry. Ensure folder-tree builders order siblings by SortOrder.
Step 4: Run, expect PASS.
Step 5: Commit (include ManagementActor + registry in the pathspec).
Acceptance: reorder swaps adjacent siblings, ends are no-ops, ordering persists + is honored by tree loads, audited; command registered.
Task T23b: Folder reorder + root context menu — UI
Classification: standard Estimated implement time: ~4 min Parallelizable with: T25 (NOT T22 — same file) blockedBy: T22, T23a
Files:
- Modify:
src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/Templates.razor - Test: CentralUI.Tests (bUnit menu render/actions)
Context: Folder context menu already has New Folder/Template, Rename, Move…, Delete (Templates.razor:217). Add Move-up/Move-down (calling the new ReorderTemplateFolderCommand via the folder service used by the page) and a root-level context menu (right-click root/empty → New Folder at root, New Template at root).
Step 1: Write failing test — folder context menu exposes Move up/Move down and invokes the reorder command; root context menu offers New Folder/Template at root.
Step 2–4: FAIL → implement → PASS.
Step 5: Commit.
Acceptance: menu-based sibling reorder works end-to-end through the page; root context menu present; no drag-drop (D4).
Task T25: Connection live-status indicators
Classification: standard Estimated implement time: ~5 min Parallelizable with: T23a, T23b, T28b (NOT T24b — DataConnections.razor shared)
Files:
- Create/Modify: a CentralUI health-query method exposing
connectionId → ConnectionHealthfor a site (add to an existing CentralUI service, e.g. alongsideServices/AuditLogQueryService.cs, or injectICentralHealthAggregatordirectly) + register insrc/ZB.MOM.WW.ScadaBridge.CentralUI/ServiceCollectionExtensions.cs - Modify:
src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/DataConnections.razor - Test:
tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/— and update existingDataConnectionsfixtures to register the new injected service (shared-component injection-regression guard).
Context (D6): Health already flows DCL → SiteHealthReport.DataConnectionStatuses (name→ConnectionHealth) → ICentralHealthAggregator.GetSiteState. Health.razor renders the badges (GetConnectionHealthBadge). Surface the SAME data on the design page: resolve names→ids via the site repo, render a per-node badge, refresh on a ~10s poll timer (mirror Health page).
Step 1: Write failing tests — (a) the query maps DataConnectionStatuses to {connectionId: health}, tolerating missing reports (empty map) and unknown names; (b) bUnit: a connection node renders the health badge for its status.
Step 2: Run, expect FAIL.
dotnet test tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests.csproj --filter "FullyQualifiedName~DataConnection"
Step 3: Implement — the query method + DI registration; in DataConnections.razor, load the health map, render a badge per connection node, add a poll timer. Reuse GetConnectionHealthBadge-style classes.
Step 4: Run, expect PASS (+ existing DataConnections fixtures green with the new service substitute).
Step 5: Commit.
Acceptance: design page shows live per-connection health (badge), refreshed on poll; missing-report tolerated; existing fixtures updated + green.
Wave 3
Task T24a: Move data connection between sites — command + handler + guards
Classification: high-risk Estimated implement time: ~5 min Parallelizable with: T32a (NOT T23a/T32c/T26a — ManagementActor.cs shared)
Files:
- Modify:
src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/DataConnectionCommands.cs - Modify:
src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs - Modify:
src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/ManagementCommandRegistry.cs - Modify (if a binding lookup is needed):
src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ISiteRepository.cs+src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/SiteRepository.cs - Test:
tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/
Context (D5): No move today; UpdateDataConnectionCommand doesn't change SiteId. Persist via existing ISiteRepository.UpdateDataConnectionAsync. Guards, server-side, all enforced before the write:
- Target site exists.
- No connection name collision at the target site.
- Reject if any
InstanceConnectionBindingreferences the connection (instances are site-scoped) — error message names the blocking instance(s). - Validate name-based references (
TemplateNativeAlarmSource.ConnectionName,InstanceNativeAlarmSourceOverride.ConnectionNameOverride) won't collide/orphan at the target. Emit an audit row on success.
Step 1: Write failing tests — success path moves SiteId; blocked-by-binding returns an error naming the instance; name-collision-at-target returns an error; audit row asserted. Use the ManagementActor test harness with substituted repos.
Step 2: Run, expect FAIL.
dotnet test tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests.csproj --filter "FullyQualifiedName~MoveDataConnection"
Step 3: Implement — MoveDataConnectionCommand(DataConnectionId, TargetSiteId) (Designer-gated); HandleMoveDataConnection with the four guards + audit; register in ManagementCommandRegistry; add a binding-count/query repo method if one doesn't exist.
Step 4: Run, expect PASS.
Step 5: Commit (include ManagementActor + registry).
Acceptance: guarded move succeeds only when safe; blockers return clear errors; audited; command registered. (Routing trace re-verified at M9-INT.)
Task T24b: Move connection — UI dialog + action
Classification: standard Estimated implement time: ~4 min Parallelizable with: T32b (NOT T25 — DataConnections.razor shared) blockedBy: T24a, T25
Files:
- Create:
src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/MoveDataConnectionDialog.razor - Modify:
src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/DataConnections.razor - Test: CentralUI.Tests (bUnit)
Context: Mirror MoveFolderDialog.razor (picker + error surface). Add a "Move to Site…" action (context menu or row action) opening the dialog (target-site picker), which calls MoveDataConnectionCommand and surfaces guard errors inline.
Step 1–5: failing bUnit (dialog opens, site picker, calls command, shows guard error) → implement → PASS → commit.
Acceptance: operator can move a connection via the dialog; guard errors shown inline; tree refreshes on success.
Task T32a: SharedSchema entity + EF config + migration + repository
Classification: high-risk Estimated implement time: ~5 min Parallelizable with: T24a, T23a (disjoint — Commons/ConfigDB only)
Files:
- Create:
src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Schemas/SharedSchema.cs - Create:
src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ISharedSchemaRepository.cs - Create:
src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Configurations/SharedSchemaConfiguration.cs - Create:
src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/SharedSchemaRepository.cs - Modify: the ConfigDB
DbContext(DbSet<SharedSchema>) + DI registration of the repo - Create: EF migration in
src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Migrations/(idempotent;dotnet ef migrations add AddSharedSchema) - Test: ConfigurationDatabase tests (round-trip) — use the existing ConfigDB test harness
Context (D2): SharedSchema { int Id; string Name (unique); string? Scope; string SchemaJson; ... }. Idempotent migration (the M2-pre PendingModelChangesWarning lesson) — after adding, run dotnet build and confirm no pending-model-changes warning. No new package.
Step 1: Write failing test — create + read-by-name a SharedSchema; unique-name enforced.
Step 2: Run, expect FAIL.
Step 3: Implement — entity + config (unique index on Name) + repo (AddAsync/GetByNameAsync/ListAsync/UpdateAsync/DeleteAsync) + DbSet + migration + DI.
Step 4: Run, expect PASS; confirm dotnet build has no model-drift warning.
Step 5: Commit (add new files explicitly).
Acceptance: entity persists + round-trips, unique name, repo wired, migration idempotent, no model drift.
Task T32b: JSON Schema $ref resolver + deploy-time validation
Classification: high-risk Estimated implement time: ~5 min Parallelizable with: T24b (NOT T28a — ValidationService shared) blockedBy: T32a
Files:
- Modify:
src/ZB.MOM.WW.ScadaBridge.Commons/Types/InboundApi/InboundApiSchema.cs(Parse/ParseSchema) - Modify:
src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Validation/ValidationService.cs(deploy-time$ref-target existence check) - Test:
tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/(resolver) + TemplateEngine.Tests (deploy-block on dangling ref)
Context (D2): Add a $ref resolver to the existing recursive parser. Resolve {"$ref":"lib:Name"} against a library-lookup seam (a Func<string,string?>/interface supplied by the caller — the library entries come from ISharedSchemaRepository from T32a). Depth/cycle-guarded; System.Text.Json only; no new package. Deploy-time validation blocks on a dangling $ref.
Step 1: Write failing tests — (a) a schema with a valid $ref resolves to the referenced shape; (b) a dangling $ref → deploy-blocking validation error; (c) cycle/depth guard returns a controlled error (no stack overflow).
Step 2: Run, expect FAIL.
dotnet test tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/ZB.MOM.WW.ScadaBridge.Commons.Tests.csproj --filter "FullyQualifiedName~InboundApiSchema"
Step 3: Implement — $ref resolution in the parser via the lookup seam; wire deploy-time target-existence validation into ValidationService.
Step 4: Run, expect PASS (+ existing Commons.Tests/ValidationService tests green).
Step 5: Commit.
Acceptance: $ref resolves through the library seam; dangling ref blocks deploy; depth/cycle-safe; no new package.
Wave 4
Task T32c: Schema library — CRUD commands + handlers + Central UI page
Classification: high-risk Estimated implement time: ~5 min Parallelizable with: T30, T31, T26b (NOT T26a/T24a/T23a — ManagementActor.cs shared) blockedBy: T32a, T32b
Files:
- Create:
src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/SchemaLibraryCommands.cs - Modify:
src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs+ManagementCommandRegistry.cs - Create: a Central UI schema-library page under
src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/(reuseSchemaBuilder.razor) + nav entry - Modify:
src/ZB.MOM.WW.ScadaBridge.CentralUI/ServiceCollectionExtensions.cs(query service if needed) - Test: ManagementService.Tests (CRUD handlers) + CentralUI.Tests (page bUnit)
Step 1: Write failing tests — create/list/update/delete SharedSchema via commands (Designer-gated); page lists + edits schemas via SchemaBuilder.
Step 2: Run, expect FAIL.
Step 3: Implement — CRUD commands + handlers (call ISharedSchemaRepository) + registry; the page (list + SchemaBuilder editor) + nav entry under Design.
Step 4: Run, expect PASS.
Step 5: Commit (include ManagementActor + registry).
Acceptance: schema-library CRUD round-trips through ManagementActor; UI authors library schemas; commands registered.
Task T30: Schema-driven nested value-entry forms
Classification: standard Estimated implement time: ~5 min Parallelizable with: T32c, T26a, T26b (NOT T31 — value-entry surface shared) blockedBy: T32b
Files:
- Modify:
src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/ParameterValueForm.razor - Test: CentralUI.Tests (bUnit)
Context: ParameterValueForm.razor:52 renders scalars but falls back to a JSON textarea for Object/List. Extend it to recursively render object fields + list items as typed inputs, driven by the parsed InboundApiSchema (including $ref-resolved schemas from T32b). Per-field validation via InboundApiSchema.Validate; collect to canonical JSON.
Step 1: Write failing tests — a nested object schema renders per-field inputs (not a raw textarea); a list renders add/remove typed item rows; per-field validation error surfaces; round-trips to canonical JSON; a $ref-bearing schema renders the resolved shape.
Step 2: Run, expect FAIL.
dotnet test tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests.csproj --filter "FullyQualifiedName~ParameterValue"
Step 3: Implement — recursive render of object/list via the schema model; per-field validate; keep the JSON textarea only as an "advanced/raw" escape hatch if useful.
Step 4: Run, expect PASS.
Step 5: Commit.
Acceptance: object/list values entered via typed nested forms; per-field validation; $ref-resolved schemas render; canonical JSON output preserved.
Task T31: Monaco JSON-Schema hover/completion
Classification: standard Estimated implement time: ~4 min Parallelizable with: T32c, T26a (NOT T30 — value-entry surface shared) blockedBy: T32b, T30
Files:
- Modify:
src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/MonacoEditor.razor(+ its JS interop file, if separate) - Modify: the value-entry surface that uses Monaco for JSON (e.g. the raw-JSON escape hatch in
ParameterValueForm.razor) - Test: CentralUI.Tests where feasible (interop is JS — assert the schema is passed to the editor config; otherwise a smoke render)
Context: Monaco is already integrated for script editing. Use Monaco's built-in JSON language schema support (no new package): feed the resolved JSON Schema to the editor so JSON value editing gets hover + completion.
Step 1: Write failing test — the editor is configured with the JSON-schema option carrying the (resolved) schema for the current parameter/value.
Step 2–4: FAIL → implement (pass schema into the Monaco JSON config via interop) → PASS.
Step 5: Commit.
Acceptance: JSON value editing surfaces schema-driven hover + completion; no new package.
Task T26a: Inheritance resolve service + query command
Classification: high-risk Estimated implement time: ~5 min Parallelizable with: T30, T31 (NOT T32c/T24a/T23a — ManagementActor.cs shared)
Files:
- Create/Modify: a read-only resolve method — extend
src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateResolver.cs(reuseBuildInheritanceChain) or a newTemplateInheritanceResolver - Create:
GetResolvedTemplateMembersCommand(Commons/Messages/Management) - Modify:
src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs+ManagementCommandRegistry.cs - Test:
tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/
Context (D1): Read-only. Given a derived/child template, walk the FULL inheritance chain (BuildInheritanceChain, arbitrary depth) and return the effective inherited member set — including base members added AFTER the derived template was created and across ≥2 levels — annotated per member with origin (own override / inherited-from-X / locked) + a staleness summary (stored derived rows vs. freshly-resolved chain). No mutation — flattening at deploy is already correct; this only feeds the editor.
Step 1: Write failing tests — A→B→C chain: editing C resolves members inherited transitively from A; a base member added to A after C was created appears in C's resolved set; locked members flagged; staleness summary true when stored rows differ, false when in sync; composition-derived template handled.
Step 2: Run, expect FAIL.
dotnet test tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests.csproj --filter "FullyQualifiedName~Resolve"
Step 3: Implement — the resolve method + GetResolvedTemplateMembersCommand + handler + registry. Strictly read-only.
Step 4: Run, expect PASS.
Step 5: Commit (include ManagementActor + registry).
Acceptance: full multi-level inherited member set resolved (incl. post-creation base additions), origin + staleness annotated, read-only, command registered.
Task T26b: TemplateEdit — full inherited set + staleness banner
Classification: standard Estimated implement time: ~5 min Parallelizable with: T30, T31, T32c (NOT T28b — TemplateEdit shared; T28b is wave 1) blockedBy: T26a
Files:
- Modify:
src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/TemplateEdit.razor - Test: CentralUI.Tests (bUnit)
Context: Today the editor loads only the immediate base (_baseTemplate, _baseAttributesByName, …). Switch it to render the FULL resolved inherited set from GetResolvedTemplateMembersCommand (T26a), and show a read-only banner when the staleness summary reports the derived template differs from its base ("Base changed — N inherited members differ"). No mutation — existing override actions unchanged.
Step 1: Write failing test — for a multi-level derived template the editor lists transitively-inherited members; the staleness banner appears when the resolve result flags drift and is absent when in sync.
Step 2–4: FAIL → implement → PASS.
Step 5: Commit.
Acceptance: editor shows the full multi-level inherited set + a read-only staleness banner; no deploy-path change.
Wave 5
Task INT: Integration — build, docker, Playwright, smoke, end-to-end trace
Classification: high-risk Estimated implement time: ~ (controller-run verification phase, not a single implementer) blockedBy: all of T22, CLI, T28a/b, T23a/b, T25, T24a/b, T32a/b/c, T30, T31, T26a/b
Files: docs sync (docs/requirements/Component-*.md for TreeView/TemplateEngine/CLI/Health as touched; README component table if changed); no new feature code (fixes only if a check fails).
Verification checklist (per integration-catches-cross-cutting-gaps):
- Full-solution build:
dotnet build ZB.MOM.WW.ScadaBridge.slnx— zero warnings (TreatWarningsAsErrors). EF model-drift check: confirm noPendingModelChangesWarningforSharedSchema. - End-to-end command trace (ManagementActor routing + registry name resolution) for every new command:
ReorderTemplateFolderCommand,MoveDataConnectionCommand,GetResolvedTemplateMembersCommand, theSchemaLibraryCRUD commands, and the CLI'sRetryParkedMessageCommand/DiscardParkedMessageCommandmappings. Confirm each resolves a name inManagementCommandRegistryand reaches its handler (these are central-only — no site hop — but verify the registry + handler wiring; the CLI fails silently without the name mapping). - Re-run full bUnit suites of every shared component touched: TreeView,
TemplateFolderTree,Templates,DataConnections,TemplateEdit,ParameterValueForm,SchemaBuilder— register substitutes for any newly-injected service in their existing fixtures. (Shared-component injection-regression guard.) - Run all touched test projects unfiltered: CentralUI.Tests, TemplateEngine.Tests, ManagementService.Tests, Commons.Tests, CLI.Tests, ConfigurationDatabase tests. Before calling any failure "pre-existing," PROVE it:
git diff --stat <base>..HEAD -- <failing test's target files>empty. - Docker rebuild:
bash docker/deploy.sh;/health/readyon central-a (9001), central-b (9002), LB (9000) all 200. - Playwright: coverage for the new UI surfaces — template tree search, folder reorder menu + root context menu, move-connection dialog, connection health badge, schema-library page, schema-driven nested form, strict-trigger selector.
- Docs: sync the affected component docs + README table; clear any stale markers for the now-shipped items.
Acceptance: full build green + no model drift; every new command traced; all touched suites green; cluster healthy; Playwright covers new UI; docs synced.
Post-milestone follow-ups to log (not in M9)
- Unified notifications + site-calls outbox page (deferred per D3 — union view-model is high-risk; revisit if operators want one place).
- Folder drag-drop (HTML5 DnD via JS interop —
[PERM]). - T26 explicit
RefreshDerivedTemplatemutation (only if stored-row resync is later wanted; deploys are already fresh).