using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.EntityFrameworkCore; using OpenTelemetry.Metrics; using Serilog; using ZB.MOM.WW.OtOpcUa.Admin.Components; using ZB.MOM.WW.OtOpcUa.Admin.Hubs; using ZB.MOM.WW.OtOpcUa.Admin.Security; using ZB.MOM.WW.OtOpcUa.Admin.Services; using ZB.MOM.WW.OtOpcUa.Configuration; var builder = WebApplication.CreateBuilder(args); builder.Host.UseSerilog((ctx, cfg) => cfg .MinimumLevel.Information() .WriteTo.Console() .WriteTo.File("logs/otopcua-admin-.log", rollingInterval: RollingInterval.Day)); builder.Services.AddRazorComponents().AddInteractiveServerComponents(); builder.Services.AddHttpContextAccessor(); builder.Services.AddSignalR(); builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(o => { o.Cookie.Name = "OtOpcUa.Admin"; o.LoginPath = "/login"; o.ExpireTimeSpan = TimeSpan.FromHours(8); }); builder.Services.AddAuthorizationBuilder() .AddPolicy("CanEdit", p => p.RequireRole(AdminRoles.ConfigEditor, AdminRoles.FleetAdmin)) .AddPolicy("CanPublish", p => p.RequireRole(AdminRoles.FleetAdmin)); builder.Services.AddCascadingAuthenticationState(); builder.Services.AddDbContext(opt => opt.UseSqlServer(builder.Configuration.GetConnectionString("ConfigDb") ?? throw new InvalidOperationException("ConnectionStrings:ConfigDb not configured"))); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); // Phase 7 Stream F — scripting + virtual tag + scripted alarm draft services, test // harness, and historian diagnostics. The historian sink is the Null variant here — // the real SqliteStoreAndForwardSink lives in the server process. Admin reads status // from whichever sink is provided at composition time. builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton( ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.NullAlarmHistorianSink.Instance); // Cert-trust management — reads the OPC UA server's PKI store root so rejected client certs // can be promoted to trusted via the Admin UI. Singleton: no per-request state, just // filesystem operations. builder.Services.Configure(builder.Configuration.GetSection(CertTrustOptions.SectionName)); builder.Services.AddSingleton(); // LDAP auth — parity with ScadaLink's LdapAuthService (decision #102). builder.Services.Configure( builder.Configuration.GetSection("Authentication:Ldap")); builder.Services.AddScoped(); // SignalR real-time fleet status + alerts (admin-ui.md §"Real-Time Updates"). builder.Services.AddHostedService(); // OpenTelemetry Prometheus exporter — Meter stream from RedundancyMetrics + any future // Admin-side instrumentation lands on the /metrics endpoint Prometheus scrapes. Pull-based // means no OTel Collector deployment required for the common deploy-in-a-K8s case; appsettings // Metrics:Prometheus:Enabled=false disables the endpoint entirely for locked-down deployments. var metricsEnabled = builder.Configuration.GetValue("Metrics:Prometheus:Enabled", true); if (metricsEnabled) { builder.Services.AddOpenTelemetry() .WithMetrics(m => m .AddMeter(RedundancyMetrics.MeterName) .AddPrometheusExporter()); } var app = builder.Build(); app.UseSerilogRequestLogging(); app.UseStaticFiles(); app.UseAuthentication(); app.UseAuthorization(); app.UseAntiforgery(); app.MapPost("/auth/logout", async (HttpContext ctx) => { await ctx.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); ctx.Response.Redirect("/"); }); app.MapHub("/hubs/fleet"); app.MapHub("/hubs/alerts"); if (metricsEnabled) { // Prometheus scrape endpoint — expose instrumentation registered in the OTel MeterProvider // above. Emits text-format metrics at /metrics; auth is intentionally NOT required (Prometheus // scrape jobs typically run on a trusted network). Operators who need auth put the endpoint // behind a reverse-proxy basic-auth gate per fleet-ops convention. app.MapPrometheusScrapingEndpoint(); } app.MapRazorComponents().AddInteractiveServerRenderMode(); await app.RunAsync(); public partial class Program;