refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)
Solution + 23 src projects + 26 test projects renamed; folders, csproj, namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated. ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated. SQL roles/logins, LDAP domains, CLI command name, and CLI config dir (~/.scadalink → ~/.scadabridge) also renamed. Build green; 5 Host.Tests fail awaiting SQL login rename in next commit. Pre-existing StaleTagMonitor timing flakes unchanged. Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Module | `src/ScadaLink.Host` |
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.Host` |
|
||||
| Design doc | `docs/requirements/Component-Host.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
@@ -12,12 +12,12 @@
|
||||
|
||||
## Summary
|
||||
|
||||
The Host module is the composition root for the entire ScadaLink system: a single
|
||||
The Host module is the composition root for the entire ScadaBridge system: a single
|
||||
binary whose behaviour (`Central` vs `Site`) is driven entirely by configuration. The
|
||||
implementation is generally faithful to `Component-Host.md` — startup validation,
|
||||
role-based registration, Serilog enrichment, Windows Service support, dead-letter
|
||||
monitoring, CoordinatedShutdown, and gRPC hosting on site nodes are all present and
|
||||
backed by a solid test suite (`tests/ScadaLink.Host.Tests`).
|
||||
backed by a solid test suite (`tests/ZB.MOM.WW.ScadaBridge.Host.Tests`).
|
||||
|
||||
The most significant problem is the readiness endpoint: `/health/ready` runs **all**
|
||||
registered health checks, including the leader-only `active-node` check, so a fully
|
||||
@@ -74,7 +74,7 @@ call passes `default` for `CancellationToken`, so a SIGTERM during the
|
||||
bounded-retry window is ignored for up to ~2 minutes (Host-019);
|
||||
`LoggerConfigurationFactory` layers `MinimumLevel.Is` over
|
||||
`ReadFrom.Configuration`, so any `Serilog:MinimumLevel` an operator sets is
|
||||
silently overridden by `ScadaLink:Logging:MinimumLevel` (Host-020); the
|
||||
silently overridden by `ScadaBridge:Logging:MinimumLevel` (Host-020); the
|
||||
shipped `appsettings.json` carries a Microsoft `Logging:LogLevel` block but
|
||||
Serilog is the only logger provider and the section is dead config (Host-021);
|
||||
and `ParseLevel` silently swallows an unrecognised `MinimumLevel` value (e.g.
|
||||
@@ -108,7 +108,7 @@ _Re-review (2026-05-28, `1eb6e97`):_
|
||||
| 7 | Design-document adherence | ☑ | Re-review: REQ-HOST-7 site-shutdown ordering — stop accepting new streams, cancel active streams via `ApplicationStopping`, then tear down actors — is not wired in `Program.cs` (Host-017). |
|
||||
| 8 | Code organization & conventions | ☑ | Re-review: `NodeOptions.NodeName` is absent from the shipped per-role configs even though it stamps `AuditLog.SourceNode` (Host-018); the appsettings `Logging:LogLevel` Microsoft section is dead config under Serilog (Host-021). |
|
||||
| 9 | Testing coverage | ☑ | Strong existing suite. No coverage for the Site `CentralContactPoints` second-entry rule (Host-016), the site-shutdown ordering (Host-017), the `NodeName`-absent shipped config (Host-018), the unused `CancellationToken` parameter (Host-019), the `MinimumLevel.Is` override semantics (Host-020) or the `ParseLevel` silent fallback (Host-022). |
|
||||
| 10 | Documentation & comments | ☑ | Re-review: layered `MinimumLevel.Is` / `ReadFrom.Configuration` semantics are not surfaced — an operator-set `Serilog:MinimumLevel` is silently overridden by `ScadaLink:Logging:MinimumLevel` (Host-020); `ParseLevel` silently coerces a misspelled level to `Information` with no warning (Host-022). |
|
||||
| 10 | Documentation & comments | ☑ | Re-review: layered `MinimumLevel.Is` / `ReadFrom.Configuration` semantics are not surfaced — an operator-set `Serilog:MinimumLevel` is silently overridden by `ScadaBridge:Logging:MinimumLevel` (Host-020); `ParseLevel` silently coerces a misspelled level to `Information` with no warning (Host-022). |
|
||||
|
||||
## Findings
|
||||
|
||||
@@ -119,7 +119,7 @@ _Re-review (2026-05-28, `1eb6e97`):_
|
||||
| Severity | High |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/Program.cs:135-145` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/Program.cs:135-145` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -165,7 +165,7 @@ response body; it failed before the fix and passes after. Full Host suite green
|
||||
| Severity | Medium |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/Actors/AkkaHostedService.cs:70-108`, `docs/requirements/Component-Host.md` REQ-HOST-6 |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs:70-108`, `docs/requirements/Component-Host.md` REQ-HOST-6 |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -173,7 +173,7 @@ REQ-HOST-6 states the Host "must configure the Akka.NET actor system using
|
||||
Akka.Hosting with ... **Persistence**: Configured with the appropriate journal and
|
||||
snapshot store (SQL for central, SQLite for site)." The HOCON built in
|
||||
`AkkaHostedService.StartAsync` contains no `akka.persistence` section, no journal and
|
||||
no snapshot-store plugin, and `ScadaLink.Host.csproj` references neither
|
||||
no snapshot-store plugin, and `ZB.MOM.WW.ScadaBridge.Host.csproj` references neither
|
||||
`Akka.Persistence.Hosting` nor any persistence plugin (the design doc Dependencies
|
||||
list `Akka.Persistence.Hosting`). A repo-wide search finds **no** `PersistentActor` /
|
||||
`ReceivePersistentActor` subclasses — the system deliberately uses custom SQLite
|
||||
@@ -193,7 +193,7 @@ add the plugin packages and HOCON. Either way, code and doc must agree.
|
||||
Resolved 2026-05-16 (commit `<pending>`). Confirmed accurate: a repo-wide search
|
||||
finds **no** `PersistentActor` / `ReceivePersistentActor` subclasses anywhere in
|
||||
`src/`, no `akka.persistence` section in the HOCON built by
|
||||
`AkkaHostedService.StartAsync`, and `ScadaLink.Host.csproj` references no
|
||||
`AkkaHostedService.StartAsync`, and `ZB.MOM.WW.ScadaBridge.Host.csproj` references no
|
||||
persistence plugin packages. The Host code is correct and internally consistent;
|
||||
the defect was a stale design doc. Fix: updated `docs/requirements/Component-Host.md`
|
||||
— REQ-HOST-6 no longer lists Persistence as a configured subsystem and now carries
|
||||
@@ -211,15 +211,15 @@ agree; no code or test change required.
|
||||
| Severity | Medium |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/appsettings.Central.json:20-31` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/appsettings.Central.json:20-31` |
|
||||
|
||||
**Description**
|
||||
|
||||
`appsettings.Central.json` contains real-looking secrets in plaintext, checked into
|
||||
source control: SQL Server passwords in the `ConfigurationDb` / `MachineDataDb`
|
||||
connection strings (`Password=ScadaLink_Dev1#`), an LDAP service-account password
|
||||
connection strings (`Password=ScadaBridge_Dev1#`), an LDAP service-account password
|
||||
(`LdapServiceAccountPassword: "password"`), and a JWT signing key
|
||||
(`JwtSigningKey: "scadalink-dev-jwt-signing-key-..."`). Even though these are
|
||||
(`JwtSigningKey: "scadabridge-dev-jwt-signing-key-..."`). Even though these are
|
||||
intended as development defaults, shipping them in the default config invites them
|
||||
being reused verbatim in production, and a committed JWT signing key allows anyone
|
||||
with repo access to forge session tokens. `TrustServerCertificate=true` additionally
|
||||
@@ -237,16 +237,16 @@ environment.
|
||||
|
||||
Resolved 2026-05-16 (commit `pending`). Root cause confirmed against
|
||||
`appsettings.Central.json`: the committed file carried real-looking secrets in
|
||||
plaintext — SQL Server passwords (`Password=ScadaLink_Dev1#`) in both connection
|
||||
plaintext — SQL Server passwords (`Password=ScadaBridge_Dev1#`) in both connection
|
||||
strings, an LDAP service-account password, and a JWT signing key. Fix: all four
|
||||
secrets were removed from the committed file and replaced with non-functional
|
||||
`${...}` placeholder references (`ConfigurationDb` / `MachineDataDb`,
|
||||
`LdapServiceAccountPassword`, `JwtSigningKey`). A new top-level `_secrets` note
|
||||
documents that the Host's configuration builder (`AddEnvironmentVariables()`)
|
||||
overlays the real values supplied via environment variables
|
||||
(`ScadaLink__Database__ConfigurationDb`, `ScadaLink__Database__MachineDataDb`,
|
||||
`ScadaLink__Security__LdapServiceAccountPassword`,
|
||||
`ScadaLink__Security__JwtSigningKey`); the placeholders are intentionally invalid so
|
||||
(`ScadaBridge__Database__ConfigurationDb`, `ScadaBridge__Database__MachineDataDb`,
|
||||
`ScadaBridge__Security__LdapServiceAccountPassword`,
|
||||
`ScadaBridge__Security__JwtSigningKey`); the placeholders are intentionally invalid so
|
||||
a misconfigured deployment fails loudly rather than silently using a committed key.
|
||||
Regression test class `ConfigSecretsTests` asserts the committed file carries no
|
||||
plaintext `Password=` value, no committed LDAP service-account password, and no
|
||||
@@ -266,13 +266,13 @@ scope; they should receive the same treatment in a follow-up.
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/appsettings.Site.json:10-19` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/appsettings.Site.json:10-19` |
|
||||
|
||||
**Description**
|
||||
|
||||
The shipped site config sets `Node:RemotingPort = 8082` and `Node:GrpcPort = 8083`,
|
||||
but `Cluster:SeedNodes` is `["akka.tcp://scadalink@localhost:8082",
|
||||
"akka.tcp://scadalink@localhost:8083"]`. The second seed node targets `8083`, which
|
||||
but `Cluster:SeedNodes` is `["akka.tcp://scadabridge@localhost:8082",
|
||||
"akka.tcp://scadabridge@localhost:8083"]`. The second seed node targets `8083`, which
|
||||
is the Kestrel HTTP/2 gRPC port — not an Akka remoting endpoint. A node attempting to
|
||||
join via that seed will try to establish an Akka.Remote TCP association against the
|
||||
gRPC listener and fail. `StartupValidator` only checks that ≥2 seed nodes exist
|
||||
@@ -290,7 +290,7 @@ to reject a seed node whose port equals this node's `GrpcPort`.
|
||||
|
||||
Resolved 2026-05-16 (commit `pending`). Root cause confirmed against
|
||||
`appsettings.Site.json`: with `Node:RemotingPort = 8082` and `Node:GrpcPort = 8083`,
|
||||
the second `Cluster:SeedNodes` entry was `akka.tcp://scadalink@localhost:8083` — the
|
||||
the second `Cluster:SeedNodes` entry was `akka.tcp://scadabridge@localhost:8083` — the
|
||||
Kestrel HTTP/2 gRPC port, not an Akka.Remote endpoint. `StartupValidator` only
|
||||
checked seed-node *count* (≥2), so the misconfiguration passed silently. Fix, two
|
||||
parts: (1) the shipped `appsettings.Site.json` second seed entry was corrected to a
|
||||
@@ -313,7 +313,7 @@ appropriately before the fix and pass after.
|
||||
| Severity | Low |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/Actors/AkkaHostedService.cs:345` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs:345` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -354,7 +354,7 @@ async startup pipeline and remain green (175 passed).
|
||||
| Severity | Low |
|
||||
| Category | Akka.NET conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/Actors/AkkaHostedService.cs:70-108` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs:70-108` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -404,7 +404,7 @@ suite green (175 passed).
|
||||
| Severity | Low |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/StartupValidator.cs:43-47` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/StartupValidator.cs:43-47` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -421,7 +421,7 @@ piece.
|
||||
|
||||
Add a check in the `role == "Site"` block: if `GrpcPort` (resolved, including the
|
||||
8083 default) equals `RemotingPort`, add an error
|
||||
`"ScadaLink:Node:GrpcPort must differ from RemotingPort"`.
|
||||
`"ScadaBridge:Node:GrpcPort must differ from RemotingPort"`.
|
||||
|
||||
**Resolution**
|
||||
|
||||
@@ -429,7 +429,7 @@ Resolved 2026-05-16 (commit `pending`). Root cause confirmed against
|
||||
`StartupValidator.cs`: the `role == "Site"` block validated the GrpcPort range but
|
||||
never compared it to `RemotingPort`, so a site config setting both ports equal
|
||||
passed validation and then failed opaquely when Kestrel and Akka.Remote both bound
|
||||
the port. Fix: added `if (port == grpcPort) errors.Add("ScadaLink:Node:GrpcPort must
|
||||
the port. Fix: added `if (port == grpcPort) errors.Add("ScadaBridge:Node:GrpcPort must
|
||||
differ from RemotingPort")` in the Site block, using the resolved GrpcPort (including
|
||||
the 8083 `NodeOptions` default when the key is absent) against the parsed
|
||||
RemotingPort. Regression tests in `StartupValidatorTests`:
|
||||
@@ -445,11 +445,11 @@ before the fix and pass after. Full Host suite green (175 passed).
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/StartupValidator.cs:33-34`, `src/ScadaLink.Host/DatabaseOptions.cs:6` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/StartupValidator.cs:33-34`, `src/ZB.MOM.WW.ScadaBridge.Host/DatabaseOptions.cs:6` |
|
||||
|
||||
**Description**
|
||||
|
||||
`StartupValidator` requires a non-empty `ScadaLink:Database:MachineDataDb` connection
|
||||
`StartupValidator` requires a non-empty `ScadaBridge:Database:MachineDataDb` connection
|
||||
string for Central nodes, and `DatabaseOptions` exposes a `MachineDataDb` property,
|
||||
but a repo-wide search shows the value is never read anywhere outside the Host module
|
||||
— only `ConfigurationDb` is passed to `AddConfigurationDatabase`
|
||||
@@ -489,7 +489,7 @@ key harmlessly — it will simply be ignored; a follow-up may remove it for tidi
|
||||
| Severity | Low |
|
||||
| Category | Akka.NET conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/Actors/AkkaHostedService.cs:127-141` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs:127-141` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -540,7 +540,7 @@ Host suite green (175 passed).
|
||||
| Severity | Low |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/Program.cs:112-125` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/Program.cs:112-125` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -585,40 +585,40 @@ the pre-fix code (the helper did not exist) and pass after. Full Host suite gree
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/LoggingOptions.cs:5`, `src/ScadaLink.Host/Program.cs:42-50` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/LoggingOptions.cs:5`, `src/ZB.MOM.WW.ScadaBridge.Host/Program.cs:42-50` |
|
||||
|
||||
**Description**
|
||||
|
||||
`LoggingOptions` exposes a `MinimumLevel` property bound from `ScadaLink:Logging`
|
||||
`LoggingOptions` exposes a `MinimumLevel` property bound from `ScadaBridge:Logging`
|
||||
(`SiteServiceRegistration.BindSharedOptions`), and both `appsettings.Central.json`
|
||||
and `appsettings.Site.json` set `"Logging": { "MinimumLevel": "Information" }`.
|
||||
However Serilog is configured purely via `ReadFrom.Configuration(configuration)`,
|
||||
which reads the standard `Serilog` section — not `ScadaLink:Logging`. The
|
||||
which reads the standard `Serilog` section — not `ScadaBridge:Logging`. The
|
||||
`LoggingOptions.MinimumLevel` value is never read by any code, so changing it has no
|
||||
effect. This is misleading: an operator editing `ScadaLink:Logging:MinimumLevel`
|
||||
effect. This is misleading: an operator editing `ScadaBridge:Logging:MinimumLevel`
|
||||
expecting a log-level change will see nothing happen.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Either consume `LoggingOptions.MinimumLevel` when configuring the Serilog
|
||||
`LoggerConfiguration` (e.g. set `MinimumLevel.Is(...)` from it), or remove the option
|
||||
class and the `ScadaLink:Logging` sections and rely solely on the `Serilog`
|
||||
class and the `ScadaBridge:Logging` sections and rely solely on the `Serilog`
|
||||
configuration section. Keep one mechanism, not two.
|
||||
|
||||
**Resolution**
|
||||
|
||||
Resolved 2026-05-16 (commit `pending`). Root cause confirmed: `LoggingOptions.MinimumLevel`
|
||||
was bound from `ScadaLink:Logging` but Serilog was configured purely via
|
||||
was bound from `ScadaBridge:Logging` but Serilog was configured purely via
|
||||
`ReadFrom.Configuration` (standard `Serilog` section), so editing
|
||||
`ScadaLink:Logging:MinimumLevel` had no effect. Re-triaged the two options: the
|
||||
`ScadaBridge:Logging:MinimumLevel` had no effect. Re-triaged the two options: the
|
||||
"remove the option class" branch was rejected because `Component-Host.md` REQ-HOST-3
|
||||
explicitly lists `ScadaLink:Logging` → `LoggingOptions` and that design doc is outside
|
||||
explicitly lists `ScadaBridge:Logging` → `LoggingOptions` and that design doc is outside
|
||||
this task's edit scope — removing the class would create a fresh code-vs-doc drift.
|
||||
Fix (the "consume it" branch): added a `LoggerConfigurationFactory.Build` that binds
|
||||
`LoggingOptions`, parses `MinimumLevel` into a Serilog `LogEventLevel` (falling back
|
||||
to `Information` for null/blank/unrecognised values), and pins it via
|
||||
`MinimumLevel.Is(...)` on top of `ReadFrom.Configuration`; `Program.cs` now builds the
|
||||
logger through this factory. `ScadaLink:Logging:MinimumLevel` is now live.
|
||||
logger through this factory. `ScadaBridge:Logging:MinimumLevel` is now live.
|
||||
Regression tests in new `LoggerConfigurationTests`:
|
||||
`MinimumLevel_Warning_SuppressesInformationLogs`,
|
||||
`MinimumLevel_Debug_AllowsDebugLogs`, and `MinimumLevel_Absent_DefaultsToInformation`
|
||||
@@ -633,17 +633,17 @@ key). Full Host suite green (175 passed).
|
||||
| Severity | Medium |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/Actors/AkkaHostedService.cs:146-148` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs:146-148` |
|
||||
|
||||
**Description**
|
||||
|
||||
`AkkaHostedService.BuildHocon` emits the split-brain-resolver block with
|
||||
`keep-oldest { down-if-alone = on }` as a literal constant. `ClusterOptions`
|
||||
(`src/ScadaLink.ClusterInfrastructure/ClusterOptions.cs:74`) exposes a
|
||||
`DownIfAlone` property — bound from `ScadaLink:Cluster` via the Options pattern,
|
||||
(`src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptions.cs:74`) exposes a
|
||||
`DownIfAlone` property — bound from `ScadaBridge:Cluster` via the Options pattern,
|
||||
documented as "the design-doc requirement", default `true` — but a repo-wide search
|
||||
shows it is referenced **nowhere outside its own declaration**. The Host therefore
|
||||
ignores the bound value entirely: setting `ScadaLink:Cluster:DownIfAlone` to `false`
|
||||
ignores the bound value entirely: setting `ScadaBridge:Cluster:DownIfAlone` to `false`
|
||||
in `appsettings.json` has no effect, the resolver still runs with `down-if-alone =
|
||||
on`. This is the same class of defect as the resolved Host-011
|
||||
(`LoggingOptions.MinimumLevel` was dead config) — a configuration option that is
|
||||
@@ -659,7 +659,7 @@ Make `BuildHocon` consume `clusterOptions.DownIfAlone` — emit `down-if-alone =
|
||||
{(clusterOptions.DownIfAlone ? "on" : "off")}` (the value is a bool, so no escaping
|
||||
is needed). Add a `HoconBuilderTests` case asserting both `true` and `false` produce
|
||||
the corresponding `down-if-alone` token. If the flag is genuinely meant to be fixed
|
||||
at `on` for ScadaLink's two-node clusters, remove the `DownIfAlone` property and its
|
||||
at `on` for ScadaBridge's two-node clusters, remove the `DownIfAlone` property and its
|
||||
doc comment instead so code and configuration contract agree — but do not leave it
|
||||
declared-but-dead.
|
||||
|
||||
@@ -682,7 +682,7 @@ the fix (token was still `on`) and passes after. Full Host suite green (182 pass
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/Actors/AkkaHostedService.cs:135-136,145,151-152` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs:135-136,145,151-152` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -727,7 +727,7 @@ after. Full Host suite green (182 passed).
|
||||
| Severity | Low |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/Program.cs:43-48`, `src/ScadaLink.Host/appsettings.json:1-7` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/Program.cs:43-48`, `src/ZB.MOM.WW.ScadaBridge.Host/appsettings.json:1-7` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -738,7 +738,7 @@ configuration section — but neither `appsettings.json` nor either role-specifi
|
||||
contains a `Serilog` section (only a Microsoft `Logging` section with a `LogLevel`
|
||||
map, which Serilog's `ReadFrom.Configuration` does not consume). The two sinks that
|
||||
actually run are appended in `Program.cs` as hard-coded `.WriteTo.Console(...)` and
|
||||
`.WriteTo.File("logs/scadalink-.log", rollingInterval: Day)` calls. As a result the
|
||||
`.WriteTo.File("logs/scadabridge-.log", rollingInterval: Day)` calls. As a result the
|
||||
console output template, the file path, and the rolling interval cannot be changed
|
||||
without recompiling — an operator cannot redirect logs, change the file location, or
|
||||
add a sink via configuration, contrary to REQ-HOST-8. The `ReadFrom.Configuration`
|
||||
@@ -761,7 +761,7 @@ role-specific file contained a `Serilog` section, so the two sinks that actually
|
||||
were hard-coded `.WriteTo.Console(...)` / `.WriteTo.File(...)` calls in `Program.cs`
|
||||
— uneditable without recompiling, contrary to REQ-HOST-8. Fix: added a `Serilog`
|
||||
section to `appsettings.json` with `WriteTo` entries for the Console and File sinks
|
||||
(carrying the output template, file path `logs/scadalink-.log` and `Day` rolling
|
||||
(carrying the output template, file path `logs/scadabridge-.log` and `Day` rolling
|
||||
interval as args) and removed the hard-coded `.WriteTo` calls from `Program.cs`; the
|
||||
factory's `ReadFrom.Configuration` (backed by the transitive
|
||||
`Serilog.Settings.Configuration` package) now drives the sinks. Regression tests in
|
||||
@@ -779,7 +779,7 @@ green (182 passed).
|
||||
| Severity | Low |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/StartupRetry.cs:36-45` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/StartupRetry.cs:36-45` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -832,13 +832,13 @@ Full Host suite green (182 passed).
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/appsettings.Site.json:33-37` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/appsettings.Site.json:33-37` |
|
||||
|
||||
**Description**
|
||||
|
||||
The shipped site config sets `Node:RemotingPort = 8082` and lists
|
||||
`Communication:CentralContactPoints` as
|
||||
`["akka.tcp://scadalink@localhost:8081", "akka.tcp://scadalink@localhost:8082"]`.
|
||||
`["akka.tcp://scadabridge@localhost:8081", "akka.tcp://scadabridge@localhost:8082"]`.
|
||||
The second contact point — port `8082` — is the **site's own** remoting endpoint,
|
||||
not a central node. `SiteCommunicationActor` / `ClusterClient` uses these
|
||||
addresses as initial contacts when discovering the central
|
||||
@@ -861,7 +861,7 @@ multi-node layout). Consider extending `StartupValidator` to reject any
|
||||
node's `NodeHostname`+`RemotingPort`. Add a regression test in
|
||||
`StartupValidatorTests` mirroring `Site_SeedNodeOnGrpcPort_FailsValidation`.
|
||||
|
||||
**Resolution (2026-05-28):** The shipped `appsettings.Site.json` `CentralContactPoints` entry that pointed at the site's own remoting port (`localhost:8082`) was removed — the dev-loopback default now lists only the single central node (`akka.tcp://scadalink@localhost:8081`), which is the actually-reachable target in the single-node dev layout. A `_centralContactPoints` doc-key comment was added immediately above the array calling out the per-entry rule (each entry MUST be a central node's remoting endpoint, not the site's own remoting port) and explaining how to extend the list with a second central node (`akka.tcp://scadalink@central-b-host:8081`) in a multi-central deployment so ClusterClient can fail over. The dangerous example pattern that would have been copied into multi-central configs no longer exists in the template. `StartupValidator` cross-check is left as a follow-up — the documented rule plus the corrected template removes the immediate misconfiguration risk.
|
||||
**Resolution (2026-05-28):** The shipped `appsettings.Site.json` `CentralContactPoints` entry that pointed at the site's own remoting port (`localhost:8082`) was removed — the dev-loopback default now lists only the single central node (`akka.tcp://scadabridge@localhost:8081`), which is the actually-reachable target in the single-node dev layout. A `_centralContactPoints` doc-key comment was added immediately above the array calling out the per-entry rule (each entry MUST be a central node's remoting endpoint, not the site's own remoting port) and explaining how to extend the list with a second central node (`akka.tcp://scadabridge@central-b-host:8081`) in a multi-central deployment so ClusterClient can fail over. The dangerous example pattern that would have been copied into multi-central configs no longer exists in the template. `StartupValidator` cross-check is left as a follow-up — the documented rule plus the corrected template removes the immediate misconfiguration risk.
|
||||
|
||||
### Host-017 — Site-shutdown ordering from REQ-HOST-7 is not wired
|
||||
|
||||
@@ -870,7 +870,7 @@ node's `NodeHostname`+`RemotingPort`. Add a regression test in
|
||||
| Severity | Medium |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/Program.cs:229-265`, `src/ScadaLink.Communication/Grpc/SiteStreamGrpcServer.cs` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/Program.cs:229-265`, `src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcServer.cs` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -904,7 +904,7 @@ before the Akka hosted service runs `CoordinatedShutdown` (or order via
|
||||
reverse-registration order, so the gRPC server's lifetime can be sequenced
|
||||
before Akka shutdown). Alternatively, reconcile REQ-HOST-7 with the actual
|
||||
implementation if the explicit ordering is no longer intended. Add an
|
||||
integration test under `tests/ScadaLink.Host.Tests` that starts a site host,
|
||||
integration test under `tests/ZB.MOM.WW.ScadaBridge.Host.Tests` that starts a site host,
|
||||
opens a stream, triggers shutdown, and asserts the stream completes with
|
||||
`Cancelled` before the actor system tears down.
|
||||
|
||||
@@ -928,7 +928,7 @@ ordering. Clients observe a clean `Cancelled` and reconnect rather than a
|
||||
silent stream that times out via keepalive (~25 s).
|
||||
|
||||
Two unit regression tests added to
|
||||
`tests/ScadaLink.Communication.Tests/Grpc/SiteStreamGrpcServerTests.cs`:
|
||||
`tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/SiteStreamGrpcServerTests.cs`:
|
||||
`Host017_CancelAllStreams_CancelsActiveStreamsAndRefusesNewOnes` (active
|
||||
streams complete, new ones rejected) and `Host017_CancelAllStreams_IsIdempotent`
|
||||
(double-call safe). A full site-host integration test was deferred — the
|
||||
@@ -942,7 +942,7 @@ unit suite covers both server-side invariants and the wiring is a single
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/appsettings.Central.json`, `src/ScadaLink.Host/appsettings.Site.json`, `src/ScadaLink.Host/NodeOptions.cs:10-16` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/appsettings.Central.json`, `src/ZB.MOM.WW.ScadaBridge.Host/appsettings.Site.json`, `src/ZB.MOM.WW.ScadaBridge.Host/NodeOptions.cs:10-16` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -955,8 +955,8 @@ telemetry and reconciliation, and is indexed via
|
||||
`IX_AuditLog_Node_Occurred (SourceNode, OccurredAtUtc)`. The docker per-node
|
||||
configs (`docker/central-node-a/appsettings.Central.json`,
|
||||
`docker/site-a-node-a/appsettings.Site.json`, etc.) all set
|
||||
`ScadaLink:Node:NodeName`. The **shipped, default** per-role files in
|
||||
`src/ScadaLink.Host/` — the templates a developer running the binary
|
||||
`ScadaBridge:Node:NodeName`. The **shipped, default** per-role files in
|
||||
`src/ZB.MOM.WW.ScadaBridge.Host/` — the templates a developer running the binary
|
||||
directly will use — do not. `NodeIdentityProvider` normalises an empty
|
||||
`NodeName` to `null`, so dev audit rows carry a null `SourceNode` and the
|
||||
indexed lookup never narrows. The dev examples should match the docker
|
||||
@@ -972,7 +972,7 @@ per-node in multi-node deployments. Consider validating in `StartupValidator`
|
||||
that `NodeName` is non-empty, or accept the null and document explicitly that
|
||||
single-node dev deployments leave `SourceNode` null.
|
||||
|
||||
**Resolution (2026-05-28):** The shipped per-role templates now set `ScadaLink:Node:NodeName` — `central-a` in `appsettings.Central.json` and `node-a` in `appsettings.Site.json` — so dev audit rows are stamped with a real `SourceNode` value (instead of `NodeIdentityProvider` normalising the missing key to `null`) and the indexed `IX_AuditLog_Node_Occurred` lookup actually narrows. A `_nodeName` doc-key comment was added beside each `Node` section explaining the convention (`central-a`/`central-b` for central, `node-a`/`node-b` for site), pointing at the docker per-node configs (which already overrode the field), and noting that the value must be overridden per-node in multi-node deployments and that an empty value still normalises to a `NULL` SourceNode. The shipped dev templates now match the per-node docker examples — a developer running the binary directly no longer sees a null `SourceNode`.
|
||||
**Resolution (2026-05-28):** The shipped per-role templates now set `ScadaBridge:Node:NodeName` — `central-a` in `appsettings.Central.json` and `node-a` in `appsettings.Site.json` — so dev audit rows are stamped with a real `SourceNode` value (instead of `NodeIdentityProvider` normalising the missing key to `null`) and the indexed `IX_AuditLog_Node_Occurred` lookup actually narrows. A `_nodeName` doc-key comment was added beside each `Node` section explaining the convention (`central-a`/`central-b` for central, `node-a`/`node-b` for site), pointing at the docker per-node configs (which already overrode the field), and noting that the value must be overridden per-node in multi-node deployments and that an empty value still normalises to a `NULL` SourceNode. The shipped dev templates now match the per-node docker examples — a developer running the binary directly no longer sees a null `SourceNode`.
|
||||
|
||||
### Host-019 — Migration `StartupRetry` call drops the host `CancellationToken`
|
||||
|
||||
@@ -981,7 +981,7 @@ single-node dev deployments leave `SourceNode` null.
|
||||
| Severity | Low |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/Program.cs:154-165` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/Program.cs:154-165` |
|
||||
|
||||
**Resolution (2026-05-28):** Added a `Func<CancellationToken, Task>` overload of `StartupRetry.ExecuteWithRetryAsync` that forwards the retry-loop token into the operation, and the migration call site in `Program.cs` now passes `app.Lifetime.ApplicationStopping` as both the operation token (threaded to `MigrationHelper.ApplyOrValidateMigrationsAsync`) and the loop's `cancellationToken` (already honoured by the inter-attempt `Task.Delay`). A SIGTERM during the bounded retry window now tears down cleanly instead of waiting up to ~2 minutes for the loop to exhaust. The original `Func<Task>` overload still exists and delegates, so existing callers/tests are unchanged.
|
||||
|
||||
@@ -1018,7 +1018,7 @@ _Open._
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/LoggerConfigurationFactory.cs:36-43` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/LoggerConfigurationFactory.cs:36-43` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1026,10 +1026,10 @@ _Open._
|
||||
via `ReadFrom.Configuration(configuration)` (which can include a
|
||||
`MinimumLevel` block — the standard Serilog way to set the floor) and **then**
|
||||
calls `.MinimumLevel.Is(minimumLevel)` derived from
|
||||
`ScadaLink:Logging:MinimumLevel`. Serilog's fluent builder applies the later
|
||||
`ScadaBridge:Logging:MinimumLevel`. Serilog's fluent builder applies the later
|
||||
call, so any `Serilog:MinimumLevel:Default` an operator sets is silently
|
||||
overridden by `ScadaLink:Logging:MinimumLevel` (or by its
|
||||
`Information` fallback when the ScadaLink key is absent). There are now two
|
||||
overridden by `ScadaBridge:Logging:MinimumLevel` (or by its
|
||||
`Information` fallback when the ScadaBridge key is absent). There are now two
|
||||
documented configuration paths for the same setting with non-obvious
|
||||
precedence, and the override direction is the opposite of what most Serilog
|
||||
users would expect (the more-specific `Serilog` section being the authority).
|
||||
@@ -1041,12 +1041,12 @@ but does not warn that the floor *overrides* the Serilog section's own
|
||||
|
||||
Pick one mechanism: either (a) drop the `MinimumLevel.Is` call and let
|
||||
`ReadFrom.Configuration` consume `Serilog:MinimumLevel`, migrating any docs/
|
||||
deployments that reference `ScadaLink:Logging:MinimumLevel`; or (b) keep the
|
||||
current "ScadaLink:Logging" path and reject `Serilog:MinimumLevel` if present
|
||||
deployments that reference `ScadaBridge:Logging:MinimumLevel`; or (b) keep the
|
||||
current "ScadaBridge:Logging" path and reject `Serilog:MinimumLevel` if present
|
||||
(throw at startup so the operator sees the conflict). At minimum, expand the
|
||||
XML doc + REQ-HOST-8 to spell out the precedence explicitly.
|
||||
|
||||
**Resolution (2026-05-28):** `ScadaLink:Logging:MinimumLevel` is now the documented single source of truth for the Serilog floor (Host-011's `LoggingOptions` binding), and the precedence is made visible — `LoggerConfigurationFactory.Build` writes a one-shot warning to `Console.Error` when both `ScadaLink:Logging:MinimumLevel` and `Serilog:MinimumLevel` (or `Serilog:MinimumLevel:Default`) are present, naming both values and pointing the operator at the documented key. Order of operations is unchanged — `MinimumLevel.Is(...)` deliberately runs after `ReadFrom.Configuration(...)` so the ScadaLink value wins — but the silent-override behaviour is now loud. The class XML doc gained a Host-020 paragraph explicitly spelling out the precedence. A test-visible `Build(..., TextWriter warningWriter)` overload mirrors the `ParseLevel` Host-022 pattern so the warning can be asserted in unit tests; the production four-arg overload delegates with `Console.Error`.
|
||||
**Resolution (2026-05-28):** `ScadaBridge:Logging:MinimumLevel` is now the documented single source of truth for the Serilog floor (Host-011's `LoggingOptions` binding), and the precedence is made visible — `LoggerConfigurationFactory.Build` writes a one-shot warning to `Console.Error` when both `ScadaBridge:Logging:MinimumLevel` and `Serilog:MinimumLevel` (or `Serilog:MinimumLevel:Default`) are present, naming both values and pointing the operator at the documented key. Order of operations is unchanged — `MinimumLevel.Is(...)` deliberately runs after `ReadFrom.Configuration(...)` so the ScadaBridge value wins — but the silent-override behaviour is now loud. The class XML doc gained a Host-020 paragraph explicitly spelling out the precedence. A test-visible `Build(..., TextWriter warningWriter)` overload mirrors the `ParseLevel` Host-022 pattern so the warning can be asserted in unit tests; the production four-arg overload delegates with `Console.Error`.
|
||||
|
||||
### Host-021 — Microsoft `Logging:LogLevel` section in `appsettings.json` is dead config under Serilog
|
||||
|
||||
@@ -1055,7 +1055,7 @@ XML doc + REQ-HOST-8 to spell out the precedence explicitly.
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/appsettings.json:2-6` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/appsettings.json:2-6` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1075,10 +1075,10 @@ the .NET convention) sees no behaviour change — the section is dead config.
|
||||
Either remove the `Logging:LogLevel` block from `appsettings.json` (Serilog
|
||||
owns logging configuration in this Host), or replace it with a brief comment
|
||||
explaining it is intentionally retained for non-Serilog tooling. Document the
|
||||
authoritative location (`Serilog` + `ScadaLink:Logging`) in
|
||||
authoritative location (`Serilog` + `ScadaBridge:Logging`) in
|
||||
`Component-Host.md` REQ-HOST-8 if not already explicit.
|
||||
|
||||
**Resolution (2026-05-28):** Confirmed by repository-wide grep that no code reads `Logging:LogLevel` (the Host calls `builder.Host.UseSerilog()` which replaces the default `ILoggerFactory` setup with Serilog as the only provider), so the block was pure dead config. Removed the `Logging:LogLevel:Default = Information` block from `appsettings.json` and replaced it with a `_logging` doc-key comment explaining the rationale (Serilog is the sole provider) and pointing operators at the two authoritative keys: `ScadaLink:Logging:MinimumLevel` for the floor (bound to `LoggingOptions` per Host-011) and the `Serilog` section for sinks (Host-014's `ReadFrom.Configuration`). The Host-014 regression test (`SerilogSinkConfigTests.ShippedAppSettings_HasSerilogSection_WithConsoleAndFileSinks`) still asserts the surviving `Serilog` section's shape, so removing the Microsoft block did not break the existing pinning.
|
||||
**Resolution (2026-05-28):** Confirmed by repository-wide grep that no code reads `Logging:LogLevel` (the Host calls `builder.Host.UseSerilog()` which replaces the default `ILoggerFactory` setup with Serilog as the only provider), so the block was pure dead config. Removed the `Logging:LogLevel:Default = Information` block from `appsettings.json` and replaced it with a `_logging` doc-key comment explaining the rationale (Serilog is the sole provider) and pointing operators at the two authoritative keys: `ScadaBridge:Logging:MinimumLevel` for the floor (bound to `LoggingOptions` per Host-011) and the `Serilog` section for sinks (Host-014's `ReadFrom.Configuration`). The Host-014 regression test (`SerilogSinkConfigTests.ShippedAppSettings_HasSerilogSection_WithConsoleAndFileSinks`) still asserts the surviving `Serilog` section's shape, so removing the Microsoft block did not break the existing pinning.
|
||||
|
||||
### Host-022 — `ParseLevel` silently coerces unrecognised `MinimumLevel` to `Information`
|
||||
|
||||
@@ -1087,7 +1087,7 @@ authoritative location (`Serilog` + `ScadaLink:Logging`) in
|
||||
| Severity | Low |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/LoggerConfigurationFactory.cs:50-55` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/LoggerConfigurationFactory.cs:50-55` |
|
||||
|
||||
**Resolution (2026-05-28):** `ParseLevel` now writes a one-shot warning to `Console.Error` (the logger isn't built yet at this point) when a non-null/non-blank `MinimumLevel` value fails to parse, naming the offending value and the `Information` fallback. Null/blank values continue to default silently (treated as "unset"). The helper gained a test-visible `TextWriter` overload so unit tests can capture the warning; the production path delegates to it with `Console.Error`. Tests `ParseLevel_UnrecognisedValue_FallsBackAndWarns`, `ParseLevel_NullOrBlank_FallsBackSilently`, and `ParseLevel_RecognisedValue_NoWarning` pin the behaviour.
|
||||
|
||||
@@ -1097,7 +1097,7 @@ authoritative location (`Serilog` + `ScadaLink:Logging`) in
|
||||
`Enum.TryParse<LogEventLevel>(level, ignoreCase: true, out var parsed)` and
|
||||
returns `LogEventLevel.Information` when parsing fails — without logging the
|
||||
fallback. An operator who sets
|
||||
`ScadaLink:Logging:MinimumLevel = "Informaiton"` (a common typo) or
|
||||
`ScadaBridge:Logging:MinimumLevel = "Informaiton"` (a common typo) or
|
||||
`"Verbose,Debug"` or any unrecognised value gets the default level silently;
|
||||
there is no warning, no log line, no startup error. Combined with Host-020
|
||||
(this is the only mechanism that pins the floor), a misspelt value is
|
||||
@@ -1111,7 +1111,7 @@ In `LoggerConfigurationFactory.Build`, when `loggingOptions.MinimumLevel` is
|
||||
non-null/non-blank but does not parse to a valid `LogEventLevel`, write a
|
||||
`Console.Error.WriteLine` warning (the logger is not yet built) and proceed
|
||||
with `Information`. Alternatively, validate the value in `StartupValidator`
|
||||
and fail fast — that matches the pattern used for other ScadaLink
|
||||
and fail fast — that matches the pattern used for other ScadaBridge
|
||||
configuration keys. Add a `LoggerConfigurationTests` case asserting the
|
||||
behaviour you choose.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user