feat(realtime): live enclave member + room lists on the landing page (LC-170) #215

Merged
nrupard merged 2 commits from feat/lc-170-live-enclave-lists into main 2026-05-26 17:23:36 +02:00
Owner

Closes LC-170 (the in-scope surfaces). Follow-up to LC-161 (live invitations), extending the LC-160 typed-topic WebSocket channel to the /enclave/{id} landing page so the member and room lists update live instead of needing a manual reload. Addresses audit gap C1 (docs/audit/2026-05-22-lc148-audit-report.md).

What updates live

  • Member list: member join (discover-join, join-by-code, accept-invite), kick, leave, and role change.
  • Room list: room add, room remove.

Wiring

  • The landing page subscribes to the enclave:{id} topic via data-lc-live-topic. live.js sends the SubscribeTopic frame; ws.rs::topic_subscribe_allowed authorizes enclave membership before joining the fan-out set (the LC-160 contract).
  • The member and room lists are extracted into shared partials (enclave/members_items.html, enclave/rooms_items.html) so the full page and the OOB fragments (ws/enclave_members_live.html, ws/enclave_rooms_live.html) render identically. The static controls (Add room form, invite box, settings link) stay in the page, outside the swapped #lc-enclave-members / #lc-enclave-rooms regions.
  • The five list-mutating handlers now broadcast on the topic via a new broadcast_enclave_topic helper. A new EnclaveMemberRoleChanged event carries role changes (none existed). This replaces the older per-member broadcast_to_user / broadcast_to_enclave fan, whose four enclave events sat in render_event's None arm (rendered nothing) and only reached the mutation's own subject anyway. broadcast_to_enclave is removed.
  • The WS send task renders the member list once per recipient (read-only label + role, identical for all) and the room list per recipient (the per-row Remove control and the access-filtered room set depend on the viewer's manage rights, mirroring get_landing).

Out of scope (separate C1 gaps, spin-out candidates)

  • Whole-enclave-delete fan (post_delete) stays on broadcast_to_user - that is a sidebar/switcher concern, not a landing-list one.
  • Sidebar live-refresh on room add.
  • The settings-page member list (where kick / role actually happen for managers).
  • Settings own-profile, which the issue flags as cross-surface and hardest.

Known limitation (follow-up)

A kicked or departed user keeps their enclave:{id} topic subscription until they disconnect - the hub has only disconnect, no unsubscribe-by-topic. So they can briefly see further list updates until they navigate (any navigation/reload then 403s on the lost access). This is inherent to the LC-160 topic model, low severity, and worth a dedicated follow-up if we want hard cutoff.

Verification

just test and just test-saas both green (106 test result: ok each, zero failures); cargo clippy and cargo fmt clean. No existing test asserts on the rewired broadcasts.

Closes LC-170 (the in-scope surfaces). Follow-up to LC-161 (live invitations), extending the LC-160 typed-topic WebSocket channel to the `/enclave/{id}` landing page so the member and room lists update live instead of needing a manual reload. Addresses audit gap C1 (`docs/audit/2026-05-22-lc148-audit-report.md`). ## What updates live - **Member list**: member join (discover-join, join-by-code, accept-invite), kick, leave, and role change. - **Room list**: room add, room remove. ## Wiring - The landing page subscribes to the `enclave:{id}` topic via `data-lc-live-topic`. `live.js` sends the `SubscribeTopic` frame; `ws.rs::topic_subscribe_allowed` authorizes enclave membership before joining the fan-out set (the LC-160 contract). - The member and room lists are extracted into shared partials (`enclave/members_items.html`, `enclave/rooms_items.html`) so the full page and the OOB fragments (`ws/enclave_members_live.html`, `ws/enclave_rooms_live.html`) render identically. The static controls (Add room form, invite box, settings link) stay in the page, outside the swapped `#lc-enclave-members` / `#lc-enclave-rooms` regions. - The five list-mutating handlers now broadcast on the topic via a new `broadcast_enclave_topic` helper. A new `EnclaveMemberRoleChanged` event carries role changes (none existed). This replaces the older per-member `broadcast_to_user` / `broadcast_to_enclave` fan, whose four enclave events sat in `render_event`'s `None` arm (rendered nothing) and only reached the mutation's own subject anyway. `broadcast_to_enclave` is removed. - The WS send task renders the member list once per recipient (read-only label + role, identical for all) and the room list per recipient (the per-row Remove control and the access-filtered room set depend on the viewer's manage rights, mirroring `get_landing`). ## Out of scope (separate C1 gaps, spin-out candidates) - Whole-enclave-delete fan (`post_delete`) stays on `broadcast_to_user` - that is a sidebar/switcher concern, not a landing-list one. - Sidebar live-refresh on room add. - The settings-page member list (where kick / role actually happen for managers). - Settings own-profile, which the issue flags as cross-surface and hardest. ## Known limitation (follow-up) A kicked or departed user keeps their `enclave:{id}` topic subscription until they disconnect - the hub has only `disconnect`, no unsubscribe-by-topic. So they can briefly see further list updates until they navigate (any navigation/reload then 403s on the lost access). This is inherent to the LC-160 topic model, low severity, and worth a dedicated follow-up if we want hard cutoff. ## Verification `just test` and `just test-saas` both green (106 `test result: ok` each, zero failures); `cargo clippy` and `cargo fmt` clean. No existing test asserts on the rewired broadcasts.
feat(realtime): live enclave member + room lists on the landing page (LC-170)
All checks were successful
check-secrets / Nosey parker (push) Successful in 4s
check-secrets / TruffleHog (push) Successful in 7s
check-secrets / Kingfisher (push) Successful in 9s
check-secrets / Nosey parker (pull_request) Successful in 3s
check-secrets / Kingfisher (pull_request) Successful in 4s
check-secrets / TruffleHog (pull_request) Successful in 5s
Check / clippy + fmt + tests (pull_request) Successful in 1m33s
fa12548eca
Follow-up to LC-161 (live invitations), extending the LC-160 typed-topic channel to the /enclave/{id} landing page. The member list and room list now update over the WebSocket when membership, roles, or rooms change, instead of requiring a manual reload.

Wiring:
- The landing page subscribes to the enclave:{id} topic via data-lc-live-topic (live.js sends the SubscribeTopic frame; ws.rs::topic_subscribe_allowed authorizes enclave membership before joining the fan-out set).
- The member and room lists are extracted into shared partials (enclave/members_items.html, enclave/rooms_items.html) so the full page and the OOB fragments (ws/enclave_members_live.html, ws/enclave_rooms_live.html) render identically. The static controls (Add room form, invite box, settings link) stay in the page outside the swapped regions.
- The five list-mutating handlers now broadcast on the topic: member add (discover-join, join-by-code, accept-invite), member remove (kick, leave), role change (new EnclaveMemberRoleChanged event), and room add/remove. These replace the older per-member broadcast_to_user / broadcast_to_enclave fan, whose events rendered nothing (the four enclave events were in render_event's None arm) and only reached the mutation's own subject. broadcast_to_enclave is removed; broadcast_enclave_topic replaces it.
- The WS send task renders the member list once per recipient (read-only label + role, identical for everyone) and the room list per recipient (the per-row Remove control and the access-filtered room set depend on the viewer's manage rights, mirroring get_landing).

Out of scope (separate gaps from the audit's C1): the whole-enclave-delete fan (post_delete) stays on broadcast_to_user since it is a sidebar/switcher concern, not a landing-list one; sidebar live-refresh on room add; the settings-page member list; and settings own-profile, which the issue flags as cross-surface and hardest. A kicked/left user keeps their topic subscription until they disconnect (the hub has no unsubscribe-by-topic API), so they can briefly see further list updates before they navigate and lose access; this is inherent to the LC-160 topic model and tracked as a follow-up.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
fix(realtime): let non-member site admins subscribe to the enclave topic (LC-170 review)
All checks were successful
check-secrets / Kingfisher (push) Successful in 4s
check-secrets / TruffleHog (pull_request) Successful in 4s
check-secrets / Nosey parker (push) Successful in 5s
check-secrets / Kingfisher (pull_request) Successful in 5s
check-secrets / TruffleHog (push) Successful in 6s
check-secrets / Nosey parker (pull_request) Successful in 5s
Create release / Create release from merged PR (pull_request) Has been skipped
Check / clippy + fmt + tests (pull_request) Successful in 5m30s
43740714da
get_landing grants site admins god-mode read of any enclave's member/room lists, but topic_subscribe_allowed only permitted actual enclave members on the enclave:{id} topic. A non-member admin viewing the landing page therefore saw a static list with no live updates. Allow user.role == "admin" on the enclave topic for parity with the page-level access they already have; this exposes no data an admin cannot already fetch via get_landing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
nrupard deleted branch feat/lc-170-live-enclave-lists 2026-05-26 17:23:36 +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!215
No description provided.