313 lines
40 KiB
Markdown
313 lines
40 KiB
Markdown
# 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/`](ZB.MOM.WW.Auth/) (login/identity),
|
||
[`ZB.MOM.WW.Theme/`](ZB.MOM.WW.Theme/) (UI kit),
|
||
[`ZB.MOM.WW.Health/`](ZB.MOM.WW.Health/) (health checks),
|
||
[`ZB.MOM.WW.Telemetry/`](ZB.MOM.WW.Telemetry/) (observability),
|
||
[`ZB.MOM.WW.Configuration/`](ZB.MOM.WW.Configuration/) (config validation),
|
||
[`ZB.MOM.WW.Audit/`](ZB.MOM.WW.Audit/) (audit event model),
|
||
[`ZB.MOM.WW.Secrets/`](ZB.MOM.WW.Secrets/) (encrypted secret store),
|
||
[`ZB.MOM.WW.GalaxyRepository/`](ZB.MOM.WW.GalaxyRepository/) (Galaxy browse), and
|
||
[`ZB.MOM.WW.LocalDb/`](ZB.MOM.WW.LocalDb/) (embedded cache + 2-node sync) — all the realized output of their
|
||
respective component normalizations (see [Component normalization](#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](#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`: `3a217b94` (2026-09-29: stale per-node secret-mirror status rows are now pruned; 2026-09-28: site-secrets follow-ups — a central secret-store outage fails only the sites that reference a secret, removal-only deploys are audited, each site node's mirror failures (standby included) ride the health report via a 12th replicated table `secret_mirror_status`, artifact-deploy CLI commands exit 1 on a failed site; rig gate 9/9). Previous tip `23a139a4` (2026-09-28: site secrets ride the artifact deploy and the gRPC secrets hub is retired — see below). Previous tip `5cc09b08` (2026-09-28: site scripts gain a read-only, allow-listed `Files` global — `ScadaBridge:SiteRuntime:ScriptFileAccess:AllowedRoots`, off by default — used by the in-memory MES/Delmia/alarm-API test machines SBTEST01/02 (`docker/seed-mes-test-machine.sh`) to parse downloaded recipe files; DEV vd03 and QA vq03/vq04 on `b3021e7c` since 2026-09-28, migrations 57; register row 126 closed) — 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: `b3021e7c` on DEV vd03 and the QA pair vq03/vq04 (2026-09-28, migrations 55→57; records `docs/deployment/deployment-records/2026-09-28-wonder-app-{vd03-fe89363b,vq03-vq04-b3021e7c}.md`).** **Merged `23a139a4` (2026-09-28): site secrets now ride the artifact deploy and the pull-only gRPC secrets hub is retired in ScadaBridge** (design `docs/plans/2026-09-28-site-secrets-artifact-deploy-design.md`; central shares one secret store via `Secrets:Store:Provider=SqlServer`; rig live gate PASS 8/8, vd03/QA NOT deployed — rollout tracked as ScadaBridge register row 127 — see `ZB.MOM.WW.Secrets/CLAUDE.md` for the unpublished `0.6.3` hub-logging work this superseded for ScadaBridge). 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/`](components/) normalizes them: per component, the **one target** spec,
|
||
each project's **code-verified current state**, and the **gaps** between. See
|
||
[`components/README.md`](components/README.md) for the convention and workflow.
|
||
|
||
| Component | Status | Goal | Design | Implementation |
|
||
|---|---|---|---|---|
|
||
| Auth (login / identity / authz) | Adopted. Feed carries **`0.4.0`** (2026-10-01; opt-in user bind) — the four apps still pin `0.3.0` (released 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.4.0 (2026-10-01) — opt-in LDAP user bind:** adds `LdapOptions.BindMode` (`ServiceAccount` default | `User`), `UserBindTemplate` and `AcceptDomainQualifiedNames`; in `User` mode `LdapAuthService` binds as the signing-in user and reads their own groups on the same connection, so no service account is needed. Bind failures map to `BadCredentials` (bad password, unusable name, or bind result 49) or `GroupLookupFailed` (zero/multiple entries or no groups after a good bind), other bind/connect/search errors fail over to the next endpoint, and `ServiceAccount` stays the default so no existing consumer needs to change. **0.4.1** fixes the review findings: `Username` is canonicalised from the entry's `UserNameAttribute`, empty passwords are refused in both bind modes (a `ServiceAccount`-mode behaviour change), and the docs prefer the down-level `EXAMPLE\{0}` template over UPN. First consumer: BTDB Manager; ScadaBridge/HistorianGateway/OtOpcUa/MxAccessGateway stay on `0.3.0` until they move to AD. `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`](ZB.MOM.WW.Auth/CLAUDE.md). | Shared `ZB.MOM.WW.Auth` lib | [`components/auth/`](components/auth/) | [`ZB.MOM.WW.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`](ZB.MOM.WW.Theme/CLAUDE.md). | Shared `ZB.MOM.WW.Theme` RCL | [`components/ui-theme/`](components/ui-theme/) | [`ZB.MOM.WW.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`](ZB.MOM.WW.Health/CLAUDE.md). | Shared `ZB.MOM.WW.Health` lib | [`components/health/`](components/health/) | [`ZB.MOM.WW.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/`](components/observability/) | [`ZB.MOM.WW.Telemetry/`](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/`](components/configuration/) | [`ZB.MOM.WW.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/`](components/audit/) | [`ZB.MOM.WW.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/`](ZB.MOM.WW.GalaxyRepository/) |
|
||
| Secrets (encrypted store + `${secret:}` resolution) | Built; family version **`0.6.3`** (published 2026-09-28; apps still pin `0.6.2`). 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 (still on the hub for its own topology); **ScadaBridge retired the gRPC secrets hub for itself (2026-09-28, ScadaBridge `main` @ `23a139a4`, follow-ups `2650f794`) — sites now receive secrets via Deploy Artifacts instead of the hub, central still on ONE shared SQL store (`Secrets:Store:Provider=SqlServer`, `0.5.1` minimum)**; both default-off elsewhere. **`0.6.3` (hub sweep outcome logging + public `ISecretsHubSyncStatus`) is PUBLISHED to the feed (2026-09-28) but pinned by no app** — ScadaBridge, its original motivation, retired the hub for itself; it is there for the other hub consumer (OtOpcUa) to adopt. Full narrative (`0.1.x`→`0.6.3`, incidents, live gates, container caveat, `NU1102` note): [`ZB.MOM.WW.Secrets/CLAUDE.md`](ZB.MOM.WW.Secrets/CLAUDE.md). | Shared `ZB.MOM.WW.Secrets` lib (5 packages + CLI) | [`components/secrets/`](components/secrets/) | [`ZB.MOM.WW.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`](ZB.MOM.WW.LocalDb/CLAUDE.md). | Shared `ZB.MOM.WW.LocalDb` lib | [`docs/plans/2026-07-17-localdb-design.md`](docs/plans/2026-07-17-localdb-design.md) + the two adoption designs ([ScadaBridge](docs/plans/2026-07-19-scadabridge-localdb-design.md), [OtOpcUa](docs/plans/2026-07-20-otopcua-localdb-design.md)) | [`ZB.MOM.WW.LocalDb/`](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>/`](components/).
|
||
|
||
## Hosted application: ZB.MOM.WW.Overview (family dashboard)
|
||
|
||
Besides the shared libraries, this repo hosts one **application**:
|
||
[`ZB.MOM.WW.Overview/`](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`](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`](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`](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; ScadaBridge DEV + QA updated 2026-09-28):** DEV and both QA nodes run every repo tip — ScadaBridge `2b9a7c3c`
|
||
> (**DEV vd03 now `b3021e7c`**, migrations 57, script `Files` access on `E:\ApiInstall\ScadaBridge\recipes`, plus the in-memory MES/Delmia/alarm test machines SBTEST01/02 — QA vq03/vq04 also `b3021e7c` since 2026-09-28, migrations 57, file access off, `SimpleAlarmStatusRequest` copied from DEV),
|
||
> 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`).
|
||
|
||
```bash
|
||
# 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/`](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:
|
||
|
||
```bash
|
||
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.
|