Skip to content

feat: one Set Password operation over the delivery queue; Synchronise Password withdrawn (#1635, layer 3 of 3) - #1639

Merged
JayVDZ merged 24 commits into
mainfrom
feature/operations-health-stack-password-pipeline
Sep 6, 2026
Merged

feat: one Set Password operation over the delivery queue; Synchronise Password withdrawn (#1635, layer 3 of 3)#1639
JayVDZ merged 24 commits into
mainfrom
feature/operations-health-stack-password-pipeline

Conversation

@JayVDZ

@JayVDZ JayVDZ commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Layer 3 of #1635 (plan: engineering/plans/done/PASSWORD_PIPELINE_CONVERGENCE.md), stacked on #1638 and #1636. Closes #1635 when the stack lands.

  • One password operation. 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.
  • Explicit rows carry their account and the enable-account decision, need no Password Synchronisation configuration, use default retry settings where none exists, and are delivered even where a configuration is switched off (decision D1). Propagated rows behave exactly as before. Migration AddPasswordChangeOrigin.
  • One delivery core. The open channel, security refusal, set and classify sequence lives once in PasswordDeliveryCore, used by the delivery lane and by initial password provisioning. The immediate ConnectedSystemServer set-password methods, the queue forwarders and their result DTOs are gone.
  • Portal. The person page has one administrator-only Password tab: attention strip with Retry for parked changes, the Set Password card with the capable-account count, still-to-be-delivered rows with Retry and Stop trying, and history with a Set or Propagated chip. SetPasswordDialog keeps 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.
  • REST and PowerShell. POST /api/v1/metaverse/objects/{id}/password is Set Password: password, optional connectedSystemObjectIds, expiryBehaviour (defaults differ by mode), enableAccount (named accounts only), wait (default 10 s with accounts, 0 when propagating); per-target outcome including failureReason, 200 settled or 202. The account-scoped endpoint is a one-target wrapper with the same response. Set-JIMMetaverseObjectPassword gains -ConnectedSystemId <int[]> naming accounts, the propagate default when none is named (decision D5), -Wait, and the outcome shape; -AllAccounts and Sync-JIMMetaverseObjectPassword are withdrawn; Set-JIMConnectedSystemObjectPassword returns the same shape.
  • Wording (decision D4). Set Password no longer claims to store nothing: JIM holds the password encrypted only until each system has it, and keeps a refused one so it can finish the job. Dialog, endpoint remarks and docs/concepts/passwords.md say so; the "two ways to give somebody a password" section is now one operation with two target modes.
  • Service Health cards, after review: uptime sits on the heartbeat line; host, version and instance id move behind a per-card details control with copy buttons; the version skew chip stays visible; no card has an empty slot.
  • The person's password history is a day-grouped timeline in the Changes tab's visual language: one entry per change, an outcome pill per Connected System in the same vocabulary as Service Health, words only where something needs them, Retry and Stop trying inline.
  • Changelog amended under Unreleased so 0.15.0 describes the final shape; the plan document moves to done.

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.
  • Pester: 1834 passed across the module; Lint-Changelog.ps1 and Lint-DocExamples.ps1 pass.
  • Runtime on the devcontainer stack: an explicit set for one person on Panoply AD was claimed within half a second and Set within a second; the Password tab, the dialog's result stage and the history chip verified in the browser; scenario 20's latency steps passed on Layer 2 and its script now uses Set-JIMMetaverseObjectPassword.

🤖 Generated with Claude Code

JayVDZ and others added 23 commits September 5, 2026 09:20
…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>
)

Both seeded rows share a CreatedAt, so the claim's Id tiebreak chose either; CI took the other one. The test now reads back which row the claim returned.

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
@JayVDZ
JayVDZ enabled auto-merge (squash) September 6, 2026 18:28
@JayVDZ
JayVDZ merged commit ae99beb into main Sep 6, 2026
17 checks passed
@JayVDZ
JayVDZ deleted the feature/operations-health-stack-password-pipeline branch September 6, 2026 18:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

One password pipeline: converge Set and Synchronise, deliver off the worker queue, surface service health in Operations

2 participants