The lib row still claimed ScadaBridge was the only consumer (verified 2026-07-20); OtOpcUa's Phase 1 adoption merged 2026-07-21. Records 0.1.2 and the wiped-peer snapshot fix it carries. Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
62 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
What this repository is
scadaproj is primarily an umbrella/index workspace that aggregates a family of
related SCADA / OT / Wonderware / OPC UA "sister projects" that live as sibling
directories under ~/Desktop/. It now also hosts nine shared libraries itself (verified
2026-07-18) — ZB.MOM.WW.Auth/ (login/identity),
ZB.MOM.WW.Theme/ (UI kit),
ZB.MOM.WW.Health/ (health checks),
ZB.MOM.WW.Telemetry/ (observability),
ZB.MOM.WW.Configuration/ (config validation),
ZB.MOM.WW.Audit/ (audit event model),
ZB.MOM.WW.Secrets/ (encrypted secret store),
ZB.MOM.WW.GalaxyRepository/ (Galaxy browse), and
ZB.MOM.WW.LocalDb/ (embedded cache + 2-node sync) — all the realized output of their
respective component normalizations (see Component normalization).
(A tenth directory, ZB.MOM.WW.SPHistorianClient/, is retired — 0 tracked files, only
gitignored build residue left on disk.)
The point of this file is to give a high-level scan of each sister project — its purpose,
location, stack, and primary commands — so a fresh Claude Code session can orient across
the whole family without opening each repo first.
Each sister project keeps its own authoritative CLAUDE.md. This index is a
summary of those files; when you actually work in a project, open that project's
own CLAUDE.md for the full picture. See Refreshing this index.
The project list below is curated manually. Add or remove entries as the family changes — do not assume every directory under
~/Desktop/belongs here.
Sister projects (core SCADA/OT family)
Runtime / implementation (active code)
| Project | Location | Stack | Repo | Summary |
|---|---|---|---|---|
| OtOpcUa | ~/Desktop/OtOpcUa |
.NET 10, OPC UA, gRPC | gitea.dohertylan.com/dohertj2/lmxopcua |
OPC UA server that exposes industrial data sources under a two-subtree (dual-namespace) address space (v3.0, merged to master 2026-07-16, PR #472, merge ec6598ce) — native-protocol drivers (Modbus, S7, AB CIP/Legacy, TwinCAT, FOCAS, OpcUaClient) and AVEVA System Platform (Wonderware) Galaxy, now a standard Equipment-kind driver (the old SystemPlatform mirror / alias-tag model was retired ~2026-06-12). v3.0 NodeId/namespace scheme (this is the wire contract ScadaBridge binds against — a CUTOVER for its Data-Connection-Layer bindings, coordination pending): the single https://zb.com/otopcua/ns namespace is replaced by two — https://zb.com/otopcua/raw (device tree Folder→Driver→Device→TagGroup→Tag, NodeId s=<RawPath>) and https://zb.com/otopcua/uns (equipment tree Area→Line→Equipment→signal, NodeId s=<Area>/<Line>/<Equipment>/<EffectiveName>); every value has ONE source (the raw tag) fanned to both NodeIds (identical value/quality/timestamp), each UNS variable Organizes-references its raw node, writes route through either NodeId, HistoryRead works via both under one historian tagname, and native alarms fan (one ReportEvent) to the raw device folder + every referencing equipment folder. The old EquipmentNodeIds ({equipmentId}/{folderPath}/{name}) scheme is retired. (v3 program = Batches 1–4: greenfield Raw-tree schema + /raw authoring UI + Calculation driver + UNS reference-only Equipment + {{equip}} reference-relative scripts, all merged; Batch 4 lit up the address space.) Galaxy access flows through the in-process GalaxyDriver → gRPC → the mxaccessgw gateway. Surfaces live read + authorized write, native OPC UA Part 9 alarms, and server-side HistoryRead. Historian backend (merged 2026-06-27, PR #423): OtOpcUa's sole historian read/write backend is HistorianGateway (via ZB.MOM.WW.HistorianGateway.Client — continuous historization + alarms), replacing the retired bespoke Wonderware historian driver. |
MxAccessGateway (mxaccessgw) |
~/Desktop/MxAccessGateway |
.NET 10 gateway (x64) + .NET 4.8 worker (x86), gRPC | gitea.dohertylan.com/dohertj2/mxaccessgw |
gRPC gateway giving modern clients full MXAccess parity without loading 32-bit COM. Two-process: gateway (ASP.NET Core gRPC + Blazor dashboard) + per-session x86 worker that owns the MXAccess COM STA. OtOpcUa depends on this. |
| ScadaBridge | ~/Desktop/ScadaBridge |
.NET 10, Akka.NET, Docker | gitea.dohertylan.com/dohertj2/ScadaBridge |
Full implementation of the distributed SCADA platform — hub-and-spoke (1 central cluster + N site clusters). Projects prefixed ZB.MOM.WW.ScadaBridge.*; solution ZB.MOM.WW.ScadaBridge.slnx. Ships src/, tests/, docker/ topology, and the design docs that are the spec. |
| HistorianGateway | ~/Desktop/HistorianGateway |
.NET 10 x64, gRPC, Blazor | gitea.dohertylan.com/dohertj2/historiangw |
Single-process gRPC sidecar exposing (1) full read/write API to the AVEVA Historian (5 gRPC services; 15 retrieval modes; historical/backfill writes; tag-config lifecycle; SQL live-value path; store-forward + redundancy resilience; all default-disabled) and (2) read-only Galaxy object-hierarchy browse via the shared ZB.MOM.WW.GalaxyRepository lib (consumed as a Gitea-feed package). No COM, no x86 worker. Dev: two plaintext endpoints from appsettings.Development.json — dashboard on :5220 (HTTP/1.1), gRPC h2c on :5221. Production: single Kestrel:Endpoints:Https endpoint with Protocols: Http1AndHttp2 multiplexes dashboard + gRPC over one TLS port (ALPN); warn-only if no TLS endpoint configured (valid behind a reverse proxy / Kubernetes ingress; the warn predicate covers any non-Development environment, i.e. Production + Staging). In a non-Development environment the gateway also logs warn-only production-readiness checks (pending.md D2/D3) — relative runtime-artifact paths + secret hygiene (ApiKeys:Mode=Disabled, empty/dev-placeholder pepper, dev-placeholder LDAP password). Owns AVEVA.Historian.Client — imported into the repo at histsdk/ (subtree, folded in with history 2026-06-28; the old "vendored / re-vendor from upstream" model is retired — histsdk/** is ordinary owned source, edited in place; the gateway builds it directly). Store-forward uses a crash-safe FasterLog append-only outbox (Microsoft.FASTER.Core 2.6.5; CommitMode PerEntry/Periodic), not SQLite. Handshake amortization (pending.md A1) done + live-validated — a default-on leased-session pool (Historian:SessionPool) reuses pre-authenticated sessions across reads/writes/status ops/tag-browse/metadata (~4.7× measured; probe and blocks stay per-call), with a <~15 s keepalive + reactive re-auth, surfaced via a PooledHistorianClient facade so services are unchanged; the HistorianSession primitive lives in the owned AVEVA.Historian.Client under histsdk/; browse/metadata + SendEvent broadening merged to main + pushed. SendEvent is also amortized via a separate, parallel event-session pool (Historian:EventSessionPool, default-on; v8/ECDH auth — kept distinct from the v6 pool), warranted by a GREEN v8 Event-session reuse spike (~10–16×); ReadEvents stays per-call / gated (C2). The full offline suite is green on macOS (0 warnings); the env-gated live historian + Galaxy integration suite exercises the amortized path and otherwise skips without a live server. Part C minimal (pending.md C1/C3a) merged to main: Int8/UInt8 live write types un-gated + live-proven against wonder-sql-vd03 (in the owned histsdk/ SDK); the 2023 R2 gRPC interface-version integers recorded as evidence (C3a); UInt1 attempted but server-blocked (the historian accepts EnsureTags(UInt1) yet stores a degenerate analog tag) → re-gated fail-closed. C2 closed won't-fix (2026-06-26): event reads are server-gated on the 2023 R2 historian over both transports — gRPC retrieval-server-gated (0 rows scoped to a managed connection), and the WCF certificate transport + auth DO reach the historian cross-platform (CM_EVENT registers on the 0x501 event connection) but the query still returns 0 rows: the same server-side per-connection row gate. (An earlier note saying "WCF not served on 2023 R2" was a test error vs the reverse tunnel.) Not client-fixable. Reusable SDK wins: ConnectViaAddress (WCF Via for tunneled access), EventReadConnectionModeOverride. SQL-path ReadEvents (merged 2026-06-26) — the practical workaround for the C2 gate: event reads now ship via the historian's Runtime.dbo.Events SQL view (config-gated RuntimeDb:EventReadsEnabled + EventReadMaxRows), mirroring the SQL live-write path (no COM/native); live-proven streaming real events (incl. an INSQL NOT-NULL CAST fix the live test caught) while the native event-query stays gated — so event reads work despite the native dead-end. All of Part C + C2 + SQL-ReadEvents are merged to origin/main (gateway @ fabab1a, histsdk @ f0a1b04). Follow-ups merged to main (2026-06-27): historiangw PR #6 — WriteLiveValues UTC→server-local timestamp fix (live-validated exact) + a documented SendEvent Source_Object protocol limitation (pending.md C4); the Plan-1 .NET client lib + packable Contracts (PR #5); and a C2 cross-ref docs PR (#4). First consumer (merged): OtOpcUa PR #423 (→ master) adopts ZB.MOM.WW.HistorianGateway.Client as its sole historian read/write backend, retiring its bespoke Wonderware historian driver. Local Docker (2026-06-29): HistorianGateway/docker/ runs the sidecar in a container against the REAL wonder datasources (historian wonder-sql-vd03:32565 TLS + Galaxy SQL wonder-app-vd03/ZB), dashboard login + gRPC API-key auth disabled — verified /health/ready Healthy. Runtime-only image: publish framework-dependent on the host (authed Gitea feed → no Docker restore), COPY into aspnet:10.0; secrets via gitignored env_file; isolated compose project zb-historiangw; container egress needs the host VPN up. Client surface (clients/dotnet/): the published ZB.MOM.WW.HistorianGateway.Client lib + a ZB.MOM.WW.HistorianGateway.Client.Cli smoke CLI (probe/read-raw/browse, JSON out; in the .slnx, not packed). Current: origin/main @ 56a70df — AHC-002 string READS landed via native RE from the Mac (PR #17 merged 2026-07-16): the SDK now parses string historian result-buffer rows into HistorianSample.StringValue (a per-row valueType discriminator — 1=numeric→double, 3=string; value = u32 charLen + UTF-16LE), and the gateway forwards string_value through HistorianRead.ReadRaw — no contract change, no package bump (the proto already carried the field; mapper already forwarded it); live-proven against 2023 R2. String tag-create is now doable via SQL aaStringTagInsert (SqlStringTagProvisioner, test tooling — single + double byte). String historical writes were probed (bounded 6-candidate native blob probe) but non-convergent — tag-info resolves a real GUID + SingleByteString (unlike the Int1/UInt1 degenerate stub) so writes aren't blocked at resolution, but BSuccess is not an oracle (all framings optimistically accepted) and none round-tripped → deferred, needs a native-client write wire capture (blind guessing can't finish). Eight follow-up issues filed (historiangw #18–#25): datatype gaps (native string tag-create #18, string writes #19, DateTime/FileTime #20, Guid/Structure/Event #21, Int1/UInt1 discrete-create #22 [follow-on to #11], DoubleByteString read-verify #23) + SQL-fallback trackers (event reads via Runtime.dbo.Events #24, live writes via History/v_StringHistory #25). Also folded a doc-truth reconcile of the "Key structural decisions" bullets (GalaxyRepository PackageReference, Contracts/Client 0.3.0 published, OtOpcUa on 0.3.0). Prior: ArchReview #2 (cycle-2) remediation MERGED 2026-07-14 (PR #12): all 8 workstreams A–H — store-forward drain honesty (IsRetryableDeliveryFault, typed HistorianOutboxFullException→ResourceExhausted), streaming-lease caps (per-key cap 8→4, MaxStreamLeaseHold, bounded EndQuery teardown, client StreamTimeout default null→10min), a live idle-out capture (GREEN-A, native (type 4, code 51)) that narrowed the session-expiry classifier and closed the stability-F5 tail, histsdk residue (typed event-send FromNativeError, HistorianProtocolIntegrityException, IP/host scrub), read-path caps (MaxAggregateIntervals/MaxAtTimeTimestamps, 5M→ResourceExhausted, verify-once-per-connection version gate, batched audit drain), release hygiene (csproj <Version> sole authority + packed-README/AnyCPU/FQDN/CHANGELOG guards), security/ops (opt-in ForwardedHeaders trust list, k8s key-bootstrap runbook, dashboard pool/dead-letter signals), and doc-truth #2 (Historian:VerifyServerInterfaceVersion now a real fail-closed key). Verified 0-warn, offline 1388 pass + live suite green vs wonder-sql-vd03. Contracts+Client 0.3.0 PUBLISHED to the dohertj2-gitea feed (2026-07-14; carries the StreamTimeout default change) and OtOpcUa bumped to 0.3.0 (lmxopcua master @ 7f79cd59, PR #440 — only consumer delta is the StreamTimeout default; driver tests 103/0). Boolean historization remapped Int1→Int2 (2026-07-14, lmxopcua master @ 38e21df2, PR #442, closes #441): the R2-06 live gate surfaced that the historian's Int1 analog-tag creation path is server-degenerate (EnsureTags(Int1) stores an unusable stub) — filed gateway-side as historiangw #11, documented there as an explicit evidence-based exclusion (PR #16), and routed back to OtOpcUa as issue #441. OtOpcUa's HistorianTypeMapper now maps Boolean → Int2 (0/1 integer); the value-write path already sends every value as a double on WriteLiveValues regardless of tag type, so Boolean 0.0/1.0 round-trips cleanly and a pre-existing Float Boolean tag retypes cleanly (both are supported analog types). Closes the last R2-06 red test → live gate 6/6; gateway driver unit 103/103. archreview round-2 deferred gates closed 2026-07-14 (lmxopcua master @ 30672888): R2-04 T13+T15 LIVE-PASSED (PR #444) — the behavior-affecting S4 primary-gate default-deny verified on a 2-node docker-dev rig (ServiceLevel 250/240; boot-window write rejected not primary (role unknown) + denial meter reason=role-unknown; steady-state secondary rejected + node reverted; primary write reaches device) plus the in-process PrimaryGateFailoverTests (delivered-snapshot drives the gate); and the pre-existing red S7.Cli phrase-scan test fixed (PR #443, restored a stripped rationale comment). Round-2 remainder is now only infra-gated: R2-08 live-GLAuth outage, R2-01 S7 SYN-blackhole. The full *.IntegrationTests sweep is now COMPLETE (2026-07-15, lmxopcua master @ 152a5645): ran all 10 driver + server integration suites serially (on the Mac against native fixtures on the 10.100.0.35 Docker host), then closed all 7 resulting follow-ups (PRs #446–#452) — every driver + server integration suite is verified GREEN with ZERO OtOpcUa production regressions; every original "failure" was fixture/harness/test rot, not product. Highlights of cross-repo relevance: (a) the Host integration harness's LDAP swapped from the retired bitnami/openldap:2.6 to GLAuth (PR #451) — the harness now runs its own ephemeral GLAuth container on :3894 (seeded by tests/.../Host.IntegrationTests/glauth/config.toml, distinct from the shared dev GLAuth on :3893), unifying the whole family's opt-in real-LDAP path on GLAuth and removing the lone OpenLDAP outlier; (b) the deploy-E2E "amd64-emulated-SQL timing" complaint was misdiagnosed — those tests had only ever run against the EF in-memory provider (no FK enforcement) and lacked the ServerCluster/ClusterNode seed the real-SQL NodeDeploymentState FK requires; fixed with a harness seeding helper + OTOPCUA_HARNESS_SQL_HOST/_LDAP_HOST overrides so the fixtures run on the native-amd64 Docker host (PR #452). See archreview/plans/INTEGRATION-SWEEP-STATUS.md for the full record. Cycle-2 gated tail: CI first-green-run (needs a Gitea act_runner), the histsdk protobuf/Grpc.Tools major bump (03-CC-8, blocked on migrating off obsolete GrpcWebHandler.HttpVersion), and the 1.0.0 TLS-default flip. The prior cycle-1 ArchReview remediation is fully merged (Now + Next tiers 2026-07-09 @ 508b9c3; Later tier 2026-07-13 via a coordinated two-PR merge, 12385c8 + 6a24a30). Off a 2026-07-08 six-report architecture review (archreview/) turned into 8 workstream plans (docs/plans/2026-07-09-archreview-*; tracker ...-remediation-tracking.md), shipped clean under TreatWarningsAsErrors: a hard-fail ResilienceComposition validator (StoreForward+Redundancy both-on), corruption-tolerant outbox + dead-letter/overflow knobs, /auth/login rate-limit, server-side max_values cap, a headless apikey CLI, histsdk parser hardening, Gitea Actions CI + warnings-as-errors on all trees, pool observability (lease-wait/waiter/counters) + session-pool health probe, and a typed histsdk fault taxonomy with type-based classification + session churn-guard. Contracts+Client 0.2.0 published to the dohertj2-gitea feed (2026-07-09; Int1 proto3-optional, opc_quality doc, CA-chain pin). Cross-repo 0.2.0/G-2 chain COMPLETE: OtOpcUa bumped to Client+Contracts 0.2.0 (2026-07-10, master @ f6eaa267; M3 opc_quality gate clear), and the shared ZB.MOM.WW.Auth.ApiKeys gained ExpiresUtc at 0.1.4 — consumed by HistorianGateway (apikey create --expires + dashboard Expires column) and mxaccessgw (verifier enforcement). Later tier (roadmap 16–21) MERGED to main 2026-07-13 (coordinated two-PR merge; both branches now deleted): PR #8 (archreview-later-21-and-maxsessions, merge 12385c8) = Later-21 (audit off the mutating-RPC critical path + WriteLiveValues SQL batching + composed-failure/shutdown tests) + the MaxSessions 4→8 raise (live concurrent-session spike GREEN); PR #9 (later-16-page-through-streaming, merge 6a24a30) = Later-16 ReadRaw page-through streaming (streams per server result buffer; pool holds the lease across the enumeration; new Historian:ReadLimits:AllowStreamingBeyondCeiling), Later-17 redundancy honest-hardening (exposes the Redundancy:WriteAcknowledgement/WriteFanout/FailureThreshold ack policy the docs already claimed), Later-18 per-key concurrency cap (ApiKeys:MaxConcurrentCallsPerKey → gRPC ResourceExhausted) + api-key-rotation runbook, Later-19 CHANGELOG-discipline guard test, Later-20 K8s manifests + hardened prod Dockerfile + operator runbooks (deploy/k8s/, docker/Dockerfile.production, docs/runbooks/). The two PRs conflicted on CLAUDE.md/tracking/ValidatorTests, resolved as the union (MaxSessions lands at 8). Live-proven vs wonder-sql-vd03; offline gateway 729 pass / client 140 pass / 0 warnings. Only remaining archreview item: the secure-by-default TLS-default flip, deferred to 1.0.0 (pre-announced in the client 0.2.0 CHANGELOG). (Prior tip 93c6051 = the 2026-07-07 CommentChecker XML-doc sweep.) |
Cross-project relationships
The three indexed projects are separate repos and separate processes, coupled at
runtime over wire protocols (gRPC + OPC UA) — not by project/compile references. They
share the ZB.MOM.WW.* product namespace (ZB.MOM.WW.OtOpcUa.*, ZB.MOM.WW.ScadaBridge.*;
the gateway uses MxGateway.*). The common subject is AVEVA System Platform (Wonderware)
"Galaxy" data, and mxaccessgw is the linchpin that the other two connect through.
Data flow
AVEVA System Platform — Wonderware "Galaxy"
(OT source of truth: runtime tags + Galaxy Repository SQL DB)
▲
│ MXAccess COM (32-bit, STA message pump)
│
┌────────────────┴─────────────────┐
│ MxAccessGateway (mxaccessgw) │ gateway x64 .NET10 + worker x86 net48
│ gRPC service; OWNS the 32-bit │ protos: mxaccess_gateway / mxaccess_worker
│ COM bitness + STA pump │ / galaxy_repository
└──────┬─────────────────────┬──────┘
gRPC (MxCommand/MxEvent + │ gRPC (ScadaBridge "MxGateway" adapter:
GalaxyRepository browse) │ native MxAccess data + A&C alarms)
│ │
┌─────┴──────┐ │
│ OtOpcUa │ GalaxyDriver maps │
│ OPC UA srv │ Galaxy hierarchy → │
│ (.NET 10) │ OPC UA addr space │
└─────┬──────┘ │
│ OPC UA (opc.tcp; data + A&C alarms)
▼ ▼
┌──────────────────────────────────────────────┐
│ ScadaBridge — Data Connection Layer (DCL) │
│ OPC UA adapter │ MxGateway adapter │ custom │
└─────────────────────────┬────────────────────┘
▼
Instance Actors → site clusters → central cluster / UI
Edge-by-edge
- MxAccessGateway is the foundation. It is the only component that loads 32-bit MXAccess COM (its x86 net48 worker owns the COM apartment + STA pump). It exposes that to modern x64/.NET-10 callers over gRPC, and also serves Galaxy Repository SQL browse RPCs. This is why the other two exist as .NET 10 / x64 and never touch COM directly.
- OtOpcUa → MxAccessGateway (gRPC client). OtOpcUa's in-process
GalaxyDriver(src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/) uses two gateway channels: theGalaxyRepositoryClientfor the static hierarchy, and an MXAccess session (MxCommand/MxEventprotos) for live read/write/subscribe. ADeployWatcherpolls the gateway's deploy-event signal to rebuild the OPC UA address space on Galaxy redeploy. OtOpcUa's job is a protocol bridge: it republishes Galaxy — now bound as a standard Equipment-kind driver alongside its native-protocol drivers, not a special SystemPlatform mirror — as an OPC UA address space (live values, Part 9 alarms, HistoryRead) for any OPC UA client. - ScadaBridge → OPC UA (OPC UA client). ScadaBridge's DCL has an OPC UA adapter that collects data and mirrors native OPC UA Alarms & Conditions. OtOpcUa is exactly such a server, so ScadaBridge can ingest Wonderware data indirectly via OtOpcUa.
- ScadaBridge → MxAccessGateway (gRPC client). The DCL also has a dedicated
MxGateway adapter that talks to
mxaccessgwdirectly for native MxAccess data and alarms — so ScadaBridge can reach Wonderware data directly, bypassing OtOpcUa. Both adapters implement the sameIAlarmSubscribableConnectionseam, and a read-onlyNativeAlarmActorunifies OPC-UA-A&C and MxAccess alarms onto one condition model.
Net effect
mxaccessgwis a shared dependency of both OtOpcUa and ScadaBridge.- ScadaBridge has two paths to the same Wonderware data: (1) OPC UA → OtOpcUa → gateway, or (2) MxGateway adapter → gateway directly. Path 1 gives standards-based OPC UA decoupling; path 2 gives a more direct/native feed.
- HistorianGateway is a near-independent sidecar. It reaches the Historian via its owned gRPC
client (
AVEVA.Historian.Client, imported into the repo athistsdk/) and the Galaxy Repository SQL DB directly, not throughmxaccessgw. It consumes the sharedZB.MOM.WW.GalaxyRepositorylib (Gitea-feed package). Any client that needs Historian data or Galaxy browse can target it independently. As of 2026-06-27 it is a runtime dependency of OtOpcUa (ScadaBridge still has no coupling): OtOpcUa adopted HistorianGateway as its sole historian read/write backend (continuous historization + alarms via the publishedZB.MOM.WW.HistorianGateway.Client), retiring its bespoke Wonderware historian driver — merged to OtOpcUamaster(PR #423). The gateway-side follow-ups also merged tohistoriangwmain: the Plan-1 client lib + packable Contracts (PR #5) and theWriteLiveValuesUTC→server-local timestamp fix + a documentedSendEventSource_Objectprotocol limitation (PR #6,pending.mdC4). OtOpcUa pre-existing test failure tracked aslmxopcuaissue #424. - Coupling is loose: each repo references the others only as sibling context (the
## Sister Projectsnote in ScadaBridge's ownCLAUDE.mdlistsMxAccessGatewayandOtOpcUawith their Gitea URLs but states they are not part of its solution). - The break surface is the wire contracts, not code. Because coupling is by network
protocol, the things that break across repo boundaries are: the gateway's
.protofiles (mxaccess_gateway.proto,mxaccess_worker.proto,galaxy_repository.proto), thehistorian_gateway.v1proto (HistorianGateway's own contract), and the OPC UA address-space shape OtOpcUa publishes (browse paths, node IDs, A&C alarm model). Changes to any of these must be coordinated across the affected repos — a green build in one repo does not prove the others still interoperate.
Component normalization
Because the sister repos re-implement the same cross-cutting concerns separately and drift
apart, components/ normalizes them: per component, the one target spec,
each project's code-verified current state, and the gaps between. See
components/README.md for the convention and workflow.
| Component | Status | Goal | Design | Implementation |
|---|---|---|---|---|
| Auth (login / identity / authz) | Adopted (lib 0.1.4; all 3 apps, merged to local default main/master + pushed to origin (gitea)) |
Shared ZB.MOM.WW.Auth lib |
components/auth/ |
ZB.MOM.WW.Auth/ |
| UI Theme (layout / tokens / components) | Adopted (feed 0.2.0–0.3.1; all 4 apps pinned 0.3.1, pushed to origin) |
Shared ZB.MOM.WW.Theme RCL |
components/ui-theme/ |
ZB.MOM.WW.Theme/ |
| Health (readiness / liveness / active-node) | Adopted (lib 0.1.0, on the feed; all 4 apps reference it + wire MapZbHealth — verified 2026-07-18; the prior "not yet adopted" claim was stale) |
Shared ZB.MOM.WW.Health lib |
components/health/ |
ZB.MOM.WW.Health/ |
| Observability (metrics / traces / logs) | Adopted (libs 0.1.0, on the feed; all 4 apps call AddZbTelemetry; AddZbSerilog in all but ScadaBridge, which keeps its own factory by design) |
Shared ZB.MOM.WW.Telemetry lib + .Serilog |
components/observability/ |
ZB.MOM.WW.Telemetry/ |
| Config + validation (options / startup validation) | Adopted (lib 0.1.0, on the feed; all 4 apps, pushed — the "local only / not yet pushed" caveat was stale) |
Shared ZB.MOM.WW.Configuration lib |
components/configuration/ |
ZB.MOM.WW.Configuration/ |
| Audit (event model + writer seam) | Adopted (lib 0.1.0, on the feed; all 4 apps, pushed to origin) |
Shared ZB.MOM.WW.Audit lib |
components/audit/ |
ZB.MOM.WW.Audit/ |
| Galaxy Repository (object-hierarchy SQL browse + gRPC service) | Adopted (feed 0.1.0 + 0.2.0; consumed at 0.2.0 by HistorianGateway and mxaccessgw, whose Server wires AddZbGalaxyRepository — verified 2026-07-18; the prior "mxaccessgw adoption is a follow-on" claim was stale) |
Shared ZB.MOM.WW.GalaxyRepository lib |
(design in histsdk + design doc 2026-06-23) | ZB.MOM.WW.GalaxyRepository/ |
Secrets (encrypted store + ${secret:} resolution) |
Built (libs 0.1.3, published to the Gitea feed; HistorianGateway adopted + live-proven 2026-07-16; mxaccessgw adopted G-4/G-5/G-6 + merged to origin/main @ e088dfa 2026-07-16, box-verified fail-closed + encrypt-at-rest; ScadaBridge adopted G-3/G-4/G-5/G-6 + merged to origin/main @ 128f1596 2026-07-16, G-3 live-proven vs the real production MxGateway gateway wonder-app-vd03:5120 (secret-ref ApiKey → Connected + browsed real Galaxy); OtOpcUa adopted G-2/G-4/G-5/G-6 + merged to origin/master @ 872cf7e3 (lmxopcua) 2026-07-16 — the last app, so all four now adopted; Layer-B driver secrets (Galaxy API key + OpcUaClient Password/UserCertificatePassword) resolve fail-closed at driver session-open, AddZbSecrets registered unconditionally so driver-only nodes work; G-2 live-proven vs the real production MxGateway wonder-app-vd03:5120 — a secret:-ref Galaxy ApiKey resolved through OtOpcUa's real GalaxyDriverBrowser → dummy key MxGatewayAuthenticationException, real key → browsed the real Galaxy root (10 nodes); temp key minted+revoked+deleted); G-8 KEK-rotation BUILT (lib bumped 0.1.2→0.1.3, 2026-07-17): Rewrap DEK primitive + CAS-guarded ApplyRewrapAsync + KekRotationService.RewrapAllAsync + secret rewrap-all CLI + operator runbook (ZB.MOM.WW.Secrets/docs/operations/kek-rotation.md), full suite green + CLI smoke + adversarial crypto review PASS (TOCTOU closed via compare-and-swap). G-7 clustered replication BUILT 2026-07-18 (lib 0.2.0) — BOTH fork options, packaged as two new libs: ZB.MOM.WW.Secrets.Replicator.SqlServer (shared SQL-Server ISecretStore = the plan's Option A, plus a local-store-with-hub mode) and ZB.MOM.WW.Secrets.Replicator.AkkaDotNet (peer-to-peer over distributed pub/sub = Option B, which the plan had deferred). Core gained ISecretsStoreMigrator, SecretLastWriterWins (one shared LWW tie-break so stores cannot diverge), SecretReplicationReconciler, and ReplicatingSecretStore — the last of which fixes a real gap: nothing had ever called ISecretReplicator.PublishAsync, so the seam was inert. Verified 170 offline / 184 with the live SQL suite / 1 skip / 0 warnings, including 15 live tests vs a real SQL Server 2022 (the SQLite suite ported case-for-case) and a 9-test in-process 2-node Akka cluster (real remoting; write→peer, delete propagation, both anti-entropy directions, late-joiner catch-up). Hard constraints across all topologies: same KEK on every node, ciphertext-only across trust boundaries, and re-wraps deliberately do NOT replicate (rewrap-all runs once per independent store). Operator runbook ZB.MOM.WW.Secrets/docs/operations/clustered-secrets.md. Corrected 2026-07-18: G-8 + the G-7 plan ARE committed and pushed on main (d82d345) — the "not yet committed/pushed" note was stale. 0.1.3 PUBLISHED to the feed 2026-07-18 (restore-verified), carrying a transitive security pin (SQLitePCLRaw.lib.e_sqlite3 → 2.1.12, advisory GHSA-2m69-gcr7-jv3q) that 0.1.2 lacked. All 4 apps bumped to 0.2.1 2026-07-18 (local feat/secrets-0.2.0 branches, NOT pushed). 0.2.1 fixes a critical defect: 0.2.0's Akka replicator was silently inert (TryAdd registration order — it published into a no-op sink and never spawned an actor). SQL-Server replication was never affected. Clustered topology is wired but default-OFF and NOT live-validated — OtOpcUa on Akka peer-to-peer, ScadaBridge on SQL-hub mode (its central and site clusters are separate Akka clusters, so pub/sub cannot cross them). Separately, a live SQLitePCLRaw vulnerability was found and fixed in ScadaBridge, where a NuGetAuditSuppress had been masking vulnerable 2.1.11 in 15 projects. 0.2.0 PUBLISHED to the feed 2026-07-18 (5 packages, restore-verified from a scratch consumer; merged to main @ 15ef1f3) — no app has adopted a clustered topology yet. 0.2.2 (2026-07-18, scadaproj#1 FIXED): the Akka replicator's hosted-process DI deadlock root-caused — a circular singleton dependency hidden in factory lambdas (ISecretStore decorator → ISecretReplicator → actor provider → ISecretCacheInvalidator → DefaultSecretResolver → ISecretStore), which MS.DI's StackGuard turns into a silent cross-thread call-site-lock deadlock instead of a stack overflow; NOT DistributedPubSub/HOCON (both disproven — the actor was never constructed). Fixed by deferring the one cycle edge (DeferredSecretCacheInvalidator, resolves on first eviction); Akka live gate now 6/6 (was: hang), regression coverage HostedProcessResolutionTests (container-built graph + hosted services; red on 0.2.1), suite 180/0/15. Fourth defect class only visible with the DI graph built in a real host. 0.2.2 PUBLISHED to the feed 2026-07-18 (5 packages, restore-verified from a scratch consumer). OtOpcUa bumped to 0.2.2 same day (lmxopcua master @ 2254ae3d): Akka gate re-run vs the FEED packages 6/6, plus a new provider-based The_startup_hook_actually_creates_the_replication_actor test of OtOpcUa's own wiring (8/8); ENABLED + live-proven on the OtOpcUa docker-dev rig same day (lmxopcua master @ c878fbbd, #482 CLOSED): all six rig nodes on Secrets:Replication:Enabled=true with one shared dev KEK; seeded row on site-a-1 alone converged to all five peers ≤60 s, byte-identical ciphertext on all six + decrypt-verified; site-node restart clean. Flag remains default-false in the product; production enablement would need real KEK distribution (own decision/issue). Side-find lmxopcua#483 FIXED+CLOSED 2026-07-19 (lmxopcua @ 5f72ff85): OtOpcUa's static per-page-rendermode router made the routed Secrets.Ui RCL page a dead SSR page; fixed with a host-side wrapper page carrying @rendermode InteractiveServer (RCL unchanged — the other three apps' globally-interactive routers would throw on a nested render mode), pin tests + Playwright-verified live. Family-wide /admin/secrets sweep 2026-07-19: ScadaBridge's page had a DIFFERENT defect — Secrets.Ui injects the shared ZB.MOM.WW.Audit.IAuditWriter seam, which ScadaBridge never registered (it has its own deliberately-distinct Commons IAuditWriter) → hard 500 on every render, hidden behind the login wall. FIXED+CLOSED as ScadaBridge#22 (ScadaBridge main @ fc86e8bf): CentralSharedSeamAuditWriter bridges the shared seam to ICentralAuditWriter (central direct-write → dbo.AuditLog, NOT the site SQLite chain), pinned by red-first WebApplicationFactory DI tests over the real Program.cs, live-proven on the local docker cluster (page interactive; secret.add/secret.delete audit rows landed in dbo.AuditLog). The sweep also surfaced a Secrets.Ui library defect, scadaproj#2 — FIXED+CLOSED same day via 0.2.3 (5 packages PUBLISHED to the feed; scadaproj main @ 62ab8f6): ConfirmDeleteModal used bare class="modal" and the RCL shipped no CSS, so Bootstrap 5's .modal{display:none} made the delete modal permanently invisible on every Bootstrap host (confirmed live on ScadaBridge AND OtOpcUa; HG links the same bundle; only mxgw is Bootstrap-free) — handlers worked, only CSS hid it. Fix entirely in the RCL: zb-secrets-modal* class rename + a component-emitted <style> block (Theme tokens w/ fallbacks; testids unchanged; bUnit pins). Scoped .razor.css was deliberately rejected — only ScadaBridge links a scoped-CSS bundle, so isolation CSS would silently not load on the other 3 hosts (the same silent-failure class). Live-verified on ScadaBridge (bumped to 0.2.3 @ 63510778): modal visible display=flex with zero JS forcing, delete flows through the real path. Rollout COMPLETE 2026-07-19 — all four apps on 0.2.3, pushed: OtOpcUa (lmxopcua master @ be87ddeb; AdminUI 665/665 + secrets integration 8/8; docker-dev rig rebuilt, modal gate PASS live), HistorianGateway (main @ 7452eb4, from 0.2.0; plus 6bc005d fixing the pre-existing AngleSharp NU1902 warn-as-error that had made the whole HG test suite unbuildable — test-only pin to 1.5.2, suite now runs: 854 pass / 1 pre-existing VPN-gated live failure, histsdk failures identical to the pre-bump baseline; docker container rebuilt, modal gate PASS live), and mxgw (main @ 8f7ee49, from 0.2.0; Bootstrap-free so never affected; 780 pass / 45 macOS-only NamedPipe-harness failures identical before and after the bump — zero delta) 0.3.0 (2026-07-19, merged to main @ 87ffe1b) added a menu-driven Spectre.Console interactive operator console to the secret CLI: bare secret with no verb on a real TTY (redirected I/O or any verb → unchanged headless behavior, no-args-redirected → usage/exit-2) opens a guided session that targets a deployment's store by reading the target app's own appsettings.json (base + env overlay + env vars → Secrets section, so it hits exactly the store the app reads; recents cached at ~/.zb-secrets/recent-targets.json, names/paths only). Eight flows: list/set/reveal/delete plus the two headline ones — Reference audit & seed (classifies every ${secret:NAME} ref Ok/PresentUnverified/Missing/Tombstoned/Undecryptable/InvalidName, then seeds the gaps — deployment setup) and KEK doctor (per-row Ok/WrongKek/Corrupt verdicts + guided rewrap-all or lost-KEK re-set — lockout recovery), plus ciphertext-only bundle export/import. Degraded (no-KEK) sessions open metadata-only and upgrade in place via masked paste/key-file; no login (local trust); key material never a literal arg. 5 packages published at 0.3.0 to the Gitea feed (restore-verified); the four apps still pin the 0.2.3 libraries (the console ships in the CLI tool, not the app packages). Runbook ZB.MOM.WW.Secrets/docs/operations/interactive-console.md + quickstart ZB.MOM.WW.Secrets/docs/interactive-console-quickstart.md. |
Shared ZB.MOM.WW.Secrets lib (5 packages + CLI) |
components/secrets/ |
ZB.MOM.WW.Secrets/ |
| LocalDb (embedded cache + 2-node sync) | Adopted by ScadaBridge — Phases 1 AND 2 COMPLETE and MERGED to main (2026-07-20, PR #23, merge 28ca04d7, pushed to origin; both feat/localdb-phase* branches fully contained and kept only as history). Libs 0.1.2 on the Gitea feed (3 pkgs, restore-verified; 0.1.0/0.1.1 also present). 0.1.2 (2026-07-21) fixes snapshot resync for a WIPED peer: a converged pair prunes every oplog row on ack, and the handshake read an empty oplog as "no gap possible" — so the steady state of a healthy pair was the one state from which a rebuilt node (lost volume, re-imaged host) could never be back-filled; it came back empty and stayed empty until the next deploy. The gap is now measured against last_acked_seq when the oplog is empty. Found by the OtOpcUa Phase 1 live gate (check 4) as a documented limitation, then closed. 0.1.1 fixes a latent boot defect: the library never created the parent directory of LocalDb:Path and SqliteLocalDb opens the file eagerly, so a missing directory was a hard boot failure (SQLite Error 14) — routine given the relative ./data/… defaults, and masked on docker only by the volume mount. ScadaBridge had worked around it app-side; the fix moved into the library and the shim is deleted. Phase 1 (merged in PR #23): OperationTracking + site_events consolidated into one LocalDb:Path database, fail-closed bearer interceptor on the sync endpoint, ISyncStatus health bridge, one-time legacy migrator. Phase 2 (merged in the same PR): adds sf_messages + 7 site config tables (10 replicated in total) and deletes ScadaBridge's bespoke SiteReplicationActor, its messages, StoreAndForward's ReplicationService, and ReplaceAllAsync — including the whole notify-and-fetch exchange, so a standby now makes NO config fetch on deploy (SiteReconciliationActor's startup fetch survives; different path). notification_lists/smtp_configurations are created but deliberately NEVER registered — permanently empty, and a channel for them would carry plaintext SMTP passwords. Live gate PASS, all 10 checks (ScadaBridge/docs/plans/2026-07-19-localdb-phase2-live-gate.md): byte-identical config rows with identical HLC + origin node id, standby zero-fetch, 3-table cascade delete converging with tombstones on both nodes, site failover with zero duplicate ids, clean both-nodes-together restart. Replication remains default-OFF, enabled on the docker rig's site-a pair only (site-b/site-c unreplicated as the default-OFF pin); no production deployment has it enabled. Operational constraints: a site pair must be stopped/started TOGETHER (rolling upgrade unsupported since the SfBufferSnapshot compat handler was deleted), a node offline beyond TombstoneRetention (7 d) can resurrect deleted rows, and MaxBatchSize batches by ROW COUNT against a 4 MB gRPC cap (rig pins 16; the 500 default would allow ~35 MB). Also adopted by OtOpcUa — Phase 1 COMPLETE and MERGED to master 2026-07-21 (merge c957db52): a pair-local consolidated SQLite cache of the deployed-configuration artifact (chunked, SHA-256-verified, newest-2 retention) that lets a driver node boot from cache when central SQL Server is unreachable, registered in the hasDriver branch only (admin-only nodes never register it), with the same default-OFF fail-closed replication. Its live gate passed 8/8 and caught four defects offline tests missed. Consumers: ScadaBridge (Phases 1+2) and OtOpcUa (Phase 1) — verified 2026-07-21. |
Shared ZB.MOM.WW.LocalDb lib |
docs/plans/2026-07-17-localdb-design.md + docs/plans/2026-07-19-scadabridge-localdb-design.md (adoption design lives here); ScadaBridge holds the Phase 1 plan + Phase 2 gate under its own docs/plans/ |
ZB.MOM.WW.LocalDb/ |
The auth component is fully populated: a normalized spec, a
proposed shared-contract, three
current-state docs, and an adoption GAPS
backlog. Common ground = LDAP/GLAuth identity + peppered-HMAC API keys; left per-project =
the authz vocabularies (OPC-UA permissions / gRPC scopes / roles + site-scoping).
The shared library is built and lives in this repo at ZB.MOM.WW.Auth/
(its own nested git repo; .NET 10; 4 packages — Abstractions, Ldap, ApiKeys, AspNetCore;
215 tests passing + 1 skipped (verified 2026-07-18); feed carries 0.1.0–0.1.4). The implementation plan is at
docs/plans/2026-06-01-zb-mom-ww-auth-shared-library.md.
Adopted across all three apps on 2026-06-02 (auth GAPS #1–#8) on each repo's feat/adopt-zb-auth branch —
committed + reviewed, then fast-forward-merged into the repo's local default (main/master) and PUSHED to origin
(gitea) on 2026-06-03 (in sync; the feat/* branches kept locally as history). Cutover: shared Auth.Ldap,
Auth.ApiKeys (ScadaBridge inbound fully re-architected to the keyId/Bearer model), IGroupRoleMapper<TRole> seam,
Transport-enum config, canonical ZbClaimTypes/ZbCookieDefaults, unified dev base DN dc=zb,dc=local, and the
canonical-six role vocabulary (with ScadaBridge's accepted auditor/admin SoD collapse). Consumer pins: OtOpcUa 0.1.1,
MxGateway 0.1.4 (bumped 0.1.2→0.1.4 2026-07-09 for the G-2 API-key-expiry verifier enforcement — commit 197731a, now on origin/main; also required implementing the 0.1.3 IApiKeyAdminStore.SetScopes/SetEnabled members on a test fake), HistorianGateway 0.1.4, ScadaBridge 0.1.3. Per-repo detail in components/auth/GAPS.md +
docs/plans/2026-06-02-auth-audit-normalization*.md.
Build/test from ZB.MOM.WW.Auth/: dotnet test. Consumer matrix: OtOpcUa → Abstractions+Ldap+AspNetCore;
MxAccessGateway & ScadaBridge → all four (ApiKeys not used by OtOpcUa).
0.1.4 (2026-07-09, archreview G-2): optional ApiKeyRecord.ExpiresUtc (nullable = never-expires), ApiKeyFailure.KeyExpired, ApiKeyVerifier expiry enforcement (inclusive, injected clock), a CreateKeyAsync(expiresUtc) overload, and a SQLite schema v3 additive expires_utc column (idempotent ALTER; donor v2 DBs upgrade in place, no key invalidated). All four Auth pkgs published to the Gitea feed at 0.1.4 (2026-07-09). HistorianGateway consumes 0.1.4 — apikey create --expires + dashboard Expires column (Task-10 done). mxaccessgw may bump to 0.1.4 to gain the same verifier expiry enforcement (not yet done).
The UI-theme component is fully populated: a normalized spec,
a design-tokens reference, a
shared-contract, three
current-state docs, and an adoption GAPS
backlog. Shared = Technical-Light tokens + IBM Plex fonts + side-rail shell + widgets; left
per-project = each app's site.css page layout, route content, scoped .razor.css.
The shared RCL is built and lives in this repo at ZB.MOM.WW.Theme/
(.NET 10 Razor Class Library; single package; 48 bUnit tests (verified 2026-07-18); feed carries 0.2.0–0.3.1,
published to the Gitea feed). The build plan is at
docs/plans/2026-06-01-zb-mom-ww-theme-shared-library.md;
the adoption plan at docs/plans/2026-06-03-ui-theme-adoption.md.
Adopted across all three apps on 2026-06-03 (full canonical cutover, SPEC §7) on each repo's
feat/adopt-zb-theme branch — committed + spec/code-reviewed, then fast-forward-merged into each repo's local
default (master/main) and PUSHED to origin (gitea) (in sync; feat/* kept locally as history): OtOpcUa
→lmxopcua master@11de14d, ScadaBridge main@58352a6, MxGateway→mxaccessgw main@73e54e2. The 0.1.0 → 0.2.0 bump first promoted nav-expand persistence
into the kit (NavRailSection.Key/data-nav-key + a localStorage nav-state.js enhancer emitted by a new
<ThemeScripts/>), so all three apps share one persistence mechanism (OtOpcUa's bespoke cookie/JS-interop nav
island retired); MxGateway additionally gained a net-new Blazor <LoginCard> /login page over its existing
hardened endpoint. Per-app result in components/ui-theme/GAPS.md.
Build/test from ZB.MOM.WW.Theme/: dotnet test. Consumer matrix: all three apps consume
the single ZB.MOM.WW.Theme package (OtOpcUa AdminUI, MxGateway Server, ScadaBridge Host + CentralUI).
The health component is fully populated: a normalized spec, a
shared-contract, three
current-state docs, and an adoption GAPS
backlog. Shared = three-tier endpoint convention (ready/active/healthz) + canonical JSON writer +
IActiveNodeGate seam + GrpcDependencyHealthCheck + AkkaClusterHealthCheck + ActiveNodeHealthCheck
DatabaseHealthCheck<TContext>; left per-project = which probes each app registers, orchestrator wiring, and ScadaBridge's distributed health-monitoring pipeline.
The shared library is built and lives in this repo at ZB.MOM.WW.Health/
(.NET 10; 3 packages — ZB.MOM.WW.Health, ZB.MOM.WW.Health.Akka, ZB.MOM.WW.Health.EntityFrameworkCore;
64 tests (verified 2026-07-18); dotnet pack → 3 nupkgs @ 0.1.0).
Adopted by all four apps — verified 2026-07-18 (each references the package and wires MapZbHealth); the previous "not yet adopted" claim was stale. Per-app probe coverage vs the spec has NOT been re-verified — see components/health/GAPS.md.
Build/test from ZB.MOM.WW.Health/: dotnet test. Consumer matrix: MxAccessGateway → core only;
OtOpcUa & ScadaBridge → all three packages.
The observability component is fully populated: a normalized spec,
a metric-conventions reference, a
shared-contract, three
current-state docs, and an adoption GAPS
backlog. Shared = OTel Resource (service.name/site.id/node.role identity triple) + standard instrumentation
(ASP.NET Core, HttpClient, gRPC client, runtime, process) + Prometheus always-on exporter + OTLP opt-in
- Serilog two-stage bootstrap + SiteId/NodeRole/NodeHostname enrichers + TraceContextEnricher (trace_id/span_id)
- ILogRedactor seam; left per-project = application Meters/ActivitySources, sink config, per-operation enrichers, and redaction policies.
The shared library is built and lives in this repo at ZB.MOM.WW.Telemetry/
(.NET 10; 2 packages — ZB.MOM.WW.Telemetry, ZB.MOM.WW.Telemetry.Serilog; 34 tests (verified 2026-07-18);
dotnet pack → 2 nupkgs @ 0.1.0). Adopted across all three apps on 2026-06-01 (branch
feat/adopt-zb-telemetry per repo, behaviour-preserving): AddZbTelemetry (Resource + standard
instrumentation + Prometheus /metrics) everywhere; OtOpcUa + MxGateway on AddZbSerilog (MxGateway's
MEL→Serilog migration + metrics export both landed in this pass — they were not actually done
beforehand despite an earlier claim); ScadaBridge keeps its LoggerConfigurationFactory (min-level
governance) and only adds the shared TraceContextEnricher. Deferred: MxGateway ms→s + Meter
rename, ScadaBridge app instruments + Site-node HTTP/1.1 metrics listener, OTLP wiring. Per-repo
result tracked in components/observability/GAPS.md.
Build/test from ZB.MOM.WW.Telemetry/: dotnet test. Consumer matrix: all three apps consume both
packages after adoption (OtOpcUa, MxGateway Server, ScadaBridge Host + any instrumented project).
The configuration component is fully populated: a normalized spec, a
shared-contract, three
current-state docs, and an adoption GAPS
backlog. Shared = the IValidateOptions<T> failure-accumulation base (OptionsValidatorBase<T>) +
reusable rule primitives (ValidationBuilder: port / host:port / required / positive-duration / one-of /
min-count) + AddValidatedOptions<TOptions,TValidator>() (bind + validate + ValidateOnStart) + the
pre-host ConfigPreflight aggregator (generalizes ScadaBridge's StartupValidator, byte-compatible
message); left per-project = each app's options classes + domain rules, and OtOpcUa's
draft/generation-content validation (DB-side sp_ValidateDraft; its C# DraftValidator is dormant).
The shared library is built and lives in this repo at ZB.MOM.WW.Configuration/
(.NET 10; single package ZB.MOM.WW.Configuration; 42 tests (verified 2026-07-18); dotnet pack → 1 nupkg @ 0.1.0).
The implementation plan is at
docs/plans/2026-06-01-zb-mom-ww-configuration-shared-library.md.
Adopted across all three apps on 2026-06-01 (OtOpcUa, MxAccessGateway, ScadaBridge) on each repo's
local default branch (main/master) — merged, not yet pushed to remotes; the package was first
published to the Gitea feed. Behaviour-preserving onto OptionsValidatorBase/AddValidatedOptions
for MxGateway + ScadaBridge (validator messages byte-identical), StartupValidator → ConfigPreflight
for ScadaBridge, and net-new Ldap/OpcUa validators for OtOpcUa. Per-app result tracked in
components/configuration/GAPS.md.
Build/test from ZB.MOM.WW.Configuration/: dotnet test. Consumer matrix: all three apps consume the
single package; ScadaBridge is the heaviest adopter (per-module validators + StartupValidator →
ConfigPreflight); OtOpcUa adoption is additive (it has no IValidateOptions usage today).
The audit component is fully populated: a normalized spec, an
event-model reference, a
shared-contract, three
current-state docs, and an adoption GAPS
backlog. Common ground = canonical AuditEvent record + AuditOutcome enum + IAuditWriter /
IAuditRedactor seams + helpers (NullAuditRedactor, TruncatingAuditRedactor, NoOpAuditWriter,
CompositeAuditWriter, RedactingAuditWriter) + AddZbAudit DI registration; left per-project =
transport/storage and domain vocabulary. Closes the loop on Auth — audit's Actor field = the Auth
principal. IAuditRedactor is aligned with Telemetry's ILogRedactor seam convention.
The shared library is built and lives in this repo at ZB.MOM.WW.Audit/
(.NET 10; 1 package — ZB.MOM.WW.Audit; only non-BCL dependency Microsoft.Extensions.DependencyInjection.Abstractions;
23 tests (verified 2026-07-18); dotnet pack → 1 nupkg @ 0.1.0). Repo: https://gitea.dohertylan.com/dohertj2/zb-mom-ww-audit.
Adopted across all three apps on 2026-06-02 (audit GAPS #1–#6) on each repo's feat/adopt-zb-audit branch
(stacked on feat/adopt-zb-auth) — committed + reviewed, then merged into the repo's local default (main/master)
and PUSHED to origin (gitea) on 2026-06-03 (in sync). Depth =
DEEP adopt (the canonical 9-field AuditEvent is the record everywhere; domain fields ride in DetailsJson).
OtOpcUa: canonical record + AuditWriterActor : IAuditWriter + Outcome column/migration + ClusterAudit fix.
MxGateway: new canonical SQLite audit_event store + IAuditWriter + IApiKeyAuditStore→canonical adapter.
ScadaBridge: a full audit-subsystem re-architecture (the program's largest task) — canonical record everywhere via a
deterministic codec; site SQLite split into audit_event + an audit_forward_state forwarding sidecar; central
partitioned dbo.AuditLog collapsed to 10 canonical cols + persisted computed cols (CollapseAuditLogToCanonical
migration, MSSQL-verified). Phase 3 wires Actor from the Auth principal at authenticated emit sites (per-app
IAuditActorAccessor). Per-repo detail in components/audit/GAPS.md +
docs/plans/2026-06-02-auth-audit-normalization-phase2-deep.md + …-scadabridge-audit-rearch.md.
Build/test from ZB.MOM.WW.Audit/: dotnet test. Consumer matrix: all three apps consume the single
ZB.MOM.WW.Audit package (OtOpcUa, MxAccessGateway, ScadaBridge — DEEP-adopted as the canonical record).
The Galaxy Repository component normalizes the Galaxy object-hierarchy SQL browse + reusable gRPC service
that was previously embedded in mxaccessgw. Shared = canonical galaxy_repository.v1 proto (wire-compatible
with mxaccessgw's existing contract so OtOpcUa's GalaxyRepositoryClient is unaffected), the SQL browse
provider (HierarchySql / AttributesSql validated reverse-engineered queries), in-memory hierarchy cache +
snapshot + deploy-poll refresh BackgroundService, GalaxyHierarchyProjector, and AddZbGalaxyRepository /
MapZbGalaxyRepository DI extension. Left per-consumer = section path, subtree auth filtering, and any
app-specific paging defaults.
The shared library is built and lives in this repo at ZB.MOM.WW.GalaxyRepository/
(.NET 10; single package ZB.MOM.WW.GalaxyRepository; dotnet pack → 1 nupkg @ 0.1.0, published to
the Gitea NuGet feed gitea.dohertylan.com/api/packages/dohertj2/nuget). The design doc is at
docs/plans/2026-06-23-historian-gateway-design.md (§10, component 1).
Consumed by HistorianGateway as a PackageReference from that Gitea feed, pinned at 0.2.0 (originally a
cross-repo ProjectReference to this scadaproj tree; switched to the feed package 2026-06-24; bumped 0.1.0 → 0.2.0 2026-06-25).
mxaccessgw adoption is a tracked follow-on — once adopted, mxaccessgw's inline Galaxy browse code is replaced
by the shared lib (the galaxy_repository.v1 wire contract is already identical, so OtOpcUa and ScadaBridge
clients are unaffected). Build/test from ZB.MOM.WW.GalaxyRepository/: dotnet test.
Consumer matrix: HistorianGateway (initial); mxaccessgw (follow-on adoption).
Per-project primary commands
Run these from inside each project directory (not from scadaproj).
# OtOpcUa
dotnet build ZB.MOM.WW.OtOpcUa.slnx
dotnet test ZB.MOM.WW.OtOpcUa.slnx
dotnet test --filter "FullyQualifiedName~MyTestClass.MyMethod" # single test
# Docker fixtures run on shared host 10.100.0.35; control via `lmxopcua-fix` (in ~/bin)
# MxAccessGateway (PowerShell on Windows)
dotnet build src/MxGateway.sln
dotnet build src/MxGateway.Worker/MxGateway.Worker.csproj -p:Platform=x86 # worker MUST be x86
dotnet test src/MxGateway.Tests/MxGateway.Tests.csproj # no MXAccess needed (fake worker)
dotnet run --project src/MxGateway.Server/MxGateway.Server.csproj
# ScadaBridge (~/Desktop/ScadaBridge)
dotnet build ZB.MOM.WW.ScadaBridge.slnx
bash docker/deploy.sh # rebuild + redeploy the 8-node cluster
cd infra && docker compose up -d # local test services (SQL, OPC UA, SMTP, REST, Traefik) — LDAP is NOT here
# HistorianGateway (~/Desktop/HistorianGateway)
dotnet build ZB.MOM.WW.HistorianGateway.slnx
dotnet test ZB.MOM.WW.HistorianGateway.slnx # unit + golden; live integration tests skip without env vars
dotnet run --project src/ZB.MOM.WW.HistorianGateway.Server/ZB.MOM.WW.HistorianGateway.Server.csproj
# Dev: dashboard on :5220 (HTTP/1.1), gRPC h2c on :5221 (from appsettings.Development.json)
# Production: single Kestrel:Endpoints:Https with Protocols=Http1AndHttp2 (ALPN, one TLS port)
# Live integration (need HISTORIAN_GRPC_HOST + HISTORIAN_GRPC_WRITE_SANDBOX_TAG + GALAXY_SQL_CONNSTR set)
dotnet test ZB.MOM.WW.HistorianGateway.slnx --filter "Category=LiveIntegration"
# Local Docker (HistorianGateway/docker/): publish FDD on the host (authed Gitea feed → no Docker restore),
# runtime-only aspnet:10.0 image; login + gRPC API-key auth disabled; points at the REAL wonder datasources.
dotnet publish src/ZB.MOM.WW.HistorianGateway.Server/ZB.MOM.WW.HistorianGateway.Server.csproj -c Release -o docker/publish -p:UseAppHost=false
cd docker && docker compose up -d --build # dashboard/health/metrics :5220, gRPC h2c :5221 (needs host VPN for egress)
# ZB.MOM.WW.LocalDb (~/Desktop/scadaproj/ZB.MOM.WW.LocalDb — a hosted shared lib)
dotnet build ZB.MOM.WW.LocalDb.slnx
dotnet test ZB.MOM.WW.LocalDb.slnx # 148 tests, fully offline (no live deps)
dotnet pack ZB.MOM.WW.LocalDb.slnx -c Release -o artifacts # 3 nupkgs @ 0.1.2
Shared GLAuth (all three apps + HistorianGateway): LDAP auth for every local dev/test stack is provided by a single
zb-shared-glauthcontainer on the Linux fixture host10.100.0.35:3893(baseDN dc=zb,dc=local, Transport=None). Source of truth and deploy runbook:scadaproj/infra/glauth/(config.toml+docker-compose.yml+README.md).
Refreshing this index
This file is meant to be re-scanned when scadaproj is opened in Claude Code:
- List sibling SCADA/OT directories:
find ~/Desktop -maxdepth 2 -iname "claude.md". - For each project the user wants indexed, read the top of its
CLAUDE.md(project overview + build/run sections) and update its row above. - Keep the project set curated — only the SCADA/OT/Wonderware/OPC-UA family belongs here.
- Flag new duplicates/overlaps and namespace mismatches rather than silently merging them.
Other workspace projects with a CLAUDE.md (not indexed — promote on request)
Listed so they can be pulled into the index above if you decide they belong.
SCADA/OT, de-indexed (still have a CLAUDE.md under ~/Desktop/):
- OpcUaTestServer — dual-instance OPC UA test server (.NET 10) for testing OPC UA clients / simulating automation.
- scada (ScadaLink) — design docs + scaffolding for the distributed SCADA platform (
ZB.MOM.ScadaLink.*). - scadalink-design-opcua-browser — ScadaBridge variant focused on OPC UA browser / Data Connection Layer work.
- DARS — DARS → Wonderware SCADA migration (design phase, Q2 2026 go-live).
- DARS_BU — backup snapshot of DARS.
- plan — 3-year SCADA IT/OT modernization roadmap (markdown).
Outside the SCADA family:
- delmia — DELMIA / Apriso (Intercim Velocity) MES customization export (ASP.NET
.asmx); MES-adjacent. - lightctrl — Raspberry Pi Python I/O control (edge hardware).
- codestats — Rust CLI for code statistics on .NET solutions/dirs (dev tooling for the .NET repos).
- servecli — Rust portable SSH/SFTP server for Windows remote task management (greenfield).
- JdeScopingTool — JD Edwards "LotFinder" .NET 4.8 → .NET 10 migration (ERP, not SCADA).
- chat — local-first roleplay chat engine.
- candy2 — candy.ai chat scraping / browser automation.
- menardslist — Menards.com cart → printable picklist PDF.