45 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 six pieces of source itself —
the shared ZB.MOM.WW.Auth/ library, the shared
ZB.MOM.WW.Theme/ UI kit, the shared
ZB.MOM.WW.Health/ health-check library, the shared
ZB.MOM.WW.Telemetry/ observability library, the shared
ZB.MOM.WW.Configuration/ config-validation library, and the new
ZB.MOM.WW.GalaxyRepository/ Galaxy browse library — all the realized output of their
respective component normalizations (see Component normalization).
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 unified Equipment-based address space — 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). 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 (lib 0.2.0; all 3 apps, merged to local default + pushed to origin (gitea)) |
Shared ZB.MOM.WW.Theme RCL |
components/ui-theme/ |
ZB.MOM.WW.Theme/ |
| Health (readiness / liveness / active-node) | Built (lib 0.1.0) |
Shared ZB.MOM.WW.Health lib |
components/health/ |
ZB.MOM.WW.Health/ |
| Observability (metrics / traces / logs) | Built (lib 0.1.0) |
Shared ZB.MOM.WW.Telemetry lib + .Serilog |
components/observability/ |
ZB.MOM.WW.Telemetry/ |
| Config + validation (options / startup validation) | Adopted (lib 0.1.0; all 3 apps, local) |
Shared ZB.MOM.WW.Configuration lib |
components/configuration/ |
ZB.MOM.WW.Configuration/ |
| Audit (event model + writer seam) | Adopted (lib 0.1.0; all 3 apps, merged to local default main/master + pushed to origin (gitea)) |
Shared ZB.MOM.WW.Audit lib |
components/audit/ |
ZB.MOM.WW.Audit/ |
| Galaxy Repository (object-hierarchy SQL browse + gRPC service) | Built (lib 0.1.0, published to the Gitea feed; consumed by HistorianGateway as a feed PackageReference) |
Shared ZB.MOM.WW.GalaxyRepository lib |
(design in histsdk + design doc 2026-06-23) | ZB.MOM.WW.GalaxyRepository/ |
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;
172 tests; dotnet pack → 4 nupkgs @ 0.1.0). 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; 44 bUnit tests; dotnet pack → 1 nupkg @ 0.2.0,
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;
58 tests; dotnet pack → 3 nupkgs @ 0.1.0).
Not yet adopted by the three apps — that's the follow-on tracked in 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; 19 tests;
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; 27 tests; 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;
19 tests; 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)
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.