Files
ScadaBridge/docs/plans/2026-09-14-pending-updates.md

27 KiB

Pending Updates Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task.

Goal: Show on the Sites page whether each site's deployed artifacts are out of date, add one rail-footer indicator summarising pending updates (stale instances + stale-artifact sites), and put the rail nav in a scroll pane.

Architecture: Two nullable columns on Site record the revision hash of the last successful artifact deploy (stamped by ArtifactDeploymentService); a new PendingUpdatesEvaluator (DeploymentManager) computes one PendingUpdatesSnapshot per fleet (artifact hash comparison per site + one shared-session instance-staleness batch), cached process-wide by a singleton PendingUpdatesCache (CentralUI, SingleFlightMemo) that the Sites page and the rail indicator both read. The scroll pane is an app-level wrapper + CSS; the theme kit is untouched.

Tech Stack: .NET 10, EF Core (SQL Server migration), Blazor Server + Bootstrap 5 + bUnit, NSubstitute, Playwright, bash + scadabridge CLI on the docker rig.

Design: docs/plans/2026-09-14-pending-updates-design.md.

Conventions for every task (from CLAUDE.md + memory):

  • Work in the dedicated worktree /Users/dohertj2/Desktop/ScadaBridge-pending-updates (Task 0). Use absolute paths in every command — Bash cwd drifts.
  • Run targeted tests per task (dotnet test <test proj> --filter …) and build the affected projects; the full solution build/test runs only in Task 11.
  • Never git stash (shared across worktrees). Commit with pathspecs. End every commit message with Claude-Session: https://claude.ai/code/session_01Y5ef34iUUMjmwnMpL9xgmB.
  • Every public member needs an XML doc comment (checkdocs enforces it).
  • Never await an audit write on a script path; never block on a Task from a Blazor lifecycle/event path.
  • Bootstrap .badge markup for badges (matches Topology); no third-party component libraries.

Task 0: Worktree

Classification: trivial · Estimated implement time: ~2 min · Parallelizable with: none

cd /Users/dohertj2/Desktop/ScadaBridge
git worktree add -b feat/pending-updates ../ScadaBridge-pending-updates main
cd /Users/dohertj2/Desktop/ScadaBridge-pending-updates && dotnet build ZB.MOM.WW.ScadaBridge.slnx 2>&1 | tail -3

Expected: 0 Error(s).


Task 1: Site artifact revision columns, migration, repository projection, Transport guard

Classification: high-risk (schema + data contract) · Estimated implement time: ~5 min · Parallelizable with: Task 8

Files:

  • Modify: src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Sites/Site.cs — add public string? ArtifactRevisionHash { get; set; } and public DateTimeOffset? ArtifactsDeployedAt { get; set; } with XML docs ("revision hash of the per-site artifact set last deployed successfully; null = never deployed; NOT transported").
  • Modify: src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Configurations/SiteConfiguration.cs — ArtifactRevisionHash HasMaxLength(100).
  • Create: migration AddSiteArtifactRevision in src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Migrations/ — build first, then dotnet ef migrations add AddSiteArtifactRevision --project src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase --startup-project src/ZB.MOM.WW.ScadaBridge.Host (check how AddAlarmMesReportable was generated: git log -1 --stat 4cfa645b^.. / the previous plan; NO --no-build). Verify the migration is non-empty (two AddColumn).
  • Modify: src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/IDeploymentManagerRepository.cs — add Task<IReadOnlyDictionary<int, string>> GetDeployedRevisionHashesAsync(CancellationToken cancellationToken = default); ("InstanceId → RevisionHash for every DeployedConfigSnapshot; a projection — never materialises ConfigurationJson").
  • Modify: src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/DeploymentManagerRepository.cs — implement with .AsNoTracking().Select(s => new { s.InstanceId, s.RevisionHash }).
  • Modify: Transport round-trip guard exclusions (find with grep -rn "GrpcNodeBAddress\|RoundTrip" tests/ZB.MOM.WW.ScadaBridge.Transport.Tests tests/ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests) — add the two new Site properties to the deliberately-not-transported list with the reason "environment-specific deploy state, like Instance.State".
  • Test: tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/ — a repository test for GetDeployedRevisionHashesAsync (SQLite in-memory pattern used by neighbouring tests): two snapshots → dictionary of two; empty → empty.

Steps: write the failing repo test → implement → migration → run dotnet test tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests --filter "FullyQualifiedName~DeployedRevisionHashes" and the Transport guard tests (--filter "FullyQualifiedName~RoundTrip") → commit feat(sites): record the artifact revision last deployed to a site.


Task 2: ArtifactRevisionHashService + stamping on successful deploy

Classification: standard · Estimated implement time: ~5 min · Parallelizable with: Task 3, Task 8

Files:

  • Create: src/ZB.MOM.WW.ScadaBridge.DeploymentManager/ArtifactRevisionHashService.cs — public sealed class ArtifactRevisionHashService { public string ComputeHash(DeployArtifactsCommand command) }. Canonical projection: private Hashable* records with alphabetically declared properties, each artifact list OrderBy(Name, StringComparer.Ordinal), DeploymentId and the timestamp excluded, JsonNamingPolicy.CamelCase, WhenWritingNull, SHA-256 hex (lower-case, like RevisionHashService). Cover every field of SharedScriptArtifact, ExternalSystemArtifact, DatabaseConnectionArtifact, DataConnectionArtifact (read the record definitions under Commons/Messages/Artifacts/).
  • Modify: src/ZB.MOM.WW.ScadaBridge.DeploymentManager/ArtifactDeploymentService.cs:
    • inject ArtifactRevisionHashService;
    • DeployCoreAsync: after Task.WhenAll, for each site whose result Success, set site.ArtifactRevisionHash = hash(siteCommands[site.Id]), site.ArtifactsDeployedAt = DateTimeOffset.UtcNow, then persist through ISiteRepository (use its existing update + save methods — check the interface; the Site entities came from _siteRepo.GetAllSitesAsync and are tracked, so an UpdateSiteAsync/SaveChangesAsync pair is enough). Failed sites are NOT stamped.
    • RetryForSiteAsync: same stamp on success (load the site by id).
    • add public async Task<IReadOnlyDictionary<int, string>> ComputeCurrentArtifactHashesAsync(IReadOnlyList<Site> sites, CancellationToken ct) — one FetchGlobalArtifactsAsync, then per site BuildDeployArtifactsCommandAsync(site.Id, globals, ct, deploymentId: "current") → hash. (deploymentId is excluded from the hash, so any constant is fine.)
  • Modify: src/ZB.MOM.WW.ScadaBridge.DeploymentManager/ServiceCollectionExtensions.cs — register ArtifactRevisionHashService singleton.
  • Modify: every test that constructs ArtifactDeploymentService directly (grep new ArtifactDeploymentService( under tests/ — at least tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Admin/SitesPageTests.cs and the DeploymentManager tests) — pass new ArtifactRevisionHashService().
  • Test: tests/ZB.MOM.WW.ScadaBridge.DeploymentManager.Tests/ArtifactRevisionHashServiceTests.cs — same input twice → same hash; lists reordered → same hash; one field changed (per artifact class) → different hash; different DeploymentId/timestamp → same hash; HashableRecords_PropertiesDeclaredAlphabetically (reflection, mirror RevisionHashServiceTests). ArtifactDeploymentServiceTests — success stamps hash + timestamp; failure leaves the previous stamp; ComputeCurrentArtifactHashesAsync equals the hash stamped by a deploy of the same definitions.

Steps: failing tests → implement → dotnet test tests/ZB.MOM.WW.ScadaBridge.DeploymentManager.Tests --filter "FullyQualifiedName~ArtifactRevisionHash|FullyQualifiedName~ArtifactDeploymentService" → commit feat(deploy): hash the per-site artifact set and stamp it on a successful artifact deploy.


Task 3: DeploymentService.GetInstanceStalenessAsync (shared session, mismatch-only normalisation)

Classification: standard · Estimated implement time: ~5 min · Parallelizable with: Task 2, Task 8

Files:

  • Modify: src/ZB.MOM.WW.ScadaBridge.DeploymentManager/DeploymentService.cs:
    • extract from GetDeploymentComparisonAsync a private string ComputeDeployedHash(DeployedConfigSnapshot snapshot, out FlattenedConfiguration? normalized) that applies the I-1 List normalisation and recomputes the hash (returns snapshot.RevisionHash when the JSON cannot be deserialised) — GetDeploymentComparisonAsync must keep behaving byte-for-byte (its existing tests pin it).
    • add public async Task<IReadOnlyDictionary<int, bool?>> GetInstanceStalenessAsync(IReadOnlyCollection<int> instanceIds, CancellationToken cancellationToken = default): read GetDeployedRevisionHashesAsync() once; var session = _flatteningPipeline.CreateSession(); for each id with a snapshot hash: FlattenAndValidateAsync(id, ct, validateScripts:false, session); failure → null; equal hash → false; mismatch → read the full snapshot (GetDeployedSnapshotByInstanceIdAsync), ComputeDeployedHash, compare again → true/false. Ids without a snapshot are omitted from the result. Sequential (the repository DbContext is not thread-safe); add a doc comment saying so.
  • Test: tests/ZB.MOM.WW.ScadaBridge.DeploymentManager.Tests/DeploymentServiceStalenessTests.cs (or extend the existing DeploymentService test class; check how it is constructed — IFlatteningPipeline is substitutable): shared session passed to every flatten call (Received with the same FlattenSession instance, CreateSession called once); equal → false; mismatch that normalises equal → false; real mismatch → true; flatten failure → null; no snapshot → absent.

Steps: failing tests → implement → dotnet test tests/ZB.MOM.WW.ScadaBridge.DeploymentManager.Tests --filter "FullyQualifiedName~Staleness|FullyQualifiedName~DeploymentComparison" → commit feat(deploy): batch instance staleness over one flatten session.


Task 4: PendingUpdatesEvaluator + snapshot types + options

Classification: standard · Estimated implement time: ~5 min · Parallelizable with: none (needs 2 + 3)

Files:

  • Create: src/ZB.MOM.WW.ScadaBridge.DeploymentManager/PendingUpdates/PendingUpdatesSnapshot.cs — public enum ArtifactDeploymentState { NeverDeployed, Current, OutOfDate }; public sealed record SitePendingUpdates(int SiteId, string SiteIdentifier, string SiteName, ArtifactDeploymentState ArtifactState, DateTimeOffset? ArtifactsDeployedAt, int DeployedInstanceCount, int StaleInstanceCount, int UnknownInstanceCount); public sealed record PendingUpdatesSnapshot(DateTimeOffset ComputedAt, IReadOnlyList<SitePendingUpdates> Sites) with computed StaleInstanceCount, StaleArtifactSiteCount, HasPendingUpdates, and ForSites(IReadOnlySet<int> siteIds) returning a filtered copy (for site-scoped users).
  • Create: src/ZB.MOM.WW.ScadaBridge.DeploymentManager/PendingUpdates/IPendingUpdatesEvaluator.cs + PendingUpdatesEvaluator.cs (scoped): Task<PendingUpdatesSnapshot> EvaluateAsync(CancellationToken). Reads all sites (ISiteRepository.GetAllSitesAsync), all instances (ITemplateEngineRepository.GetAllInstancesAsync), ArtifactDeploymentService.ComputeCurrentArtifactHashesAsync(sites), DeploymentService.GetInstanceStalenessAsync(ids of instances with State != NotDeployed). Artifact state: null stored hash → NeverDeployed; equal → Current; else OutOfDate. Per site: deployed = instances of that site present in the staleness result; stale = true; unknown = null. A per-site artifact-hash failure must not sink the whole snapshot — log and mark that site OutOfDate? No: mark it unknown is not a state we have; keep it simple: let the exception propagate (the cache discards faulted flights and the UI shows unavailable).
  • Modify: src/ZB.MOM.WW.ScadaBridge.DeploymentManager/DeploymentManagerOptions.cs — public TimeSpan PendingUpdatesCacheTtl { get; set; } = TimeSpan.FromSeconds(60); + validation > TimeSpan.Zero wherever the other TimeSpans are validated (find the validator; §1.5 eager validation).
  • Modify: ServiceCollectionExtensions.cs (DeploymentManager) — services.AddScoped<IPendingUpdatesEvaluator, PendingUpdatesEvaluator>().
  • Test: tests/ZB.MOM.WW.ScadaBridge.DeploymentManager.Tests/PendingUpdatesEvaluatorTests.cs — build with substitutes for the repositories and (if DeploymentService/ArtifactDeploymentService are not substitutable) small interface seams: prefer adding IArtifactRevisionSource / reuse the real services with substituted repos as TopologyPageTests does. Cases: never-deployed site; current; out-of-date; instance counts (2 deployed, 1 stale, 1 unknown); ForSites filter; options validation rejects zero TTL.

Steps: failing tests → implement → dotnet test tests/ZB.MOM.WW.ScadaBridge.DeploymentManager.Tests --filter "FullyQualifiedName~PendingUpdates" → commit feat(deploy): PendingUpdatesEvaluator — one fleet snapshot of stale instances and stale-artifact sites.


Task 5: PendingUpdatesCache (CentralUI singleton)

Classification: small · Estimated implement time: ~3 min · Parallelizable with: none (needs 4)

Files:

  • Create: src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/IPendingUpdatesCache.cs — Task<PendingUpdatesSnapshot> GetAsync(bool forceRefresh = false, CancellationToken cancellationToken = default);
  • Create: src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/PendingUpdatesCache.cs — mirrors KpiSnapshotCache: ctor (IServiceScopeFactory, IOptions<DeploymentManagerOptions>), one SingleFlightMemo<PendingUpdatesSnapshot>(options.Value.PendingUpdatesCacheTtl), factory opens a scope and resolves IPendingUpdatesEvaluator.
  • Modify: src/ZB.MOM.WW.ScadaBridge.CentralUI/ServiceCollectionExtensions.cs — services.AddSingleton<IPendingUpdatesCache, PendingUpdatesCache>(); next to IKpiSnapshotCache.
  • Test: tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Services/PendingUpdatesCacheTests.cs — two concurrent GetAsync → evaluator invoked once; forceRefresh:true after completion → invoked again; a faulted evaluation is not cached (next call re-invokes).

Steps: failing tests → implement → dotnet test tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests --filter "FullyQualifiedName~PendingUpdatesCache" → commit feat(central-ui): process-wide single-flight cache for the pending-updates snapshot.


Task 6: Rail indicator component

Classification: standard · Estimated implement time: ~5 min · Parallelizable with: Task 7

Files:

  • Create: src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Layout/PendingUpdatesIndicator.razor (+ .razor.cs if the file grows). Injects IPendingUpdatesCache, SiteScopeService, AuthenticationStateProvider, ILogger<>. Renders nothing unless the user has the Deployer or Administrator role claim (JwtTokenService.RoleClaimType, like NavMenu's Notifications block). Markup (all inside <div class="rail-updates" title="as of HH:mm UTC">):
    • loading: <span class="rail-updates-muted"><i class="bi bi-arrow-repeat"></i> Checking for updates…</span>
    • up to date: <span class="rail-updates-muted"><i class="bi bi-check2-circle"></i> Up to date</span>
    • pending: <span class="rail-updates-head"><i class="bi bi-exclamation-triangle"></i> Pending updates <span class="badge bg-warning text-dark">N</span></span> (N = stale instances + stale-artifact sites) then <span class="rail-updates-detail"> with X instance(s) (an <a href="/deployment/topology"> when Deployer, else text) · Y site(s) (an <a href="/admin/sites"> when Administrator, else text); omit a zero part.
    • error: <span class="rail-updates-muted" title="@error"><i class="bi bi-question-circle"></i> Updates unavailable</span>.
    • Site-scoped user: snapshot.ForSites(await SiteScope.PermittedSiteIdsAsync()) unless IsSystemWideAsync().
    • Poll: System.Threading.Timer every 30 s through a PollGate, first load in OnInitializedAsync; IDisposable disposes the timer and sets _disposed; every re-render via InvokeAsync(StateHasChanged) guarded by _disposed; catch ObjectDisposedException/InvalidOperationException from a torn-down circuit.
  • Modify: src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Layout/MainLayout.razor — inside <RailFooter> <Authorized>, place <PendingUpdatesIndicator /> immediately above <span class="rail-user">.
  • Modify: src/ZB.MOM.WW.ScadaBridge.CentralUI/wwwroot/css/site.css — append a /* ── Rail pending-updates indicator ── */ block: .rail-updates { padding: 0 0.6rem 0.5rem; font-size: 0.8rem; display:flex; flex-direction:column; gap:0.15rem; }, .rail-updates-muted { color: var(--ink-faint); }, .rail-updates-head { font-weight: 600; color: var(--ink-soft); display:flex; align-items:center; gap:0.35rem; }, .rail-updates-detail { color: var(--ink-faint); }, .rail-updates a { color: inherit; text-decoration: underline dotted; }. Use tokens only (dark mode follows).
  • Test: tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Layout/PendingUpdatesIndicatorTests.cs — bUnit with Substitute.For<IPendingUpdatesCache>(), TestAuthStateProvider (copy NavMenuTests' claims helper), a SiteScopeService over that provider: hidden for Viewer; "Up to date" for empty snapshot; "Pending updates" + 3 + links for Deployer+Admin; Deployer-only renders the site count as text (no /admin/sites href); faulted cache → "Updates unavailable"; scoped user sees filtered counts. Use WaitForAssertion.

Steps: failing tests → implement → dotnet test tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests --filter "FullyQualifiedName~PendingUpdatesIndicator" → commit feat(central-ui): rail-footer pending-updates indicator.


Task 7: Sites page artifact badges, instance line, re-check

Classification: standard · Estimated implement time: ~5 min · Parallelizable with: Task 6

Files:

  • Modify: src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Admin/Sites.razor:
    • @inject IPendingUpdatesCache PendingUpdates; fields PendingUpdatesSnapshot? _updates; string? _updatesError; bool _recheckingUpdates;.
    • LoadDataAsync loads sites/connections as today, then (not blocking the card render) await LoadUpdatesAsync(force:false); LoadUpdatesAsync catches, sets _updatesError, logs a Warning.
    • Header: after the title, a small muted caption Updates as of HH:mm:ss UTC when _updates is set, and a btn btn-outline-secondary btn-sm Re-check button (spinner + disabled while _recheckingUpdates) calling LoadUpdatesAsync(force:true).
    • Card: next to <code>@site.SiteIdentifier</code> render ArtifactBadge(site.Id): _updates is null && _updatesError is null → <span class="badge bg-light text-dark ms-2">Artifacts: checking…</span>; error → <span class="badge bg-light text-dark ms-2" title="@_updatesError">Artifacts: unavailable</span>; else by state: Current → bg-light text-dark, OutOfDate → bg-warning text-dark with <i class="bi bi-exclamation-triangle me-1"></i> and title="Artifacts deployed <ArtifactsDeployedAt:u>", NeverDeployed → bg-secondary. All carry aria-label="Artifacts: <state>". Text: Artifacts current / Artifacts out of date / Artifacts never deployed.
    • Under the description: when the site's DeployedInstanceCount > 0, <div class="small text-muted mb-2">@stale of @deployed instances out of date — <a href="/deployment/topology">Topology</a></div> (say all current when stale == 0 and unknown == 0; append , N unknown when unknown > 0).
    • DeployArtifacts / DeployArtifactsToAllSites: on success call LoadUpdatesAsync(force:true).
  • Modify: tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Admin/SitesPageTests.cs — register Substitute.For<IPendingUpdatesCache>() (and ArtifactRevisionHashService in the ArtifactDeploymentService ctor if Task 2 added it); new tests: badge text per state; instance line; Re-check calls GetAsync(true); cache fault → "unavailable" and the site list still renders.

Steps: failing tests → implement → dotnet test tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests --filter "FullyQualifiedName~SitesPage" → commit feat(central-ui): Sites page shows whether a site's deployed artifacts are current.


Task 8: Scrollable rail nav

Classification: small · Estimated implement time: ~3 min · Parallelizable with: Task 1, 2, 3 (must land BEFORE Task 6 — both append to site.css)

Files:

  • Modify: src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Layout/NavMenu.razor — wrap everything (the Dashboard item and the <AuthorizeView>) in <div class="rail-scroll">…</div>; extend the header comment (scroll pane rationale, kit untouched).
  • Modify: src/ZB.MOM.WW.ScadaBridge.CentralUI/wwwroot/css/site.css — append:
    /* ── Rail nav scroll pane ────────────────────────────────────────────────
       ThemeShell's nav.side-rail is a flex column inside the sticky 100vh
       #theme-rail (lg+), but it never fills that height and nothing inside it
       scrolls, so a long nav grows past the viewport and carries the footer
       with it. Fill the rail and let the nav items scroll while the brand bar
       and .rail-foot stay put. Below lg the rail is inline and scrolls with the
       page, so the rule is scoped to lg+. The kit is untouched (deferred-work
       register: upstream into ZB.MOM.WW.Theme). */
    @media (min-width: 992px) {
        #theme-rail > .side-rail { height: 100%; }
        .rail-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
    }
    
  • Modify: tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Layout/NavMenuTests.cs — one test: markup contains class="rail-scroll" and the rail-section details sit inside it.

Steps: test → change → dotnet test tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests --filter "FullyQualifiedName~NavMenuTests" → commit fix(central-ui): rail nav scrolls in its own pane so the footer stays pinned.


Task 9: Documentation

Classification: small · Estimated implement time: ~4 min · Parallelizable with: Task 10

Files:

  • docs/requirements/Component-CentralUI.md: Site & Data Connection Management — artifact badge, instance line, Re-check; System-Wide Artifact Deployment — "Track per-site deployment status" now includes the revision stamp + Current/Out of date/Never deployed; new bullet under Technology (or near the nav description) for the rail Pending updates indicator (roles, 30 s poll over the 60 s cache, scope filter) and the rail scroll pane.
  • docs/requirements/Component-DeploymentManager.md: Deployable Artifacts — ArtifactRevisionHashService, stamping rule (success only), ComputeCurrentArtifactHashesAsync, GetInstanceStalenessAsync (shared session, mismatch-only normalisation), PendingUpdatesEvaluator, PendingUpdatesCacheTtl option, not transported.
  • docs/requirements/Component-ConfigurationDatabase.md: migration list entry AddSiteArtifactRevision (mirror how AddAlarmMesReportable is listed).
  • CLAUDE.md (repo root): one bullet under Templates & Deployment after the bulk-deploy bullet: the site artifact revision + pending-updates indicator (short; link the design doc).
  • docs/plans/2026-07-08-deferred-work-register.md: rows 123 (upstream the rail scroll pane into ZB.MOM.WW.Theme), 124 (watermark short-circuit so an unchanged fleet costs no re-flatten), 125 (expose the pending-updates counts on the Health dashboard / CLI).
  • src/ZB.MOM.WW.ScadaBridge.CLI/README.md: note that site list --detail now shows artifactRevisionHash / artifactsDeployedAt (only if the projection shows them; otherwise skip).

Commit docs: site artifact revision, pending-updates indicator, rail scroll pane.


Task 10: Rig — rebuild, verify script, Playwright smoke

Classification: standard · Estimated implement time: ~5 min (+ rig build time) · Parallelizable with: Task 9

Files:

  • Create: docker/verify-pending-updates.sh — uses the rig CLI (see .claude/skills/scadabridge-cluster-ops/SKILL.md and docker/verify-mes-alarm-demo.sh for the conventions: management URL, scadabridge CLI invocation, PASS/FAIL counters). Steps: (A) site list → every site has artifactRevisionHash (null allowed on a fresh DB); (B) deploy artifacts to all sites via CLI (artifact deploy — find the command in the CLI README) → every site's hash non-null and artifactsDeployedAt recent; (C) create/update a throwaway shared script (shared-script …) → GET the Central UI /admin/sites is auth-gated, so instead verify via CLI: site list hashes unchanged and a fresh artifact deploy changes the hash (proves the hash tracks content); (D) delete the throwaway script, redeploy → hash returns to the value from (B). Print PASS/FAIL summary, exit non-zero on FAIL.
  • Create: tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/PendingUpdatesTests.cs — copy the fixture-login pattern from FlowDebuggerTests: after login the rail contains either "Up to date" or "Pending updates"; /admin/sites renders an aria-label^="Artifacts:" badge per card and a Re-check button.
  • Run: bash docker/deploy.sh (from the worktree — check the script builds from its own checkout), wait for /health/ready, bash docker/verify-pending-updates.sh, then the Playwright test (dotnet test tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests --filter "FullyQualifiedName~PendingUpdates"). Also load /admin/sites and the rail once through the browser tools and confirm the indicator sits above the user name and the nav scrolls at a short viewport (record what was seen in the commit message).

Commit test(rig): verify-pending-updates.sh + Playwright smoke for the indicator and Sites badge.


Task 11: Finish — sweep, review, merge, memory

Classification: standard · Estimated implement time: ~5 min (+ test time) · Parallelizable with: none

  1. dotnet build ZB.MOM.WW.ScadaBridge.slnx → 0 warnings/0 errors; dotnet test ZB.MOM.WW.ScadaBridge.slnx (skip Playwright unless the rig is up) → all green; note any pre-existing flake by name.
  2. Final code review of git diff main..HEAD (subagent).
  3. Fast-forward merge into main in the primary checkout (git -C /Users/dohertj2/Desktop/ScadaBridge merge --ff-only feat/pending-updates), remove the worktree, delete the branch.
  4. Update the plan's .tasks.json, memory (pending-updates-state.md + MEMORY.md index), and the ScadaBridge row of ../scadaproj/CLAUDE.md (tip changed; NOT pushed unless asked).