# Component: Host ## Purpose The Host component is the single deployable executable for the entire ScadaBridge system. The same binary runs on every node — central and site alike. The node's role is determined entirely by configuration (`appsettings.json`), not by which binary is deployed. On central nodes the Host additionally bootstraps ASP.NET Core to serve the Central UI and Inbound API web endpoints. ## Location All nodes (central and site). ## Responsibilities - Serve as the single entry point (`Program.cs`) for the ScadaBridge process. - Read and validate node configuration at startup before any actor system is created. - Register the correct set of component services and actors based on the configured node role. - Bootstrap the Akka.NET actor system with Remoting, Clustering, and split-brain resolution from a hand-assembled, injection-safe HOCON document (`AkkaHostedService.BuildHocon`; see REQ-HOST-6). - Host ASP.NET Core web endpoints on central nodes only. - Configure structured logging (Serilog) with environment-specific enrichment. - Support running as a Windows Service in production and as a console application during development. - Perform graceful shutdown via Akka.NET CoordinatedShutdown when the service is stopped. --- ## Requirements ### REQ-HOST-1: Single Binary Deployment The same compiled binary must be deployable to both central and site nodes. The node's role (Central or Site) is determined solely by configuration values in `appsettings.json` (or environment-specific overrides). There must be no separate build targets, projects, or conditional compilation symbols for central vs. site. ### REQ-HOST-2: Role-Based Service Registration At startup the Host must inspect the configured node role and register only the component services appropriate for that role: - **Shared** (both Central and Site): ClusterInfrastructure, Communication, HealthMonitoring, ExternalSystemGateway. - **Central only**: TemplateEngine, DeploymentManager, Security, AuditLogging, CentralUI, InboundAPI, ManagementService, NotificationService, NotificationOutbox, SiteCallAudit. - **Site only**: SiteRuntime, DataConnectionLayer, StoreAndForward, SiteEventLogging. Components not applicable to the current role must not be registered in the DI container or the Akka.NET actor system. ### REQ-HOST-3: Configuration Binding The Host must bind configuration sections from `appsettings.json` to strongly-typed options classes using the .NET **Options pattern** (`IOptions` / `IOptionsSnapshot`). Each component has its own configuration section under `ScadaBridge`, mapped to a dedicated configuration class owned by that component. #### Infrastructure Sections | Section | Options Class | Owner | Contents | |---------|--------------|-------|----------| | `ScadaBridge:Node` | `NodeOptions` | Host | Role, NodeHostname, SiteId, RemotingPort, GrpcPort (site: hosts `SiteCommandService` + `SiteStreamGrpcServer`, default 8083), CentralGrpcPort (central: hosts `CentralControlService`, default 8083) | | `ScadaBridge:Cluster` | `ClusterOptions` | ClusterInfrastructure | SeedNodes, SplitBrainResolverStrategy, StableAfter, HeartbeatInterval, FailureDetectionThreshold, MinNrOfMembers | | `ScadaBridge:Database` | `DatabaseOptions` | Host | Central: ConfigurationDb, MachineDataDb connection strings; Site: SQLite paths | #### Per-Component Sections | Section | Options Class | Owner | Contents | |---------|--------------|-------|----------| | `ScadaBridge:DataConnection` | `DataConnectionOptions` | Data Connection Layer | ReconnectInterval, TagResolutionRetryInterval, WriteTimeout | | `ScadaBridge:StoreAndForward` | `StoreAndForwardOptions` | Store-and-Forward | SqliteDbPath, ReplicationEnabled | | `ScadaBridge:HealthMonitoring` | `HealthMonitoringOptions` | Health Monitoring | ReportInterval, OfflineTimeout | | `ScadaBridge:SiteEventLog` | `SiteEventLogOptions` | Site Event Logging | RetentionDays, MaxStorageMb, PurgeScheduleCron | | `ScadaBridge:Communication` | `CommunicationOptions` | Communication | DeploymentTimeout, LifecycleTimeout, QueryTimeout, TransportHeartbeatInterval, TransportFailureThreshold | | `ScadaBridge:Security` | `SecurityOptions` | Security & Auth | LdapServer, LdapPort, LdapUseTls, JwtSigningKey, JwtExpiryMinutes, IdleTimeoutMinutes | | `ScadaBridge:InboundApi` | `InboundApiOptions` | Inbound API | DefaultMethodTimeout, MaxRequestBodyBytes, ApiMethodCacheTtl, StandbyForwardPeerEndpoints (HTTP base URLs of the other central node — a standby relays `POST /api/*` there instead of answering 503), StandbyForwardTimeout | | `ScadaBridge:Notification` | `NotificationOptions` | Notification Service | (SMTP config is stored in the central config DB, not in appsettings) | | `ScadaBridge:NotificationOutbox` | `NotificationOutboxOptions` | Notification Outbox | Dispatcher poll interval, stuck-age threshold, retention window (delivery retry settings reuse the central SMTP configuration) | | `ScadaBridge:SiteCallAudit` | `SiteCallAuditOptions` | Site Call Audit | Reconciliation pull interval, stuck-age threshold, retention window | | `ScadaBridge:ManagementService` | `ManagementServiceOptions` | Management Service | (Reserved for future configuration) | | `ScadaBridge:Logging` | `LoggingOptions` | Host | Serilog sink configuration, log level overrides | #### Convention - Each component defines its own options class (e.g., `DataConnectionOptions`) in its own project. The class is a plain POCO with properties matching the JSON section keys. - The Host binds each section during startup via `services.Configure(configuration.GetSection("ScadaBridge:"))`. - Each component's `AddXxx()` extension method accepts `IServiceCollection` and reads its options via `IOptions` — the component never reads `IConfiguration` directly. - Options classes live in the component project, not in Commons, because they are component-specific configuration — not shared contracts. - Startup validation (REQ-HOST-4) validates all required options before the actor system starts. ### REQ-HOST-4: Startup Validation Before the Akka.NET actor system is created, the Host must validate all required configuration values and fail fast with a clear error message if any are missing or invalid. Validation rules include: - `NodeConfiguration.Role` must be a valid `NodeRole` value. - `NodeConfiguration.NodeHostname` must not be null or empty. - `NodeConfiguration.RemotingPort` must be in valid port range (1–65535). - Site nodes must have `GrpcPort` in valid port range (1–65535) and different from `RemotingPort`. - Site nodes must have a non-empty `SiteId`. - Central nodes must have non-empty `ConfigurationDb` and `MachineDataDb` connection strings. - Site nodes must have non-empty SQLite path values. Site nodes do **not** require a `ConfigurationDb` connection string — all configuration is received via artifact deployment and read from local SQLite. - At least two seed nodes must be configured. ### REQ-HOST-4a: Readiness Gating On central nodes, the ASP.NET Core web endpoints (Central UI, Inbound API) must **not accept traffic** until the node is fully operational: - Akka.NET cluster membership is established. - Database connectivity (MS SQL) is verified. - Required cluster singletons are running (if applicable). These are implemented as three `Ready`-tagged health checks registered in the Central-role branch of `Program.cs` (so they are naturally role-scoped — site nodes do not run them): `database` (`DatabaseHealthCheck`), `akka-cluster` (`AkkaClusterHealthCheck`), and `required-singletons` (`RequiredSingletonsHealthCheck`). The last verifies each *required-always* central singleton is reachable by Asking its local `ClusterSingletonProxy` an `Identify` with a short bounded timeout (~2s, probes run concurrently) and treating a non-null `ActorIdentity.Subject` as reachable; any unreachable required singleton degrades the check to **Unhealthy**, naming it. The required-always set is the five unconditional central singletons: notification-outbox, audit-log-ingest, site-call-audit, audit-log-purge, and site-audit-reconciliation. Feature-gated singletons are the "if applicable" case and are not probed when their feature is off. The check is leadership-agnostic — the proxy reaches the singleton from either central node, so a ready standby still reports ready (readiness must NOT require cluster leadership; that is the `Active` tier's job). During a brief singleton handover the probe may momentarily time out and the node may flap to not-ready, which is correct: a node mid-handover is legitimately not fully ready (no retries are used, to keep readiness polling fast). A standard ASP.NET Core health check endpoint (`/health/ready`) reports readiness status. The load balancer uses this endpoint to determine when to route traffic to the node. During startup or failover, the node returns `503 Service Unavailable` until ready. ### REQ-HOST-5: Windows Service Hosting The Host must support running as a Windows Service via `UseWindowsService()`. When launched outside of a Windows Service context (e.g., during development), it must run as a standard console application. No code changes or conditional compilation are required to switch between the two modes. ### REQ-HOST-6: Akka.NET Bootstrap The Host bootstraps the Akka.NET actor system from a **hand-assembled, injection-safe HOCON document** (`AkkaHostedService.BuildHocon`) passed to `ActorSystem.Create` — **not** Akka.Hosting's `AkkaConfigurationBuilder`/`AddAkka()`. Every interpolated value is routed through `QuoteHocon`/`DurationHocon` so a hostname, seed URI or strategy containing a quote, backslash or whitespace cannot corrupt the document, and each timing is rendered in milliseconds so sub-second values survive. The bound `ClusterOptions`/`NodeOptions`/`CommunicationOptions` are startup-validated (`ClusterOptionsValidator`, `StartupValidator`) so a misconfiguration fails fast with a key-naming message. The document configures: - **Remoting**: the node's hostname and port from `NodeOptions`. - **Clustering**: seed nodes and the node's cluster role(s) from configuration. - **Split-Brain Resolver**: the strategy, stable-after and `down-if-alone` from `ClusterOptions`, **with the downing provider named explicitly** (`akka.cluster.downing-provider-class = "Akka.Cluster.SBR.SplitBrainResolverProvider, Akka.Cluster"`). Akka's default is `NoDowning`, under which the entire SBR section is inert and singletons never migrate on a crash or partition — omitting this one line silently disables failover. This exact omission was arch-review 01's Critical finding; it is now guarded by tests asserting the parsed HOCON key **and** a two-node kill test asserting the behavior (downing + singleton migration). - **Coordinated shutdown**: `run-coordinated-shutdown-when-down = on` plus a cluster-leave phase timeout above the singleton drain budget (see REQ-HOST-4a / the down-if-alone recovery contract in Component-ClusterInfrastructure.md). - **Actor registration**: each component's actors registered conditional on the node's role. > **Bootstrap-mechanism note.** The `Akka.Hosting` / `Akka.Cluster.Hosting` / `Akka.Remote.Hosting` typed-builder packages were referenced but unused and were dropped (arch-review 01 Task 22). The hand-rolled HOCON path is a single hardened, production-shape code path with full test coverage; migrating to Akka.Hosting's typed options is deliberately **not** done here and is tracked as possible future work rather than carried as an unused dependency. Only `Akka.Cluster.Tools` (ClusterSingleton, transitively Akka.Cluster) remains — its ClusterClient/ClusterClientReceptionist surface is no longer used since Phase 4 moved cross-cluster messaging to gRPC. > **Persistence note.** ScadaBridge does not use Akka.Persistence. Durable state > (store-and-forward buffers, site event logs, static attribute writes, > deployment records, configuration) is owned by the individual components and > persisted through component-owned stores — SQLite at sites, MS SQL centrally — > not through an Akka journal/snapshot store. The Host therefore configures no > `akka.persistence` section and references no persistence plugin. There are no > `PersistentActor` subclasses in the system by design. ### REQ-HOST-6a: gRPC command/control services (no ClusterClientReceptionist) As of Phase 4 of the ClusterClient→gRPC migration (2026-07-23), the Host configures **no** `ClusterClientReceptionist` and registers **no** actor with one. Both remaining receptionist registrations were removed: - The central **CentralCommunicationActor** registration is gone. Sites now reach central command/control over gRPC by dialling the central-hosted **`CentralControlService`** (`GrpcCentralTransport`, sticky central-a→central-b failover), not via a ClusterClient locating a receptionist. - The site **SiteCommunicationActor** registration is gone. Central now reaches site command/control over gRPC by dialling the site-hosted **`SiteCommandService`** (`GrpcSiteTransport`, per-site NodeA→NodeB failover). Central hosts `CentralControlService` (site dials in); each site hosts `SiteCommandService` and `SiteStreamGrpcServer` (central dials in). Endpoints are dialled directly from configuration/DB, so there is no cross-cluster actor discovery. **The ManagementActor was never reachable over the receptionist either** (its registration was removed 2026-07-22, ahead of Phase 4). That registration was written for an out-of-cluster CLI that was never built: the shipped CLI speaks HTTP Basic to the central `/management` endpoints, which ask the ManagementActor **in-process** via `ManagementActorHolder` (`ManagementEndpoints.cs`). The actor itself still runs at `/user/management`. > **Migration note.** `Akka.Cluster.Tools` stays a Host dependency for ClusterSingleton; only its ClusterClient/ClusterClientReceptionist surface is now unused. See `docs/plans/2026-07-22-clusterclient-to-grpc-plan.md`. ### REQ-HOST-7: ASP.NET Web Endpoints On central nodes, the Host must use `WebApplication.CreateBuilder` to produce a full ASP.NET Core host with Kestrel, and must map web endpoints for: - Central UI (via `MapCentralUI()` extension method). - Inbound API (via `MapInboundAPI()` extension method). On site nodes, the Host must also use `WebApplication.CreateBuilder` (not `Host.CreateDefaultBuilder`) to host the **SiteStreamGrpcServer** and the **SiteCommandService** (central→site command/control, added in Phase 4) via Kestrel HTTP/2 on the configured `GrpcPort` (default 8083). Kestrel is configured with `HttpProtocols.Http2` on the gRPC port only — no HTTP/1.1 web endpoints are exposed. On central nodes, the Host additionally hosts the **CentralControlService** (site→central command/control) via Kestrel HTTP/2 on the configured `CentralGrpcPort` (default 8083), alongside the HTTP/1.1 Central UI / Inbound API endpoints. The gRPC services are mapped via `MapGrpcService<>()`. **Startup ordering (site nodes)**: 1. Actor system and SiteStreamManager must be initialized before gRPC begins accepting connections. 2. The gRPC server rejects streams with `StatusCode.Unavailable` until the actor system is ready. **Shutdown ordering (site nodes)**: 1. On `CoordinatedShutdown`, stop accepting new gRPC streams first. 2. Cancel all active gRPC streams (triggering client-side reconnect). 3. Tear down actors. 4. Use `IHostApplicationLifetime.ApplicationStopping` to signal the gRPC server. ### REQ-HOST-8: Structured Logging The Host must configure Serilog as the logging provider with: - Configuration-driven sink setup (console and file sinks at minimum). - Automatic enrichment of every log entry with `SiteId`, `NodeHostname`, and `NodeRole` properties sourced from `NodeConfiguration`. - Structured (machine-parseable) output format. ### REQ-HOST-8a: Dead Letter Monitoring The Host must subscribe to the Akka.NET `DeadLetter` event stream and log dead letters at Warning level. Dead letters indicate messages sent to actors that no longer exist — a common symptom of failover timing issues, stale actor references, or race conditions during instance lifecycle transitions. The dead letter count is reported as a health metric (see Health Monitoring). ### REQ-HOST-9: Graceful Shutdown When the Host process receives a stop signal (Windows Service stop, `Ctrl+C`, or SIGTERM), it must trigger Akka.NET CoordinatedShutdown to allow actors to drain in-flight work before the process exits. The Host must not call `Environment.Exit()` or forcibly terminate the actor system without coordinated shutdown. ### REQ-HOST-10: Extension Method Convention Each component library must expose its services to the Host via a consistent set of extension methods: - `IServiceCollection.AddXxx()` — registers the component's DI services. - `AkkaConfigurationBuilder.AddXxxActors()` — registers the component's actors with the Akka.NET actor system (for components that have actors). - `WebApplication.MapXxx()` — maps the component's web endpoints (only for CentralUI and InboundAPI). The Host's `Program.cs` calls these extension methods; the component libraries own the registration logic. This keeps the Host thin and each component self-contained. (The ManagementService component no longer registers the ManagementActor with ClusterClientReceptionist — that registration was removed 2026-07-22, and the whole receptionist surface went in Phase 4.) ### REQ-HOST-11: Dependency-Injection Lifetime Discipline Akka actors are constructed by the actor system, not by the DI container, so the container cannot enforce lifetimes on them. Three rules therefore apply, and each has a mechanical guard. **1. An actor may hold the root `IServiceProvider` (or an `IServiceScopeFactory`) for its lifetime — never a scope or anything resolved from one.** Every operation that needs a scoped service opens its own scope (`CreateScope` / `CreateAsyncScope`, disposed in the same handler) and resolves inside it. Caching the resolved instance in a field is a **captive dependency**: the actor keeps one EF `DbContext` alive for its whole lifetime and any concurrency the actor allows then collides on it. This is not hypothetical — it shipped as register row 45, where `NotificationOutboxActor` cached delivery adapters from an actor-lifetime scope and `MaxParallelDeliveries` concurrent deliveries drove EF's "a second operation was started on this context instance" continuously on the rig. *Guard:* `CaptiveDependencyGuard` (Host test suite) reflects over every actor class in the shipped assemblies and fails on any instance field — auto-property backing fields included — typed as `IServiceScope`, as a `DbContext`, or as a service the real composition roots register `Scoped`. The scoped set is derived from the actual registrations of the built Central and Site containers rather than hand-listed, so changing a registration's lifetime automatically re-scopes the check. `IServiceProvider` / `IServiceScopeFactory` fields are explicitly permitted. Exemptions live in a small allowlist inside the guard, each carrying its safety argument, and a self-test fails any allowlist entry that has stopped matching a real field. **2. Scope validation is on in Development, for both roles.** `Program.cs` applies `ValidateScopes` + `ValidateOnBuild` via `ServiceProviderValidation` in both the Central and Site branches. This is the container-visible half of the same concern — it catches a singleton whose *constructor* takes a scoped service, which is the ordinary form of the bug in non-actor code. It cannot see rule 1's hand-rolled `GetRequiredService` calls, and rule 1's guard cannot see constructor injection; neither subsumes the other. Production runs unvalidated deliberately: a validation failure is fail-closed, and a defect must not take a production node down at boot when Development and CI give the same signal. **3. A registration must be constructible on every role that registers it.** A service whose dependencies exist only on the other role belongs in a role-scoped extension method (`AddCentralCommunication`, `AddAuditLogCentralKpi`), not in a shared `AddXxx()` on the argument that "only the central actor ever resolves it". An unconstructible descriptor is not inert: it fails `ValidateOnBuild` for the *entire* graph, which is what previously kept rule 2 switched off on the Site branch and would have masked any genuine defect the validator found there. --- ### Boot-time facts the Host supplies to Health Monitoring Health Monitoring reports two things the Host alone knows, and in both cases the Host reads the configuration and hands over a captured value rather than teaching the component to read it. This is the `SiteEventLogReplicationCheck` pattern generalised: configuration-reading rules live in one place, and a component that merely *reports* a fact never grows a dependency on where it comes from. **LocalDb replication configured-ness** (`SiteServiceRegistration`, register row 80). The replication engine is registered unconditionally and idles without a peer, so `ISyncStatus` answers `Connected = false` with a real `0` backlog on a node that was never meant to replicate. Passing that through made every deliberately-unreplicated site publish "disconnected, empty backlog". The Host now evaluates `SiteLocalDbSetup.ReplicationIsConfigured(config)` **once** and passes it into `AddLocalDbReplicationHealthBridge` as a third provider; an unconfigured node publishes `null` for both LocalDb health fields. The same captured value feeds `SiteEventLogReplicationCheck`, so the two surfaces can never disagree about whether this node replicates. Note the rule is `PeerAddress` **OR** `ApiKey` — only the dialling half sets a peer address. **Startup reconciler presence** (`AkkaHostedService`, register row 73). The Host decides whether to spawn `SiteReconciliationActor` — it needs an `IDeploymentConfigFetcher`, and skips the actor when none is resolvable. That skip was previously a lone boot Warning, so a node with no self-heal path at all reported itself perfectly healthy. Both branches now call `ISiteHealthCollector.SetStartupReconcilerActive(...)`, latching the fact onto every subsequent health report as tri-state `StartupReconcilerActive`; the skip branch additionally logs at **Error** (raised from Warning — no configuration makes the fetcher legitimately absent on a site node) and writes a best-effort `deployment`/`Error` site event so the evidence outlives the boot log. Central renders it as a *No self-heal* warning badge. See [Component-HealthMonitoring.md](Component-HealthMonitoring.md) → *Startup Reconciler Presence*. ### Standby health-check log noise `LoggerConfigurationFactory` installs one Serilog exclusion filter, for the by-design standby verdict of the `active-node` probe (register row 71). `DefaultHealthCheckService` logs one line per check per probe and picks the level from the verdict — Debug / Warning / **Error** for Healthy / Degraded / Unhealthy. Both active-node checks (`SitePairActiveNodeHealthCheck` on a site, the shared `ActiveNodeHealthCheck` on central) answer Unhealthy on the standby, because 503 is exactly the contract `/health/active` publishes to Traefik and the family dashboard. A perfectly healthy standby therefore logged ERR on every probe — every 10 s on a site node, every 5 s on a central node behind Traefik's active-node probe — which floods the log and trains operators to ignore ERR. Serilog's `MinimumLevel.Override` can only *raise* a per-source floor and a `LogEvent`'s level is immutable, so neither an override nor an enricher can rewrite the event; excluding it is the only surgical option. The predicate matches on `Level == Error` **and** the health-check logger category **and** `HealthCheckName == "active-node"`. Keying on the check name rather than the category is what preserves the constraint: a failing `localdb`, `akka-cluster`, `database` or `required-singletons` check still logs at Error, and the active-node check has no error path of its own (the active node always answers Healthy), so the only event this can ever suppress is a standby reporting that it is a standby. The verdict remains fully observable through the HTTP status of `/health/active` itself, the readiness probe's cluster view, and the health dashboard. --- ## Secrets (2026-09-28) The Host composes `ZB.MOM.WW.Secrets` (a `scadaproj` shared library, not a ScadaBridge component — hence it has no row in the Component Registration Matrix below) via `SecretsRegistration.AddScadaBridgeSecrets`, called on both roles from their respective composition-root `Configure` methods. Design: `docs/plans/2026-09-28-site-secrets-artifact-deploy-design.md` (see its *Implementation notes (as built)* section for where the shipped code deviates from the original design). - **Site role — always a local, undecorated store.** A site node's `ISecretStore`/`ISecretResolver` is unconditionally the plain node-local SQLite store (`AddZbSecrets`), regardless of any `Secrets:Store:Provider` value — a Site never reads that key. This is a hard invariant, not just a default: `DeployedSecretsMirror` (Component-SiteRuntime.md) calls `ISecretStore.DeleteAsync` directly on the assumption that a delete is node-local; a replication-decorated store would have published that delete fleet-wide. Secrets reach a site only via Deploy Artifacts (see Component-DeploymentManager.md → *Site secrets*) — there is no other channel. - **Central role — `Secrets:Store:Provider`** (`Sqlite` default | `SqlServer`), read **only** on the Central role: - `Sqlite` (default): each central node keeps its own per-node SQLite secret store. On a **clustered** central (≥ 2 seed nodes) this is a documented misconfiguration — `StartupValidator` logs a boot-time Warning (same shape as the inbound-API-key-store precedent, register row 108), because the two nodes would deploy different secret sets to sites depending on which is active. - `SqlServer`: both central nodes read and write **one shared SQL Server store** (`Secrets:SqlServer:ConnectionString`, a literal/env value — never a `${secret:}` reference, the same bootstrap-circularity rule the inbound-API-key shared store follows) — the `/admin/secrets` page edits it, and it is the store `SiteSecretsCollector` resolves from. The pre-host `${secret:}` expander (the Layer-A pass that resolves `${secret:X}` tokens inside `appsettings` itself, before the DI container is built) reads the **same** shared store when `Provider = SqlServer`, so a central node's boot-time config tokens and its running `ISecretResolver` never disagree about what a token resolves to. - A connection string that is blank, or is itself a `${secret:}` reference (a bootstrap circularity), fails fast naming the offending key. - **Bootstrap secrets are never deployed.** The site's gRPC PSK (`SB-GRPC-PSK-{siteId}`), the LocalDb replication key, and the secrets master key (KEK) are never data-connection-referenced and are never shipped through Deploy Artifacts — a site needs them *before* it can receive a deploy at all, and pushing a rotated one through the channel it authenticates would sever that channel. They stay out-of-band: service environment variables, a hand-seeded store, or a master-key file, exactly as before this feature. Every node — every central and every site — **must share the same KEK**; see `docs/deployment/topology-guide.md` → *Secrets: shared store, per-site deploy, and the KEK* for where that is (and is not) checked. - **The gRPC secrets hub is retired in ScadaBridge (2026-09-28).** ScadaBridge no longer composes `ZB.MOM.WW.Secrets.Replicator.Grpc` on either role, and the historical SQL-Server *replication* mode (`Secrets:Replication:Mode=SqlServer` — distinct from `Secrets:Store:Provider=SqlServer` above, which shares one store rather than replicating between two) is retired too. Setting `Secrets:Replication:Enabled` to anything other than absent/empty/`false` now fails `StartupValidator.Validate` outright, on **either** role, with a message pointing at Deploy Artifacts (sites) and `Secrets:Store:Provider=SqlServer` (a clustered central). A leftover `Secrets:Replication:Mode` or any `Secrets:GrpcHub:*` key (harmless once `Enabled` is unset, but dead configuration) logs a startup Warning naming both sections so they can be cleaned up. The library itself keeps the hub for other consumers; only ScadaBridge stops using it. --- ## Component Registration Matrix | Component | Central | Site | DI (`AddXxx`) | Actors (`AddXxxActors`) | Endpoints (`MapXxx`) | |---|---|---|---|---|---| | ClusterInfrastructure | Yes | Yes | Yes | Yes | No | | Communication | Yes | Yes | Yes | Yes | No | | HealthMonitoring | Yes | Yes | Yes | Yes | No | | ExternalSystemGateway | Yes | Yes | Yes | Yes | No | | AuditLog | Yes | Yes | Yes | Yes | No | | NotificationService | Yes | No | Yes | Yes | No | | NotificationOutbox | Yes | No | Yes | Yes | No | | SiteCallAudit | Yes | No | Yes | Yes | No | | KpiHistory | Yes | No | Yes | Yes | No | | Transport | Yes | No | Yes | No | No | | TemplateEngine | Yes | No | Yes | Yes | No | | DeploymentManager | Yes | No | Yes | Yes | No | | Security | Yes | No | Yes | Yes | No | | CentralUI | Yes | No | Yes | No | Yes | | InboundAPI | Yes | No | Yes | No | Yes | | ManagementService | Yes | No | Yes | Yes | No | | SiteRuntime | No | Yes | Yes | Yes | No | | DataConnectionLayer | No | Yes | Yes | Yes | No | | StoreAndForward | No | Yes | Yes | Yes | No | | SiteEventLogging | No | Yes | Yes | Yes | No | | ConfigurationDatabase | Yes | No | Yes | No | No | --- ## Dependencies - **All 19 component libraries**: The Host references every component project to call their extension methods (excludes CLI, which is a separate executable). Audit Log (#23) ships its central+site code in `ZB.MOM.WW.ScadaBridge.AuditLog`; the Host calls `AddAuditLog()` on both roles, M2+ will add `AddAuditLogActors()`. - **Akka.Cluster.Tools**: ClusterSingleton (singleton manager/proxy); transitively pulls Akka.Cluster (SBR provider) and Akka.Remote. Its ClusterClient/ClusterClientReceptionist surface is no longer used — cross-cluster messaging is gRPC as of Phase 4. The actor system is built from hand-assembled HOCON (REQ-HOST-6), so the `Akka.Hosting`/`Akka.Remote.Hosting`/`Akka.Cluster.Hosting` typed-builder packages are **not** referenced (dropped in arch-review 01 Task 22). No Akka.Persistence plugin — see the Persistence note under REQ-HOST-6. - **Serilog.AspNetCore**: For structured logging integration. - **Microsoft.Extensions.Hosting.WindowsServices**: For Windows Service support. - **ASP.NET Core** (central only): For web endpoint hosting. ## Interactions - **All components**: The Host is the composition root — it wires every component into the DI container and actor system. - **Configuration Database**: The Host registers the DbContext and wires repository implementations to their interfaces. In development, triggers auto-migration; in production, validates schema version. - **ClusterInfrastructure**: The Host configures the underlying Akka.NET cluster that ClusterInfrastructure manages at runtime. - **CentralUI / InboundAPI**: The Host maps their web endpoints into the ASP.NET Core pipeline on central nodes. - **ManagementService**: The Host registers the ManagementActor on central nodes; the CLI reaches it over the HTTP `/management` endpoints (in-process via `ManagementActorHolder`), not via any cluster transport. No ClusterClientReceptionist is configured (removed in Phase 4). - **HealthMonitoring**: The Host's startup validation and logging configuration provide the foundation for health reporting.