Claude-Session: https://claude.ai/code/session_01Y5ef34iUUMjmwnMpL9xgmB
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 withClaude-Session: https://claude.ai/code/session_01Y5ef34iUUMjmwnMpL9xgmB. - Every public member needs an XML doc comment (
checkdocsenforces it). - Never
awaitan audit write on a script path; never block on a Task from a Blazor lifecycle/event path. - Bootstrap
.badgemarkup 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— addpublic string? ArtifactRevisionHash { get; set; }andpublic 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—ArtifactRevisionHashHasMaxLength(100). - Create: migration
AddSiteArtifactRevisioninsrc/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Migrations/— build first, thendotnet ef migrations add AddSiteArtifactRevision --project src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase --startup-project src/ZB.MOM.WW.ScadaBridge.Host(check howAddAlarmMesReportablewas generated:git log -1 --stat 4cfa645b^../ the previous plan; NO--no-build). Verify the migration is non-empty (twoAddColumn). - Modify:
src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/IDeploymentManagerRepository.cs— addTask<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 newSiteproperties 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 forGetDeployedRevisionHashesAsync(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: privateHashable*records with alphabetically declared properties, each artifact listOrderBy(Name, StringComparer.Ordinal),DeploymentIdand the timestamp excluded,JsonNamingPolicy.CamelCase,WhenWritingNull, SHA-256 hex (lower-case, likeRevisionHashService). Cover every field ofSharedScriptArtifact,ExternalSystemArtifact,DatabaseConnectionArtifact,DataConnectionArtifact(read the record definitions underCommons/Messages/Artifacts/). - Modify:
src/ZB.MOM.WW.ScadaBridge.DeploymentManager/ArtifactDeploymentService.cs:- inject
ArtifactRevisionHashService; DeployCoreAsync: afterTask.WhenAll, for each site whose resultSuccess, setsite.ArtifactRevisionHash = hash(siteCommands[site.Id]),site.ArtifactsDeployedAt = DateTimeOffset.UtcNow, then persist throughISiteRepository(use its existing update + save methods — check the interface; theSiteentities came from_siteRepo.GetAllSitesAsyncand are tracked, so anUpdateSiteAsync/SaveChangesAsyncpair 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)— oneFetchGlobalArtifactsAsync, then per siteBuildDeployArtifactsCommandAsync(site.Id, globals, ct, deploymentId: "current")→ hash. (deploymentIdis excluded from the hash, so any constant is fine.)
- inject
- Modify:
src/ZB.MOM.WW.ScadaBridge.DeploymentManager/ServiceCollectionExtensions.cs— registerArtifactRevisionHashServicesingleton. - Modify: every test that constructs
ArtifactDeploymentServicedirectly (grepnew ArtifactDeploymentService(undertests/— at leasttests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Admin/SitesPageTests.csand the DeploymentManager tests) — passnew 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; differentDeploymentId/timestamp → same hash;HashableRecords_PropertiesDeclaredAlphabetically(reflection, mirrorRevisionHashServiceTests).ArtifactDeploymentServiceTests— success stamps hash + timestamp; failure leaves the previous stamp;ComputeCurrentArtifactHashesAsyncequals 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
GetDeploymentComparisonAsynca privatestring ComputeDeployedHash(DeployedConfigSnapshot snapshot, out FlattenedConfiguration? normalized)that applies the I-1 List normalisation and recomputes the hash (returnssnapshot.RevisionHashwhen the JSON cannot be deserialised) —GetDeploymentComparisonAsyncmust keep behaving byte-for-byte (its existing tests pin it). - add
public async Task<IReadOnlyDictionary<int, bool?>> GetInstanceStalenessAsync(IReadOnlyCollection<int> instanceIds, CancellationToken cancellationToken = default): readGetDeployedRevisionHashesAsync()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.
- extract from
- Test:
tests/ZB.MOM.WW.ScadaBridge.DeploymentManager.Tests/DeploymentServiceStalenessTests.cs(or extend the existing DeploymentService test class; check how it is constructed —IFlatteningPipelineis substitutable): shared session passed to every flatten call (Receivedwith the sameFlattenSessioninstance,CreateSessioncalled 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 computedStaleInstanceCount,StaleArtifactSiteCount,HasPendingUpdates, andForSites(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 siteOutOfDate? 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.Zerowherever 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 (ifDeploymentService/ArtifactDeploymentServiceare not substitutable) small interface seams: prefer addingIArtifactRevisionSource/ reuse the real services with substituted repos asTopologyPageTestsdoes. Cases: never-deployed site; current; out-of-date; instance counts (2 deployed, 1 stale, 1 unknown);ForSitesfilter; 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— mirrorsKpiSnapshotCache: ctor(IServiceScopeFactory, IOptions<DeploymentManagerOptions>), oneSingleFlightMemo<PendingUpdatesSnapshot>(options.Value.PendingUpdatesCacheTtl), factory opens a scope and resolvesIPendingUpdatesEvaluator. - Modify:
src/ZB.MOM.WW.ScadaBridge.CentralUI/ServiceCollectionExtensions.cs—services.AddSingleton<IPendingUpdatesCache, PendingUpdatesCache>();next toIKpiSnapshotCache. - Test:
tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Services/PendingUpdatesCacheTests.cs— two concurrentGetAsync→ evaluator invoked once;forceRefresh:trueafter 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.csif the file grows). InjectsIPendingUpdatesCache,SiteScopeService,AuthenticationStateProvider,ILogger<>. Renders nothing unless the user has theDeployerorAdministratorrole claim (JwtTokenService.RoleClaimType, likeNavMenu'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">withX 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())unlessIsSystemWideAsync(). - Poll:
System.Threading.Timerevery 30 s through aPollGate, first load inOnInitializedAsync;IDisposabledisposes the timer and sets_disposed; every re-render viaInvokeAsync(StateHasChanged)guarded by_disposed; catchObjectDisposedException/InvalidOperationExceptionfrom a torn-down circuit.
- loading:
- 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 withSubstitute.For<IPendingUpdatesCache>(),TestAuthStateProvider(copyNavMenuTests' claims helper), aSiteScopeServiceover 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/siteshref); faulted cache → "Updates unavailable"; scoped user sees filtered counts. UseWaitForAssertion.
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; fieldsPendingUpdatesSnapshot? _updates; string? _updatesError; bool _recheckingUpdates;.LoadDataAsyncloads sites/connections as today, then (not blocking the card render)await LoadUpdatesAsync(force:false);LoadUpdatesAsynccatches, sets_updatesError, logs a Warning.- Header: after the title, a small muted caption
Updates as of HH:mm:ss UTCwhen_updatesis set, and abtn btn-outline-secondary btn-smRe-check button (spinner + disabled while_recheckingUpdates) callingLoadUpdatesAsync(force:true). - Card: next to
<code>@site.SiteIdentifier</code>renderArtifactBadge(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-darkwith<i class="bi bi-exclamation-triangle me-1"></i>andtitle="Artifacts deployed <ArtifactsDeployedAt:u>",NeverDeployed→bg-secondary. All carryaria-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>(sayall currentwhen stale == 0 and unknown == 0; append, N unknownwhen unknown > 0). DeployArtifacts/DeployArtifactsToAllSites: on success callLoadUpdatesAsync(force:true).
- Modify:
tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Admin/SitesPageTests.cs— registerSubstitute.For<IPendingUpdatesCache>()(andArtifactRevisionHashServicein theArtifactDeploymentServicector if Task 2 added it); new tests: badge text per state; instance line; Re-check callsGetAsync(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 containsclass="rail-scroll"and therail-sectiondetails 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,PendingUpdatesCacheTtloption, not transported.docs/requirements/Component-ConfigurationDatabase.md: migration list entryAddSiteArtifactRevision(mirror howAddAlarmMesReportableis 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 intoZB.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 thatsite list --detailnow showsartifactRevisionHash/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.mdanddocker/verify-mes-alarm-demo.shfor the conventions: management URL,scadabridgeCLI invocation, PASS/FAIL counters). Steps: (A)site list→ every site hasartifactRevisionHash(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 andartifactsDeployedAtrecent; (C) create/update a throwaway shared script (shared-script …) → GET the Central UI/admin/sitesis auth-gated, so instead verify via CLI:site listhashes 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 fromFlowDebuggerTests: after login the rail contains either "Up to date" or "Pending updates";/admin/sitesrenders anaria-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/sitesand 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
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.- Final code review of
git diff main..HEAD(subagent). - Fast-forward merge into
mainin the primary checkout (git -C /Users/dohertj2/Desktop/ScadaBridge merge --ff-only feat/pending-updates), remove the worktree, delete the branch. - Update the plan's
.tasks.json, memory (pending-updates-state.md+MEMORY.mdindex), and the ScadaBridge row of../scadaproj/CLAUDE.md(tip changed; NOT pushed unless asked).