Skip to content

feat(content): ✨ notify scoped moderators about submissions awaiting approval - #907

Open
aXenDeveloper wants to merge 1 commit into
canaryfrom
claude/scoped-approval-notifications-eurnda
Open

aXenDeveloper wants to merge 1 commit into
canaryfrom
claude/scoped-approval-notifications-eurnda

Conversation

@aXenDeveloper

Copy link
Copy Markdown
Owner

Description

What?

Adds an approvalPending preset to buildContentNotifications. When a new record or an edit proposal enters the ModCP approval queue, the moderators who could decide it get one in-app notification. The notification links to the existing submission page at /modcp/approvals/:id. The Forum plugin registers the preset for topics and comments.

  • Recipients: moderators whose can_approve for that content type covers the submission. Grants come from the user's own entry or their roles, scoped to the container, its subtree or all resources. Root roles and unrestricted entries also count. Topic approval does not cover comments. The submitter is never notified.
  • Two containers: a recipient must cover both the container the queue lists the submission under and the record's current container. The first lets them open it; the second lets them decide it.
  • Shared check: the scope rule moved out of canModerateContent into hasContentModeratorPermission, so the queue, the decision and the notifications apply the same rule.
  • Fresh, batched permissions: two new helpers in check-staff-permission.ts read permissions straight from the database, not the cache, in three queries per 500 users. At most 1000 candidates are considered per submission, with a warning logged past that.
  • Durable delivery: openContentSubmission(c, tx, …) stores the intent in the same transaction that opens the submission. The worker publishes once per submission with the key approval:{contentTypeId}:{submissionId}. It publishes only while holding a share lock on the submission row and only if it is still pending.
  • Access re-check: a new "approval" access policy re-checks the pending status, the record and each recipient's permissions on delivery and on every inbox load. The text carries no title or body.
  • Cleanup: when a submission is decided, replaced by a newer proposal or cancelled, a cleanup intent removes the items from moderators' inboxes.
  • Category and email: the preset uses a new Moderation preferences category and sends no email.
  • Tests: new approval-notifications.postgres.test.ts with 28 cases. It covers scopes, role plus own grant, permission removal, moves, retries, resubmission, rollback, races with a decision, cleanup, and the 404 after losing access.
  • Harness fix: tests/postgres.ts only treats a polish search configuration as present when it is visible in the test's own schema. Without this, two PostgreSQL test files running in parallel crashed.
  • Docs: notifications-api.mdx, notifications.mdx, approvals.mdx, review-approvals.mdx (new "Get notified" section) and scoped-permissions.mdx.

No migration is needed: the existing notification tables, queue and idempotency index are enough.

Known limitation, documented: a pending submission keeps the container it was opened in until a moderator decides it, so the queue keeps listing it there. Because of that, content moved before delivery only notifies moderators who cover both the old and the new container. Content moved after delivery leaves the item unavailable for the old container's moderators and does not notify the new container's moderators. The follow-up is to update the pending submission's container when its record moves and store a new intent with a key that includes the container.

Checks:

  • vitest unit, isolated and dom projects: 9,863 passed.
  • PostgreSQL integration files: 73 passed.
  • tsc --noEmit and oxfmt --check are clean on the changed files.
  • oxlint --type-aware is clean on the core package. The forum file shows 3 no-unsafe-argument errors that also appear on the base commit, because the core package isn't built in that environment.

Why?

Moderators had no way to learn that content was waiting for them except by opening the ModCP queue. This tells exactly the moderators who can act on a submission, without revealing it to anyone outside their scope.


Generated by Claude Code

…approval

Add an `approvalPending` preset to `buildContentNotifications`. When a
submission opens, a durable intent is stored in the same transaction; the
worker resolves moderators whose `can_approve` covers both the container
the queue lists the submission under and the record's current container
(own and role grants, subtrees, all resources, root roles, unrestricted
entries), reading permissions fresh from the database in batches.

- One event per submission (idempotency key), the submitter excluded
- Access re-checked on delivery and inbox load (`"approval"` policy)
- Decided, superseded and cancelled submissions clean up their items
- Forum registers the preset for topics and comments
- Docs for moderators and plugin authors; PostgreSQL regression tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CnAiFYAi5Vz6WxSaXS8Z9X
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-28T21:08:12.889791Z 2754195 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions github-actions Bot added the 💡 Feature A new feature label Oct 28, 2026
@github-actions

Copy link
Copy Markdown

React Doctor found 1 new issue in 1 file · 1 warning · score 90 / 100 (Great) · 0 fixed · vs canary

1 warning

src/content/server/notifications/process.ts

  • ⚠️ L366 await inside a loop async-await-in-loop

Reviewed by React Doctor for commit 2754195. See inline comments for fixes.

if (locked?.status !== "pending") return 0;

for (const type of types) {
await c.get("notifications").publish({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/async-await-in-loop (warning)

This for…of loop waits before starting the next iteration. If iterations perform independent asynchronous work, consider bounded concurrency; await syntax alone does not establish a speedup.

Fix → Consider concurrent calls only for independent asynchronous work. Shared queues or synchronous work may not benefit. Preserve resource limits, transaction ordering, and failure/cancellation semantics; observe all callback promises.

Docs

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💡 Feature A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants