Repository navigation
feat(moderation): ✨ warnings, warning thresholds and account suspensions - #910
aXenDeveloper wants to merge 4 commits into
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
React Doctor found no new issues. 🎉 Reviewed by React Doctor for commit |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b57da887c4
ℹ️ 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".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const values = form.state.values as FormValues; | ||
| const previous = reasonOf(values.reasonId); | ||
| const next = reasonOf(String(value)); | ||
| form.setFieldValue("points", next?.points ?? 0); | ||
| if (!values.message || values.message === messageOf(previous)) { |
There was a problem hiding this comment.
Capture the previous reason before the select updates it
When a moderator changes between reasons with different default messages, AutoFormSelect calls field.onChange(value) before this callback, so form.state.values.reasonId already identifies the newly selected reason. Consequently, previous and next are the same reason, and an untouched message from the original reason is not replaced. The warning can then be issued with the new reason's points but the old reason's member-facing message. Capture the previous reason before the field update, or track the previously selected template separately.
Useful? React with 👍 / 👎.
| resolveReport: | ||
| draft.report !== null && draft.report.status !== "done", |
There was a problem hiding this comment.
Check report-management permission in the preview response
For a moderator with scoped can_view_reports and can_warn but without can_manage_reports, this returns resolveReport: true whenever the report is open. WarnForm uses this flag to expose the resolve-report switch, but selecting it makes issueWarning reject the entire submission with 403 because that service correctly checks MODERATION_PERMISSIONS.manageReports. Include the same scoped management check here so the form does not advertise an action that prevents an otherwise authorized warning from being submitted.
Useful? React with 👍 / 👎.
Moderators can warn members from a profile, a comment, a ModCP report or the ModCP member page with one shared form that previews the consequences. Administrators configure warning reasons and point thresholds in the AdminCP; crossing a threshold suspends the account temporarily or indefinitely. - Warnings, reasons, thresholds, sanctions and an audit trail in new core tables, with reason and content snapshots so history never changes. - Active points and suspension state derive from timestamps on every read. - Issuance is serialized per member with an advisory lock and made idempotent per moderator; the warning, threshold decision, sanction, audit entries, optional report resolution and notifications commit together. - Report targets gain optional `resolveAuthors`; content-linked warnings check the scoped `can_warn` permission and resolve author and scope on the server. Content Engine types get it automatically and `canWarn` on /permissions. - Suspended sessions are treated as guests on every API path, refused by the AdminCP and disconnected from WebSockets; /account-status explains why. - ModCP warnings log and member standing, AdminCP reason and threshold screens, comment "Warn author", WarnDialog and WarnAuthorButton. - Docs for administrators, moderators, members and plugin developers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CnAiFc4WPMdVa99L7jsTXY
- Pure point, threshold and sanction rules. - Routes and service over the memory DB: reasons and snapshots, overrides, protected targets, scoped content warnings, threshold crossings, idempotent retries, concurrency, revocation, report linking and that staff notes never reach members, notifications, events or the audit log. - Concurrency and idempotency against real PostgreSQL. - Update existing expectations for `canWarn`, the session's `suspension` and the Account status settings link. Also answer a concurrent reuse of one idempotency key for two members with the same result as a sequential one instead of a unique-index error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CnAiFc4WPMdVa99L7jsTXY
…tion log
- Record warning.issued, warning.revoked, sanction.applied and
sanction.revoked in the unified Activity Logs (moderators' log), in the
same transaction as the change; warning reason and threshold changes go
to the administrators' log with their before and after values. Replaces
the separate core_moderation_audit table.
- GET /moderation/members/{id}/log and a Moderation log section on the
ModCP member page, for moderators with `can_view`; suspension entries
also need `can_view_sanctions`. No staff notes, override explanations or
revocation reasons are ever recorded.
- Regenerate the warnings migration on top of canary's migrations.
- Reuse resolveLangValue and MultiLangValue instead of local copies, drop
an unused helper, a needless cast and closure fallbacks.
- Docs: where warning history lives, the member log, the log actions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CnAiFc4WPMdVa99L7jsTXY
- Plainer UI strings and docs sentences: no spaced dashes, a named actor instead of passive voice, no pep-talk on an empty warning history. - Cache the UTC date formatter suspension notifications use, save a reason's translations in parallel, and split the staff warning card into WarningFacts and StaffNotes. - Give the Postgres warnings test the realtime model every request has. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CnAiFc4WPMdVa99L7jsTXY
b57da88 to
d214576
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d2145764a0
ℹ️ 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".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| export const accountStatusQueryOptions = () => | ||
| queryOptions({ | ||
| queryFn: fetchAccountStatus, | ||
| queryKey: ACCOUNT_STATUS_QUERY_KEY, | ||
| retry: false, | ||
| staleTime: 10_000, |
There was a problem hiding this comment.
Clear account-status data when the authenticated identity changes
When one member visits account status and then signs out or switches accounts in the same tab, this identity-independent key retains their warning messages and suspension history. The sign-in and sign-out actions in tanstack/auth/actions.ts call removeUserIdentityQueries, but tanstack/auth/queries.ts does not remove this new query. Consequently, another visitor can see the previous member's private history: the account-status loader explicitly reads the cache with staleTime: "static", and the screen also renders cached data while refetching. Add this query to identity-boundary cleanup or scope it to the authenticated account.
Useful? React with 👍 / 👎.
| export const previewWarning = async (draft: WarningDraft) => | ||
| await settle<WarningPreview>( | ||
| async () => | ||
| await fetcherClient({ |
There was a problem hiding this comment.
Use the universal fetcher in the shared warning API modules
The new shared warning API modules use fetcherClient for preview and mutations, despite AGENTS.md explicitly requiring shared views/* query modules to “call fetcher directly.” In particular, previewWarning is used as a React Query query function by the shared warn dialog, but this browser-only transport bypasses the universal fetcher's server request context and cookie forwarding. Replace these calls with the already imported universal fetcher; apply the same correction to warnings-admin-api.ts.
AGENTS.md reference: AGENTS.md:L54-L54
Useful? React with 👍 / 👎.
Moderators can warn members from a profile, a comment, a ModCP report or the
ModCP member page with one shared form that previews the consequences.
Administrators configure warning reasons and point thresholds in the AdminCP;
crossing a threshold suspends the account temporarily or indefinitely.
tables, with reason and content snapshots so history never changes.
idempotent per moderator; the warning, threshold decision, sanction,
audit entries, optional report resolution and notifications commit together.
resolveAuthors; content-linked warnings checkthe scoped
can_warnpermission and resolve author and scope on the server.Content Engine types get it automatically and
canWarnon /permissions.AdminCP and disconnected from WebSockets; /account-status explains why.
screens, comment "Warn author", WarnDialog and WarnAuthorButton.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01CnAiFc4WPMdVa99L7jsTXY