feat(overview): wonder-app-vd03 registry overlay

Adds appsettings.Wonder.json for the first deployment of this dashboard to a
real box. Named "Wonder" rather than "Production", mirroring the existing
Docker overlay: this is ONE deployment's fleet, not a claim about what every
production deployment looks like.

Every port was established by probing the running box, not inferred from the
docker rig - and the two disagree, so inference would have been wrong:

  OtOpcUa             :9000  (AdminUI on the same listener)
  ScadaBridge Central :8085  (rig serves this on :5000)
  ScadaBridge Site    :8084  (no UI - / is 404, so the card carries no link)
  MxAccessGateway     :5130  (:5120 is gRPC)
  HistorianGateway    :5220  (:5221 is gRPC h2c)

:8083 and :8086 answer 400 to an HTTP/1.1 GET - h2c gRPC - and are excluded.

Both gateways are HasActiveRole:false. Their /health/active does return 200,
but only because the active tier runs an EMPTY check set when no Active-tagged
check is registered, so an Active badge would assert a role neither has.
Central and site are separate groups because they are separate Akka clusters;
one shared group would manufacture a leader disagreement.

BaseUrl is localhost (probed by the service, on the box) while ManagementUrl
carries the box address (followed by the operator's browser, elsewhere).

Serilog's file path is absolute and outside the Server directory an upgrade
replaces - a relative path resolves against the service AppDirectory, so every
upgrade would silently eat the log history. Same class of defect that cost
HistorianGateway its secrets store on a swap.

Boot-verified locally under ASPNETCORE_ENVIRONMENT=Wonder: registry passes
OverviewOptionsValidator and the host starts.
This commit is contained in:
Joseph Doherty
2026-08-11 06:44:17 -04:00
parent 8f31184b7d
commit c582508ae5
@@ -0,0 +1,127 @@
{
// Registry for the wonder-app-vd03 deployment (ASPNETCORE_ENVIRONMENT=Wonder), which runs as a
// Windows service under NSSM rather than in a container. Named "Wonder" and not "Production" on
// purpose, mirroring appsettings.Docker.json: this is ONE deployment's fleet, not a statement
// about what any production deployment looks like. A second box gets its own overlay.
//
// Every port below was established by probing the running box on 2026-08-11 — curl against each
// candidate port — NOT read off a compose file or inferred from the docker rig, whose numbers
// differ. The rig's ScadaBridge central serves health on :5000; here it is :8085.
//
// OtOpcUa :9000 service OtOpcUaHost (AdminUI on the same listener)
// ScadaBridge Central:8085 service ScadaBridge-Central (CentralUI on the same listener)
// ScadaBridge Site :8084 service ScadaBridge-Site (no UI: / returns 404)
// MxAccessGateway :5130 service MxAccessGw (dashboard; :5120 is gRPC)
// HistorianGateway :5220 service HistorianGateway (dashboard; :5221 is gRPC h2c)
//
// Ports NOT to probe, confirmed 400 (h2c gRPC, which rejects an HTTP/1.1 health GET):
// :8083 ScadaBridge central control plane, :8086.
"Serilog": {
// Absolute, and deliberately OUTSIDE the Server directory the upgrade procedure replaces.
// A relative path resolves against the service's AppDirectory (…\Overview\Server), so every
// upgrade would silently discard the log history — the same class of defect that cost
// HistorianGateway its secrets store on a swap.
"WriteTo": [
{ "Name": "Console" },
{
"Name": "File",
"Args": {
"path": "E:\\ApiInstall\\Overview\\logs\\overview-.log",
"rollingInterval": "Day"
}
}
]
},
"Kestrel": {
// Bind explicitly; never also set ASPNETCORE_URLS, which would put Kestrel into
// explicit-endpoints mode with one of the two silently discarded.
"Endpoints": {
"Http": {
"Url": "http://+:5320"
}
}
},
"Overview": {
"PollIntervalSeconds": 10,
"TimeoutSeconds": 3,
"StaleAfterSeconds": 45,
"Applications": [
{
"Name": "OtOpcUa",
"ManagementLabel": "AdminUI",
"Instances": [
{
// A one-node cluster, so Group is still its real cluster identity: leader aggregation
// is scoped to it and the split-brain check cannot false-positive on a single member
// (it triggers on more than one DISTINCT reported address).
"Name": "wonder",
"Group": "MAIN",
// Probed by THIS service, on this box — localhost is the shortest path and is immune
// to any host firewall rule. ManagementUrl is the opposite case: the operator's
// browser follows it from their own machine, so it must carry the box address.
"BaseUrl": "http://localhost:9000",
"ManagementUrl": "http://10.220.157.247:9000/",
"HasActiveRole": true
}
]
},
{
"Name": "ScadaBridge",
"ManagementLabel": "CentralUI",
"Instances": [
{
"Name": "central",
"Group": "central",
"BaseUrl": "http://localhost:8085",
"ManagementUrl": "http://10.220.157.247:8085/",
"HasActiveRole": true
},
{
// Central and site are SEPARATE Akka clusters, so they are separate groups; putting
// them in one would make the dashboard compare two unrelated leaders and report a
// disagreement that does not exist.
"Name": "site",
"Group": "site",
"BaseUrl": "http://localhost:8084",
// No ManagementUrl: the site node serves no UI (/ returns 404), and a card linking to
// a 404 is worse than a card with no link.
"HasActiveRole": true
}
]
},
{
"Name": "MxAccessGateway",
"ManagementLabel": "Dashboard",
"Instances": [
{
// HasActiveRole false: mxgw is a standalone gateway with no cluster. Its
// /health/active does answer 200, but only because the active tier runs an EMPTY
// check set — an Active badge here would assert a role that does not exist.
"Name": "wonder",
"BaseUrl": "http://localhost:5130",
"ManagementUrl": "http://10.220.157.247:5130/",
"HasActiveRole": false
}
]
},
{
"Name": "HistorianGateway",
"ManagementLabel": "Dashboard",
"Instances": [
{
"Name": "wonder",
"BaseUrl": "http://localhost:5220",
"ManagementUrl": "http://10.220.157.247:5220/",
"HasActiveRole": false,
// Its readiness probe reaches the historian and the Galaxy SQL DB over the network,
// so it is the one instance that legitimately exceeds the 3s global timeout.
"TimeoutSeconds": 10
}
]
}
]
}
}