Admin channel management: create, rename, archive/restore#36
Conversation
Adds admin-only channel management, the first way to create channels in LOAM (previously the two seed channels were the only ones that could exist). Schema: ChannelCreateRequestSchema / ChannelUpdateRequestSchema. Server: - GET /api/admin/channels — full list incl. archived (admin only), so archived channels stay visible/restorable even though GET /api/channels hides them. - POST /api/admin/channels — create a public, discoverable channel; the server assigns a slug id (unique, with a random suffix on collision / emoji-only names), createdAt, and ownerUserId. Posting policy + replies are configurable. - PATCH /api/admin/channels/:id — rename / set posting policy / archive / restore. - New channelUpserted broadcast so every client's channel list updates live. Created channels are public and GET /api/channels already returns all non-archived channels to everyone, so the event goes to all sockets; a comment flags that this must filter by membership once private channels gain real enforcement. Client: - parseSocketEvent handles channelUpserted (validated with ChannelSchema). - upsertChannels drops archived channels from the sidebar, so archiving hides a channel live and restoring re-adds it. - Admin view gains a Channels panel: a create form and a per-channel row with rename and archive/restore. It fetches its own full list so archived channels remain manageable after a reload. Private channels are deliberately not creatable yet — visibility is not enforced anywhere, so offering a "private" option would be a false privacy guarantee. Tests: 9 server route tests (auth matrix, create/validate/slug-collision/emoji fallback, rename, archive hides from public list + restores, 404, admin list shows archived, hyphenated-slug persistence across restart) and a protocol parse test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 25 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds admin channel management: new Zod schemas for channel create/update requests, server endpoints (GET/POST/PATCH) for admin channel CRUD with unique-id generation and realtime broadcast, client protocol support for a ChangesAdmin Channel Management
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant AdminUI
participant ServerAPI
participant Store
participant Sockets
participant ClientApp
AdminUI->>ServerAPI: POST /api/admin/channels (create)
ServerAPI->>ServerAPI: uniqueChannelId(name)
ServerAPI->>Store: upsertChannel(channel)
ServerAPI->>Sockets: broadcast channelUpserted
Sockets-->>ClientApp: channelUpserted event
ClientApp->>ClientApp: upsertChannels(channel)
AdminUI->>ServerAPI: PATCH /api/admin/channels/:id (rename/archive)
ServerAPI->>ServerAPI: applyChannelUpdate(channel, update)
ServerAPI->>Store: upsertChannel(updatedChannel)
ServerAPI->>Sockets: broadcast channelUpserted
Sockets-->>ClientApp: channelUpserted event
ClientApp->>ClientApp: upsertChannels(channel)
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
apps/client/src/app.tsx (1)
2614-2786: 📐 Maintainability & Code Quality | 🔵 TrivialAdmin channel list can go stale across concurrent admin sessions.
AdminChannelsPanelfetches/api/admin/channelsonce on mount and afterward only updates its localadminChannelsstate via its ownapplyChannelcalls (create/rename/archive). It does not listen to thechannelUpsertedwebsocket broadcasts thatLoamAppalready handles at Line 724-727, so if another admin (or another tab) creates/renames/archives a channel while this panel is open, the list here won't reflect it until a manual reload.This is a minor architectural gap worth tracking for a follow-up (e.g., wiring the panel into the same
channelUpsertedstream, or lifting a full admin-channel list — including archived — up toLoamAppso it stays consistent with the socket connection already open there).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/client/src/app.tsx` around lines 2614 - 2786, AdminChannelsPanel only loads /api/admin/channels once and then mutates its own local adminChannels state, so it can miss channel changes made by other admins or tabs. Update AdminChannelsPanel to subscribe to the same channelUpserted updates that LoamApp already receives, or move the shared admin channel list state up into LoamApp and pass it down so AdminChannelRow/AdminChannelsPanel stay in sync with websocket broadcasts.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/server/src/app.test.ts`:
- Around line 1038-1052: The test for persisting channels across a restart can
leak the initial LoamApp instance if createChannel or the created.id assertion
fails before reopenApp runs. Wrap the
buildApp/newSession/createChannel/reopenApp sequence in a try/finally so the
original app is always closed on failure, while keeping the existing reopenApp
teardown behavior intact; use the buildApp, createChannel, and reopenApp flow as
the place to add the cleanup.
---
Nitpick comments:
In `@apps/client/src/app.tsx`:
- Around line 2614-2786: AdminChannelsPanel only loads /api/admin/channels once
and then mutates its own local adminChannels state, so it can miss channel
changes made by other admins or tabs. Update AdminChannelsPanel to subscribe to
the same channelUpserted updates that LoamApp already receives, or move the
shared admin channel list state up into LoamApp and pass it down so
AdminChannelRow/AdminChannelsPanel stay in sync with websocket broadcasts.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 99ca5710-c956-4521-a9d4-23c03dc7d4b4
📒 Files selected for processing (7)
apps/client/src/app.tsxapps/client/src/global.cssapps/client/src/lib/protocol.test.tsapps/client/src/lib/protocol.tsapps/server/src/app.test.tsapps/server/src/app.tspackages/schema/src/index.ts
The persistence test dropped the initial Fastify/DB handle if an assertion (or createChannel) threw before reopenApp ran, since reopenApp is what closes it. Wrap the body in try/finally so the initial instance is closed on that early- failure path, while keeping the happy path free of the double-close that reopenApp would otherwise cause. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Dark mode: tokenize the admin-channel colours (last hardcoded-colour gap) The admin channel-management CSS (from #36, which branched before dark mode existed) used raw hex values, so those five rules were the only colours in the stylesheet that didn't adapt to dark mode. Point them at the existing tokens (--c-border, --c-surf-2, --c-ink-green, --c-danger, --c-white) so the admin channels panel matches the rest of the admin UI in both themes. No light-theme change (the tokens' light values equal the hexes they replace). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: add maps & location-sharing investigation (10) Captures the owner's map idea: a MapLibre GL JS + PMTiles + Protomaps stack that serves an OpenStreetMap vector basemap entirely from the host (no internet, no third-party calls), with tap-to-drop-a-pin location messages that are opt-in, ephemeral, and flag-gated. Documents the two real challenges (offline tile-data acquisition; Geolocation blocked over LAN's insecure context) and the open scope questions. No code yet — briefing for a future decision. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Message deletion: authors delete their own, admins moderate any Adds a DELETE /api/messages/:messageId endpoint and a delete affordance in the client — closing the "no message deletion" gap noted in CLAUDE.md and fitting LOAM's trusted-host / admin-moderation model. Server: - DELETE /api/messages/:id. Authorisation: an admin may delete any message (moderation); a non-admin may delete only their own, and only when the cascade won't remove another user's reply (clearing others' reactions is fine). A message that's still streaming is refused (409) so its writer can't re-persist it. - Cascade: deleting a message also removes its reactions, and — for a channel post that roots a thread — its replies and the reactions on those. Two shared helpers (collectDeletionSet, deleteMessages); the retention reaper now reuses deleteMessages, so the persist-then-broadcast-then-drop ordering lives in one place (broadcasting before the in-memory mirror is torn down keeps reaction DM-audience lookups resolvable). Client: - A "Delete" action on each message, shown for your own messages or, for admins, any message (never on a streaming message). Confirms, calls the endpoint, and removes the target immediately; cascaded replies/reactions arrive over the existing messageDeleted socket events. Hover-revealed on desktop, always shown on touch, danger-styled on hover. Tests: 6 server route tests (author deletes own; non-author 403; admin deletes any; 404; thread-root cascade removes replies + reactions; non-admin blocked from deleting a thread others replied to while an admin can). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Admin channel management: create, rename, archive/restore
The first way to create channels in LOAM. Until now the two seed channels (Announcements, General) were the only channels that could ever exist — the schema and data model anticipated more, but nothing was exposed. This adds an admin-only Channels panel to the existing
/adminarea.What an admin can do
All changes propagate to every connected client immediately via a new
channelUpsertedwebsocket event.Why no private channels (yet)
Channel
visibilityexists in the schema but is not enforced anywhere —GET /api/channelsreturns every non-archived channel to everyone. Offering a "private" option now would be a false privacy guarantee, which cuts against LOAM's whole intent. So created channels are public + discoverable, and the code has comments flagging exactly what must change (membership + broadcast filtering) before private channels can be honestly offered.Server
GET /api/admin/channels— full list including archived (admin only), so archived channels stay visible and restorable even thoughGET /api/channelshides them.POST /api/admin/channels— creates a public channel; the server assigns a URL-friendly slug id (unique, with a short random suffix on collision or emoji-only names),createdAt, andownerUserId.PATCH /api/admin/channels/:id— rename / posting policy / archive / restore.channelUpsertedbroadcast. Persist-before-mutate throughout, mirroringapplyUserUpdate(store write first, then in-memory mutation, then broadcast — a failed write never surfaces an unpersisted channel).Client
parseSocketEventhandleschannelUpserted(validated withChannelSchema).upsertChannelsdrops archived channels from the sidebar + local cache, so archive/restore is reflected live.Tests (10 new)
Server: auth matrix (non-admin → 403 on list/create/update), create validates + assigns slug, slug-collision suffix, emoji-name fallback, rename, archive hides from the public list + restore re-adds, 404 on unknown channel, admin list shows archived, hyphenated-slug survives a restart +
ChannelSchema.parse. Client:channelUpsertedparse (valid + invalid).Full suite green (server 73, client 31). CodeRabbit: reviewed against
master, clean — an earlier pass flagged persist-order and a double-close in one test; both fixed in this branch before push.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Tests