feat(sidebar): per-user channel-category schema + CRUD (LC-79 phase 1a) #129

Merged
nrupard merged 1 commit from feat/lc-79-sidebar-categories-phase1 into main 2026-05-18 17:05:16 +02:00
Owner

Summary

Lands LC-79 phase 1a: the persistence layer and HTMX endpoints for Slack-style per-user channel categories. Phase 1b (sidebar template grouped render + view-struct fan-out) is split into a follow-up PR to keep this diff focused.

Closes LC-79 phase 1a.

Schema

Two new tables in auth.db (per LC-79's "categorization is private" decision):

  • sidebar_categories (id, user_id, name, position, collapsed, created_at)
  • sidebar_category_rooms (user_id, room_id, category_id, position) with PRIMARY KEY (user_id, room_id) so a room belongs to at most one category per user, and ON DELETE CASCADE on category_id so deleting a category drops its assignments but leaves the rooms joined.

Position is a sparse integer assigned as MAX(position) + 1 on insert; rebalancing is deferred until drag-and-drop lands.

Endpoints

All return the existing flat sidebar fragment via SidebarUpdateFragment (the same shape WebSocket-driven sidebar updates use). The grouped render arrives in phase 1b.

  • POST /sidebar/categories (create)
  • PATCH /sidebar/categories/{id} (rename / toggle collapsed; either field independent)
  • DELETE /sidebar/categories/{id} (cascade clears assignments)
  • PATCH /sidebar/categories/{id}/rooms/{room_id} (assign; gated by is_room_accessible so the endpoint cannot disclose private-room existence)
  • DELETE /sidebar/categories/rooms/{room_id} (unassign, falls back to "All rooms")

Phase 1b (follow-up PR)

  • SidebarCategoryGroup view struct + grouped render in partials/sidebar.html.
  • Add sidebar_categories field to every view struct that today has sidebar_rooms: &[SidebarRoom] (~25 files: views/{enclave,home,bookmarks,admin,pinned,settings,voice,dm,room,not_found}.rs + ws_fragments.rs).
  • Thread the new field through every construction site (~30 in routes/).
  • Sidebar UI affordances (add category button, per-category menu, per-room move-to-category dropdown).

Test plan

  • just check (fmt + clippy across standalone and saas).
  • ./dev/cargo test -p lets-chat-server --test routes_sidebar_categories (6 tests pass: create-empty rejects, create persists, rename then delete, cascade clears assignment, assign requires room access, unassign without unjoining).
  • just test (standalone) - all green.
  • just test-saas - one known flake (routes_uploads::send_message_with_attachment_renders_inline_image, passes in isolation; documented in CLAUDE.md test-maintenance section). Unrelated to this PR.
  • Manual smoke after merge: curl -X POST -d 'name=Work' /sidebar/categories returns 200 and persists.

Drift maintenance

Migration 0016 added to every test file that hand-rolls the auth migration list (CLAUDE.md test-maintenance section 2). Covered both array-form (15 files) and verbose-per-migration shapes (3 files: db_auth.rs, db_invite.rs, rbac.rs).

## Summary Lands LC-79 phase 1a: the persistence layer and HTMX endpoints for Slack-style per-user channel categories. Phase 1b (sidebar template grouped render + view-struct fan-out) is split into a follow-up PR to keep this diff focused. Closes [LC-79](https://niceguyit.myjetbrains.com/youtrack/issue/LC-79) phase 1a. ## Schema Two new tables in `auth.db` (per LC-79's "categorization is private" decision): - `sidebar_categories (id, user_id, name, position, collapsed, created_at)` - `sidebar_category_rooms (user_id, room_id, category_id, position)` with `PRIMARY KEY (user_id, room_id)` so a room belongs to at most one category per user, and `ON DELETE CASCADE` on category_id so deleting a category drops its assignments but leaves the rooms joined. Position is a sparse integer assigned as `MAX(position) + 1` on insert; rebalancing is deferred until drag-and-drop lands. ## Endpoints All return the existing flat sidebar fragment via `SidebarUpdateFragment` (the same shape WebSocket-driven sidebar updates use). The grouped render arrives in phase 1b. - `POST /sidebar/categories` (create) - `PATCH /sidebar/categories/{id}` (rename / toggle collapsed; either field independent) - `DELETE /sidebar/categories/{id}` (cascade clears assignments) - `PATCH /sidebar/categories/{id}/rooms/{room_id}` (assign; gated by `is_room_accessible` so the endpoint cannot disclose private-room existence) - `DELETE /sidebar/categories/rooms/{room_id}` (unassign, falls back to "All rooms") ## Phase 1b (follow-up PR) - `SidebarCategoryGroup` view struct + grouped render in `partials/sidebar.html`. - Add `sidebar_categories` field to every view struct that today has `sidebar_rooms: &[SidebarRoom]` (~25 files: views/{enclave,home,bookmarks,admin,pinned,settings,voice,dm,room,not_found}.rs + ws_fragments.rs). - Thread the new field through every construction site (~30 in routes/). - Sidebar UI affordances (add category button, per-category menu, per-room move-to-category dropdown). ## Test plan - [x] `just check` (fmt + clippy across standalone and saas). - [x] `./dev/cargo test -p lets-chat-server --test routes_sidebar_categories` (6 tests pass: create-empty rejects, create persists, rename then delete, cascade clears assignment, assign requires room access, unassign without unjoining). - [x] `just test` (standalone) - all green. - [x] `just test-saas` - one known flake (`routes_uploads::send_message_with_attachment_renders_inline_image`, passes in isolation; documented in CLAUDE.md test-maintenance section). Unrelated to this PR. - [ ] Manual smoke after merge: `curl -X POST -d 'name=Work' /sidebar/categories` returns 200 and persists. ## Drift maintenance Migration 0016 added to every test file that hand-rolls the auth migration list (CLAUDE.md test-maintenance section 2). Covered both array-form (15 files) and verbose-per-migration shapes (3 files: db_auth.rs, db_invite.rs, rbac.rs).
feat(sidebar): per-user channel-category schema + CRUD endpoints
All checks were successful
Check / clippy + fmt + tests (pull_request) Successful in 44s
9e2fa0ce7b
Lands the persistence layer and HTMX endpoints for Slack-style channel categories (LC-79 phase 1a). Two new tables in auth.db cover the per-user categorization without touching chat.db: `sidebar_categories` (id, user_id, name, position, collapsed, created_at) and `sidebar_category_rooms` (user_id, room_id, category_id, position) with `ON DELETE CASCADE` so deleting a category drops its room assignments but leaves the rooms themselves joined.

Endpoints (all return the existing sidebar fragment so the requesting tab refreshes; the grouped render lands in phase 1b alongside the view-struct fan-out):

- `POST /sidebar/categories` (create)
- `PATCH /sidebar/categories/{id}` (rename / toggle collapsed)
- `DELETE /sidebar/categories/{id}` (cascade clears assignments)
- `PATCH /sidebar/categories/{id}/rooms/{room_id}` (assign; gated by `is_room_accessible` so the endpoint cannot disclose private-room existence)
- `DELETE /sidebar/categories/rooms/{room_id}` (unassign, falls back to "All rooms")

Phase 1b will add `SidebarCategory` view structs, thread them through every page (~25 construction sites in views/{enclave,home,bookmarks,admin,pinned,settings,voice,dm,room,not_found}.rs + ws_fragments.rs), and rewrite `partials/sidebar.html` to render grouped. Splitting keeps this PR's diff focused on schema + endpoints; the view fan-out is mechanical and lands separately.

Drift maintenance: added migration 0016 to every test file that hand-rolls the auth migration list (CLAUDE.md test-maintenance section 2). Both array-form and verbose-per-migration shapes covered.
nrupard deleted branch feat/lc-79-sidebar-categories-phase1 2026-05-18 17:05:16 +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!129
No description provided.