Files
scadaproj/CLAUDE.md
T
Joseph Doherty a1df7ef26c fix(secrets): root-cause + fix the Akka replicator's hosted-process DI deadlock (0.2.2)
Closes the defect in scadaproj#1. The hang was never Akka: the package's DI
wiring closed a circular singleton dependency the container cannot see through
factory lambdas — ISecretStore (ReplicatingSecretStore decorator) ->
ISecretReplicator -> SecretReplicationActorProvider -> ISecretCacheInvalidator
-> DefaultSecretResolver -> ISecretStore. Resolution recurses around the loop
until MS.DI's StackGuard hops it onto a fresh thread-pool thread, which then
blocks forever on a singleton call-site lock the first thread still holds:
a silent permanent hang instead of a stack overflow. Managed stacks from
dotnet-dump show the repeating cycle and both parked threads; both candidate
causes in the issue (DistributedPubSub.Get vs the Lazy lock, missing
Akka.Cluster.Tools HOCON) are disproven — the actor constructor was never
reached, and the deadlock reproduces on a single non-clustered node.

Fix: defer the one cycle-closing edge. The provider now gets a
DeferredSecretCacheInvalidator that resolves the real invalidator on first
eviction — which only happens when a replicated row is applied, strictly after
graph resolution. Severing the edge instead is wrong: a null-invalidator
experiment ran the live gate at 5/6, with deleted secrets still resolving on
the peer. The SqlServer package never had the cycle (its replicator chain
never touches the invalidator), which is why the hub gate always passed.

Verified: live 2-node convergence gate now 6/6 (was: infinite hang), including
the delete-visibility check that proves the deferred invalidator really evicts.
New HostedProcessResolutionTests builds the graph as a host does (container-
registered ActorSystem, hosted services, watchdogged resolves) and fails on
0.2.1; DeferredSecretCacheInvalidatorTests pins the wrapper contract. Full
suite 180 passed / 0 failed / 15 skipped (env-gated live SQL).

Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
2026-07-18 14:39:40 -04:00

53 KiB
Raw Blame History

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 twohttps://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 14: 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 retiredhistsdk/** 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 (~1016×); 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 #6WriteLiveValues 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 @ 56a70dfAHC-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.ReadRawno 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 AH — store-forward drain honesty (IsRetryableDeliveryFault, typed HistorianOutboxFullExceptionResourceExhausted), 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 1621) 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: the GalaxyRepositoryClient for the static hierarchy, and an MXAccess session (MxCommand/MxEvent protos) for live read/write/subscribe. A DeployWatcher polls 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 mxaccessgw directly for native MxAccess data and alarms — so ScadaBridge can reach Wonderware data directly, bypassing OtOpcUa. Both adapters implement the same IAlarmSubscribableConnection seam, and a read-only NativeAlarmActor unifies OPC-UA-A&C and MxAccess alarms onto one condition model.

Net effect

  • mxaccessgw is 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 at histsdk/) and the Galaxy Repository SQL DB directly, not through mxaccessgw. It consumes the shared ZB.MOM.WW.GalaxyRepository lib (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 published ZB.MOM.WW.HistorianGateway.Client), retiring its bespoke Wonderware historian driver — merged to OtOpcUa master (PR #423). The gateway-side follow-ups also merged to historiangw main: the Plan-1 client lib + packable Contracts (PR #5) and the WriteLiveValues UTC→server-local timestamp fix + a documented SendEvent Source_Object protocol limitation (PR #6, pending.md C4). OtOpcUa pre-existing test failure tracked as lmxopcua issue #424.
  • Coupling is loose: each repo references the others only as sibling context (the ## Sister Projects note in ScadaBridge's own CLAUDE.md lists MxAccessGateway and OtOpcUa with 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 .proto files (mxaccess_gateway.proto, mxaccess_worker.proto, galaxy_repository.proto), the historian_gateway.v1 proto (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.00.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.20.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_sqlite32.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 → ISecretCacheInvalidatorDefaultSecretResolverISecretStore), 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. Not yet published to the feed Shared ZB.MOM.WW.Secrets lib (5 packages + CLI) components/secrets/ ZB.MOM.WW.Secrets/
LocalDb (embedded cache + 2-node sync) Built (3 pkgs 0.1.0, published to the Gitea feed 2026-07-18, restore-verified from a scratch consumer; no app adoption yet) Shared ZB.MOM.WW.LocalDb lib docs/plans/2026-07-17-localdb-design.md 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.00.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.20.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.4apikey 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.00.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 mss + 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), StartupValidatorConfigPreflight 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 + StartupValidatorConfigPreflight); 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.00.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   # 144 tests, fully offline (no live deps)
dotnet pack  ZB.MOM.WW.LocalDb.slnx -c Release -o artifacts   # 3 nupkgs @ 0.1.0

Shared GLAuth (all three apps + HistorianGateway): LDAP auth for every local dev/test stack is provided by a single zb-shared-glauth container on the Linux fixture host 10.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:

  1. List sibling SCADA/OT directories: find ~/Desktop -maxdepth 2 -iname "claude.md".
  2. For each project the user wants indexed, read the top of its CLAUDE.md (project overview + build/run sections) and update its row above.
  3. Keep the project set curated — only the SCADA/OT/Wonderware/OPC-UA family belongs here.
  4. 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.