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.
22 KiB
Still Pending — RECONCILED 2026-06-18
★ UPDATE 2026-06-19 (master
a325ec54) — NON-ARCH FOLLOW-UPS BATCH (A/B) SHIPPED. Six code tasks merged viafeat/followups-batch: OpcUaClient stale-session race (re-resolve session inside_gatein 5 history/subscribe/ack paths); Client.CLIenable/disablecondition commands (H4 client path); cert-audit review nits; Galaxy re-pick preserves edited alarm/historize/scale via pureTagConfigJson.SetFullName; inline-script dropdown label reorder fix. The "write-outcome residuals" item (Bad blip + AuditWriteUpdateEvent + sync fail-fast) was found ALREADY-SHIPPED inbb59fd4e(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-browserhref=#nav quirk under automation — preservation unit-tested). RECONSIDER GATES still OPEN-by-choice (NOT built): F10b surgical DataType/IsArray writes; alarm-severitySetSeverity. Seedocs/plans/2026-06-19-followups-batch*+ memory.Reconciled 2026-06-18 against master HEAD
651018f9by a 7-way parallel per-subsystem code audit of the original 2026-06-15 backlog (which was generated at the older HEAD151b7165). Three days and a large run of "Phase 4 / H1–H6 / 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)
-
H2 — OPC UA
HistoryUpdateservice — 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 sameTryGetHistorizedTagname/event-notifier maps, gate on the already-shippedNodePermissions.HistoryUpdate = 1<<12/ commitc236263e) is buildable + offline-testable, BUT no backend can durably write history:IHistorianDataSourceis 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 anyIHistoryUpdateSinkwired today could only reject — a custom override would mostly restate the SDK's own defaultHistoryUpdaterejection. Deferred until a writable historian backend exists; if/when one does, build the full service surface + theIHistoryUpdateSinkseam (Phase-C shape) at that time. Moved out of §A "Genuinely OPEN" — see the Infra-gated OPEN note below + §B. -
OpcUaClient —— SHIPPED masterIHistoryProvider.ReadEventsAsyncevent-history passthrough7a3d27122026-06-18 (feat/opcuaclient-read-events). NO interface change (the "param extension" framing was overcautious): the driver builds its OWN canonical 6-field BaseEventTypeEventFilterand maps the upstreamHistoryEvent→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). -
FOCAS —SHIPPED master (branchcnc_getfigurewire commandfeat/focas-figure-ww-poison-abcip-gate) 2026-06-18.FocasWireClient.ReadPositionFiguresAsync(wire command0x00D3, self-delimiting per-axisshort decpayload) + theWireFocasClientstub wired to it; matchingfocas_mock_wire_payloadhandler +position_figuresdefault-0 state. The managed wire backend (production path to real CNCs) now auto-fetches per-axis figures; thePositionDecimalPlacesknob 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). -
Galaxy — writer item-handle cache not shared with the subscription registrySHIPPED master8480e3012026-06-18.SubscriptionRegistry.TryResolveItemHandle(authoritative forward lookup) + writersubscribedHandleSourcedelegate → first write to an already-subscribed tag borrows the live handle, skipping the redundantAddItem. Live-proven vs10.100.0.48:5120(3/3). -
Historian.Wonderware — poison-event sidecar wire protocolSHIPPED master (branchfeat/focas-figure-ww-poison-abcip-gate) 2026-06-18. Additive[Key(4)] byte[] PerEventStatus(0=Ack/1=Retry/2=Permanent) onWriteAlarmEventsReply(BOTH in-repo IPC ends — MessagePack/TCP, NOT a Commons proto;PerEventOk [Key(3)]kept for rolling-deploy back-compat). The sidecarHistorianFrameHandlernow classifies structurally-malformed events (empty SourceName/AlarmType,EventTimeUtcTicks ≤ 0) as Permanent and excludes them from the writer batch; the clientWriteBatchAsyncprefersPerEventStatus(→PermanentFail→ the sink's existing immediate dead-letter) and falls back toPerEventOk. NOIAlarmEventWriter/ Commons / Core.Abstractions change (HistorianWriteOutcome.PermanentFailalready 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-gatedAahClientManagedAlarmEventWritersurfaces richer per-event status. -
AbCip — nested-struct member expansion in productionSHIPPED master (branchfeat/hosts-rows-abcip-nested-hygiene) 2026-06-18 (3d8ce4e8+d203f31c). FINDING: simpler than this item assumed — the CIP Template Object member-infou16ALREADY 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: carryuint? NestedTemplateIdonAbCipUdtMember(decoded via the FULL 12-bit mask, not the byte-cast typeCode), thread it into the existingFetchUdtShapeAsync/@udt/{id}path — NO new CIP query. Offline-proven (decoder + no-seam threading test, AbCip 301/301). Live-gate FORMALIZED 2026-06-18 (branchfeat/focas-figure-ww-poison-abcip-gate): a skip-gated Emulate-tier integration smokeAbCipEmulateNestedUdtTests(mirrorsAbCipEmulateUdtReadTests+AbServerProfileGate.SkipUnless(Emulate)) asserts nested leaves addressable;docs/drivers/AbCip.mddocuments nested-struct as Emulate-tier verified. A LOCAL gate is architecturally impossible —ab_serverlacks CIP Template Object (class 0x6C); the test compiles + skips locally and runs only against Logix Emulate / real ControlLogix. -
AdminUI — Certificates page actionsSHIPPED masterbd791e792026-06-18. FleetAdmin-gated Trust (rejected→trusted) / Untrust (trusted→rejected) / Delete via a new in-processCertificateStoreManager(filesystem moves by thumbprint, honored live); inline Blazor confirm + server-side FleetAdmin re-check. Live-proven on docker-dev. CERT-ACTION AUDIT LOGGING SHIPPED master3eb370d4(2026-06-19): Trust/Untrust/Delete now write aConfigAuditLogrow (Certificate:Trust|Untrust|Delete, Outcome Success/Failure on every path incl. failures) via the first live structuredAuditEventemit site — newActorAuditWriter : IAuditWriter(AdminUI adapter Telling theAuditWriterActorcluster singleton resolved fromActorRegistry, mirrorsAdminOperationsClient) + pureCertAuditEvents.Buildfactory, audited at theCertificateStoreManagerchoke point (DeleteCorerefactor); actor sourced fromAuthenticationState. 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 wrotemulti-role-test | Certificate:Trust | Success | <thumbprint>toConfigAuditLog. (Was: only cert validation went to Serilog.) -
AdminUI — Hosts page per-driver-instance rowsSHIPPED master (branchfeat/hosts-rows-abcip-nested-hygiene) 2026-06-18 (cb062fce+6457eba8+07bd63f8). A new cluster-grouped "Driver Instances" section on/hostsreusing the existing driver-health DPS pipeline (added only an AdminUI-internalIDriverStatusSnapshotStore.GetAll()+ a pureHostsDriverView.Buildgrouping view-model + the Razor section) — NO Commons/interface/EF change. Grouped byClusterId(the reliable key; the health feed carries no per-Akka-member identity), nodes from ConfigDBClusterNode, driver rows enriched with Name/DriverType fromDriverInstance, live status chips via the in-process store'sSnapshotChanged(dodges the Traefik self-hub trap) + the 5 s timer. LIVE/runPASS (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 onDriverHealthChanged— a Commons change — or a per-nodeDriverHostActor-children Ask). -
AdminUI driver-pages plan — Phase 6 + Phase 10SHIPPED — RECONCILED 2026-06-18. A seam-by-seam audit confirmed the WHOLE driver-pages plan (Phases 0–10) is shipped; §A.9 was stale (it over-counted from the stale.tasks.json). Phase 6 (liveDriverStatusPanel) + Phase 8 (Reconnect/Restart) are wired end-to-end (DriverHealthChanged → AkkaDriverHealthPublisher invoked atDriverInstanceActor.PublishHealthSnapshot→ admin-gatedDriverStatusSignalRBridge→ snapshot store → panel in all 9 driver pages; pageDriverInstanceId⟷ actor_driverInstanceIdkeys match). Phase 10 E2E tests (DriverTestConnect/DriverReconnect/DriverStatusHub) shipped at494da22c. The one genuine remnant — a deployed-driver Healthy→Reconnecting→Healthy E2E + the harness-fidelity fix (AddOtOpcUaRuntimeso harness drivers actually publish health) + a controllable fake driver factory — SHIPPED onfeat/driver-pages-reconnect-e2e(docs/plans/2026-06-18-driver-pages-reconnect-e2e.md), driver E2E suite GREEN 18/0/4. The.tasks.jsonis 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. -
AdminUI — MonacoSHIPPED masterctx.SetVirtualTag(...)completions70e1bde92026-06-18 (predates this audit — was stale here). Thectx-receiver guard is inScriptAnalysisService.TryGetTagPathLiteral(:224, mirrors thectx-anchoredEquipmentScriptPaths.GetTagRefRegex) + a truthfulctx.SetVirtualTaghover note (it's a no-op in the live single-tag evaluator). -
F10b — in-place property writes vs full rebuildFIRST SLICE SHIPPED master (branchfeat/f10b-vtag-skip-rebuild-galaxy-comment) 2026-06-18 (6c6a2c42+b05c2811). The provably-safe, zero-node-mutation slice:Phase7Applier.Applynow SKIPSRebuildAddressSpacewhen a deploy's only changes are VirtualTag deltas differing solely inExpression/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 viaVirtualTagHostActor'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 (branchfeat/f10b-surgical-tag-attribute-writes) 2026-06-18 (3fc258bd+f6618144+b472bba3+fix22b0611f): equipment-tagWritable+IsHistorized/HistorianTagnametoggles now update the EXISTING node IN PLACE (AccessLevel + write handler + Historizing + historian-map +ClearChangeMasks) via a new optionalISurgicalAddressSpaceSink(OtOpcUaNodeManager.UpdateTagAttributes, mirrorsEnsureVariablevia a sharedComposeAccessLevel), 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=Falseon 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 injectDeferredAddressSpaceSink(the runtime swap-in wrapper), which forwardedIOpcUaAddressSpaceSinkbut NOTISurgicalAddressSpaceSink, 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): surgicalDataType/IsArraywrites (brief value-type mismatch; no ModelChangeEvents) + alarm-severitySetSeverity(live-shadowed by the alarm engine, operationally invisible). -
Client.UI — Galaxy sub-attribute fallback collapses operator commentsCLOSED as a documented + tested ACCEPTED-LIMITATION master (branchfeat/f10b-vtag-skip-rebuild-galaxy-comment) 2026-06-18 (fac44187). Investigation confirmed it is NOT code-fixable: Galaxy'sAckMsgattribute 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 fallbackAlarmEvent?.Invokesite + a tightenedAlarmEventArgs.OperatorCommentdoc 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). -
§9 hygiene — Galaxy stale forward-ref commentsSHIPPED master (branchfeat/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:_dataReaderreads viaReadViaSubscribeOnceAsync,_ownedMxSessionbuilt inBuildProductionRuntimeAsync,SyncPlatformsAsyncrefreshes the probe watcher post-discovery, onlyGalaxyMxGatewayexists — noGalaxy:Backendswitch, 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
IHistoryWriterfor equipment-vtag historization. TheHistorizeflag is now threaded end-to-end (commitsfc8121cb/9c5a0913/4501f126) but DI defaults toNullHistoryWriter— 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 UAHistoryUpdateservice (reclassified 2026-06-19, see §A.1). Server-side override is buildable but no historian backend can durably insert/replace/delete —IHistorianDataSourceis read-only and the Wonderware sidecar has no update RPC; build the service +IHistoryUpdateSinkseam 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 —
Phase7Applierwritable = tag.Writable && !tag.IsArray, commit3bb2031d). - 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.Stringplaceholder retained. - TwinCAT pre-declared
Structuretags rejected at parse (discovery is the supported path); Flat-modeSubSymbolslive caveat — on some TC3 firmware Flat mode may not populate struct SubSymbols → switchSymbolsLoadMode.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
ExecuteWritefire-and-forget — the .NET writer fully propagatesWriteResultstatus; 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 byTagConfigEditorMap),DraftSnapshotFactoryplaceholders (conservative fallbacks), ClusterLeaderChangedno-op (ServiceLevel lives inRedundancyStateActor).- 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
ServerUriArraychild ofServer.ServerRedundancy— pending an SDK object-type upgrade (readServer.ServerArraytoday). - 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 viadcb0be65+/alerts). - Driver-reconfigure-while-faulted T3 live
/run. - AVEVA Historian rig smokes (skip-gated
rig-requiredtests).
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).