using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Routing; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using ZB.MOM.WW.OtOpcUa.AdminUI.Hubs; using ZB.MOM.WW.OtOpcUa.AdminUI.Uns; using ZB.MOM.WW.OtOpcUa.Commons.Browsing; using ZB.MOM.WW.OtOpcUa.Configuration; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser; using ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Browser; using ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser; using ZB.MOM.WW.OtOpcUa.Driver.Sql.Browser; using ZB.MOM.WW.Secrets.Ui; namespace ZB.MOM.WW.OtOpcUa.AdminUI; public static class EndpointRouteBuilderExtensions { /// /// Mounts the AdminUI Razor components and the AdminUI static asset pipeline at the root. /// Call from the fused Host's Program.cs alongside app.MapOtOpcUaAuth(). /// /// The root component type for Razor pages. /// The endpoint route builder. /// The same endpoint route builder, for chaining. public static IEndpointRouteBuilder MapAdminUI(this IEndpointRouteBuilder app) where TApp : IComponent { // Razor class library static assets (_content/ZB.MOM.WW.OtOpcUa.AdminUI/**) are // served via the Host's app.UseStaticFiles() middleware which must run BEFORE // UseAuthentication() — see Program.cs. // The Secrets.Ui RCL's routable page is deliberately NOT registered here (lmxopcua#483): // this host's router is static with per-page @rendermode opt-in, so routing straight to // the RCL page rendered it without a circuit — it displayed but every @onclick was dead. // Pages/Secrets.razor in THIS assembly owns /admin/secrets instead (wrapping the RCL // page with the render mode); registering the RCL routes too would make the route // ambiguous. app.MapRazorComponents() .AddInteractiveServerRenderMode(); return app; } /// /// Adds AdminUI services to the dependency injection container. /// /// The service collection. /// The same service collection, for chaining. public static IServiceCollection AddAdminUI(this IServiceCollection services) { services.AddRazorComponents().AddInteractiveServerComponents(); services.AddOtOpcUaDriverStatusServices(); // Secrets-management UI (ZB.MOM.WW.Secrets.Ui RCL, mounted at /admin/secrets). Register its // "secrets:manage"/"secrets:reveal" authorization policies additively onto the AuthorizationOptions // that AddOtOpcUaAuth (called just before AddAdminUI on admin nodes) sets up — Configure stacks, // so this ADDS the two policies without disturbing FleetAdmin/DriverOperator/ConfigEditor. The // policies' AdminRole = "Administrator" reads the same ClaimTypes.Role claim as FleetAdmin, so an // existing Administrator satisfies them with no new group→role mapping. Registered here (the AdminUI // composition layer that already references the RCL) rather than in the core Security lib. services.Configure(o => o.AddSecretsAuthorization()); // Browse pipeline — see docs/plans/2026-05-28-driver-browsers-design.md services.AddSingleton(); services.AddHostedService(); services.AddScoped(); services.AddScoped(); services.AddScoped(); // v3 Batch 3 (WP2): authoring-time UNS effective-name uniqueness guard. Consumed by // UnsTreeService's reference/VirtualTag/ScriptedAlarm mutations (WP1); mirrors the // deploy-time DraftValidator UnsEffectiveNameCollision rule. services.AddScoped(); // #520 stage 7: the ACL scope picker's two trees + the authoring-time scope/principal checks that // mirror the deploy gate's NodeAclScope* rules. Scoped, because it composes the two scoped tree // services above. services.AddScoped(); // WP5 CSV export enumeration (read-only; does not extend the closed IRawTreeService prelude). services.AddScoped(); services.AddSingleton(); services.AddSingleton(); // Bespoke Sql schema browser (server → schema → table → column). Constructible bare — every // ctor param has a production default. The universal-browser DI line is deliberately NOT added // for Sql: SqlDriver.CanBrowse is false, and this bespoke IDriverBrowser wins by construction // (BrowserSessionService indexes bespoke browsers by DriverType before falling back to universal). services.AddSingleton(); services.AddSingleton(); // Universal Discover-backed fallback browser (Wave 0). IDriverFactory is bound by the // fused Host's DriverFactoryBootstrap; a standalone AdminUI has none → NullDriverFactory // → CanBrowse false → pickers gracefully stay manual-entry (design §6). services.AddSingleton(sp => new DiscoveryDriverBrowser( sp.GetService() ?? NullDriverFactory.Instance, sp.GetRequiredService>())); // Roslyn-backed Monaco script-editor analysis (diagnostics/completions/hover/...). services.AddScoped(); services.AddScoped(); // The catalog stays Scoped (per circuit); its six-table fleet-wide projection is shared across // circuits behind a 5 s TTL snapshot — Monaco fires a completion request per keystroke. services.AddSingleton(); // TryAdd so a host that already bound a clock (test harness, future fused-host registration) wins. services.TryAddSingleton(TimeProvider.System); // /deployments drift chip. A singleton so its ConfigEdit-watermark cache is shared across // circuits — otherwise every page load would recompose the whole fleet configuration. // Constructed explicitly rather than by ctor scan: the compose seam is an optional parameter // that only tests supply, and production must always get the real ConfigComposer default. services.AddSingleton(sp => new Deployments.DeploymentDriftProbe( sp.GetRequiredService>())); // Certificate-store actions (trust/untrust/delete) for the /certificates page. services.AddSingleton(); // Structured audit-event sink: forwards AuditEvents to the ControlPlane AuditWriter singleton. services.AddSingleton(); return services; } }