diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/ServiceCollectionExtensions.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..ef40b6f --- /dev/null +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/ServiceCollectionExtensions.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace ZB.MOM.WW.OtOpcUa.Configuration; + +public static class ServiceCollectionExtensions +{ + public const string ConnectionStringName = "ConfigDb"; + + /// + /// Registers for + /// using the connection string named ConfigDb from . + /// + public static IServiceCollection AddOtOpcUaConfigDb(this IServiceCollection services, IConfiguration configuration) + { + var connectionString = configuration.GetConnectionString(ConnectionStringName) + ?? throw new InvalidOperationException( + $"Connection string '{ConnectionStringName}' is required. Add it to appsettings.json or the OTOPCUA_CONFIG_CONNECTION env var."); + + services.AddDbContextFactory(opt => opt.UseSqlServer(connectionString)); + return services; + } +} diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/App.razor b/src/Server/ZB.MOM.WW.OtOpcUa.Host/App.razor new file mode 100644 index 0000000..9beb28c --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/App.razor @@ -0,0 +1,19 @@ +@* Root Blazor component for the fused OtOpcUa.Host. Pulls in the AdminUI library's + _Imports + the Deployments page. The full layout (sidebar, top bar, etc.) is part of + the legacy Admin migration tracked as F15 — for now this is the bare minimum that lets + the Razor pipeline render. *@ + + + + + + + + + + + + + + + diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/MainLayout.razor b/src/Server/ZB.MOM.WW.OtOpcUa.Host/MainLayout.razor new file mode 100644 index 0000000..d462d4e --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/MainLayout.razor @@ -0,0 +1,11 @@ +@inherits LayoutComponentBase + +
+ +
+ @Body +
+
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs index 404d207..9730780 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs @@ -1,4 +1,87 @@ +using Akka.Hosting; +using Serilog; +using ZB.MOM.WW.OtOpcUa.AdminUI; +using ZB.MOM.WW.OtOpcUa.AdminUI.Clients; +using ZB.MOM.WW.OtOpcUa.AdminUI.Hubs; +using ZB.MOM.WW.OtOpcUa.Cluster; +using ZB.MOM.WW.OtOpcUa.Configuration; +using ZB.MOM.WW.OtOpcUa.ControlPlane; +using ZB.MOM.WW.OtOpcUa.Host; +using ZB.MOM.WW.OtOpcUa.Host.Health; +using ZB.MOM.WW.OtOpcUa.Security; +using ZB.MOM.WW.OtOpcUa.Security.Endpoints; + +// Roles drive the entire conditional wiring below — see ZB.MOM.WW.OtOpcUa.Cluster.RoleParser. +var roles = RoleParser.Parse(Environment.GetEnvironmentVariable("OTOPCUA_ROLES")); +var hasAdmin = roles.Contains("admin"); +var hasDriver = roles.Contains("driver"); + var builder = WebApplication.CreateBuilder(args); + +// Per-role appsettings overlay: appsettings.{role}.json (single role) or appsettings.admin-driver.json +// (both). Optional — base appsettings.json carries enough to boot if these don't exist. +var roleSuffix = roles.Length == 0 ? null : string.Join('-', roles.OrderBy(r => r, StringComparer.Ordinal)); +if (roleSuffix is not null) + builder.Configuration.AddJsonFile($"appsettings.{roleSuffix}.json", optional: true, reloadOnChange: true); + +// Serilog — rolling daily file sink per CLAUDE.md. Console for local dev. +builder.Host.UseSerilog((ctx, lc) => lc + .ReadFrom.Configuration(ctx.Configuration) + .WriteTo.Console() + .WriteTo.File("logs/otopcua-.log", rollingInterval: RollingInterval.Day)); + +// Windows-service registration is handled at install time by scripts/install/Install-Services.ps1 +// (Task 62) rather than in-process, so the binary stays cross-platform-compilable. + +// Shared services — always registered regardless of role. ConfigDb is required for everything. +builder.Services.AddOtOpcUaConfigDb(builder.Configuration); +builder.Services.AddOtOpcUaCluster(builder.Configuration); + +// Akka cluster bootstrap. Role-specific singletons are registered on the AkkaConfigurationBuilder +// from inside the configurator lambda. AddAkka spins the ActorSystem at host start. +builder.Services.AddAkka("otopcua", (ab, _) => +{ + if (hasAdmin) + ab.WithOtOpcUaControlPlaneSingletons(); + // Driver-role startup (DriverHostActor spawn + child probes) is wired in F19 once a + // RuntimeStartup contract is added — the actor itself exists (Phase 6), the registration + // extension does not yet. Without it, driver-role nodes still join the cluster and serve + // health/redundancy traffic but won't auto-spawn DriverHostActor. +}); + +if (hasAdmin) +{ + // Auth + AdminUI surface only mounted on admin-role nodes. Driver-only nodes have no UI. + builder.Services.AddOtOpcUaAuth(builder.Configuration); + builder.Services.AddAdminUI(); + builder.Services.AddSignalR(); + builder.Services.AddOtOpcUaAdminClients(); +} + +builder.Services.AddOtOpcUaHealth(); + var app = builder.Build(); -app.MapGet("/", () => "OtOpcUa.Host scaffold"); +app.UseSerilogRequestLogging(); + +if (hasAdmin) +{ + app.UseAuthentication(); + app.UseAuthorization(); + app.UseAntiforgery(); + app.MapOtOpcUaAuth(); + app.MapAdminUI(); + app.MapOtOpcUaHubs(); +} + +app.MapOtOpcUaHealth(); + +Log.Information("OtOpcUa.Host starting with roles=[{Roles}] (admin={HasAdmin}, driver={HasDriver})", + string.Join(",", roles), hasAdmin, hasDriver); + await app.RunAsync(); + +namespace ZB.MOM.WW.OtOpcUa.Host +{ + /// Re-exported for WebApplicationFactory<Program> integration tests (F1). + public partial class Program; +} diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Routes.razor b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Routes.razor new file mode 100644 index 0000000..3707024 --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Routes.razor @@ -0,0 +1,12 @@ +@using ZB.MOM.WW.OtOpcUa.AdminUI + + + + + + + +

Page not found.

+
+
+
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/_Imports.razor b/src/Server/ZB.MOM.WW.OtOpcUa.Host/_Imports.razor new file mode 100644 index 0000000..c0434a9 --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/_Imports.razor @@ -0,0 +1,5 @@ +@using Microsoft.AspNetCore.Components +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using ZB.MOM.WW.OtOpcUa.Host