feat: one Set Password operation over the delivery queue; Synchronise Password withdrawn (#1635, layer 3 of 3) - #1639
Merged
Conversation
…ns service health (#1635) Implementation plan for three stacked layers (Operations and service health; a Password Delivery Service off the worker task queue; convergence of Set Password and Synchronise Password), and the PRD amendment recording the decisions that change requirements 21, 25 and 31. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rations (#1635) The standalone /admin/password-synchronisation page (unreleased) is dropped per decision D9. Its body moves to OperationsPasswordsTab, hosted as a fourth tab on Operations with a key icon and an amber badge of parked plus expired changes. The metaverseObjectId and connectedSystemId deep links keep working on the Operations route, and clearing a deep-linked filter also drops it from the URL so it does not return when the tab is re-created on a switch back. Deep links and wording updated: the Metaverse Object panel's queue link, the Connected Systems list indicator tooltip, the Administration index (tile removed; Operations tile mentions password delivery), the concepts doc and the unreleased changelog entry. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Each background service now writes a ServiceHeartbeat row every 5 seconds from the same place it touches its container health-check file: the Worker's synchronisation loop (with its in-flight tasks as CurrentWork) and the Scheduler (both while waiting for readiness and, in heartbeat-sized slices of its 30-second wait, in its main loop). SystemHealthServer (JimApplication.SystemHealth) derives Running / Stale / NoProgress / NotSeen per service from the newest row, with the worst state as the report's Overall, for the Operations page, REST API and PowerShell to read. - JIM.Models.Operations: JimService, ServiceHeartbeat, ServiceHealthState, ServiceHealth, ServiceHealthReport - ISystemRepository: single-statement ON CONFLICT upsert, newest-per-service read, per-service prune - Migration AddServiceHeartbeats (unique index on Service, InstanceId) - ServiceHeartbeatWriter: throttled, prunes its service's day-old rows on first write, never throws a database failure into the host loop (first failure at Warning, repeats at Debug, recovery at Information) - JimVersion.Current shared by Web, Worker and Scheduler; HealthController uses it - TaskTask carries a dispatch-time description and start so the heartbeat costs no extra reads Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…1635) GET /api/v1/system/health (Administrator) returns the ServiceHealthReport the Operations page reads: overall state, web version, generated-at and one entry per service with state, reason, current work and instance detail, enums as string names, Cache-Control: no-store. Get-JIMServiceHealth emits one JIM.ServiceHealth object per service, or a single JIM.ServiceHealthSummary with -Summary whose Overall is the worst state present, for monitoring scripts. Docs: new configuration/operations.md describing the Operations page and its Service Health strip (states, thresholds, banner, version skew, REST and PowerShell equivalents), the cmdlet reference, and the endpoint noted in the API overview and deployment health table. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Password delivery has no heartbeat writer until the Password Delivery Service lands in the next layer, so listing it as expected put a permanent "never reported" card and a permanent administrator banner on every deployment. The report now covers the expected services (Worker sync loop, Scheduler) plus any service that has actually reported, in enum order; the next layer adds password delivery to the expected list. Docs describe three cards accordingly. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
One card per background service at the top of Operations (Worker · Sync, Worker · Passwords, Scheduler) plus a Live updates card for the notification relay, polled by the page every ten seconds alongside the Passwords badge; a banner above every page for administrators when a service is not seen or a task has stalled, hidden on Operations itself; a red dot on the Operations tile of the Administration index under the same verdict. MainLayout's footer version now comes from JimVersion.Current so it matches what the strip compares service versions against. A password delivery service that has never reported beside a reporting Worker is treated as a version gap rather than an outage: the card says so, the banner stays quiet. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The read model now decides which services are expected, so the banner's special case for a never-reported password delivery loop is redundant. Until a delivery loop is in the report, the synchronisation loop down is the whole Worker down, and the banner says so in those words. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…form cards (#1635) Replace ServiceHealthState with two enums: ServiceHealthStatus (Healthy, Degraded, Unhealthy; ordered by severity, Overall is the max) and ServiceHealthCondition (Heartbeating, HeartbeatOverdue, Stalled, NoHeartbeat, NeverStarted), with the thresholds unchanged and StatusOf as the single condition-to-status mapping. Reason sentences become plain words ("Heartbeat 3 seconds ago", "No heartbeat for 4 minutes", "Never started"). The REST DTO carries status and condition, the cmdlet Status and Condition, and the docs, Pester and API tests follow. Rebuild ServiceHealthStrip as a panel: a header with the title, a worst-first summary ("1 service unhealthy, 1 degraded") and the Live updates indicator (no longer a card), over a CSS grid of identical four-slot cards (name and status pill; activity; condition; host, version, uptime). The pill is the only coloured element; version skew is a "differs from portal" chip rather than amber text. The banner sentences and the Administration index red dot (Unhealthy, or Degraded by Stalled) are unchanged in behaviour. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… task (#1635) Layer 2 of the One Password Pipeline plan. Queued Password Synchronisation changes are now delivered by a dedicated BackgroundService in the Worker process, woken by a database notification on the queue table, by the earliest scheduled retry, and by a 30 s safety poll, rather than by a Worker Task that waited behind whatever Run Profile was executing. - PendingPasswordChange gains a claim (ClaimedAt, ClaimedBy) and the Delivering status; ClaimDuePasswordChangesAsync selects, marks and returns due rows in one FOR UPDATE SKIP LOCKED statement under a 60 s lease. The attempt write is guarded on the row still being Delivering, so a cancellation or a newer password that arrives mid-flight keeps its outcome; unattempted claims are released back to Pending. - PasswordSynchronisationServer delivers over claimed rows in batches of 100, exposes the delivery outlook and due-system reads the service needs, and answers GetChangeOutcomesAsync with per-target states for callers waiting on a change. Queue, retry and release no longer request delivery: the row change wakes the service. - PasswordDeliveryScheduler holds the loop's wake and dispatch rules: one lane per Connected System, four lanes across systems, heartbeat every iteration with current work and queue counts, a per-system hold-off after a lane that could not deliver at all, and a lane fault never stops the loop. - WorkerPasswordDelivery joins the expected services in the health report. - Migration AddPasswordDeliveryClaims: the claim columns, the jim_password_change trigger, and removal of PasswordDeliveryWorkerTask rows and its column. The task type, its tasking methods, the housekeeping tick and the per-pass Activity are gone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… scenario (#1635) Documents the Password Delivery Service in the passwords concept page (what wakes it, what "on its own clock" now means, the Delivering status, the Worker · Passwords health card), adds the card to Operations' Service Health section and the third service to the deployment and API pages, and catalogues the endpoint's optional `wait` with its 200/202 answers. Sync-JIMMetaverseObjectPassword gains -Wait (0 to 30 seconds), sent as `wait`, with the per-target State, NextAttemptAt, Message and AttemptCount and the top-level Settled documented in its help and on the PowerShell page, which also gains the cmdlet's own section. Get-JIMPendingPasswordChange accepts Delivering as a status filter, and the queue cmdlets' help no longer speaks of delivery passes. Pester covers the parameter's range, the request body with and without a wait, and the output shape for a settled and an unsettled response. Scenario 20 gains two tests: a change queued with -Wait 10 while a Full Import runs must come back settled with the target Set, and be gone from the queue, within ten seconds (the measured latency is printed); a deliberately refused password parks and, retried from the queue, is attempted within five seconds. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…it on the REST endpoint (#1635) The web process now listens on jim_password_change and relays it in-process (debounced 200 ms) through IPasswordChangeNotifications, and PasswordChangeOutcomeWaiter follows a queued change until every target settles, woken by the relay and polling as a safety net. POST /api/v1/metaverse/objects/{id}/password gains an optional wait (0 to 30 s, default 0 per decision D6), answering 200 when settled and 202 when not, and the response carries each target's state, next attempt, message and attempt count plus a top-level settled flag. The Synchronise Password dialog stays open in a delivering stage with one row per Connected System, updated through the waiter for up to ten seconds. A Delivering row reads as such on the Passwords tab, cannot be retried while in flight, and the Waiting filter (Pending) also returns rows being delivered, matching the Waiting count. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ync-JIMMetaverseObjectPassword help (#1635) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…QL keys on (#1635) JimService.WorkerPasswordDelivery becomes WorkerDelivery. CodeQL's cleartext-storage heuristic treats any identifier containing "password" as sensitive data, and the enum value is written to the heartbeat table on every tick, so PR #1638 failed on four such findings. The value is a service identifier; the display label stays "Worker · Passwords". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d a shared delivery core (#1635) Set Password and Synchronise Password converge on PasswordSynchronisationServer.SetPasswordAsync(SetPasswordRequest): Targets null propagates to every Connected System configured for Password Synchronisation (unchanged behaviour, Origin Propagated, EnableAccount never carried); a list of Connected System Object ids queues one Explicit row per named account, validated before anything is recorded, with the administrator's enable decision and the system's time to live, needing no configuration and not held by a paused one (decision D1). Both share one Activity shape and coalesce onto each other by person and system. PendingPasswordChange gains Origin and EnableAccount (migration AddPasswordChangeOrigin; existing rows are Propagated). The lane claims and expires explicit rows only over an unconfigured or paused system, resolves an explicit row's account from its own id and parks it when that account is gone, retries under ConnectedSystemPasswordSynchronisation defaults where no configuration exists, and builds a Connector only once something is claimed. The due-systems, outlook, summary and header reads treat an explicit row as due wherever it is; GetChangeOutcomesAsync never reports one as Held. The open, check, set, classify, close sequence is extracted once into PasswordDeliveryCore and used by the lane, the initial-password pass (a throwing Connector is now a transient retry there rather than an escaping exception) and the interim immediate path. ConnectedSystemServer's set-password members stay as shims for the controller and Razor callers the web layer still compiles against, marked for that layer's rewrite. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The person page's Password Synchronisation and Actions tabs become one Administrator-gated Password tab: an attention strip for parked or expired changes with Retry, the Set Password card with the capable-account count, Still to be delivered with per-row Retry and Stop trying, and Recent password changes with a Set or Propagated kind chip. The tab's data loads on first activation of its slug (NavigableMudTabs now raises ActivePanelSlugChanged); only the badge count is read with the page. SetPasswordDialog keeps its composition and submits through PasswordSynchronisationServer.SetPasswordAsync with the ticked accounts as explicit targets. Its result stage is driven by the outcome waiter: one row per Connected System reading Set, Retrying (next attempt, Stop trying) or Parked (the target's words, guidance, Try another password), with the storage promise reworded per decision D4. The Connected System Object page uses the same dialog and operation with one target. The Synchronise Password dialog, the Actions tab and the progress rail are removed. Application layer, in support: SetPasswordAsync records the change's origin on the parent Activity's TargetContext and the password history projects it back as PasswordSynchronisationEvent.Origin (null for older Activities), and PasswordChangeTargetOutcome carries the queue row's FailureReason so the dialog can choose remediation guidance. Service Health cards: the footer's separator dot is drawn in the column gap and clipped at a line start so a wrap never opens with a dot, and an unhealthy card that was idle fills its condition slot with its last heartbeat. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…hronise Password withdrawn (#1635) REST: POST /api/v1/metaverse/objects/{id}/password is now SetMetaverseObjectPassword, taking password, optional connectedSystemObjectIds (omitted = every Connected System configured for Password Synchronisation), expiryBehaviour, enableAccount (named accounts only) and wait (0 to 30, default 10 with named accounts, 0 when propagating). The response is the per-target outcome shape plus origin; 200 when settled, 202 when not, 400 for validation and for the core's ArgumentException, 404 for the person. The account-scoped endpoint is a one-target wrapper over the same operation with the same response, and answers 404 when the object is not joined to a Metaverse Object. PowerShell: Set-JIMMetaverseObjectPassword gains the propagate default (D5), -Wait and four parameter sets (Named/Propagate x Supplied/Generated) with -EnableAccount only in the Named sets; -AllAccounts and Sync-JIMMetaverseObjectPassword are removed. Set-JIMConnectedSystemObjectPassword returns the same outcome shape, waits by default, gains -Wait and loses -PassThru. A Parked target is also a non-terminating error carrying the result. Docs: one operation with two target modes in docs/concepts/passwords.md, D4 wording for what JIM holds and for how long, the person's Password tab, cmdlet pages and the API overview updated, Sync- section removed. Changelog amended so 0.15.0 reads as the final shape. Scenario 20 moved to the new cmdlet. Plan carries a Deviations section. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…son on the REST outcome (#1635) The web and API now call SetPasswordAsync directly, so the ConnectedSystemServer shims, the QueuePasswordChangeAsync forwarders, MultiAccountPasswordSetResult and AccountPasswordSetOutcome go, with the two fixtures that existed only for them; the fan-out tests exercise the propagate mode of the one operation. The per-target failure reason the portal uses for its guidance is now on the REST response and the cmdlet output too. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ssword history as a timeline (#1635) Service Health cards carry the uptime on the heartbeat line and move host, version and instance behind a per-card details control, each value in monospace with a copy button; the version-skew chip stays on the card's face beside the pill and repeats on the Version row. The status pill becomes the shared .jim-status-pill vocabulary (ok, warn, err, neutral). The person page's recent password changes become a day-grouped timeline: one entry per change with the time, kind chip and initiator, a status pill per Connected System, and words only where a system refused the change or is still owed it, with Retry and Stop trying on that line. The derivation, including which change a live queue row belongs to, lives in PasswordHistoryTimelineModel with its own unit tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ivery-service Three-way merge using the layer 1 tip (63bbbfe) as the base, because #1636 squash-merged and git's default merge-base predates it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mw5XPncvpoN78zeazx5jgi
…ce' into feature/operations-health-stack-password-pipeline
Base automatically changed from
feature/operations-health-stack-password-delivery-service
to
main
September 6, 2026 18:26
…eline Three-way merge using #1638's final tip (c9404ea) as the base, because #1638 squash-merged and git's default merge-base predates it. The resulting tree is identical to the branch's previous head. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mw5XPncvpoN78zeazx5jgi
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Layer 3 of #1635 (plan:
engineering/plans/done/PASSWORD_PIPELINE_CONVERGENCE.md), stacked on #1638 and #1636. Closes #1635 when the stack lands.PasswordSynchronisationServer.SetPasswordAsync(SetPasswordRequest)replaces both the immediate set-password path and the separate synchronise path. A request names accounts (Connected System Object ids) or names none, meaning every Connected System configured for Password Synchronisation. Both origins write the same queue rows, the same Activity shape (parent per change, child per system) and are delivered by the Layer 2 service.AddPasswordChangeOrigin.PasswordDeliveryCore, used by the delivery lane and by initial password provisioning. The immediateConnectedSystemServerset-password methods, the queue forwarders and their result DTOs are gone.SetPasswordDialogkeeps its composition (picker, generate, copy while masked, expiry, enable) and its result stage now follows the queue live: Set, Retrying with next attempt and Stop trying, Parked with the target's words and guidance, Try another password. Synchronise Password and the Actions tab are removed. The account page uses the same dialog with one target.POST /api/v1/metaverse/objects/{id}/passwordis Set Password:password, optionalconnectedSystemObjectIds,expiryBehaviour(defaults differ by mode),enableAccount(named accounts only),wait(default 10 s with accounts, 0 when propagating); per-target outcome includingfailureReason, 200 settled or 202. The account-scoped endpoint is a one-target wrapper with the same response.Set-JIMMetaverseObjectPasswordgains-ConnectedSystemId <int[]>naming accounts, the propagate default when none is named (decision D5),-Wait, and the outcome shape;-AllAccountsandSync-JIMMetaverseObjectPasswordare withdrawn;Set-JIMConnectedSystemObjectPasswordreturns the same shape.docs/concepts/passwords.mdsay so; the "two ways to give somebody a password" section is now one operation with two target modes.Verification
dotnet build JIM.sln: 0 warnings, 0 errors;dotnet test JIM.sln: green (see checks). RequiresPostgres fixtures for claims, origins and outcomes run against a scratch database.Lint-Changelog.ps1andLint-DocExamples.ps1pass.Set-JIMMetaverseObjectPassword.🤖 Generated with Claude Code