Second slice of the 2026-09-09 whole-system review remediation
(docs/plans/2026-09-09-arch-review.md §10), on top of the High slice 146a1323.
Runtime/devices: SR-2 alarm on-trigger integration scope; SR-3 correlated NativeAlarmActor
subscribe retry; SR-4 immutable audited DB target; SR-5 ordered InstanceActor stop/flush;
SR-12 rehydration fence; SR-13 S&F gate recheck per delivery; DC-4 no OPC UA security
downgrade; DC-5 alarm snapshot for every new subscriber; ORCH-1 sync-dispose joins the pump.
Management/UI: MG-2 site scope on audit tree + Flow Debugger Replay; MG-3 stale-probe memo
removed; MG-4 server-owned bulk-deploy deadline; UI-5 sandbox runs in a killable child
process (new CentralUI.SandboxWorker project); UI-14 bounded sandbox output/IPC.
Edge/audit: EI-1 relay audit ownership; EI-2 SkipBodyCapture suppresses error text; EI-3/EI-11
error-field redaction + caps; EI-4 single relay deadline, pre-execution-only retry; EI-5
authenticated relay provenance; EI-10 body cap before audit read.
Persistence/contracts: DL-1 global Instance.UniqueName unique index + conflict translation;
DL-2 DataConnection protocol config encrypted at rest (migration
20260909203046_EnforceGlobalInstanceNamesAndProtectProtocolConfiguration — coordinated
central upgrade, old binaries cannot read protected rows); DL-3 bounded Replay paging;
DL-6 cross-site Overwrite of a deployed instance rejected; XC-4 proto descriptor contract test.
Config/hygiene: XC-1 central listener isolation by local port; XC-2/XC-3 inert SMTP + cron
keys removed from rig settings; CM-2 env2 bootstrap guard enabled; DC-6 deploy site exit code;
DC-7 notifier retries only explicit standby rejection; XC-5 ClusterClient purged from docs.
Gates recorded in the report: build 0/0, 9 397 passed / 0 failed, rig redeployed on all
eight nodes with the migration applied. 46 Low findings remain open.
ScadaBridge Env2 Docker Infrastructure
A second Docker deployment of a minimal ScadaBridge cluster topology, designed to run concurrently with the primary docker/ stack so the Transport (#24) feature can be exercised end-to-end across two real environments.
See docs/plans/2026-05-24-second-environment-design.md for the design rationale.
Cluster Topology
┌───────────────────┐
│ Traefik LB :9100 │ ◄── CLI / Browser
│ Dashboard :8181 │
└────────┬──────────┘
│ routes to active node
┌──────────────────────┼──────────────────────────────┐
│ Env2 Central Cluster │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ env2-central-a │◄──►│ env2-central-b │ │
│ │ Web UI :9101 │ │ Web UI :9102 │ │
│ │ Akka :9111 │ │ Akka :9112 │ │
│ └────────┬─────────┘ └─────────────────┘ │
│ │ │
└───────────┼─────────────────────────────────────────┘
│ Akka.NET Remoting
▼
┌────────────────────┐
│ Env2 Site-X │
│ (Env2 Site X) │
│ │
│ node-a ◄──► node-b│
│ Akka :9121 :9122 │
│ gRPC :9123 :9124 │
└────────────────────┘
Port Allocation
Env2 host ports are the primary's ports + 100. Both stacks can run simultaneously.
| Node | Container Name | Host Web | Host Akka | Host gRPC | Internal |
|---|---|---|---|---|---|
| Traefik LB | scadabridge-env2-traefik |
9100 | — | — | 80 (proxy), 8080 (dashboard host:8181) |
| Central A | scadabridge-env2-central-a |
9101 | 9111 | 9113 | 5000 (web), 8081 (Akka), 8083 (gRPC) |
| Central B | scadabridge-env2-central-b |
9102 | 9112 | 9114 | 5000 (web), 8081 (Akka), 8083 (gRPC) |
| Site-X A | scadabridge-env2-site-x-a |
— | 9121 | 9123 | 8082 (Akka), 8083 (gRPC) |
| Site-X B | scadabridge-env2-site-x-b |
— | 9122 | 9124 | 8082 (Akka), 8083 (gRPC) |
Shared Infrastructure
Env2 attaches to the existing scadabridge-net Docker bridge network and reuses these primary infra containers:
| Service | Container | What env2 uses it for |
|---|---|---|
| MS SQL | scadabridge-mssql |
Env2-specific databases ScadaBridgeConfig2 / ScadaBridgeMachineData2 |
| SMTP | scadabridge-smtp |
Notification capture in Mailpit (env2 emails distinguishable by FromAddress) |
| OPC UA | scadabridge-opcua |
Simulated tags for site-x data connections |
| REST API | scadabridge-restapi |
External REST API testing |
LDAP is not one of the reused infra/ containers (that stack's ldap service is
retired/commented out). Env2's central nodes bind to the same local redundant GLAuth pair
as docker/ — zb-ldap-primary on host.docker.internal:3893 with zb-ldap-backup on
host.docker.internal:3894 as FallbackServers (source of truth:
scadaproj/infra/glauth-redundant); same test users, same bind credentials as the primary
stack.
Commands
First-Time Setup
# 1. Make sure primary infra is up (creates scadabridge-net, scadabridge-mssql, etc.)
cd infra && docker compose up -d && cd ..
# 2. Build image + create env2 databases + deploy env2 containers
bash docker-env2/deploy.sh
# 3. Seed env2's single test site (first-time only)
bash docker-env2/seed-sites.sh
The seed also creates a native alarm source demo: a MxAlarmDemo template with a
GalaxyAlarms native alarm source bound to the shared MxGateway connection, plus a
deployed MxAlarmDemo-1 instance on site-x. This exercises the read-only native alarm
mirror end-to-end — mirrored alarms appear live in the Central UI Debug View with severity
and condition badges. Verify the binding with:
scadabridge --url http://localhost:9100 template native-alarm-source list --template-id <id>
After Code Changes
bash docker-env2/deploy.sh
The Docker build is shared with the primary stack — if you've just run docker/deploy.sh, the env2 build hits a fully cached image.
Running Both Stacks Concurrently
bash docker/deploy.sh # primary
bash docker-env2/deploy.sh # env2
Both UIs are now reachable:
- Primary: http://localhost:9000
- Env2: http://localhost:9100
Teardown
bash docker-env2/teardown.sh
Containers stop; node data lives in named Docker volumes (scadabridge-env2-*-data) and
logs in node directories, both left in place. To also remove the persistent data:
# Destructive: deletes all env2 SQLite data (site config cache, S&F buffers, LocalDb).
docker volume rm $(docker volume ls -q --filter name=scadabridge-env2-)
rm -rf docker-env2/*/logs
To also drop the env2 databases:
docker exec scadabridge-mssql /opt/mssql-tools18/bin/sqlcmd \
-S localhost -U sa -P 'ScadaBridge_Dev1#' -C \
-Q "DROP DATABASE ScadaBridgeConfig2; DROP DATABASE ScadaBridgeMachineData2;"
CLI Access
dotnet run --project src/ZB.MOM.WW.ScadaBridge.CLI -- \
--url http://localhost:9100 \
--username multi-role --password password \
template list
View Logs
docker compose -f docker-env2/docker-compose.yml logs -f
docker logs -f scadabridge-env2-central-a
Test Users
Same as primary (env2 shares the local redundant GLAuth pair, scadaproj/infra/glauth-redundant).
See "Shared Infrastructure" above and primary docker/README.md.
Transport Testing Workflow
See docs/plans/2026-05-24-second-environment-verification.md for the manual golden-path checklist.
What's Different from Primary
- Single site (
site-x) instead of three (site-a/b/c). - Host port range 91XX vs primary 90XX.
- Container names prefixed
scadabridge-env2-. - Databases
ScadaBridgeConfig2/ScadaBridgeMachineData2on the sharedscadabridge-mssql. Transport.SourceEnvironment="docker-cluster-env2"(stamped into exported bundle manifests).- Distinct
Security.JwtSigningKey(sessions cannot cross envs).