using HealthChecks.UI.Client; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using ScadaLink.CentralUI; using ScadaLink.ClusterInfrastructure; using ScadaLink.Communication; using ScadaLink.ConfigurationDatabase; using ScadaLink.DataConnectionLayer; using ScadaLink.DeploymentManager; using ScadaLink.ExternalSystemGateway; using ScadaLink.HealthMonitoring; using ScadaLink.Host; using ScadaLink.Host.Actors; using ScadaLink.Host.Health; using ScadaLink.InboundAPI; using ScadaLink.NotificationService; using ScadaLink.Security; using ScadaLink.SiteEventLogging; using ScadaLink.SiteRuntime; using ScadaLink.StoreAndForward; using ScadaLink.TemplateEngine; using Serilog; // SCADALINK_CONFIG determines which role-specific config to load (Central or Site) // DOTNET_ENVIRONMENT/ASPNETCORE_ENVIRONMENT stay as "Development" for dev tooling (static assets, EF migrations, etc.) var scadalinkConfig = Environment.GetEnvironmentVariable("SCADALINK_CONFIG") ?? Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production"; var configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json", optional: false) .AddJsonFile($"appsettings.{scadalinkConfig}.json", optional: true) .AddEnvironmentVariables() .AddCommandLine(args) .Build(); // WP-11: Full startup validation — fail fast before any DI or actor system setup StartupValidator.Validate(configuration); // Read node options for Serilog enrichment var nodeRole = configuration["ScadaLink:Node:Role"]!; var nodeHostname = configuration["ScadaLink:Node:NodeHostname"] ?? "unknown"; var siteId = configuration["ScadaLink:Node:SiteId"] ?? "central"; // WP-14: Serilog structured logging Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(configuration) .Enrich.WithProperty("SiteId", siteId) .Enrich.WithProperty("NodeHostname", nodeHostname) .Enrich.WithProperty("NodeRole", nodeRole) .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{NodeRole}/{NodeHostname}] {Message:lj}{NewLine}{Exception}") .WriteTo.File("logs/scadalink-.log", rollingInterval: Serilog.RollingInterval.Day) .CreateLogger(); try { Log.Information("Starting ScadaLink host as {Role} on {Hostname}", nodeRole, nodeHostname); if (nodeRole.Equals("Central", StringComparison.OrdinalIgnoreCase)) { var builder = WebApplication.CreateBuilder(args); builder.Configuration.AddConfiguration(configuration); // WP-14: Serilog builder.Host.UseSerilog(); // WP-17: Windows Service support (no-op when not running as a Windows Service) builder.Host.UseWindowsService(); // Shared components builder.Services.AddClusterInfrastructure(); builder.Services.AddCommunication(); builder.Services.AddHealthMonitoring(); builder.Services.AddCentralHealthAggregation(); builder.Services.AddExternalSystemGateway(); builder.Services.AddNotificationService(); // Central-only components builder.Services.AddTemplateEngine(); builder.Services.AddDeploymentManager(); builder.Services.AddSecurity(); builder.Services.AddCentralUI(); builder.Services.AddInboundAPI(); var configDbConnectionString = configuration["ScadaLink:Database:ConfigurationDb"] ?? throw new InvalidOperationException("ScadaLink:Database:ConfigurationDb connection string is required for Central role."); builder.Services.AddConfigurationDatabase(configDbConnectionString); // WP-12: Health checks for readiness gating builder.Services.AddHealthChecks() .AddCheck("database") .AddCheck("akka-cluster"); // WP-13: Akka.NET bootstrap via hosted service builder.Services.AddSingleton(); builder.Services.AddHostedService(sp => sp.GetRequiredService()); // Options binding BindSharedOptions(builder.Services, builder.Configuration); builder.Services.Configure(builder.Configuration.GetSection("ScadaLink:Security")); builder.Services.Configure(builder.Configuration.GetSection("ScadaLink:InboundApi")); var app = builder.Build(); // Apply or validate database migrations (skip when running in test harness) if (!string.Equals(configuration["ScadaLink:Database:SkipMigrations"], "true", StringComparison.OrdinalIgnoreCase)) { var isDevelopment = app.Environment.IsDevelopment() || string.Equals(Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"), "Development", StringComparison.OrdinalIgnoreCase); using (var scope = app.Services.CreateScope()) { var dbContext = scope.ServiceProvider.GetRequiredService(); await MigrationHelper.ApplyOrValidateMigrationsAsync(dbContext, isDevelopment); } } // Middleware pipeline app.UseStaticFiles(); app.UseWebSockets(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseAntiforgery(); // WP-12: Map readiness endpoint — returns 503 until all checks pass, 200 when ready app.MapHealthChecks("/health/ready", new HealthCheckOptions { ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse }); app.MapCentralUI(); app.MapInboundAPI(); await app.RunAsync(); } else if (nodeRole.Equals("Site", StringComparison.OrdinalIgnoreCase)) { var builder = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder(args); builder.ConfigureAppConfiguration(config => config.AddConfiguration(configuration)); // WP-14: Serilog builder.UseSerilog(); // WP-17: Windows Service support (no-op when not running as a Windows Service) builder.UseWindowsService(); builder.ConfigureServices((context, services) => { // Shared components services.AddClusterInfrastructure(); services.AddCommunication(); services.AddHealthMonitoring(); services.AddExternalSystemGateway(); services.AddNotificationService(); // Site-only components — AddSiteRuntime registers SiteStorageService with SQLite path var siteDbPath = context.Configuration["ScadaLink:Database:SiteDbPath"] ?? "site.db"; services.AddSiteRuntime($"Data Source={siteDbPath}"); services.AddDataConnectionLayer(); services.AddStoreAndForward(); services.AddSiteEventLogging(); // WP-13: Akka.NET bootstrap via hosted service services.AddSingleton(); services.AddHostedService(sp => sp.GetRequiredService()); // Options binding BindSharedOptions(services, context.Configuration); services.Configure(context.Configuration.GetSection("ScadaLink:SiteRuntime")); services.Configure(context.Configuration.GetSection("ScadaLink:DataConnection")); services.Configure(context.Configuration.GetSection("ScadaLink:StoreAndForward")); services.Configure(context.Configuration.GetSection("ScadaLink:SiteEventLog")); }); var host = builder.Build(); await host.RunAsync(); } else { throw new InvalidOperationException($"Unknown role: {nodeRole}. Must be 'Central' or 'Site'."); } } catch (Exception ex) { Log.Fatal(ex, "ScadaLink host terminated unexpectedly"); throw; } finally { await Log.CloseAndFlushAsync(); } static void BindSharedOptions(IServiceCollection services, IConfiguration config) { services.Configure(config.GetSection("ScadaLink:Node")); services.Configure(config.GetSection("ScadaLink:Cluster")); services.Configure(config.GetSection("ScadaLink:Database")); services.Configure(config.GetSection("ScadaLink:Communication")); services.Configure(config.GetSection("ScadaLink:HealthMonitoring")); services.Configure(config.GetSection("ScadaLink:Notification")); services.Configure(config.GetSection("ScadaLink:Logging")); } /// /// Exposes the auto-generated Program class for test infrastructure (e.g. WebApplicationFactory). /// public partial class Program { }