47e20dd8a8
Claude-Session: https://claude.ai/code/session_01Y5ef34iUUMjmwnMpL9xgmB
9.0 KiB
9.0 KiB
Pending Updates: site artifact staleness, rail indicator, scrollable nav — Design
Date: 2026-09-14 · Status: approved for implementation (plan: 2026-09-14-pending-updates.md)
1. Ask
Three Central UI changes, requested together:
- The Sites page does not show whether a site's deployed system-wide artifacts (shared scripts, external systems, DB connections, that site's data connections) are out of date. It should carry a Stale / Current badge like the Topology (template-instance) page does for instances.
- A single "pending updates" indicator in the rail footer, bottom-left just above the user name, summarising both kinds of pending update: stale instances and sites with stale artifacts.
- The expanding rail menu should sit in a scroll pane so a long nav never pushes the footer off-screen.
2. Current state (verified 2026-09-14)
- Instance staleness =
DeploymentService.GetDeploymentComparisonAsyncre-flattens the instance (validateScripts:false) and comparesRevisionHashwith theDeployedConfigSnapshot(after the I-1 List-value normalisation). Topology computes it per loaded page with bounded concurrency, off the 15 s live timer, with a Re-check staleness button. There is no aggregate anywhere. - Artifact deployment (
ArtifactDeploymentService) builds oneDeployArtifactsCommandper site and persists aSystemArtifactDeploymentRecordwith a per-site status JSON, but records no revision of what was sent, so "are this site's artifacts current?" is currently unanswerable. - The rail is the
ZB.MOM.WW.Themekit'sThemeShell(nav.side-rail, flex column;.rail-foothasmargin-top:auto;#theme-railis sticky100vhon ≥ lg). The nav items are direct children of.side-rail; nothing scrolls internally, so a long nav grows past the viewport. SingleFlightMemo<T>+KpiSnapshotCachealready give the process-level, per-TTL, single-flight cache shape a polled aggregate needs.
3. Design
3.1 Site artifact revision (Commons + ConfigurationDatabase + DeploymentManager)
Sitegains two nullable columns:ArtifactRevisionHash(string?, max 100) andArtifactsDeployedAt(DateTimeOffset?). MigrationAddSiteArtifactRevision. Additive; a site that has never had artifacts deployed staysnull= Never deployed.- New
ArtifactRevisionHashService(DeploymentManager) computes a deterministic SHA-256 over a canonical projection of a per-siteDeployArtifactsCommand: every artifact class sorted byName(ordinal),DeploymentId/timestamp excluded, alphabetically-declared hashable records, camelCase JSON, nulls omitted — the same determinism contract asRevisionHashService, pinned by a test. Data-connection configuration is plaintext in memory (EF value converter), so a no-op re-save that re-encrypts at rest does not change the hash. ArtifactDeploymentServicestampsArtifactRevisionHash+ArtifactsDeployedAton theSiterow only for a site whose deploy succeeded (bothDeployCoreAsyncandRetryForSiteAsync). A failed site keeps its previous stamp, so it keeps showing out of date. The stamp is written in the sameSaveChangesas the deployment record where possible.ArtifactDeploymentService.ComputeCurrentArtifactHashesAsync(sites)returnssiteId → hashfor the current definitions, hoisting the global artifact queries once (same shape asDeployCoreAsync).- Transport does NOT carry the two columns. They are environment-specific runtime state, exactly
like imported instances landing
NotDeployed; the round-trip guard exclusion list names them with that reason. An import that changes a site's data connections therefore makes the site read out of date until artifacts are redeployed — the correct outcome.
3.2 Pending-updates aggregate (DeploymentManager, cached in CentralUI)
DeploymentService.GetInstanceStalenessAsync(instanceIds)→IReadOnlyDictionary<int, bool?>: one sharedFlattenSessionfor the batch (template chains walked once, asDeploySiteAsyncdoes), compares against a projection read of(InstanceId, RevisionHash)fromDeployedConfigSnapshot(newIDeploymentManagerRepository.GetDeployedRevisionHashesAsync()— never the fatConfigurationJsonfor the whole fleet). For a hash mismatch only, the full snapshot is read and the I-1 normalised deployed hash recomputed, so the aggregate agrees exactly with the Topology badge on old-form List snapshots.null= could not evaluate (flatten failed) — counted as unknown, never as stale.PendingUpdatesEvaluator(scoped, DeploymentManager) →PendingUpdatesSnapshot:ComputedAt, per siteSitePendingUpdates(SiteId, SiteIdentifier, SiteName, ArtifactState {NeverDeployed|Current|OutOfDate}, ArtifactsDeployedAt, DeployedInstanceCount, StaleInstanceCount, UnknownInstanceCount), plus totalsStaleInstanceCountandStaleArtifactSiteCount. Instances are those with a deployed snapshot (the same gate Topology uses:State != NotDeployed).IPendingUpdatesCache/PendingUpdatesCache(CentralUI, singleton, mirrorsKpiSnapshotCache): oneSingleFlightMemo<PendingUpdatesSnapshot>with TTLDeploymentManagerOptions.PendingUpdatesCacheTtl(default 60 s, validated > 0), running the evaluator in its own DI scope.GetAsync(forceRefresh). Consequence: however many circuits poll, the fleet is flattened at most once per TTL, and not at all when nobody is looking.- Not in scope (register rows): a template-graph-watermark short-circuit so an unchanged fleet costs
no re-flatten; a
deployCLI/HealthKPI surface for the counts.
3.3 Sites page (Admin)
- Each site card shows an Artifacts badge next to the identifier:
Current(light),Out of date(warning,bi-exclamation-triangle, tooltip = deployed-at UTC),Never deployed(secondary). Below it a muted lineN of M instances out of date(link to/deployment/topology) when M > 0, since the snapshot already carries it. Badges match Topology's Bootstrap.badgeconvention (the kit'sStatusPillis unused in this app). - Header gains Re-check (forces
GetAsync(forceRefresh:true), disabled while running) and an as of HH:mm:ss UTC caption. A successful Deploy Artifacts (single or all) forces a refresh so the badge flips to Current immediately. - The snapshot is loaded after the site list, never blocking it: cards render with a checking… placeholder until it arrives; a snapshot failure shows unavailable on the badge and logs.
3.4 Rail indicator (PendingUpdatesIndicator, MainLayout RailFooter, above .rail-user)
- Rendered only for a user holding Deployer or Administrator (the two roles that can act on it);
hidden otherwise. Polls
IPendingUpdatesCache.GetAsync()every 30 s throughPollGate(cache TTL 60 s ⇒ polling is cheap), disposes its timer with the circuit, renders viaInvokeAsync. - States: Checking for updates… (first load) · Up to date (muted, check icon) · Pending updates
with a warning count pill and a breakdown line:
3 instances(links to Topology when Deployer) ·1 site(links to/admin/siteswhen Administrator, plain text otherwise) · Updates unavailable (error, tooltip has the message). Tooltip on the block: as of HH:mm UTC. - Site-scoped users see only their permitted sites' counts (
SiteScopeService), consistent with Topology.
3.5 Scrollable nav
NavMenu.razorwraps its items in<div class="rail-scroll">;site.cssadds, ≥ lg only:#theme-rail > .side-rail { height: 100%; }(fill the sticky 100 vh rail so.rail-foot'smargin-top:autoactually pins) and.rail-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }. Below lg the rail is inline and scrolls with the page, unchanged. Kit untouched; a register row proposes upstreaming it intoZB.MOM.WW.Themefor the other consumers.
4. Tests
- Unit: hash determinism (order-insensitive, field-sensitive, alphabetical-declaration guard); stamping
on success only;
GetInstanceStalenessAsync(shared session used once, mismatch-only normalisation, null on flatten failure); evaluator totals; cache single-flight/TTL/force; bUnit for the indicator (all states, role gating, scope filter), Sites page (badges, re-check, refresh-after-deploy), NavMenu wrapper; Transport round-trip guard exclusions; migration snapshot. - Rig:
docker/deploy.sh(migration auto-applies),docker/verify-pending-updates.sh(CLI: site list showsartifactRevisionHashnull → deploy artifacts → set; edit a shared script → central UI shows out of date; redeploy → current), plus a Playwright smoke for the indicator + Sites badge.
5. Operational notes
- Production/QA: the migration ships as a delta script (
dotnet ef migrations script AddAlarmMesReportable AddSiteArtifactRevision). After upgrade every site reads Never deployed until artifacts are deployed once — deliberate; one Deploy Artifacts to All Sites clears it. - Cost: one fleet flatten per
PendingUpdatesCacheTtlwhile any Deployer/Admin circuit is open.