Skip to content

Add admin announcements that fan out to every user's inbox - #842

Merged
InfinityBowman merged 2 commits into
mainfrom
worktree-announcements
Sep 19, 2026
Merged

InfinityBowman merged 2 commits into
mainfrom
worktree-announcements

Conversation

@InfinityBowman

@InfinityBowman InfinityBowman commented Sep 19, 2026

Copy link
Copy Markdown
Owner

Closes #840.

Admins get a new /admin/announcements page (Overview group in the admin sidebar) to send an operator-written title, message and optional in-app link to every user. Each user receives it as an ordinary announcement notification in their inbox.

What changed

  • Shared: new announcement notification type whose payload carries the copy as written, since there is no client-side wording to derive it from.
  • Workers: sendAnnouncement selects every user id, inserts the rows in chunks of 20 (100 bound parameters per D1 statement) and pushes notification:new to each user's session Durable Object on a best-effort basis. The per-user cap is left to the next ordinary notification to trim. Exported as @corates/workers/commands/notifications.
  • Web server: sendAnnouncementAction validates title (80), body (280) and an optional in-app path that must start with /, then checks isAdminUser like the other admin server functions.
  • Web UI: the admin form with a confirm dialog, plus a MegaphoneIcon renderer. NotificationRow shows an icon-only avatar when there is no actor and shows operator copy in full instead of truncating it to one line.
  • Docs: the notification-center plan marks admin announcements done.

Tests

  • packages/workers/src/commands/notifications/__tests__/sendAnnouncement.test.ts: 45 users span three insert chunks, every row is written, the last user's session DO received the push, and zero users sends nothing.
  • packages/web/src/server/functions/__tests__/admin-announcements.server.test.ts: non-admins are rejected with a domain error and write nothing; admins fan out to every user including themselves.

pnpm typecheck and pnpm lint are clean. The workers suite passes apart from the pre-existing email-queue hook timeout, which is unrelated.

Known limits

  • The push loop makes one Durable Object call per user in the same request as the inserts, so once the user table approaches roughly 950 rows the trailing pushes hit the 1000-subrequest ceiling. Those pushes are caught and logged; the rows are already in D1 and arrive on the next fetch.

https://claude.ai/code/session_01C35qe7ZSeyaFUnw4LXJfJT

@coderabbitai

coderabbitai Bot commented Sep 19, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 50 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 0f0cdaf6-3c40-4684-8fef-d03dc4484148

📥 Commits

Reviewing files that changed from the base of the PR and between 7582aae and 12d8d09.

⛔ Files ignored due to path filters (1)
  • packages/docs/plans/notification-center.md is excluded by !packages/docs/plans/**
📒 Files selected for processing (13)
  • packages/shared/src/notifications.ts
  • packages/web/src/components/layout/sidebar/AdminSidebar.tsx
  • packages/web/src/components/notifications/NotificationRow.tsx
  • packages/web/src/components/notifications/renderers.ts
  • packages/web/src/routeTree.gen.ts
  • packages/web/src/routes/_app/_protected/admin/announcements.tsx
  • packages/web/src/server/functions/__tests__/admin-announcements.server.test.ts
  • packages/web/src/server/functions/admin-announcements.functions.ts
  • packages/web/src/server/functions/admin-announcements.server.ts
  • packages/workers/package.json
  • packages/workers/src/commands/notifications/__tests__/sendAnnouncement.test.ts
  • packages/workers/src/commands/notifications/index.ts
  • packages/workers/src/commands/notifications/sendAnnouncement.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@InfinityBowman

Copy link
Copy Markdown
Owner Author

Follow-ups this design leaves open

Neither is needed at the current user count, recorded here so the next person does not re-derive them.

Queue-backed fan-out

sendAnnouncement does the inserts and one Durable Object push per user inside the admin's request, so it is bounded by the 1000-subrequest limit, roughly 950 users. When that gets close, move the fan-out behind a Cloudflare Queue:

  • Add a corates-announcements queue (per env, like corates-emails) with an ANNOUNCEMENT_QUEUE producer binding and a consumer with max_batch_size: 1 and a small retry count.
  • The server function validates, checks admin, writes the sent-log row (below), then sends one message { announcementId } and returns immediately. The admin UI shows "queued" instead of a recipient count.
  • The consumer loads the announcement, pages through user ids ordered by id in batches of a few hundred, and for each page does the chunked inserts and pushes it does today. Each page is its own subrequest budget because the consumer can re-enqueue { announcementId, afterUserId } for the next page rather than looping in one invocation.
  • Idempotency: the notification id becomes deterministic, for example a UUID v5 of announcementId + userId, and the insert uses onConflictDoNothing, so a retried page does not double-deliver.

Sent log

The admin page has no history, so an operator cannot see whether a colleague already sent a notice. Deriving it from notification rows decays as users dismiss and the 200-row cap trims.

  • New announcements table: id, title, body, href, sentBy (user id), sentAt, recipientCount. Written before the fan-out in the same request.
  • Notification payload gains announcementId so a row can be traced to its log entry. The renderer ignores it.
  • The admin page lists recent entries under the form, newest first, with the count. No edit or recall; those remain out of scope.

The queue work depends on the sent log, since the message carries only the id.

https://claude.ai/code/session_01C35qe7ZSeyaFUnw4LXJfJT

@InfinityBowman
InfinityBowman merged commit 4776e3b into main Sep 19, 2026
10 checks passed
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.

Product announcements: operator-authored notifications to every user's inbox

1 participant