# `docker/` — containerised overview dashboard

Runtime-only image, the HistorianGateway pattern: publish on the host (which has the Gitea feed
credentials), copy the output into `mcr.microsoft.com/dotnet/aspnet:10.0`. Docker never restores.

```bash
# from ZB.MOM.WW.Overview/
dotnet publish src/ZB.MOM.WW.Overview/ZB.MOM.WW.Overview.csproj \
  -c Release -o docker/publish -p:UseAppHost=false

cd docker && docker compose up -d --build
open http://localhost:5320
```

`docker/publish/` is build output and is gitignored.

## Why the container, and not just `dotnet run`

The dev rigs publish only some node ports to the host:

| Fleet | Health endpoint | Reachable from the Mac host? |
|---|---|---|
| ScadaBridge central-a/b | container `:5000` | yes — `localhost:9001` / `:9002` |
| ScadaBridge site-\* (6) | container `:8084` | **no** — not published |
| OtOpcUa central-1/2 | container `:9000` | **no** — only Traefik `:9200`, load-balanced across the pair |
| OtOpcUa site-\* (4) | container `:8080` | **no** — not published |
| HistorianGateway | container `:5220` | yes — `localhost:5220` |
| MxAccessGateway | windev `:5130` | yes, with the VPN up |

So a host-run dashboard can only see part of the fleet. This stack joins the three rig networks
(`otopcua-dev_default`, `scadabridge-net`, `zb-historiangw_default`, all declared `external`) and
addresses every node by container DNS name, which is how it sees all of them.

Ports above were established by probing the running rigs on 2026-07-24, **not** read off the
compose files — the two disagree. In particular OtOpcUa's site nodes serve health on `:8080`, not
the `:9000` their compose anchor implies, because their explicit Kestrel listeners for LocalDb sync
re-bind the primary HTTP port.

Start the rigs before this stack. The networks are `external`, so bringing the dashboard up or
down never disturbs a running rig.

## Registry shape

Bound from the `Overview` section; `appsettings.Docker.json` is the worked example.

```jsonc
"Overview": {
  "PollIntervalSeconds": 10,      // sweep cadence; per-app and per-instance overrides allowed
  "TimeoutSeconds": 3,            // per-request timeout
  "StaleAfterSeconds": 45,        // must exceed the poll interval, or every card reads Stale
  "Applications": [
    {
      "Name": "ScadaBridge",
      "ManagementLabel": "CentralUI",   // label on each instance's management link
      "Instances": [
        {
          "Name": "central-a",
          "Group": "central",            // one Akka cluster = one group; leader + split-brain
                                         // detection are scoped to it. Omit for standalone apps.
          "BaseUrl": "http://host:5000", // /health/ready and /health/active are probed under this
          "ManagementUrl": "http://localhost:9001/",  // followed by the OPERATOR'S browser, so it
                                                      // must be host-reachable, not container DNS
          "HasActiveRole": true          // false for single-instance gateways: no Active badge,
                                         // and /health/active is never probed
        }
      ]
    }
  ]
}
```

`appsettings.json` ships the tuning defaults with an **empty** `Applications` list, so a deployment
that supplies no registry fails at startup naming the missing key rather than booting into an empty
dashboard.

## Configuration traps

- **Never set `ASPNETCORE_URLS` alongside `Kestrel__Endpoints__*`.** Declaring both puts Kestrel
  into explicit-endpoints mode and one of them is silently discarded.
- No `env_file`, no secrets, no login. The dashboard reads anonymous health endpoints and holds no
  credentials. That is a requirement of the design, not an omission.
