Skip to content

feat(moderation): ✨ warnings, warning thresholds and account suspensions - #910

Open
aXenDeveloper wants to merge 4 commits into
canaryfrom
claude/vitnode-warning-sanctions-0ugeet
Open

aXenDeveloper wants to merge 4 commits into
canaryfrom
claude/vitnode-warning-sanctions-0ugeet

Conversation

@aXenDeveloper

Copy link
Copy Markdown
Owner

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

@github-actions github-actions Bot added the 💡 Feature A new feature label Oct 30, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 30, 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-30T19:53:14.192412Z d214576 New commits
ℹ️ 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 commented Oct 30, 2026 •

Copy link
Copy Markdown

React Doctor found no new issues. 🎉

Reviewed by React Doctor for commit d214576.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +285 to +289
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)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment on lines +287 to +288
resolveReport:
draft.report !== null && draft.report.status !== "done",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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
@aXenDeveloper
aXenDeveloper force-pushed the claude/vitnode-warning-sanctions-0ugeet branch from b57da88 to d214576 Compare October 30, 2026 19:50

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +195 to +200
export const accountStatusQueryOptions = () =>
queryOptions({
queryFn: fetchAccountStatus,
queryKey: ACCOUNT_STATUS_QUERY_KEY,
retry: false,
staleTime: 10_000,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment on lines +303 to +306
export const previewWarning = async (draft: WarningDraft) =>
await settle<WarningPreview>(
async () =>
await fetcherClient({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

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