Files
scadaproj/CLAUDE.md
T
Joseph Doherty 646af887d1 docs: winrm re-enabled for deployment activities only (2026-09-28)
Replaces the 2026-09-11 remote-exec hold: WinRM exec is allowed only for the on-box steps of a
user-requested deploy; ad-hoc diagnostics/version checks use SMB, HTTP or SQL. SMB file copies and
direct SQL logins remain always allowed. Scope rules live in the winrm skill's Scope section.
2026-09-28 09:25:12 -04:00

37 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 address space (v3.0, master since 2026-07-16) — 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); every value has one source fanned to both NodeIds. This is the wire contract ScadaBridge binds against (ScadaBridge consumes v3 end-to-end; its #14 is closed). Drivers: Modbus, S7, AB CIP/Legacy, TwinCAT, FOCAS, OpcUaClient, Sql (SQL Server / PostgreSQL / Oracle, read-only poll), Mqtt (Plain + Sparkplug B, writable where a tag declares a destination), MTConnect (read-only), Calculation, and AVEVA Galaxy as a standard Equipment-kind driver via in-process GalaxyDriver → gRPC → mxaccessgw. Sole historian backend is HistorianGateway. Address-space availability guarantee: an unreadable deployment artifact is no answer, never an empty configuration — nodes hold last-known-good, and a driver-only node boots and serves from its LocalDb cache with central SQL down (ServiceLevel stays 240/250). Redundancy: one independent 2-node Akka mesh per application Cluster; Primary = oldest Up driver member (ServiceLevel 250) + one Secondary (240) per Cluster; cross-mesh commands ride gRPC with per-cluster PSK auth (Akka ClusterClient fully deleted 2026-08-06). Data plane is machine-to-machine: OPC UA UserName apikey + otua_<keyId>_<secret> minted on AdminUI /api-keys; OpcUa:AllowAnonymous defaults true (anonymous is a normal grantable principal); LDAP is AdminUI-login only. Node ACLs are enforced only behind Security:Acl:Enabled (default false); with it on, scoped NodeAcl grants decide every data-plane operation incl. browse/read/subscribe, fail closed, and reach open sessions. Every driver's authoring path now consults the driver's own runtime parse, so what authoring accepts is what the driver serves (tracker zeroed 2026-08-10, origin d06e92c6). Full change history, live-gate records and the per-issue consumer-impact record for ScadaBridge: .claude/skills/sister-project-history/otopcua.md (skill sister-project-history).
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 Current main: fb54ed42, pushed 2026-09-14 (register row 126 = TODO: roll migrations 56/57 to vd03/vq03/vq04 with the 55→57 delta script, blocked by the remote-access hold) — adds site artifact staleness (Site.ArtifactRevisionHash stamped on every successful artifact deploy, migration AddSiteArtifactRevision; Sites page Artifacts current / out of date / never deployed badge), a rail-footer Pending updates indicator over a process-wide PendingUpdatesCache (stale instances + stale-artifact sites, Deployer/Admin), and a scrollable rail nav; design docs/plans/2026-09-14-pending-updates-design.md. Previous tip 9d529b82 (2026-09-11) — adds the Gitea #36 reset-aware handshake (WriteBatchAndWaitAsync reset-echo overload), the computed-alarm IsMesReportable flag (template checkbox + nullable instance override, Alarms.MesReportableAsync(), migration AddAlarmMesReportable; native alarms stay on the 900–999 band) and a nav fix nesting Flow Debugger in the Deployment section. DEPLOYED: still 2b9a7c3c on DEV vd03 and the QA pair vq03/vq04 (2026-09-10, migration 53→55, protocol config row encrypted → rollback is the DB restore; records docs/deployment/deployment-records/2026-09-10-wonder-app-{vd03,vq03-vq04}-2b9a7c3c.md) — the 2026-09-11 tip is NOT deployed, blocked by the wonder-box remote-access hold. Management/API/CLI parity complete (docs/plans/2026-09-10-management-api-cli-parity-review.md): all 149 eligible contracts exposed in 20 CLI groups, 19 missing operations added, deadline/registry/outcome gaps fixed; CLI documentation checked against all 159 leaf commands; 9554 tests verified and primary 8-node rig rebuilt with read-only smoke checks. Previously completed 2026-09-09 whole-system arch review remediation (docs/plans/2026-09-09-arch-review.md: 92 findings; all 13 High + 33 Medium fixed; all 46 Low findings also committed and pushed (§11); 9478 solution tests and deployed browser smoke pass, primary 8-node rig rebuilt). Adds the ZB.MOM.WW.ScadaBridge.CentralUI.SandboxWorker child-process project and a coordinated central-pair migration that encrypts DataConnection protocol config at rest (old binaries cannot read protected rows). 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. Inbound API keys live on the shared SQL Server store on a central pair (design 2026-09-02, ScadaBridge register row 108): ScadaBridge is Auth's first consumer of the new ZB.MOM.WW.Auth.ApiKeys.SqlServer package at 0.3.0, pointing both central nodes at one zbauth schema in the configuration database instead of each node's own per-node SQLite file. Flow Debugger (including the instance picker and v1.4 step logs, 2026-09-09): Central UI + CLI trace of one inbound-API method or value-change flow across Central / Site / Tags / External swimlanes, Live or Replay, built on ScriptTrace audit rows and the outbound-call audit rows; arch-reviewed and remediated the same day (F1–F13 — redacted + byte-capped inline trace copy, route-time site scope on Live runs, Replay open to OperationalAudit roles under RequireFlowDebugger; docs/plans/2026-09-06-flow-debugger-arch-review.md + -remediation.md); script entry (site → instance → script, 2026-09-08).
HistorianGateway ~/Desktop/HistorianGateway .NET 10 x64, gRPC, Blazor gitea.dohertylan.com/dohertj2/historiangw Single-process gRPC sidecar exposing (1) a 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. No COM, no x86 worker. Owns AVEVA.Historian.Client at histsdk/ (ordinary owned source, edited in place — the re-vendor model is retired). Dev: dashboard :5220 (HTTP/1.1) + gRPC h2c :5221; production: one Kestrel:Endpoints:Https with Protocols: Http1AndHttp2 (ALPN). Default-on leased session pools amortize the historian handshake (v6 pool + a separate v8 event-session pool). Client lib ZB.MOM.WW.HistorianGateway.Client + Contracts are on the Gitea feed at 0.3.0; OtOpcUa is the first consumer (its sole historian backend since PR #423); ScadaBridge has no coupling. Known 2023 R2 server-side gates: native event reads return 0 rows (C2, won't-fix — SQL-path ReadEvents via Runtime.dbo.Events is the shipped workaround), Int1/UInt1 analog-tag creation is degenerate (OtOpcUa maps Boolean → Int2), string historical writes non-convergent (follow-ups #18–#25). Only remaining archreview item: the secure-by-default TLS flip, deferred to 1.0.0. A Java client, com.zb.mom.ww.historiangw:zb-mom-ww-historiangw-client:0.1.0 (Gitea Maven feed, clients/java/), is consumed by the ignitionoee repo's modules/historian Ignition module. Full change history (ArchReview cycles 1–2, pending.md items, PR list): .claude/skills/sister-project-history/historiangateway.md.
BTDB (Batch Tracker) ~/Desktop/btdb — local master @ 695192d (2026-09-11), ~198 commits ahead of origin/master (d147c10, 2023) and unpushed; no branch in flight (feat/transfer-machine-batch and fix/transfer-lot-walk — DB-6 reopened: forward loop guard, whole-lot chain walks incl. merges — both merged 2026-09-11; feat/manager-transfers — the Manager edits, shows and audits transfers — merged 695192d the same day). Claude artifacts are gitignored + untracked there (CLAUDE.md, .claude/, docs/plans/, docs/reviews/, *.tasks.json) — local only, never to the corporate remote. SQL Server SSDT project + SQL CLR (net472; Windows-only build, the docker rig rebuilds it cross-platform from the same sources); BTDB Manager = Blazor Web App net10.0 on the family libs (Auth 0.3.0, Theme 0.4.1, Health 0.3.0, Telemetry + Configuration 0.1.0, Secrets 0.6.2), Dapper + embedded queries, self-contained win-x64 NSSM service ZB.MOM.WW.BTDB.Manager on :9877 (→ :9876 after cut-over) ZimmerBiomet/BTDB on the corporate Gitea wonder-app-vd03:3000 (not on gitea.dohertylan.com) The BT SQL Server database that is the plant's batch / cycle / work-order system of record for data collection. Galaxy machine objects write through StartMachineBatch / StartMachineCycle / End* / AddMachineBatchWorkOrder / AddOtherData* / AddMachineBatchTransfer / TransferMachineBatch (new 2026-09-11: end source if open + start destination by Code + copy not-moved-out work orders + link, one owned transaction) (own-or-join transaction template, THROW 50000+, all GETUTCDATE()); reports pull per-batch time-series out of the AVEVA Historian through CLR sprocs (OPENQUERY(INSQL, …) + Runtime.dbo.*), so BT must be co-tenant with the Historian's Runtime DB (wonder-sql-*). MESAPI and ScadaBridge's inbound API resolve SAPID → Machine.Code; no checked-in ScadaBridge script calls a BT sproc. State 2026-09-11 (all merged on local master): the Blazor conversion phases 1–3 (legacy net6.0 MVC app, Telerik reports and the mis-named install.bat deleted), the 2026-09-04 full review remediation (12 work packages, 60 findings fixed, 0 open; static invariant tests over the SQL + CLR sources, opt-in rig integration tests behind BTDB_TEST_CONNECTION, Gitea Actions gate ./build.sh), and the DARS sprint-1 schema (MachineBatchTransfer, ContainerID/MoveOutTimestamp on MachineBatchWO, nestable MachineGroup.ParentMachineGroupID, Gen1 reactor load script) — the old DARS/WW_ARCH/BatchTracker_DB/ fork and the feat/manager-blazor worktree are superseded. Phase-4 cut-over on the DEV box (docs/operations/cut-over.md) is the open step and is blocked by the 2026-09-11 remote-access hold. Repo orientation lives in btdb's own (gitignored) CLAUDE.md. Full schema/contract notes, family edges and the gap analysis: .claude/skills/sister-project-history/btdb.md. Local Docker rig: btdb/docker/ — runtime-only image on :9877 joined to scadabridge-net + the redundant glauth pair, registered in Overview as BTDB Manager; docker/deploy-db.sh publishes BT (dacpac + CLR via two side projects under docker/db/, plus a Runtime stand-in) into scadabridge-mssql; smoke-queries.sh, validate-config-import.sh, db/validate-clr.sql and Database/Scripts/Validation/*.sql are the rig gates.

Cross-project relationships

The indexed sister 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.

BTDB edges (batch tracker)

BTDB is a database, not a service, so it couples by SQL, not gRPC/OPC UA:

  • Galaxy → BT (write): per-machine Galaxy scripts EXEC the lifecycle sprocs; the sproc signatures and Machine.Code/SAPID/ZTag semantics are the contract.
  • ScadaBridge → BT (read, verified): inbound-API method scripts use the Database helper on the named db-connection BTDB (prod id 1) for SELECT Code FROM dbo.Machine WHERE SAPID=@s before Route.To(instance). Site scripts can also write via Database.Connection/CachedWrite, but that is customer script config, not platform code. ScadaBridge's inbound API is the designated successor of MESAPI's move-in/move-out surface.
  • BT → Historian (read): CLR sprocs resolve hierarchical tag names via Runtime.dbo.TagExtendedPropertyInfo and query WideHistory through the INSQL linked server; alarm wrappers read Runtime.dbo.Events (the same view HistorianGateway's SQL-path ReadEvents uses). No coupling to OtOpcUa or mxaccessgw.

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.
  • BTDB's break surface is its SQL contract: the lifecycle sproc signatures, the Machine key columns (Code/SAPID/ZTag), the OtherData JSON key conventions (DARS sprint 1 adds ~14), and the INSQL/Runtime co-location assumption. Galaxy scripts, MESAPI and ScadaBridge inbound methods all bind to them.

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. Feed carries 0.3.0 (2026-09-02; 5 packages, newest ZB.MOM.WW.Auth.ApiKeys.SqlServer). All four apps pin 0.3.0 (2026-09-03, all on their default branch and pushed — and all four are DEPLOYED at those tips on DEV vd03 and QA vq03/vq04 since 2026-09-10: OtOpcUa b8b4c18f, mxgw 4f931e4, HistorianGateway d8d890c; ScadaBridge already had it on main via the inbound-api-key-shared-store work, e45c4af9). 0.2.1 is a functional fix, not housekeeping — 0.1.5 cannot log in against an AD domain root at all. FallbackServers (0.2.0) live-gated on the redundant local GLAuth pair. Version narrative, consumer matrix, tools/LdapProbe: ZB.MOM.WW.Auth/CLAUDE.md. Shared ZB.MOM.WW.Auth lib components/auth/ ZB.MOM.WW.Auth/
UI Theme (layout / tokens / components) Adopted. Feed 0.2.0–0.4.1; five consumers (the ignitionoee router joined), all on 0.4.1 and pushed (2026-08-11); Every deployed wonder box (DEV vd03 + QA vq03/vq04) runs its repo tip as of 2026-09-10, so the deployed Theme is 0.4.1 everywhere. On bump, TRIM the local .btn block, don't delete it — mxgw's carried more than sizing. Narrative: ZB.MOM.WW.Theme/CLAUDE.md. Shared ZB.MOM.WW.Theme RCL components/ui-theme/ ZB.MOM.WW.Theme/
Health (readiness / liveness / active-node) Adopted — lib 0.3.0, all 4 apps reference it + wire MapZbHealth. 0.2.1/0.3.0 changed the active tier's behaviour (standby now answers 503; active = oldest Up member via ClusterActiveNode, not RoleLeader — the two diverge after any restart), and both Akka apps' private copies were deleted in its favour (merged + pushed). Narrative: ZB.MOM.WW.Health/CLAUDE.md. 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; family version 0.6.2, published. Pins: all four apps 0.6.2 (verified against each repo's checked-in pins AND its origin default branch 2026-09-03 — the earlier "OtOpcUa unpushed / mxgw 0.6.0 / ScadaBridge 0.5.1" notes were stale). Target 0.6.1+, never 0.6.0, and use the 4-arg AddZbSecrets(..., contentRootPath) overload at every pre-host ${secret:} expander call site — the pin alone does not close the hole. Secrets:SqlitePath must be absolute and outside the content root (fail-closed, no opt-out; the 2026-08-09 mxgw outage). Production topologies decided (scadaproj#3/#4 closed): OtOpcUa = Akka pair-local, ScadaBridge = gRPC hub over ONE shared central SQL store (0.5.1 minimum for central pairs); both default-off. Full narrative (0.1.x→0.6.2, incidents, live gates, container caveat, NU1102 note): ZB.MOM.WW.Secrets/CLAUDE.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+2 merged; pins 0.3.4) and OtOpcUa (Phases 1+2 merged; pins 0.3.4 since 2026-09-03, b8b4c18f) — no version skew remains (every release is wire-compatible, so one would be supported). Replication is default-OFF; enabled on the docker rigs' site-a pairs and on the QA pair vq03/vq04 (ScadaBridge site pair + OtOpcUa pair — OtOpcUa's sync only actually CONNECTED after the 2026-09-10 b8b4c18f upgrade; the old build answered the sync POST with the login 302); DEV vd03 is single-node. Operational constraints: a pair stops/starts together (no rolling upgrade), a node offline > TombstoneRetention (7 d) can resurrect deleted rows. Full release narrative (0.1.1→0.3.4) + gates: ZB.MOM.WW.LocalDb/CLAUDE.md. Shared ZB.MOM.WW.LocalDb lib docs/plans/2026-07-17-localdb-design.md + the two adoption designs (ScadaBridge, OtOpcUa) ZB.MOM.WW.LocalDb/

Each hosted library directory carries its own CLAUDE.md with the full version narrative, consumer matrix and adoption record — it loads automatically when you work under that directory. Health, Telemetry and Configuration had one already; Auth, Theme, Audit, Secrets, GalaxyRepository, LocalDb and Overview got theirs on 2026-09-03 when this history was split out of the index. Per-component specs, current-state docs and GAPS backlogs live under components/<name>/.

Hosted application: ZB.MOM.WW.Overview (family dashboard)

Besides the shared libraries, this repo hosts one application: ZB.MOM.WW.Overview/ — a Blazor Server single-pane dashboard that polls every registered instance's anonymous /health/ready + /health/active and renders per-cluster state including the Akka leader chip. No login, strictly read-only, holds no credentials — a requirement, not an omission. Deployed on wonder-app-vd03:5320 (DEV) and on both QA pair nodes wonder-app-vq03/vq04:5320. Design docs, the two deploy constraints (self-contained publish; registry BaseUrl host rule) and the QA topology note: ZB.MOM.WW.Overview/CLAUDE.md.

⚠️ wonder-app-vd03 is the DEV box, not production (vd = DEV, vq = QA — confirmed by mes-delmia-integration-api.md §1.1). Its ASPNETCORE_ENVIRONMENT genuinely is Production, but that is a .NET config-overlay name and is orthogonal to the deployment tier. No production box has been identified. Full host/port inventory for both tiers: deployment.md §2.1 + §7.1.

⛔ Remote command execution on the wonder boxes: DEPLOYMENT ACTIVITIES ONLY (2026-09-28). The winrm skill was disabled 2026-09-11 (WinRM exec/ps set off the security software) and re-enabled 2026-09-28 scoped to deploys: remote exec only for the on-box steps of a deploy the user asked for (preflight, backup, stop/swap/start, migration, post-deploy verify, rollback). No ad-hoc diagnostics, log grepping or version checks over WinRM — use SMB, HTTP or SQL. Always allowed: UNC/SMB file copies and logging directly into the SQL Servers. Scope rules live in the skill's Scope section.

Deployed state (2026-09-10): DEV and both QA nodes run every repo tip — ScadaBridge 2b9a7c3c, OtOpcUa b8b4c18f, mxaccessgw 4f931e4 (Server + x86 Worker), HistorianGateway d8d890c, Overview 846b77f/818775b3 (code-identical). All driven from the Mac by the winrm skill (WinRM 5985 + SMB 445, nothing installed on the boxes); psbridge and servecli are retired. Per-box records: deployment.md §9.

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)

# BTDB  (~/Desktop/btdb) — Windows only: SSDT + SQL CLR + Telerik + System.DirectoryServices; nothing builds on macOS
dotnet build Manager\BTDBManager.csproj                        # net6.0 MVC; needs Telerik Reporting R2 2022 at the HintPath
msbuild Database\Database.sqlproj /p:Configuration=Release    # SSDT → bin\Release\Database.dacpac; publish with SqlPackage
# Target instance must have CLR enabled, the `INSQL` linked server, and the Historian `Runtime` DB reachable by 3-part name

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.
  5. Keep table rows short. Detailed per-project history lives in .claude/skills/sister-project-history/<project>.md (sister repos) and in <lib>/CLAUDE.md (hosted libraries + Overview) — append there and keep the row here to a few lines. Sister repos whose own CLAUDE.md says "update its entry in scadaproj/CLAUDE.md" should land detail in those files.

Index edits go on main, and get pushed

Every sister project's CLAUDE.md tells it to update its entry here whenever a fact this index records changes. That edit lands in this repo, which has its own checkout — so a naive git commit writes it onto whatever branch scadaproj happens to be on, usually an unrelated feature branch, sometimes one with no upstream. The index then disagrees with reality for as long as that branch stays unmerged, which is the exact drift the rule exists to prevent, and it is invisible unless someone goes looking.

So: commit index edits on main and push them. Stash or set aside unrelated work first — never commit an index edit onto a dirty branch — and restore the original branch after:

git rev-parse --abbrev-ref HEAD    # note it
git checkout main && git pull
#   …edit the entry…
git commit -am "docs(index): <what changed about <project>>" && git push origin main
git checkout -                     # restore

Observed 2026-07-27: the OtOpcUa Sql poll driver's entry had sat unpushed on feat/overview-dashboard since 2026-07-24 for exactly this reason, and the Mqtt driver's entry was about to join it. Both were recovered by cherry-picking onto main (6ab50fe, 591eaac, 7dcd93c) — cherry-pick stranded index commits rather than merging the branch they are stuck on, since that branch usually carries unrelated in-progress work.

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). Its WW_ARCH/BatchTracker_DB/ fork of BTDB's Database project was migrated onto btdb branch feat/machine-batch-transfer on 2026-09-03 (see the BTDB row).
  • 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.