Files
lmxopcua/stillpending.md
T
Joseph Doherty 384dbd7d36
v2-ci / build (push) Failing after 42s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
docs(claude): refresh CLAUDE.md + record historian-gateway integration/dev-rig state
CLAUDE.md: add scadaproj sister-project index, fix mxaccessgw path to
~/Desktop/MxAccessGateway, note the temporary CVE-2025-6965 NuGetAuditSuppress,
and correct the driver-typed tag-editor list (OpcUaClient is now mapped; only
Galaxy falls back). Also captures docker-dev data-plane GroupToRole + ServerHistorian
live-verify config and the HistorianGateway integration/backlog notes.
2026-07-07 11:46:02 -04:00

270 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Still Pending — RECONCILED 2026-06-18
> **★ UPDATE 2026-06-19 (master `a325ec54`) — NON-ARCH FOLLOW-UPS BATCH (A/B) SHIPPED.** Six code
> tasks merged via `feat/followups-batch`: OpcUaClient stale-session race (re-resolve session inside
> `_gate` in 5 history/subscribe/ack paths); Client.CLI `enable`/`disable` condition commands (H4
> client path); cert-audit review nits; Galaxy re-pick preserves edited alarm/historize/scale via
> pure `TagConfigJson.SetFullName`; inline-script dropdown label reorder fix. **The "write-outcome
> residuals" item (Bad blip + AuditWriteUpdateEvent + sync fail-fast) was found ALREADY-SHIPPED in
> `bb59fd4e` (2026-06-15) — delivered the missing behavioural test coverage instead.** Live-`/run`:
> T5 PASS; T4 substantial (picker + live gateway browse confirmed; final pick-and-assert blocked by
> the Galaxy object-browser `href=#` nav quirk under automation — preservation unit-tested).
> **RECONSIDER GATES still OPEN-by-choice (NOT built): F10b surgical DataType/IsArray writes;
> alarm-severity `SetSeverity`.** See `docs/plans/2026-06-19-followups-batch*` + memory.
>
> **Reconciled 2026-06-18** against master HEAD `651018f9` by a 7-way parallel per-subsystem
> code audit of the original 2026-06-15 backlog (which was generated at the older HEAD `151b7165`).
> Three days and a large run of "Phase 4 / H1H6 / driver" commits landed in between, so **most of
> the original backlog is now SHIPPED.** Every item below was re-verified in code (commit cited).
>
> **Legend** — `OPEN` (genuine, actionable code gap) · `DEFERRED` (intentional scope boundary —
> needs infra, a future version, an SDK/gateway/sidecar change, or a by-design decision) ·
> `SHIPPED` (the cited gap no longer exists; recorded for the audit trail so it is not re-flagged).
---
## A. Genuinely OPEN — actionable code work (ranked)
1. **H2 — OPC UA `HistoryUpdate` service****RECLASSIFIED INFRA-GATED / DEFERRED (user decision 2026-06-19).**
The server-side override (mirror the 5 HistoryRead overrides: `HistoryUpdateData`/`HistoryUpdateEvents`/
`HistoryDeleteRawModified`/`HistoryDeleteAtTime`/`HistoryDeleteEvent`, resolve nodes via the same
`TryGetHistorizedTagname`/event-notifier maps, gate on the already-shipped `NodePermissions.HistoryUpdate =
1<<12` / commit `c236263e`) is buildable + offline-testable, BUT **no backend can durably write history**:
`IHistorianDataSource` is read-only (Raw/Processed/AtTime/Events + health — no insert/replace/delete) and
the AVEVA Wonderware TCP sidecar exposes no history-update RPC (the same permanent SDK/sidecar boundary as
H5b durable-write + no-modified-reads). So any `IHistoryUpdateSink` wired today could only reject — a custom
override would mostly restate the SDK's own default `HistoryUpdate` rejection. **Deferred** until a writable
historian backend exists; if/when one does, build the full service surface + the `IHistoryUpdateSink` seam
(Phase-C shape) at that time. _Moved out of §A "Genuinely OPEN" — see the Infra-gated OPEN note below + §B._
2. ~~**OpcUaClient — `IHistoryProvider.ReadEventsAsync`** event-history passthrough~~ — **SHIPPED master
`7a3d2712` 2026-06-18** (`feat/opcuaclient-read-events`). NO interface change (the "param extension" framing
was overcautious): the driver builds its OWN canonical 6-field BaseEventType `EventFilter` and maps the
upstream `HistoryEvent``HistoricalEvent` (the server re-projects only those 6 fields). Driver-internal;
live-proven vs opc-plc (wire path; non-empty events infra-gated — opc-plc isn't a historian).
3. ~~**FOCAS — `cnc_getfigure` wire command**~~ **SHIPPED master (branch
`feat/focas-figure-ww-poison-abcip-gate`) 2026-06-18.** `FocasWireClient.ReadPositionFiguresAsync`
(wire command `0x00D3`, self-delimiting per-axis `short dec` payload) + the `WireFocasClient`
stub wired to it; matching `focas_mock` `_wire_payload` handler + `position_figures` default-0 state.
The managed wire backend (production path to real CNCs) now auto-fetches per-axis figures; the
`PositionDecimalPlaces` knob is the graceful per-axis fallback (`FocasDriver.SafeProbe` → empty on
failure → knob, so it cannot regress). **LIVE-PROVEN** against the local focas-mock
(`Position_figures_scale_axis_via_wire_backend`: 12345 ÷ 10^3 = 12.345, auto figure beat the knob).
Real-Fanuc wire-fidelity stays bench-CNC-gated (whole-backend caveat, not unique to this command).
4. ~~**Galaxy — writer item-handle cache not shared with the subscription registry**~~
**SHIPPED master `8480e301` 2026-06-18.** `SubscriptionRegistry.TryResolveItemHandle` (authoritative
forward lookup) + writer `subscribedHandleSource` delegate → first write to an already-subscribed tag
borrows the live handle, skipping the redundant `AddItem`. Live-proven vs `10.100.0.48:5120` (3/3).
5. ~~**Historian.Wonderware — poison-event sidecar wire protocol**~~ **SHIPPED master (branch
`feat/focas-figure-ww-poison-abcip-gate`) 2026-06-18.** Additive `[Key(4)] byte[] PerEventStatus`
(0=Ack/1=Retry/2=Permanent) on `WriteAlarmEventsReply` (BOTH in-repo IPC ends — MessagePack/TCP, NOT a
Commons proto; `PerEventOk [Key(3)]` kept for rolling-deploy back-compat). The sidecar
`HistorianFrameHandler` now classifies **structurally-malformed** events (empty SourceName/AlarmType,
`EventTimeUtcTicks ≤ 0`) as **Permanent** and excludes them from the writer batch; the client
`WriteBatchAsync` prefers `PerEventStatus` (→ `PermanentFail` → the sink's existing immediate
dead-letter) and falls back to `PerEventOk`. **NO `IAlarmEventWriter` / Commons / Core.Abstractions
change** (`HistorianWriteOutcome.PermanentFail` already existed). Offline-proven (sidecar classifier
tests + client round-trip + sink). _Documented boundary (follow-up):_ an SDK-**semantic** permanent
rejection of a structurally-valid event still maps to Retry→cap until the infra-gated
`AahClientManagedAlarmEventWriter` surfaces richer per-event status.
6. ~~**AbCip — nested-struct member expansion in production**~~ **SHIPPED master (branch
`feat/hosts-rows-abcip-nested-hygiene`) 2026-06-18** (`3d8ce4e8`+`d203f31c`). FINDING: simpler than this
item assumed — the CIP Template Object member-info `u16` ALREADY carries the nested template id for a
struct member (bit 15 = struct flag, lower 12 bits = template instance id, same encoding as the Symbol
Object; the decoder's own remarks documented it). The prior fix DISCARDED it (`templateInstanceId: null`).
The close: carry `uint? NestedTemplateId` on `AbCipUdtMember` (decoded via the FULL 12-bit mask, not the
byte-cast typeCode), thread it into the existing `FetchUdtShapeAsync`/`@udt/{id}` path — NO new CIP query.
Offline-proven (decoder + no-seam threading test, AbCip 301/301). **Live-gate FORMALIZED 2026-06-18
(branch `feat/focas-figure-ww-poison-abcip-gate`):** a skip-gated Emulate-tier integration smoke
`AbCipEmulateNestedUdtTests` (mirrors `AbCipEmulateUdtReadTests` + `AbServerProfileGate.SkipUnless(Emulate)`)
asserts nested leaves addressable; `docs/drivers/AbCip.md` documents nested-struct as Emulate-tier
verified. A LOCAL gate is architecturally impossible — `ab_server` lacks CIP Template Object (class 0x6C);
the test compiles + skips locally and runs only against Logix Emulate / real ControlLogix.
7. ~~**AdminUI — Certificates page actions**~~ **SHIPPED master `bd791e79` 2026-06-18.** FleetAdmin-gated
Trust (rejected→trusted) / Untrust (trusted→rejected) / Delete via a new in-process `CertificateStoreManager`
(filesystem moves by thumbprint, honored live); inline Blazor confirm + server-side FleetAdmin re-check.
Live-proven on docker-dev. **CERT-ACTION AUDIT LOGGING SHIPPED master `3eb370d4` (2026-06-19):**
Trust/Untrust/Delete now write a `ConfigAuditLog` row (`Certificate:Trust|Untrust|Delete`, Outcome
Success/Failure on every path incl. failures) via the first live structured `AuditEvent` emit site —
new `ActorAuditWriter : IAuditWriter` (AdminUI adapter Telling the `AuditWriterActor` cluster singleton
resolved from `ActorRegistry`, mirrors `AdminOperationsClient`) + pure `CertAuditEvents.Build` factory,
audited at the `CertificateStoreManager` choke point (`DeleteCore` refactor); actor sourced from
`AuthenticationState`. NO EF migration (table + actor pre-existed), NO Commons/proto change.
LIVE-PROVEN end-to-end on docker-dev: a Trust on the Certificates page wrote
`multi-role-test | Certificate:Trust | Success | <thumbprint>` to `ConfigAuditLog`. (Was: only cert
*validation* went to Serilog.)
8. ~~**AdminUI — Hosts page per-driver-instance rows**~~ **SHIPPED master (branch
`feat/hosts-rows-abcip-nested-hygiene`) 2026-06-18** (`cb062fce`+`6457eba8`+`07bd63f8`). A new
cluster-grouped "Driver Instances" section on `/hosts` reusing the existing driver-health DPS pipeline
(added only an AdminUI-internal `IDriverStatusSnapshotStore.GetAll()` + a pure `HostsDriverView.Build`
grouping view-model + the Razor section) — NO Commons/interface/EF change. Grouped by `ClusterId` (the
reliable key; the health feed carries no per-Akka-member identity), nodes from ConfigDB `ClusterNode`,
driver rows enriched with Name/DriverType from `DriverInstance`, live status chips via the in-process
store's `SnapshotChanged` (dodges the Traefik self-hub trap) + the 5 s timer. LIVE `/run` PASS (both
central nodes rebuilt; MAIN cluster shows Galaxy/Modbus/OpcUaClient HEALTHY enriched + grouped; SITE-A/B
show nodes + "No drivers"). DEFERRED follow-up: TRUE per-Akka-member nesting (needs a hosting-node field
on `DriverHealthChanged` — a Commons change — or a per-node `DriverHostActor`-children Ask).
9. ~~**AdminUI driver-pages plan — Phase 6 + Phase 10**~~ **SHIPPED — RECONCILED 2026-06-18.** A
seam-by-seam audit confirmed the WHOLE driver-pages plan (Phases 010) is shipped; §A.9 was stale
(it over-counted from the stale `.tasks.json`). Phase 6 (live `DriverStatusPanel`) + Phase 8
(Reconnect/Restart) are wired end-to-end (DriverHealthChanged → AkkaDriverHealthPublisher invoked at
`DriverInstanceActor.PublishHealthSnapshot` → admin-gated `DriverStatusSignalRBridge` → snapshot store
→ panel in all 9 driver pages; page `DriverInstanceId` ⟷ actor `_driverInstanceId` keys match). Phase
10 E2E tests (DriverTestConnect/DriverReconnect/DriverStatusHub) shipped at `494da22c`. The one genuine
remnant — a **deployed-driver Healthy→Reconnecting→Healthy E2E** + the harness-fidelity fix
(`AddOtOpcUaRuntime` so harness drivers actually publish health) + a controllable fake driver factory —
SHIPPED on `feat/driver-pages-reconnect-e2e` (`docs/plans/2026-06-18-driver-pages-reconnect-e2e.md`),
driver E2E suite GREEN 18/0/4. The `.tasks.json` is now reconciled. DEFERRED follow-ups: full-stack
WebSocket+JWT DriverStatusHub test (no repo precedent; mock-hub test + manual runbook cover it); the
§8.3 manual browser smoke recording (10.4); the dotnet-test-testhost-can't-reach-remote-docker skip
quirk on this dev Mac.
10. ~~**AdminUI — Monaco `ctx.SetVirtualTag(...)` completions**~~ **SHIPPED master `70e1bde9` 2026-06-18**
(predates this audit — was stale here). The `ctx`-receiver guard is in
`ScriptAnalysisService.TryGetTagPathLiteral` (`:224`, mirrors the `ctx`-anchored
`EquipmentScriptPaths.GetTagRefRegex`) + a truthful `ctx.SetVirtualTag` hover note (it's a no-op in the
live single-tag evaluator).
11. ~~**F10b — in-place property writes vs full rebuild**~~ **FIRST SLICE SHIPPED master (branch
`feat/f10b-vtag-skip-rebuild-galaxy-comment`) 2026-06-18** (`6c6a2c42`+`b05c2811`). The provably-safe,
zero-node-mutation slice: `Phase7Applier.Apply` now SKIPS `RebuildAddressSpace` when a deploy's only
changes are VirtualTag deltas differing *solely* in `Expression`/`DependencyRefs`/`Historize` — a vtag's
materialised node depends ONLY on `{EquipmentId,FolderPath,Name,DataType}` (Historize is write-side
only), and the vtag ENGINE adopts the edit via `VirtualTagHostActor`'s INDEPENDENT respawn
(`DriverHostActor``ApplyVirtualTags`), so editing a script no longer tears down + rebuilds the whole
server address space (preserving every client's subscriptions + live values). Whitelist-of-may-differ
(`(prev with {those 3 = cur's}).Equals(cur)`) defaults to REBUILD for any other/structural change;
`EnsureVariable`'s existing-node early-return preserves the live node when no rebuild runs. RecordingSink
unit tests pin the boundary (skip cases + DataType/Name/FolderPath/EquipmentId/mixed ⇒ rebuild).
Code-review verified all 6 safety claims; no H1a stale-node regression.
**SECOND SLICE (surgical tag-attribute writes) SHIPPED master (branch `feat/f10b-surgical-tag-attribute-writes`)
2026-06-18** (`3fc258bd`+`f6618144`+`b472bba3`+fix `22b0611f`): equipment-tag **`Writable`** +
**`IsHistorized`/`HistorianTagname`** toggles now update the EXISTING node IN PLACE (AccessLevel + write
handler + Historizing + historian-map + `ClearChangeMasks`) via a new optional `ISurgicalAddressSpaceSink`
(`OtOpcUaNodeManager.UpdateTagAttributes`, mirrors `EnsureVariable` via a shared `ComposeAccessLevel`),
instead of a full rebuild — `Phase7Applier.TagDeltaIsSurgicalEligible` (no-alarm value variable, only those
3 differ) with rebuild fallback for anything else / missing node / non-surgical sink. **LIVE-PROVEN** on
docker-dev (toggled FastUInt1 isHistorized → `changed=1, surgicalTags=1, rebuild=False` on BOTH central
nodes, server healthy). **CRITICAL bug the live-run caught (all unit tests + 3 reviews missed it): the
optimization was INERT in prod** — actors inject `DeferredAddressSpaceSink` (the runtime swap-in wrapper),
which forwarded `IOpcUaAddressSpaceSink` but NOT `ISurgicalAddressSpaceSink`, so `_sink is ISurgical…` was
false → silent rebuild fallback (`rebuild=True, surgicalTags=0`). Fixed by forwarding the surgical
capability through the wrapper (`22b0611f`) + a regression test. _Still DEFERRED (dirty for rare edits /
no value): surgical `DataType`/`IsArray` writes (brief value-type mismatch; no ModelChangeEvents) +
alarm-severity `SetSeverity` (live-shadowed by the alarm engine, operationally invisible)._
12. ~~**Client.UI — Galaxy sub-attribute fallback collapses operator comments**~~ **CLOSED as a documented +
tested ACCEPTED-LIMITATION master (branch `feat/f10b-vtag-skip-rebuild-galaxy-comment`) 2026-06-18**
(`fac44187`). Investigation confirmed it is NOT code-fixable: Galaxy's `AckMsg` attribute is WRITE-ONLY
(no readable attribute to recover the last comment) and the OPC UA event SelectClause carries no comment
field — the operator comment is only available via the gateway's NATIVE alarm feed
(`GalaxyAlarmTransition.OperatorComment`, the Galaxy driver path), not this OPC UA client fallback. Closed
with a clarifying inline comment at the fallback `AlarmEvent?.Invoke` site + a tightened
`AlarmEventArgs.OperatorComment` doc naming both concrete reasons + a pinning test
(`OnAlarmEvent_GalaxyFallback_LeavesOperatorCommentNull`) so the null behavior is intentional + regression-guarded.
A behavioral fix would need a server-side change (out of scope).
13. ~~**§9 hygiene — Galaxy stale forward-ref comments**~~ **SHIPPED master (branch
`feat/hosts-rows-abcip-nested-hygiene`) 2026-06-18** (`9cf88e33`). All 5 sites
(`GalaxyDriver.cs:52,92,669`, `GalaxyDriverFactoryExtensions.cs:19`, `Health/HostStatusAggregator.cs:21`)
rewritten to the shipped standard-driver reality (each claim re-verified against the live code:
`_dataReader` reads via `ReadViaSubscribeOnceAsync`, `_ownedMxSession` built in
`BuildProductionRuntimeAsync`, `SyncPlatformsAsync` refreshes the probe watcher post-discovery, only
`GalaxyMxGateway` exists — no `Galaxy:Backend` switch, host-status re-raise wired in the ctor). The
unshipped "PR 4.W"/`Galaxy:Backend`/"legacy-host" forward-refs are gone. Comment-only; §9 now fully clean.
> **Infra-gated OPEN (code path exists or is buildable; only a no-op until a deployment supplies the backend):**
> **H5b — production `IHistoryWriter` for equipment-vtag historization.** The `Historize` flag is now
> threaded end-to-end (commits `fc8121cb`/`9c5a0913`/`4501f126`) but DI defaults to `NullHistoryWriter` —
> no durable AVEVA sink is written/wired. Same infra-gated pattern as the alarm historian: writing a real
> sink is possible but only exercisable against the historian backend.
> **H2 — OPC UA `HistoryUpdate` service** (reclassified 2026-06-19, see §A.1). Server-side override is
> buildable but no historian backend can durably insert/replace/delete — `IHistorianDataSource` is read-only
> and the Wonderware sidecar has no update RPC; build the service + `IHistoryUpdateSink` seam when a writable
> backend exists.
---
## B. DEFERRED — intentional scope boundaries (not actionable without infra / future version)
**Driver write/array boundaries (reads shipped):**
- Array **writes** across all 5 drivers (OPC UA layer forces array nodes read-only — `Phase7Applier`
`writable = tag.Writable && !tag.IsArray`, commit `3bb2031d`).
- S7 wide-type **arrays** (Int64/UInt64/LReal/String/DateTime as arrays); S7 **Timer/Counter writes**
(read shipped); Modbus String/BitInRegister **array writes**.
- AbCip **whole-UDT bare-container** read (`BadNotSupported`) + write (throws) — operators address
individual member paths. `Structure→DriverDataType.String` placeholder retained.
- TwinCAT **pre-declared `Structure` tags** rejected at parse (discovery is the supported path);
**Flat-mode `SubSymbols` live caveat** — on some TC3 firmware Flat mode may not populate struct
SubSymbols → switch `SymbolsLoadMode.Flat``VirtualTree` (deliberately NOT changed; needs a real ADS
target to verify; would risk regressing the Phase 4c array discovery).
- AbLegacy I/O-image bit writes are NOT pre-rejected — they RMW and surface the real PCCC device status.
**Gateway-scope (mxaccessgw sibling repo, not this repo):**
- Galaxy `ExecuteWrite` fire-and-forget — the .NET writer fully propagates `WriteResult` status; the
invisible-failure concern is gateway-side.
**Reserved seams / future versions (no behavioral gap today):**
- `AuthorizationVerdict.Denied` (v2.1 explicit-deny), `NamespaceKind.Simulated` (replay driver),
`Script.Language` (multi-engine), `IDriverConfigEditor` (superseded by `TagConfigEditorMap`),
`DraftSnapshotFactory` placeholders (conservative fallbacks), Cluster `LeaderChanged` no-op
(ServiceLevel lives in `RedundancyStateActor`).
- AdminUI Monaco **InlayHints** endpoint — intentional no-op (`[]`).
- Historian **no modified-value history** (sidecar has no modified-read RPC); **Processed/AtTime
single-shot** (spec-conformant, no count cap).
- Redundancy **`ServerUriArray`** child of `Server.ServerRedundancy` — pending an SDK object-type upgrade
(read `Server.ServerArray` today).
- VirtualTags **serialized eval** under `_evalGate` (decision #19); **unbounded script memory / post-timeout
thread leak** — out-of-process runner is a v3 concern.
---
## C. Live `/run` verification gates — code done, user/operator-driven
- Galaxy Phase C historian T7 live HistoryRead (needs Wonderware sidecar + AVEVA on `10.100.0.48`).
- Galaxy Phase B native alarms T9 live `/run` (equivalent proof already achieved via `dcb0be65` + `/alerts`).
- Driver-reconfigure-while-faulted T3 live `/run`.
- AVEVA Historian rig smokes (skip-gated `rig-required` tests).
---
## D. SHIPPED since 2026-06-15 (audit trail — do NOT re-flag)
**§1:** H1a changed-only-deploy rebuild (`1e95856b`) · H1b vtag-respawn (`ada01e1a`) · H2 permission bit
(`c236263e`) · H3 health-aware ServiceLevel now live (`3e609a2b`/`70e6d3d2`) · H4 OPC UA Enable/Disable
(`328bd1b9`) · H5a Historize flag threaded (`fc8121cb`/`9c5a0913`) · H6 native-ack→AVEVA + authenticated
identity (`418663b3`/`ed941c51`/…).
**§2 drivers:** S7 wide scalars Int64/UInt64/LReal/String/DateTime r+w (`06b858eb`/`286be5df`/`5db08e9e`) ·
S7 Timer/Counter read (`8cfb8e92`) · Modbus Int64/UInt64 node type (`DriverDataType.Int64/UInt64`) ·
Wonderware `Total` aggregate (client-side `ScaleAverageToTotal`) · cross-driver 1-D array **reads** all 5
drivers (Phase 4c `a82c22c6`/`f4d5a5ee`/`95006939`/`3e742395`/`8d3dc321`) · Modbus String/Bit array read ·
AbCip controller-discovered UDT **member paths** (`4e141402`/`4a7b0fde`) · TwinCAT discovered struct
member expansion (`3699fc16`) · AbLegacy B/I/O bit writes via RMW (`5c6b7cd6`) · TwinCAT BOOL-within-word
RMW (`a73e20fb`) · FOCAS axis scaling logic + knob (`49730752`/`6855be28`) · FOCAS fail-fast on
unimplemented backend (`5c5aaef6`) · Galaxy parent-gobject nesting (`21c7645d`/`bec37848`) · all 4 (+more)
Test-Connect probes upgraded TCP→protocol handshake (`961b2b55` & per-driver).
**§4/§5 AdminUI:** typed TagConfig editors for OpcUaClient + Historian.Wonderware (`72d414ad`) ·
isHistorized/historianTagname first-class UI (`c00a5471`) · UNS-tree delete Enterprise+Cluster
(`526eebb3`) · create-new-script from inline vtag panel (`c98625fd`) · native-alarm `HistorizeToAveva`
opt-out + UI knob (`6a8020e7`) · Galaxy picker pre-fills native-alarm fields (`069a5f31`) · Client.UI
alarm Acknowledge + Shelve + Confirm surfaces (`4f55d894`/`60da9224`/`a774bb08`/`6a61f35a`).
**§6/§7:** Historian #400 within-timestamp tie-cluster paging (`2e6c6d3a`/`0f929ae6`) · per-cluster-scoping
plan COMPLETE (`.tasks.json` reconciled) · historian-tcp-transport plan COMPLETE (stale json) ·
adminui-followups plan COMPLETE — role-mapper (RoleGrants.razor + `LdapGroupRoleMappingService` +
`FleetAdmin` + `LdapGroupRoleMapping` entity/migration) + typed editors all shipped · adminui-driver-pages
Phases 7 (probes) + 9 (9 typed address pickers) shipped · Wonderware poison-event SQLite dead-letter half
(`fcb38014`).
**§9 stale comments corrected:** DriverHostActor, OpcUaPublishActor, ServiceCollectionExtensions,
IScriptLogPublisher/ScriptLogTopicSink, ScriptAnalysisService, docs/security.md (6 of 7 — only the Galaxy
PR-4.W forward-refs remain; see A.13).