Polling chosen over event-driven for initial scope: simpler, matches Admin UI consumer cadence, avoids DriverHost lifecycle-event plumbing that doesn't exist today. Event-driven push for sub-heartbeat latency is a straightforward follow-up. Admin.Services.HostStatusService left-joins DriverHostStatus against ClusterNode on NodeId so rows persist even when the ClusterNode entry doesn't exist yet (first-boot bootstrap case). StaleThreshold = 30s — covers one missed publisher heartbeat plus a generous buffer for clock skew and GC pauses. Admin Components/Pages/Hosts.razor — FleetAdmin-visible page grouped by cluster (handles the '(unassigned)' case for rows without a matching ClusterNode). Four summary cards (Hosts / Running / Stale / Faulted); per-cluster table with Node / Driver / Host / State + Stale-badge / Last-transition / Last-seen / Detail columns; 10s auto-refresh via IServiceScopeFactory timer pattern matching FleetStatusPoller + Fleet dashboard (PR 27). Row-class highlighting: Faulted → table-danger, Stale → table-warning, else default. State badge maps DriverHostState enum to bootstrap color classes. Sidebar link added between 'Fleet status' and 'Clusters'. Server csproj adds Microsoft.EntityFrameworkCore.SqlServer 10.0.0 + registers OtOpcUaConfigDbContext in Program.cs scoped via NodeOptions.ConfigDbConnectionString (no Admin-style manual SQL raw — the DbContext is the only access path, keeps migrations owner-of-record). Tests — HostStatusPublisherTests (4 new Integration cases, uses per-run throwaway DB matching the FleetStatusPollerTests pattern): publisher upserts one row per host from each probe-capable driver and skips non-probe drivers; second tick advances LastSeenUtc without creating duplicate rows (upsert pattern verified end-to-end); state change between ticks updates State AND StateChangedUtc (datetime2(3) rounds to millisecond precision so comparison uses 1ms tolerance — documented inline); MapState translates every HostState enum member. Server.Tests Integration: 4 new tests pass. Admin build clean, Admin.Tests Unit still 23 / 0. docs/v2/lmx-followups.md item #7 marked DONE with three explicit deferred items (event-driven push, failure-count column, SignalR fan-out). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
88 lines
3.2 KiB
C#
88 lines
3.2 KiB
C#
using Microsoft.AspNetCore.Authentication;
|
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
|
using Microsoft.EntityFrameworkCore;
|
|
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<OtOpcUaConfigDbContext>(opt =>
|
|
opt.UseSqlServer(builder.Configuration.GetConnectionString("ConfigDb")
|
|
?? throw new InvalidOperationException("ConnectionStrings:ConfigDb not configured")));
|
|
|
|
builder.Services.AddScoped<ClusterService>();
|
|
builder.Services.AddScoped<GenerationService>();
|
|
builder.Services.AddScoped<EquipmentService>();
|
|
builder.Services.AddScoped<UnsService>();
|
|
builder.Services.AddScoped<NamespaceService>();
|
|
builder.Services.AddScoped<DriverInstanceService>();
|
|
builder.Services.AddScoped<NodeAclService>();
|
|
builder.Services.AddScoped<ReservationService>();
|
|
builder.Services.AddScoped<DraftValidationService>();
|
|
builder.Services.AddScoped<AuditLogService>();
|
|
builder.Services.AddScoped<HostStatusService>();
|
|
|
|
// 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<CertTrustOptions>(builder.Configuration.GetSection(CertTrustOptions.SectionName));
|
|
builder.Services.AddSingleton<CertTrustService>();
|
|
|
|
// LDAP auth — parity with ScadaLink's LdapAuthService (decision #102).
|
|
builder.Services.Configure<LdapOptions>(
|
|
builder.Configuration.GetSection("Authentication:Ldap"));
|
|
builder.Services.AddScoped<ILdapAuthService, LdapAuthService>();
|
|
|
|
// SignalR real-time fleet status + alerts (admin-ui.md §"Real-Time Updates").
|
|
builder.Services.AddHostedService<FleetStatusPoller>();
|
|
|
|
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<FleetStatusHub>("/hubs/fleet");
|
|
app.MapHub<AlertHub>("/hubs/alerts");
|
|
|
|
app.MapRazorComponents<App>().AddInteractiveServerRenderMode();
|
|
|
|
await app.RunAsync();
|
|
|
|
public partial class Program;
|