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;
}
}