Skip to content

Admin channel management: create, rename, archive/restore#36

Merged
JosephMaynard merged 2 commits into
masterfrom
feat/admin-channels
Jul 3, 2026
Merged

Admin channel management: create, rename, archive/restore#36
JosephMaynard merged 2 commits into
masterfrom
feat/admin-channels

Conversation

@JosephMaynard

@JosephMaynard JosephMaynard commented Jul 2, 2026

Copy link
Copy Markdown
Owner

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 /admin area.

What an admin can do

  • Create a channel — name, optional description, who can post (everyone / admins-only), and whether threaded replies are allowed.
  • Rename any channel.
  • Archive / restore — archiving removes a channel from everyone's sidebar live; restoring brings it back.

All changes propagate to every connected client immediately via a new channelUpserted websocket event.

Why no private channels (yet)

Channel visibility exists in the schema but is not enforced anywhereGET /api/channels returns 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 though GET /api/channels hides 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, and ownerUserId.
  • PATCH /api/admin/channels/:id — rename / posting policy / archive / restore.
  • New channelUpserted broadcast. Persist-before-mutate throughout, mirroring applyUserUpdate (store write first, then in-memory mutation, then broadcast — a failed write never surfaces an unpersisted channel).

Client

  • parseSocketEvent handles channelUpserted (validated with ChannelSchema).
  • upsertChannels drops archived channels from the sidebar + local cache, so archive/restore is reflected live.
  • The admin Channels panel fetches its own full list (so archived channels remain manageable after a reload), with a create form and per-channel rename + archive/restore controls. New CSS is layout-only and reuses the existing admin panel/button styles.

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: channelUpserted parse (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

    • Added admin tools to create, rename, archive, and restore channels.
    • Channel changes now update the sidebar and admin list automatically.
    • Archived channels are hidden from the public channel list but remain visible in admin views.
  • Bug Fixes

    • Improved channel ID handling to avoid duplicate or invalid IDs.
    • Channel updates now sync more reliably across connected clients in real time.
  • Tests

    • Expanded coverage for channel management, access control, validation, and persistence.

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>
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 25 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4abd4cb2-657d-4a50-a951-252740653de2

📥 Commits

Reviewing files that changed from the base of the PR and between 0df2e8d and cc79deb.

📒 Files selected for processing (1)
  • apps/server/src/app.test.ts
📝 Walkthrough

Walkthrough

Adds 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 channelUpserted event, and a new admin UI panel to create/rename/archive channels.

Changes

Admin Channel Management

Layer / File(s) Summary
Channel request schemas
packages/schema/src/index.ts
Adds ChannelCreateRequestSchema and ChannelUpdateRequestSchema with inferred types for validating admin channel payloads.
Server update logic and id generation
apps/server/src/app.ts
Adds applyChannelUpdate() and uniqueChannelId(), extends ClientEvent with channelUpserted, and makes channelUpserted a globally broadcast event.
Admin REST endpoints and tests
apps/server/src/app.ts, apps/server/src/app.test.ts
Adds GET/POST/PATCH /api/admin/channels[/:channelId] with authorization, validation, and broadcasting, plus tests for authorization, creation, validation, id collisions, renaming, archiving, 404s, and persistence.
Client protocol support
apps/client/src/lib/protocol.ts, apps/client/src/lib/protocol.test.ts
Adds channelUpserted to SocketEvent union and parseSocketEvent, validating the channel payload via ChannelSchema, with tests for valid/invalid cases.
Client channel state and admin UI
apps/client/src/app.tsx, apps/client/src/global.css
Adds upsertChannels state merging in App, wires channelUpserted websocket handling, adds onChannelUpsert to AdminView, and implements AdminChannelsPanel/AdminChannelRow with create/rename/archive actions and supporting CSS.

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)
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: admin channel management with create, rename, and archive/restore actions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/admin-channels

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/client/src/app.tsx (1)

2614-2786: 📐 Maintainability & Code Quality | 🔵 Trivial

Admin channel list can go stale across concurrent admin sessions.

AdminChannelsPanel fetches /api/admin/channels once on mount and afterward only updates its local adminChannels state via its own applyChannel calls (create/rename/archive). It does not listen to the channelUpserted websocket broadcasts that LoamApp already 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 channelUpserted stream, or lifting a full admin-channel list — including archived — up to LoamApp so 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

📥 Commits

Reviewing files that changed from the base of the PR and between 5d67d96 and 0df2e8d.

📒 Files selected for processing (7)
  • apps/client/src/app.tsx
  • apps/client/src/global.css
  • apps/client/src/lib/protocol.test.ts
  • apps/client/src/lib/protocol.ts
  • apps/server/src/app.test.ts
  • apps/server/src/app.ts
  • packages/schema/src/index.ts

Comment thread apps/server/src/app.test.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>
@JosephMaynard JosephMaynard merged commit 6bc5574 into master Jul 3, 2026
5 checks passed
@JosephMaynard JosephMaynard deleted the feat/admin-channels branch July 3, 2026 08:30
JosephMaynard added a commit that referenced this pull request Jul 3, 2026
* 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>
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.

1 participant