feat(realtime): live admin user list (LC-175, admin-user surface) #220

Merged
nrupard merged 1 commit from feat/lc-175-live-admin-lists into main 2026-05-26 19:10:13 +02:00
Owner

First surface of LC-175 (audit gap C1). The admin user list was static-on-load: when one admin banned / muted / role-changed / re-quotaed / deleted a user, other admins' open lists (and the actor's other tabs) showed stale rows until reload.

Changes

  • New AdminUserChanged { user_id, removed } event, broadcast on the admin topic (only admins can subscribe, per topic_subscribe_allowed). render_user_row fires it on every row-changing mutation (ban / unban / mute / unmute / role / quota all route through it); post_delete_user fires it with removed = true.
  • admin_layout.html subscribes every admin page to the admin topic via data-lc-live-topic. The WS send task renders the matching #user-{id} row OOB (UserRowFragment gains an oob flag) or an hx-swap-oob="delete" tombstone (user_row_delete.html) for removals. Admin pages without that row id drop the swap.
  • The user row is identical for all admins, so one render per event serves every recipient. build_admin_user_view is factored out of render_user_row and shared with the WS renderer.

Feature gating

routes::admin (and its row builder) is #[cfg(standalone)], so the WS arm + render_admin_user_row are gated to standalone; in saas the event is never broadcast and falls through to render_event (None). This fixes the feature-gate drift that broke the saas build on the first cut (caught by just test-saas).

Out of scope (spun out)

  • Admin room list (archive / edit) - same admin topic, mechanically symmetric.
  • Per-user surfaces /saved, /inbox, /activity - user:{id} topic.
  • New-user-appears-on-registration live-append - registration is not an admin action and OOB-by-id cannot append a new row.

Verification

just test and just test-saas both green (106 test result: ok each; the one routes_uploads failure seen mid-run is the documented concurrent-load flake, 12/12 isolated, unrelated to this change). cargo clippy / cargo fmt clean. Regression test (routes_quotas::admin_user_list_is_wired_for_live_row_updates) asserts the admin topic subscription, the OOB-targetable row id, and that page-rendered rows are not themselves OOB.

First surface of LC-175 (audit gap C1). The admin user list was static-on-load: when one admin banned / muted / role-changed / re-quotaed / deleted a user, other admins' open lists (and the actor's other tabs) showed stale rows until reload. ## Changes - New `AdminUserChanged { user_id, removed }` event, broadcast on the `admin` topic (only admins can subscribe, per `topic_subscribe_allowed`). `render_user_row` fires it on every row-changing mutation (ban / unban / mute / unmute / role / quota all route through it); `post_delete_user` fires it with `removed = true`. - `admin_layout.html` subscribes every admin page to the `admin` topic via `data-lc-live-topic`. The WS send task renders the matching `#user-{id}` row OOB (`UserRowFragment` gains an `oob` flag) or an `hx-swap-oob="delete"` tombstone (`user_row_delete.html`) for removals. Admin pages without that row id drop the swap. - The user row is identical for all admins, so one render per event serves every recipient. `build_admin_user_view` is factored out of `render_user_row` and shared with the WS renderer. ## Feature gating `routes::admin` (and its row builder) is `#[cfg(standalone)]`, so the WS arm + `render_admin_user_row` are gated to standalone; in saas the event is never broadcast and falls through to `render_event` (None). This fixes the feature-gate drift that broke the saas build on the first cut (caught by `just test-saas`). ## Out of scope (spun out) - Admin **room** list (archive / edit) - same `admin` topic, mechanically symmetric. - Per-user surfaces `/saved`, `/inbox`, `/activity` - `user:{id}` topic. - New-user-appears-on-registration live-append - registration is not an admin action and OOB-by-id cannot append a new row. ## Verification `just test` and `just test-saas` both green (106 `test result: ok` each; the one `routes_uploads` failure seen mid-run is the documented concurrent-load flake, 12/12 isolated, unrelated to this change). `cargo clippy` / `cargo fmt` clean. Regression test (`routes_quotas::admin_user_list_is_wired_for_live_row_updates`) asserts the admin topic subscription, the OOB-targetable row id, and that page-rendered rows are not themselves OOB.
feat(realtime): live admin user list (ban/mute/role/quota/delete) (LC-175, admin-user surface)
All checks were successful
check-secrets / Nosey parker (push) Successful in 6s
check-secrets / Kingfisher (push) Successful in 7s
check-secrets / TruffleHog (push) Successful in 7s
check-secrets / Nosey parker (pull_request) Successful in 6s
check-secrets / TruffleHog (pull_request) Successful in 6s
check-secrets / Kingfisher (pull_request) Successful in 9s
Check / clippy + fmt + tests (pull_request) Successful in 3m13s
Create release / Create release from merged PR (pull_request) Has been skipped
d5a6a9fb7c
First surface of LC-175 (audit gap C1). The admin user list was static-on-load: when one admin banned / muted / role-changed / re-quotaed / deleted a user, other admins' open lists (and the actor's other tabs) showed stale rows until reload.

- New AdminUserChanged { user_id, removed } event, broadcast on the `admin` topic (only admins can subscribe per topic_subscribe_allowed). render_user_row now fires it on every row-changing mutation (ban/unban/mute/unmute/role/quota all route through it); post_delete_user fires it with removed=true.
- admin_layout.html subscribes every admin page to the `admin` topic via data-lc-live-topic; the WS send task renders the matching #user-{id} row OOB (UserRowFragment with a new oob flag) or an hx-swap-oob="delete" tombstone (user_row_delete.html) for removals. Admin pages without that row id drop the swap.
- The user row is identical for all admins, so one render per event serves every recipient. build_admin_user_view is factored out of render_user_row and shared with the WS renderer.

Standalone-only: routes::admin (and its row builder) is #[cfg(standalone)], so the WS arm + render_admin_user_row are gated to standalone; in saas the event is never broadcast and falls through to render_event (None). This avoids the feature-gate drift that broke the saas build on the first cut.

Out of scope (spun out): admin ROOM list (archive/edit, same admin topic, symmetric), and the per-user surfaces /saved, /inbox, /activity (user:{id} topic). New-user-appears-on-registration live-append is also deferred (registration is not an admin action and OOB-by-id cannot append).

Regression test asserts the admin user list subscribes to the admin topic, renders the OOB-targetable row id, and that page-rendered rows are not themselves OOB.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
nrupard deleted branch feat/lc-175-live-admin-lists 2026-05-26 19:10:13 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!220
No description provided.