41013dcab3
Found during the live acceptance run. The host calls UseStaticWebAssets only when the environment is literally "Development", so running the build output under any other name - checking a staging registry locally, say - leaves the Theme RCL's assets under _content/ unresolvable. The failure is silent and unhelpful: the page returns 200 with its markup fully present and renders as a blank white screen, because every stylesheet 404s. Calling it unconditionally is a no-op once published (the manifest is gone and the assets are real files on disk), so the only thing it changes is that failure. The container was never affected. Phase 4 results recorded in the ledger: 7 of 9 design section 9 checks fully pass, 2 pass for ScadaBridge and are blocked for OtOpcUa pending a rig redeploy. Notable live evidence: - Leader chip moved from site-c-a to site-c-b when site-c-a was stopped, and the split-brain chip appeared while the pair was self-formed then cleared once it genuinely joined. - Kill to Unreachable took two failing sweeps (the damping working as designed); recovery landed on the first good poll. - Five consecutive page loads in 0.00-0.02s all showed the same "last sweep" timestamp, which is cache-first stated as a measurement. - Freezing the probed endpoints while auto-refresh stayed RUNNING froze the poller mid-sweep and the cards aged into STALE on their own - staleness is computed at render, so a stuck poller needs no explicit stall detection. The run also surfaced a pre-existing rig defect in both products: self-first Akka seed lists mean a pair booted cold self-forms two 1-member clusters that never join. After a --force-recreate every ScadaBridge node named ITSELF leader and every node answered /health/active 200; restarting ONE node of each pair converged them to one Leader plus one Active plus one Standby. That is the user's own open backlog item (docs/plans/2026-07-22-initjoin-selfform-fallback.md, unexecuted), and the dashboard made it visible fleet-wide in one screen. 170 tests, 0 warnings.
82 lines
3.7 KiB
C#
82 lines
3.7 KiB
C#
using ZB.MOM.WW.Configuration;
|
|
using ZB.MOM.WW.Health;
|
|
using ZB.MOM.WW.Overview.Components;
|
|
using ZB.MOM.WW.Overview.Observability;
|
|
using ZB.MOM.WW.Overview.Polling;
|
|
using ZB.MOM.WW.Overview.Registry;
|
|
using ZB.MOM.WW.Telemetry;
|
|
using ZB.MOM.WW.Telemetry.Serilog;
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
// The host calls this itself, but ONLY when the environment is literally "Development". Running the
|
|
// build output under any other name — checking a staging registry locally, say — leaves the Theme
|
|
// RCL's assets under _content/ unresolvable, and the page renders as a blank white screen with its
|
|
// markup fully present and every stylesheet 404. Calling it unconditionally is a no-op once
|
|
// published (the manifest is gone and the assets are real files on disk), so the only thing it
|
|
// changes is that failure.
|
|
builder.WebHost.UseStaticWebAssets();
|
|
|
|
// Pre-host presence check. The validator below covers shape and consistency, but it only runs once
|
|
// the options are resolved — a registry section that is missing entirely binds to an empty
|
|
// OverviewOptions, and the failure a reader can act on is "you configured no registry", not
|
|
// "Applications must have at least 1 entry". Fail here, before the host is built, with the key name.
|
|
ConfigPreflight.For(builder.Configuration)
|
|
.RequireValue($"{OverviewOptions.SectionName}:Applications:0:Name")
|
|
.RequireValue($"{OverviewOptions.SectionName}:Applications:0:Instances:0:BaseUrl")
|
|
.ThrowIfInvalid();
|
|
|
|
builder.Services.AddValidatedOptions<OverviewOptions, OverviewOptionsValidator>(
|
|
builder.Configuration, OverviewOptions.SectionName);
|
|
|
|
builder.AddZbSerilog(o => o.ServiceName = "overview");
|
|
|
|
builder.AddZbTelemetry(o =>
|
|
{
|
|
o.ServiceName = "overview";
|
|
|
|
// Meters is an ALLOWLIST, not a filter: a Meter absent from this array is never registered with
|
|
// the MeterProvider, and its instruments vanish from /metrics with no warning anywhere. The
|
|
// dashboard's own gauge is the only thing here that would go missing silently.
|
|
o.Meters = [OverviewMetrics.MeterName];
|
|
});
|
|
|
|
builder.Services.AddOverviewPolling();
|
|
builder.Services.AddOverviewHealthChecks();
|
|
|
|
builder.Services.AddRazorComponents()
|
|
.AddInteractiveServerComponents();
|
|
|
|
var app = builder.Build();
|
|
|
|
app.UseStaticFiles();
|
|
|
|
// No UseAuthentication/UseAuthorization and no cascading auth state: the dashboard is anonymous and
|
|
// read-only by requirement (design §2). It renders health data that every one of these apps already
|
|
// serves unauthenticated, and it has no endpoint that mutates anything.
|
|
//
|
|
// UseAntiforgery IS required despite that. AddRazorComponents stamps antiforgery metadata onto every
|
|
// component endpoint unconditionally, and the endpoint middleware hard-fails a request whose endpoint
|
|
// carries that metadata with no middleware to honour it — so without this line every page returns
|
|
// 500. The alternative, MapRazorComponents(...).DisableAntiforgery(), would also boot, but it turns
|
|
// the first form anyone ever adds into a silent CSRF hole. This app has no forms, so the middleware
|
|
// is inert; it is here so that stays true by default rather than by vigilance.
|
|
app.UseAntiforgery();
|
|
|
|
app.MapRazorComponents<App>()
|
|
.AddInteractiveServerRenderMode();
|
|
|
|
// The dashboard's own three-tier health surface and Prometheus endpoint. Both are anonymous, like
|
|
// every /health/* endpoint this app polls — which also means one Overview instance can be pointed
|
|
// at another and would render it correctly.
|
|
app.MapZbHealth();
|
|
app.MapZbMetrics();
|
|
|
|
await app.RunAsync();
|
|
|
|
/// <summary>
|
|
/// Exposed so <c>WebApplicationFactory<Program></c> can boot the real host in tests
|
|
/// (top-level statements otherwise compile to an internal entry-point class).
|
|
/// </summary>
|
|
public partial class Program;
|