Files
Joseph Doherty 2977e6c45c feat(localdb): register the consolidated site database in the composition root
Task 3. Site nodes now open one ZB.MOM.WW.LocalDb-managed database holding
OperationTracking and site_events as replicated tables.

SiteLocalDbSetup.OnReady applies both schemas and then RegisterReplicated's them.
That order is load-bearing and easy to get silently wrong: capture is
trigger-based CDC, so rows written before registration are never captured or
snapshotted - they would be invisible to the peer forever, with no error anywhere.

Storage ships UNCONDITIONALLY, no feature flag. With no peer configured LocalDb is
just a local SQLite file, so this is behaviour-equivalent to what site nodes do
today. Replication is opt-in and lands in Program.cs (Task 7), which owns the gRPC
host the sync endpoint needs.

Config: LocalDb:Path added to all EIGHT site-node appsettings (the plan said six -
docker-env2's site-x pair exists too), plus the dev template and the wonder-app-vd03
production sample. All ten are required, not optional: LocalDbOptions.Path is
validated with ValidateOnStart, so a site node without it fails fast at startup.
That is the desired posture, and it is what caught ActorPathTests - the one Host
fixture that actually starts a host - whose config now sets the path too.

Note the path choice fixes an existing data-loss bug in passing: site-tracking.db
and site_events.db both defaulted to CWD-relative paths (/app/...), outside the
mounted volume, so they were silently lost on every container recreate. The
consolidated database lives on /app/data.

Tests are container-built against the REAL SiteServiceRegistration.Configure, not a
hand-assembled ServiceCollection - this family has shipped three wiring defects that
only a real graph would catch (inert Secrets replicator 0.2.0, singleton deadlock
0.2.2, Secrets.Ui missing IAuditWriter). They assert the library's own view of what
is registered rather than that we called the right method.

Verified (all after the change):
  dotnet build ZB.MOM.WW.ScadaBridge.slnx -> 0 Error(s), 0 Warning(s)
  Host.Tests            -> 294 passed (5 new, red-first)
  SiteRuntime.Tests     -> 530 passed
  CentralUI.Tests       -> 925 passed
  Commons.Tests         -> 684 passed
  Communication.Tests   -> 312 passed
  SiteEventLogging.Tests-> 70 passed

Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
2026-07-19 03:36:54 -04:00
..

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 5000 (web), 8081 (Akka)
Central B scadabridge-env2-central-b 9102 9112 5000 (web), 8081 (Akka)
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
LDAP scadabridge-ldap Authentication (same test users)
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

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:

Teardown

bash docker-env2/teardown.sh

Containers stop, volumes (data + logs) preserved. 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 LDAP). See infra/glauth/config.toml 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 / ScadaBridgeMachineData2 on the shared scadabridge-mssql.
  • Transport.SourceEnvironment = "docker-cluster-env2" (stamped into exported bundle manifests).
  • Distinct Security.JwtSigningKey (sessions cannot cross envs).