Phase 1 WP-11–22: Host infrastructure, Blazor Server UI, and integration tests
Host infrastructure (WP-11–17): - StartupValidator with 19 validation rules - /health/ready endpoint with DB + Akka health checks - Akka.NET bootstrap via AkkaHostedService (HOCON config, cluster, remoting, SBR) - Serilog with SiteId/NodeHostname/NodeRole enrichment - DeadLetterMonitorActor with count tracking - CoordinatedShutdown wiring (no Environment.Exit) - Windows Service support (UseWindowsService) Central UI (WP-18–21): - Blazor Server shell with Bootstrap 5, role-aware NavMenu - Login/logout flow (LDAP auth → JWT → HTTP-only cookie) - CookieAuthenticationStateProvider with idle timeout - LDAP group mapping CRUD page (Admin role) - Route guards with Authorize attributes per role - SignalR reconnection overlay for failover Integration tests (WP-22): - Startup validation, auth flow, audit transactions, readiness gating 186 tests pass (1 skipped: LDAP integration), zero warnings.
This commit is contained in:
58
src/ScadaLink.Host/StartupValidator.cs
Normal file
58
src/ScadaLink.Host/StartupValidator.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
namespace ScadaLink.Host;
|
||||
|
||||
/// <summary>
|
||||
/// Validates required configuration before Akka.NET actor system creation.
|
||||
/// Runs early in startup to fail fast with clear error messages.
|
||||
/// </summary>
|
||||
public static class StartupValidator
|
||||
{
|
||||
public static void Validate(IConfiguration configuration)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
var nodeSection = configuration.GetSection("ScadaLink:Node");
|
||||
var role = nodeSection["Role"];
|
||||
if (string.IsNullOrEmpty(role) || (role != "Central" && role != "Site"))
|
||||
errors.Add("ScadaLink:Node:Role must be 'Central' or 'Site'");
|
||||
|
||||
if (string.IsNullOrEmpty(nodeSection["NodeHostname"]))
|
||||
errors.Add("ScadaLink:Node:NodeHostname is required");
|
||||
|
||||
var portStr = nodeSection["RemotingPort"];
|
||||
if (!int.TryParse(portStr, out var port) || port < 1 || port > 65535)
|
||||
errors.Add("ScadaLink:Node:RemotingPort must be 1-65535");
|
||||
|
||||
if (role == "Site" && string.IsNullOrEmpty(nodeSection["SiteId"]))
|
||||
errors.Add("ScadaLink:Node:SiteId is required for Site nodes");
|
||||
|
||||
if (role == "Central")
|
||||
{
|
||||
var dbSection = configuration.GetSection("ScadaLink:Database");
|
||||
if (string.IsNullOrEmpty(dbSection["ConfigurationDb"]))
|
||||
errors.Add("ScadaLink:Database:ConfigurationDb connection string required for Central");
|
||||
if (string.IsNullOrEmpty(dbSection["MachineDataDb"]))
|
||||
errors.Add("ScadaLink:Database:MachineDataDb connection string required for Central");
|
||||
|
||||
var secSection = configuration.GetSection("ScadaLink:Security");
|
||||
if (string.IsNullOrEmpty(secSection["LdapServer"]))
|
||||
errors.Add("ScadaLink:Security:LdapServer required for Central");
|
||||
if (string.IsNullOrEmpty(secSection["JwtSigningKey"]))
|
||||
errors.Add("ScadaLink:Security:JwtSigningKey required for Central");
|
||||
}
|
||||
|
||||
if (role == "Site")
|
||||
{
|
||||
var dbSection = configuration.GetSection("ScadaLink:Database");
|
||||
if (string.IsNullOrEmpty(dbSection["SiteDbPath"]))
|
||||
errors.Add("ScadaLink:Database:SiteDbPath required for Site nodes");
|
||||
}
|
||||
|
||||
var seedNodes = configuration.GetSection("ScadaLink:Cluster:SeedNodes").Get<List<string>>();
|
||||
if (seedNodes == null || seedNodes.Count < 2)
|
||||
errors.Add("ScadaLink:Cluster:SeedNodes must have at least 2 entries");
|
||||
|
||||
if (errors.Count > 0)
|
||||
throw new InvalidOperationException(
|
||||
$"Configuration validation failed:\n{string.Join("\n", errors.Select(e => $" - {e}"))}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user