Files
ScadaBridge/docs/plans/2026-08-15-residual-remediation-plan.md
T

8.6 KiB
Raw Blame History

Residual Remediation Plan — the seven remaining deferred items

Date: 2026-08-15 · Base: main @ f228ac22 · Execution: one opus subagent per item, parallel git worktrees, merged by the orchestrator in conflict-safe order, single rig redeploy + verification pass at the end.

Scope = the seven items left open after the arch-review remediation program closed residuals #1 (DCL counter drift, 491df111) and #7 (target-scale load test, 40eff637). Sources: residuals register in 2026-08-15-arch-review-remediation-execution-log.md and deferred-work register rows.

Execution rules (all agents)

  • Verify worktree base is f228ac22 or a descendant before starting; branch names below.
  • Do NOT edit docs/plans/2026-07-08-deferred-work-register.md or the execution-log residuals register — 7 parallel editors of one file guarantees conflicts. Report closure text + any candidate new register rows in the final report; the orchestrator records them centrally.
  • Rig ownership: only R6 (Playwright) may touch the docker/ rig. R1/R2 are code+tests only; the orchestrator does one consolidated bash docker/deploy.sh + live probe after all merges.
  • Component docs / CLAUDE.md updates travel with the change per repo rules. No pushes anywhere.
  • Build dotnet build ZB.MOM.WW.ScadaBridge.slnx clean; run the tests your change touches.

R1 — Per-table needs_snapshot in LocalDb (cross-repo) — branch localdb-per-table-snapshot

Problem: baselining one table (RegisterReplicated(..., baselineExistingRows: true)) sets a global needs_snapshot, so enabling replication for one new table re-streams every registered table in both directions.

Approach: in ~/Desktop/scadaproj/ZB.MOM.WW.LocalDb (branch off its main @ f627cee): move the flag per-table (bookkeeping schema v3, upgraded in place on open, like v1→v2 was). Snapshot phase streams only flagged tables. Wire compatibility is the hard constraint: extend the snapshot negotiation additively (absent per-table scoping field ⇒ full snapshot, so 0.1.x/0.2.x peers keep syncing). Crash safety: a table's flag clears only after that table's snapshot is acked. Version per library convention (schema change, wire-compatible ⇒ 0.3.0). Update the library README + RegisterReplicated remarks (which name this as the follow-up). Publish to the Gitea feed via build/push.sh only after the library suite is green. Then, in the ScadaBridge worktree: bump Directory.Packages.props, build, run the LocalDb-touching suites, update the CLAUDE.md LocalDb bullet + the umbrella ~/Desktop/scadaproj/CLAUDE.md.

Verify: two-node integration test proving single-table baseline streams only that table; mixed-version interop test (old peer ⇒ full snapshot); ScadaBridge suite green on the bump.

R2 — gRPC event batching — branch grpc-event-batching

Problem: every AttributeValueChanged/AlarmStateChanged rides one gRPC message; at target scale that is 37.5k messages/s/site of per-message overhead.

Approach: additive wire shape in the vendored proto (Communication/SiteStreamGrpc/, manual toggle-build-copy-untoggle regeneration; field numbers never reused): a batch message (repeated events) exposed via a new RPC or additive field so old peers are untouched — new-central↔old-site and old-central↔new-site must both keep working on the existing path. Server coalesces per subscriber under a small bounded window (size + time cap, e.g. ≤100 events / ≤25 ms, configurable via options, validated); client unpacks and feeds the existing per-event pipeline so timestamps, generation fencing, reconnect-on-graceful-completion, and IsLive semantics are untouched. Batch-size histogram metric. Note interplay with register row 31: batching sits downstream of the shared publish stage and does not change the burst ceiling — say so in the component doc.

Verify: batcher unit tests (window/cap/flush-on-close), client/server round-trip integration, version-skew tests both directions, existing Communication suites green. No rig redeploy (orchestrator does it post-merge).

R3 — Deployments page server-side paging + status counts — branch deployments-server-paging

Problem: the Central UI Deployments page client-materializes the full deployment list.

Approach: find the page's data path first; add a paged repository query (EF, following the repo's existing paged patterns — Audit Log keyset paging is the reference) plus a status-count aggregation query (counts by status computed server-side, one grouped query). Wire through whatever service layer the page uses; UI gets paging (pager or Virtualize, matching an existing page's idiom), 500 ms filter debounce, and status-count tiles. Bootstrap only, no third-party components; match the corporate UI conventions.

Verify: repository paging + count tests; existing CentralUI/ConfigurationDatabase suites green; page behavior exercised via whatever bUnit/unit seam exists for sibling pages.

R4 — OtOpcUa LocalDb pin bump — branch (in ~/Desktop/OtOpcUa) localdb-bump

Problem: OtOpcUa pins LocalDb 0.1.3 — supported skew, but drifting further as LocalDb moves.

Approach: bump to the newest LocalDb on the Gitea feed (0.2.1 now; if R1's 0.3.0 lands on the feed before you finish, take that instead). Reconcile API deltas (0.2.x added DeregisterReplicated, MaxBatchBytes, baselineExistingRows; check what OtOpcUa's usage touches). Build + full OtOpcUa test suite. Update its own docs/CLAUDE.md where the pin is recorded, and the umbrella ~/Desktop/scadaproj/CLAUDE.md if it records the skew.

Verify: OtOpcUa builds clean, full suite green. No push.

R5 — SandboxTests timing pin + flake-pattern sweep — branch sandbox-timing-fix

Problem: a fragile timing pin in the SandboxTests (noted pre-existing during the remediation program); the AwaitAssert-then-bare-assert flake class it belongs to had 3 other instances root-caused and fixed (c4caebe9, cfa6acbf).

Approach: locate the pin, root-cause it causally (delay-injection reproduction + negative control, same discipline as the prior flake fixes), replace the timing assumption with deterministic synchronization — test-side only unless the product is genuinely wrong. Then do the deferred bounded sweep: grep the test suites for the same pattern (AwaitAssert on one observable followed by a bare assert on a strictly-later observable), fix what's clearly the same class, list anything ambiguous as candidate register rows instead of guessing.

Verify: fixed tests re-run ≥5× green; touched suites green; fail-before-fix demonstrated via injected delay.

R6 — Playwright environment failures — branch playwright-env-fixes (owns the rig)

Problem: 14 Playwright failures present on main before the remediation branch, rig-state related, never triaged.

Approach: run the suite against the current rig for a fresh baseline; catalog and classify every failure (stale rig data assumption, timing, selector drift, genuine app bug). Fix causes at the right layer — test fixes, or rig seed-state fixes via the ScadaBridge CLI (preferred over DB edits). Rig redeploy allowed for this agent if needed. Anything irreducible or app-side gets a precise writeup as a candidate register row rather than a papered-over test.

Verify: clean full-suite re-run; report before/after failure counts with per-test root causes. A genuine app bug found here is a finding to report, not necessarily to fix.

R7 — site_events purge oplog-visibility operator note — branch site-events-purge-note

Problem: WP3.2's sliced retention DELETE is CDC-captured on a replicated site (correct by design — no separate resync path), so a purge burst shows up in LocalDbOplogBacklog; an operator watching the gauge can misread it as a replication problem.

Approach: documentation + one log line, no behavior change: a note in docs/deployment/topology-guide.md and Component-SiteEventLogging.md (what the burst looks like, why it is expected, how it drains), plus an Information-level log when a purge on a replication-enabled node deletes rows ("N rows purged; transient oplog backlog expected").

Verify: build green; the purge path's existing tests still green; log line unit-covered if the purge path has a test seam.

Merge & close-out (orchestrator)

Merge order: R5 → R7 → R4 (separate repo, no merge here) → R3 → R2 → R1 → R6. Then: record all closures + candidate rows in the registers, consolidated bash docker/deploy.sh + live probe (stream batching live check, replication convergence, site_events purge log line), full-solution build + test, hold push for the user.