ec57df1009
Closes the remaining loop on user-visible Modbus tag editing. Pre-#155 tags arrived only via SQL seeding or runtime ITagDiscovery; the Admin UI had no interactive surface for creating / editing / deleting tag rows. Changes: - TagService.cs (Admin/Services/) — CRUD wrapper around OtOpcUaConfigDbContext.Tags. ListAsync supports optional driver / equipment filters; CreateAsync auto-derives TagId; UpdateAsync persists editable fields; DeleteAsync removes the row. Mirrors the EquipmentService shape. - TagsTab.razor (Components/Pages/Clusters/) — list + filter + add/edit/remove form. The address/config editor is conditional: when the selected DriverInstance is Modbus, ModbusAddressEditor (#145) renders with live-parse preview; otherwise a generic JSON textarea (matches the DriversTab pattern from #147). Save-side serializes the address-string into TagConfig as `{"addressString":"..."}` JSON. - ClusterDetail.razor — new "Tags" tab in the cluster-detail nav strip + the routing switch. - Program.cs — TagService registered as a scoped DI service. Drive-by fix: ModbusDriverFactoryExtensions.CreateInstance promoted from internal to public — Admin.Tests was using it via reflection-friendly internal access that broke under the #153 logger overload addition. Public is the right access modifier anyway since the Server-side bootstrapper calls it from a different assembly. Drive-by fix #2: ModbusDriverConfigDto was missing MaxReadGap (#143) — surfaced by the #147 round-trip test that flips MaxReadGap=12 in the view model and asserts it lands on the resolved options. Added the field + binding line. Confirms #143's DriverConfig JSON binding was incomplete since the original commit; no production deployment configured this knob through JSON until now so the gap stayed hidden. Tests (4 new TagServiceTests): - Create_And_List_Surfaces_The_Tag — CreateAsync auto-assigns TagId; list returns the row. - List_Filters_By_DriverInstance — driver-scoped filter works. - Update_Persists_Editable_Fields — Name / DataType / AccessLevel / TagConfig all persist through Update. - Delete_Removes_The_Row — basic delete verification. 113 + 4 (TagService) + 2 (DriversTab round-trip restored after compile fix) = 119 Admin tests green. Solution build clean. Caveat: bUnit-style render tests for TagsTab still aren't included — Admin.Tests doesn't have bUnit set up. The TagService logic is fully covered; the razor component's parser/save glue is exercised by hand at runtime for now.
141 lines
6.0 KiB
C#
141 lines
6.0 KiB
C#
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<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<TagService>();
|
|
builder.Services.AddScoped<UnsService>();
|
|
builder.Services.AddScoped<NamespaceService>();
|
|
builder.Services.AddScoped<DriverInstanceService>();
|
|
builder.Services.AddScoped<FocasDriverDetailService>();
|
|
|
|
// #154 — Server diagnostics client. Default base URL points at the same machine's
|
|
// HealthEndpointsHost (loopback :4841); deployments with remote Servers set
|
|
// "DriverDiagnostics:ServerBaseUrl" in appsettings.json.
|
|
builder.Services.AddHttpClient<DriverDiagnosticsClient>(client =>
|
|
{
|
|
var baseUrl = builder.Configuration["DriverDiagnostics:ServerBaseUrl"] ?? "http://localhost:4841/";
|
|
client.BaseAddress = new Uri(baseUrl);
|
|
});
|
|
builder.Services.AddScoped<NodeAclService>();
|
|
builder.Services.AddScoped<PermissionProbeService>();
|
|
builder.Services.AddScoped<AclChangeNotifier>();
|
|
builder.Services.AddScoped<ReservationService>();
|
|
builder.Services.AddScoped<DraftValidationService>();
|
|
builder.Services.AddScoped<AuditLogService>();
|
|
builder.Services.AddScoped<HostStatusService>();
|
|
builder.Services.AddScoped<ClusterNodeService>();
|
|
builder.Services.AddSingleton<RedundancyMetrics>();
|
|
builder.Services.AddScoped<EquipmentImportBatchService>();
|
|
builder.Services.AddScoped<ZB.MOM.WW.OtOpcUa.Configuration.Services.ILdapGroupRoleMappingService,
|
|
ZB.MOM.WW.OtOpcUa.Configuration.Services.LdapGroupRoleMappingService>();
|
|
|
|
// 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<ScriptService>();
|
|
builder.Services.AddScoped<VirtualTagService>();
|
|
builder.Services.AddScoped<ScriptedAlarmService>();
|
|
builder.Services.AddScoped<ScriptTestHarnessService>();
|
|
builder.Services.AddScoped<HistorianDiagnosticsService>();
|
|
builder.Services.AddSingleton<ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.IAlarmHistorianSink>(
|
|
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<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>();
|
|
|
|
// 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<FleetStatusHub>("/hubs/fleet");
|
|
app.MapHub<AlertHub>("/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<App>().AddInteractiveServerRenderMode();
|
|
|
|
await app.RunAsync();
|
|
|
|
public partial class Program;
|