From 31cdb31e869a9242e5bacd6b9aaa4dccfe94c0d2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 22:11:23 +0000 Subject: [PATCH 1/9] =?UTF-8?q?feat(moderation):=20=E2=9C=A8=20warnings,?= =?UTF-8?q?=20warning=20thresholds=20and=20account=20suspensions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01CnAiFc4WPMdVa99L7jsTXY --- .../docs/content-engine/comments-api.mdx | 2 +- .../content-engine/public-permissions-api.mdx | 1 + .../docs/content-engine/public-write-api.mdx | 2 +- .../docs/dev/events/built-in-events.mdx | 15 +- .../dev/moderation/configure-warnings.mdx | 80 ++ .../web/content/docs/dev/moderation/index.mdx | 3 +- .../docs/dev/moderation/issue-warnings.mdx | 104 ++ .../web/content/docs/dev/moderation/meta.json | 6 +- .../content/docs/dev/moderation/reference.mdx | 57 ++ .../docs/dev/moderation/warn-content.mdx | 90 ++ .../docs/dev/moderation/warning-form.webp | Bin 0 -> 74576 bytes .../docs/dev/moderation/warning-member.webp | Bin 0 -> 80526 bytes .../docs/dev/moderation/warning-reasons.webp | Bin 0 -> 41202 bytes .../dev/moderation/warning-thresholds.webp | Bin 0 -> 42626 bytes .../content/docs/dev/moderation/warnings.mdx | 53 + apps/web/src/page-cache.gen.ts | 5 + .../src/api/lib/moderation/registry.ts | 8 +- .../vitnode/src/api/lib/moderation/targets.ts | 19 + .../src/api/lib/moderation/warnings/access.ts | 181 ++++ .../api/lib/moderation/warnings/reasons.ts | 119 +++ .../api/lib/moderation/warnings/suspension.ts | 114 +++ .../src/api/lib/notifications/core-types.ts | 128 ++- .../src/api/middlewares/global.middleware.ts | 28 +- packages/vitnode/src/api/models/events.ts | 46 + packages/vitnode/src/api/moderation.ts | 1 + .../src/api/modules/admin/admin.module.ts | 2 + .../warnings/routes/warning-reasons.routes.ts | 282 ++++++ .../routes/warning-thresholds.routes.ts | 279 ++++++ .../admin/warnings/warnings.admin.module.ts | 28 + .../moderation/lib/warnings-service.ts | 915 ++++++++++++++++++ .../modules/moderation/lib/warnings-view.ts | 223 +++++ .../modules/moderation/moderation.module.ts | 20 + .../moderation/routes/account-status.route.ts | 126 +++ .../modules/moderation/routes/show.route.ts | 6 + .../moderation/routes/warnings.routes.ts | 633 ++++++++++++ .../api/modules/users/routes/session.route.ts | 16 +- packages/vitnode/src/api/plugin.ts | 17 + .../src/content/server/public-permissions.ts | 32 +- .../src/content/server/report-target.ts | 32 + packages/vitnode/src/database/warnings.ts | 218 +++++ packages/vitnode/src/lib/moderation/const.ts | 2 + .../vitnode/src/lib/moderation/warnings.ts | 193 ++++ packages/vitnode/src/locales/api/en.json | 25 + packages/vitnode/src/locales/en.json | 350 ++++++- packages/vitnode/src/pages/account-status.tsx | 22 + .../admin/moderation/warning-reasons.tsx | 25 + .../admin/moderation/warning-thresholds.tsx | 25 + packages/vitnode/src/pages/modcp/member.tsx | 24 + packages/vitnode/src/pages/modcp/warnings.tsx | 34 + packages/vitnode/src/routes.tsx | 36 + .../src/tanstack/account-status/route.ts | 33 + .../src/tanstack/account-status/screen.tsx | 18 + .../src/tanstack/admin/warnings/route.ts | 62 ++ .../src/tanstack/admin/warnings/screen.tsx | 188 ++++ .../vitnode/src/tanstack/comments/author.ts | 21 + .../vitnode/src/tanstack/comments/item.tsx | 51 +- .../vitnode/src/tanstack/comments/root.tsx | 32 +- .../tanstack/comments/use-content-comments.ts | 3 + .../src/tanstack/content/permissions.ts | 12 +- .../src/tanstack/layout/main-header.tsx | 13 +- .../src/tanstack/moderation/breadcrumb.tsx | 16 + .../vitnode/src/tanstack/moderation/index.ts | 6 + .../vitnode/src/tanstack/moderation/route.ts | 6 +- .../tanstack/moderation/suspension-notice.tsx | 61 ++ .../src/tanstack/moderation/warn-button.tsx | 155 +++ .../src/tanstack/moderation/warnings-route.ts | 154 +++ .../tanstack/moderation/warnings-screen.tsx | 62 ++ .../vitnode/src/tanstack/profile/screen.tsx | 51 +- .../admin/layouts/sidebar/nav/nav-model.tsx | 10 + .../warnings/warning-reason-form.tsx | 205 ++++ .../warnings/warning-reasons-content.tsx | 216 +++++ .../warnings/warning-threshold-form.tsx | 166 ++++ .../warnings/warning-thresholds-content.tsx | 241 +++++ .../moderation/warnings/warnings-admin-api.ts | 191 ++++ .../views/core/shared/admin-permissions.ts | 5 + .../admin/views/core/shared/admin-scope.ts | 2 + .../src/views/auth/settings/nav-content.tsx | 2 + .../src/views/auth/settings/settings-nav.ts | 4 +- .../src/views/moderation/modcp/modcp-nav.tsx | 5 +- .../moderation/modcp/report-activity.tsx | 7 + .../views/moderation/modcp/report-sidebar.tsx | 16 + .../warnings/account-status-content.tsx | 268 +++++ .../warnings/modcp-member-content.tsx | 138 +++ .../warnings/modcp-warnings-content.tsx | 257 +++++ .../warnings/warn-dialog-content.tsx | 706 ++++++++++++++ .../moderation/warnings/warning-format.tsx | 30 + .../moderation/warnings/warning-list.tsx | 438 +++++++++ .../views/moderation/warnings/warnings-api.ts | 348 +++++++ 88 files changed, 8771 insertions(+), 55 deletions(-) create mode 100644 apps/web/content/docs/dev/moderation/configure-warnings.mdx create mode 100644 apps/web/content/docs/dev/moderation/issue-warnings.mdx create mode 100644 apps/web/content/docs/dev/moderation/warn-content.mdx create mode 100644 apps/web/content/docs/dev/moderation/warning-form.webp create mode 100644 apps/web/content/docs/dev/moderation/warning-member.webp create mode 100644 apps/web/content/docs/dev/moderation/warning-reasons.webp create mode 100644 apps/web/content/docs/dev/moderation/warning-thresholds.webp create mode 100644 apps/web/content/docs/dev/moderation/warnings.mdx create mode 100644 packages/vitnode/src/api/lib/moderation/warnings/access.ts create mode 100644 packages/vitnode/src/api/lib/moderation/warnings/reasons.ts create mode 100644 packages/vitnode/src/api/lib/moderation/warnings/suspension.ts create mode 100644 packages/vitnode/src/api/modules/admin/warnings/routes/warning-reasons.routes.ts create mode 100644 packages/vitnode/src/api/modules/admin/warnings/routes/warning-thresholds.routes.ts create mode 100644 packages/vitnode/src/api/modules/admin/warnings/warnings.admin.module.ts create mode 100644 packages/vitnode/src/api/modules/moderation/lib/warnings-service.ts create mode 100644 packages/vitnode/src/api/modules/moderation/lib/warnings-view.ts create mode 100644 packages/vitnode/src/api/modules/moderation/routes/account-status.route.ts create mode 100644 packages/vitnode/src/api/modules/moderation/routes/warnings.routes.ts create mode 100644 packages/vitnode/src/database/warnings.ts create mode 100644 packages/vitnode/src/lib/moderation/warnings.ts create mode 100644 packages/vitnode/src/pages/account-status.tsx create mode 100644 packages/vitnode/src/pages/admin/moderation/warning-reasons.tsx create mode 100644 packages/vitnode/src/pages/admin/moderation/warning-thresholds.tsx create mode 100644 packages/vitnode/src/pages/modcp/member.tsx create mode 100644 packages/vitnode/src/pages/modcp/warnings.tsx create mode 100644 packages/vitnode/src/tanstack/account-status/route.ts create mode 100644 packages/vitnode/src/tanstack/account-status/screen.tsx create mode 100644 packages/vitnode/src/tanstack/admin/warnings/route.ts create mode 100644 packages/vitnode/src/tanstack/admin/warnings/screen.tsx create mode 100644 packages/vitnode/src/tanstack/comments/author.ts create mode 100644 packages/vitnode/src/tanstack/moderation/suspension-notice.tsx create mode 100644 packages/vitnode/src/tanstack/moderation/warn-button.tsx create mode 100644 packages/vitnode/src/tanstack/moderation/warnings-route.ts create mode 100644 packages/vitnode/src/tanstack/moderation/warnings-screen.tsx create mode 100644 packages/vitnode/src/views/admin/views/core/moderation/warnings/warning-reason-form.tsx create mode 100644 packages/vitnode/src/views/admin/views/core/moderation/warnings/warning-reasons-content.tsx create mode 100644 packages/vitnode/src/views/admin/views/core/moderation/warnings/warning-threshold-form.tsx create mode 100644 packages/vitnode/src/views/admin/views/core/moderation/warnings/warning-thresholds-content.tsx create mode 100644 packages/vitnode/src/views/admin/views/core/moderation/warnings/warnings-admin-api.ts create mode 100644 packages/vitnode/src/views/moderation/warnings/account-status-content.tsx create mode 100644 packages/vitnode/src/views/moderation/warnings/modcp-member-content.tsx create mode 100644 packages/vitnode/src/views/moderation/warnings/modcp-warnings-content.tsx create mode 100644 packages/vitnode/src/views/moderation/warnings/warn-dialog-content.tsx create mode 100644 packages/vitnode/src/views/moderation/warnings/warning-format.tsx create mode 100644 packages/vitnode/src/views/moderation/warnings/warning-list.tsx create mode 100644 packages/vitnode/src/views/moderation/warnings/warnings-api.ts diff --git a/apps/web/content/docs/content-engine/comments-api.mdx b/apps/web/content/docs/content-engine/comments-api.mdx index 77137ca5e2..a885e0d7bc 100644 --- a/apps/web/content/docs/content-engine/comments-api.mdx +++ b/apps/web/content/docs/content-engine/comments-api.mdx @@ -58,7 +58,7 @@ const comments = useContentComments({ queryKey, thread, ...actions }) | `liveStatus` | `"off"` (no `live`, or no open WebSocket), `"joining"`, `"live"`, or `"denied"` when the server refused or revoked the subscription | | `trackDraft(id, open)` | Call with `true` while an edit or reply form is open on `id` and `false` when it closes. A live update that removes that comment keeps it on screen with `removed: true` and its record until then. `useContentCommentItem` calls it for you | | `replyCount`, `truncated` | Every public comment at any depth, and whether a page held more replies than one answer carries | -| `permissionsOf(id)` | `{ canDelete, canEdit, canHide, canReport }`, all `false` until known or after a failed batch | +| `permissionsOf(id)` | `{ canDelete, canEdit, canHide, canReport, canWarn }`, all `false` until known or after a failed batch | | `canComment`, `canReply(node)` | Whether the composer and **Reply** show: signed in, `create` given, under `maxDepth`, not hidden | | `features` | Which actions you wired up | | `busy` | The action running right now, `{ action, id }` or `null`. A second action waits for it | diff --git a/apps/web/content/docs/content-engine/public-permissions-api.mdx b/apps/web/content/docs/content-engine/public-permissions-api.mdx index d7e6e3a5ff..9b907f21e1 100644 --- a/apps/web/content/docs/content-engine/public-permissions-api.mdx +++ b/apps/web/content/docs/content-engine/public-permissions-api.mdx @@ -37,6 +37,7 @@ Cookie: vitnode_auth= | --- | --- | --- | | `canEdit` | `GET /{id}/edit` and `PATCH /{id}` would accept this visitor: a moderator with `can_edit` in the record's container, or the owner while the record is not hidden, `update.own` is on and the container's `authorEdit` policy still allows it | Always `false` | | `canReport` | The content type has `reports`, the record is public, and the visitor did not write it | `true` on public records. The report dialog asks them to sign in | +| `canWarn` | The content type has `reports`, the visitor is a moderator with `can_warn` for the record's scope, and did not write it. See [Warn content authors](/docs/dev/moderation/warn-content) | `false` | A record is **left out of `items`** when the visitor can neither read it publicly nor edit it: a missing id, someone else's draft, or a hidden record for anyone but a moderator with `can_edit`. The route answers the same way for all of them, so it never confirms that a draft exists. Your own draft comes back with `canEdit: true` and `canReport: false`. diff --git a/apps/web/content/docs/content-engine/public-write-api.mdx b/apps/web/content/docs/content-engine/public-write-api.mdx index c35f0fb3a0..8cc9665719 100644 --- a/apps/web/content/docs/content-engine/public-write-api.mdx +++ b/apps/web/content/docs/content-engine/public-write-api.mdx @@ -74,7 +74,7 @@ The routes live in the public module, under `/api/{pluginId}/content/{publicApi. | `POST /` | `create` | `{ values }` | `201` with the record | | `GET /{id}/edit` | `update` | | `200` with the record, drafts included, `Cache-Control: private, no-store` | | `PATCH /{id}` | `update` | `{ values, expectedVersion }` | `200` with the record | -| `GET /permissions?ids=1,2` | `publicWrite`, `reports` or `visibility` | | `200` with `{ items: [{ id, canDelete, canEdit, canHide, canReport }] }`, `Cache-Control: private, no-store` | +| `GET /permissions?ids=1,2` | `publicWrite`, `reports` or `visibility` | | `200` with `{ items: [{ id, canDelete, canEdit, canHide, canReport, canWarn }] }`, `Cache-Control: private, no-store` | `GET /{id}/edit` and `PATCH /{id}` answer only for the owner and for moderators. There is no public delete, publish or hide route. diff --git a/apps/web/content/docs/dev/events/built-in-events.mdx b/apps/web/content/docs/dev/events/built-in-events.mdx index 675f9647df..4fdcf19ac5 100644 --- a/apps/web/content/docs/dev/events/built-in-events.mdx +++ b/apps/web/content/docs/dev/events/built-in-events.mdx @@ -565,11 +565,13 @@ member is notified. ## Moderation events -Eleven names, declared in `VitNodeEvents` in +Twenty names, declared in `VitNodeEvents` in `packages/vitnode/src/api/models/events.ts` and emitted by core with `pluginId: "@vitnode/core"`. Each fires **after** its database write commits. The envelope's `actor` is the reporter for `created`, the -administrator for `report_reason.*` and the moderator for the rest. Adding an internal note emits nothing. See +administrator for `report_reason.*`, `warning_reason.*` and +`warning_threshold.*`, and the moderator for the rest. No payload carries a +staff note. Adding an internal note emits nothing. See [Moderation](/docs/dev/moderation). | Event | Payload | Fires when | @@ -585,6 +587,15 @@ administrator for `report_reason.*` and the moderator for the rest. Adding an in | `moderation.report_reason.updated` | `{ reasonId, code }` | An admin edits a report reason | | `moderation.report_reason.deleted` | `{ reasonId, code }` | An admin deletes a report reason. Its reports stay | | `moderation.report_reason.reordered` | `{ reasonIds }` | An admin saves a new order. `reasonIds` lists every reason in that order | +| `moderation.warning.issued` | `{ warningId, userId, issuedById, points, reasonId, reportId, pluginId, targetType, targetId }` | A moderator issues a [warning](/docs/dev/moderation/warnings). Not emitted again for a retried request | +| `moderation.warning.revoked` | `{ warningId, userId, revokedById }` | A moderator revokes a warning | +| `moderation.sanction.applied` | `{ sanctionId, userId, type, source, endsAt, thresholdId, warningId }` | A threshold suspends an account. `endsAt` is `null` for an indefinite suspension | +| `moderation.sanction.revoked` | `{ sanctionId, userId, revokedById }` | A moderator lifts a suspension | +| `moderation.warning_reason.created` | `{ reasonId }` | An admin adds a warning reason | +| `moderation.warning_reason.updated` | `{ reasonId }` | An admin edits or disables a warning reason | +| `moderation.warning_threshold.created` | `{ thresholdId }` | An admin adds a warning threshold | +| `moderation.warning_threshold.updated` | `{ thresholdId }` | An admin edits, enables or disables a threshold | +| `moderation.warning_threshold.deleted` | `{ thresholdId }` | An admin deletes a threshold | A hide or restore from the ModCP also emits the content type's own `content.{id}.hidden` or `unhidden` event, because it goes through the same diff --git a/apps/web/content/docs/dev/moderation/configure-warnings.mdx b/apps/web/content/docs/dev/moderation/configure-warnings.mdx new file mode 100644 index 0000000000..21a429e79e --- /dev/null +++ b/apps/web/content/docs/dev/moderation/configure-warnings.mdx @@ -0,0 +1,80 @@ +--- +title: Configure warnings +description: Set up VitNode warning reasons with default points and expiry, and warning thresholds that suspend an account automatically, in the AdminCP. +icon: Settings2 +--- + +import { ImgDocs } from "@/components/fumadocs/img"; +import reasons from "./warning-reasons.webp"; +import thresholds from "./warning-thresholds.webp"; + +Warning reasons give moderators consistent points and messages, and thresholds decide when points turn into a suspension. This guide sets up the example from [Warnings](/docs/dev/moderation/warnings): a 2-point **Spam** reason and suspensions at 8 and 10 points. + +## Before you begin + +You need an AdminCP account with the **Warning settings** permissions: **View warning reasons and thresholds**, and **Manage warning reasons and thresholds** to change them. Admins with a root role have both. + + + + + +### Add a warning reason + +Open **AdminCP → Moderation → Warning reasons** and choose **Add reason**. Fill in: + +| Field | Example | What it does | +| --- | --- | --- | +| Title | Spam | What moderators pick and what the member reads | +| Description | Ads or repeated junk | Helps moderators choose; members never see it | +| Points | 2 | Added to the member's active points. `0` makes an official warning without points | +| Points expire after | 30 days | After this the points stop counting. Check **Never expire** for permanent points | +| Default message | Please stop posting spam. | Prefilled in the warn form; moderators can edit it | +| Enabled | On | Disabled reasons can't be picked for new warnings | + +Title, description and message are translated per language, like report reasons. Drag rows to set the order moderators see. + + + +Reasons are never deleted, only disabled. Every warning keeps a copy of its reason's title and the points it was issued with, so editing or disabling a reason never changes a warning already issued. + + + + + +### Add thresholds + +Open **AdminCP → Moderation → Warning thresholds** and choose **Add threshold** twice: + +| Active points | Action | Duration | +| --- | --- | --- | +| 8 | Temporary suspension | 24 hours | +| 10 | Indefinite suspension | - | + + + +Only one enabled threshold can use each number of points; saving a second one answers **Another enabled threshold already uses this number of points**. Disable or delete a threshold to stop it. Suspensions it already created keep a copy of the rule and stay until they end or are lifted. + + + + + +## Check the result + +Warn a test member twice with **Spam** (4 points), then preview a third time in the warn form: **Automatic action** says nothing. Change the points to 4 (with an explanation) and the preview shows **Account suspended for 1 day**. See [Issue warnings](/docs/dev/moderation/issue-warnings). + +Changes apply to warnings issued from now on. Saving a lower threshold does not suspend members who are already above it, and raising one does not lift anything. + +## Related + +- [Warnings](/docs/dev/moderation/warnings) +- [Configure moderators](/docs/dev/moderation/configure-moderators) diff --git a/apps/web/content/docs/dev/moderation/index.mdx b/apps/web/content/docs/dev/moderation/index.mdx index adbc7ad926..0424b45cb6 100644 --- a/apps/web/content/docs/dev/moderation/index.mdx +++ b/apps/web/content/docs/dev/moderation/index.mdx @@ -24,6 +24,7 @@ A moderator signs in with their normal user session. The ModCP never asks for an | Give a user or role moderator permissions in the AdminCP | [Configure moderators](/docs/dev/moderation/configure-moderators) | | Review, assign, resolve and act on reports | [Process reports](/docs/dev/moderation/process-reports) | | Approve or reject new content and edits before they go live | [Review approvals](/docs/dev/moderation/review-approvals) | +| Warn members and suspend accounts automatically at a points limit | [Warnings](/docs/dev/moderation/warnings) | | Look up routes, options, events and limits | [Moderation reference](/docs/dev/moderation/reference) | ## How the parts fit together @@ -37,4 +38,4 @@ Reports belong to core, not to the plugin. They keep their snapshot after the ar ## What is not included yet -This version covers reports, hiding and restoring content, and [reviewing approvals](/docs/dev/moderation/review-approvals). It has no warnings, bans, automatic moderation, anonymous reports, report notifications or permanent deletion from the ModCP. Moderators still have to read things themselves, sorry. +This version covers reports, hiding and restoring content, [reviewing approvals](/docs/dev/moderation/review-approvals) and [warnings with automatic suspensions](/docs/dev/moderation/warnings). It has no manual bans, appeals, automatic content moderation, anonymous reports, report notifications or permanent deletion from the ModCP. Moderators still have to read things themselves, sorry. diff --git a/apps/web/content/docs/dev/moderation/issue-warnings.mdx b/apps/web/content/docs/dev/moderation/issue-warnings.mdx new file mode 100644 index 0000000000..5ac4ed8563 --- /dev/null +++ b/apps/web/content/docs/dev/moderation/issue-warnings.mdx @@ -0,0 +1,104 @@ +--- +title: Issue warnings +description: Warn a VitNode member from their profile, a comment, a report or the Moderator CP, preview the consequences, and revoke warnings or lift suspensions. +icon: TriangleAlert +--- + +import { ImgDocs } from "@/components/fumadocs/img"; +import form from "./warning-form.webp"; +import member from "./warning-member.webp"; + +Moderators warn members with one shared form, opened from wherever the problem is. This guide warns a member for a blog comment, then revokes the warning and lifts the suspension it caused. + +## Before you begin + +Warnings need [warning reasons](/docs/dev/moderation/configure-warnings) and a moderator entry with the right permissions. Grant them in **AdminCP → Staff → Moderators**: + +| Permission | Where | Lets the moderator | +| --- | --- | --- | +| **Warn authors of this content** (`can_warn`) | The content type's module, for example **Comment** in the blog. Can be [scoped](/docs/dev/moderation/scoped-permissions) | Warn the author of that content | +| **Warn members directly, without content** | Core → **Warnings** | Warn from a profile or the ModCP without content | +| **Change a warning's points** / **Change when a warning's points expire** | Core → **Warnings** | Override the reason's defaults, with an explanation | +| **View every member's warnings** | Core → **Warnings** | See all warnings, not only those in their scope | +| **Read staff notes on warnings** | Core → **Warnings** | Read private notes and override explanations | +| **Revoke warnings** | Core → **Warnings** | Revoke a warning | +| **View suspensions** / **Lift suspensions** | Core → **Warnings** | See and lift suspensions | + +A moderator limited to one forum section can warn for content in that section only. Without **Warn members directly** they cannot warn from a profile, and without **View every member's warnings** they see only the warnings they issued or that belong to their scope. The member's total of active points is always shown, because a warning anywhere counts toward account-wide thresholds. + + + + + +### Open the warn form + +Pick the entry point that matches what you are looking at: + +| From | How | +| --- | --- | +| A comment | **More actions** (`…`) → **Warn author** | +| A report | **Warn author** under the status button in the ModCP report | +| A profile | **Warn** next to **Warnings** on the member's profile | +| The ModCP | **Moderator CP → Warnings → Find a member**, then **Warn member** | + +Each button appears only when the server says you may use it. + + + + + +### Fill in the warning + +Pick a reason. The points, expiry and message follow the reason; change the points or expiry only if you may, and say why in **Why the change?**. + +- **Message to the member** is what the member reads in the notification and on their account status page. +- **Staff note** is private. The member never sees it, and only staff who can read notes do. + +**What happens** shows the member's active points now, this warning's points, the total after and the automatic action. It is a preview: the server calculates everything again when you submit, in case someone else warned the member meanwhile. + + + +From a report, **Also close the report as action taken** closes it in the same step. If either part fails, neither happens. Without it, the report stays open and its timeline records the warning. + + + + + +### Submit + +Choose **Issue warning**. The toast says how many active points the member has now and whether their account was suspended, from the server's actual result. If that differs from the preview, the toast says so. Sending the same form twice, for example after a network error, issues it once. + + + + + +### Revoke a warning or lift a suspension + +Open the member in **Moderator CP → Warnings** (or **Warnings** on their profile). **Revoke** on a warning asks for a reason; its points stop counting at once and the member is notified. The warning stays in the history, marked **Revoked**. + +Revoking a warning never lifts the suspension it caused. Lift it separately with **Lift** on the suspension. Other active suspensions keep applying. + + + + + + + +## Check the result + +Sign in as the warned member. A notification links to **Account status**, which lists the warning with your message and its points. If the warning crossed a threshold, every page shows the suspension notice and actions such as commenting answer like a signed-out visitor's. + +## Related + +- [Warnings](/docs/dev/moderation/warnings) +- [Process reports](/docs/dev/moderation/process-reports) diff --git a/apps/web/content/docs/dev/moderation/meta.json b/apps/web/content/docs/dev/moderation/meta.json index fa3c4e79af..3e93372d21 100644 --- a/apps/web/content/docs/dev/moderation/meta.json +++ b/apps/web/content/docs/dev/moderation/meta.json @@ -1,6 +1,6 @@ { "title": "Moderation", - "description": "Reports, the Moderator CP and scoped moderator permissions", + "description": "Reports, warnings, the Moderator CP and scoped moderator permissions", "icon": "ShieldAlert", "pages": [ "index", @@ -10,6 +10,10 @@ "configure-moderators", "process-reports", "review-approvals", + "warnings", + "configure-warnings", + "issue-warnings", + "warn-content", "reference" ] } diff --git a/apps/web/content/docs/dev/moderation/reference.mdx b/apps/web/content/docs/dev/moderation/reference.mdx index c31ff51a26..6105a79110 100644 --- a/apps/web/content/docs/dev/moderation/reference.mdx +++ b/apps/web/content/docs/dev/moderation/reference.mdx @@ -223,12 +223,67 @@ Statuses are `pending`, `approved`, `rejected`, `superseded` (replaced by the au | `moderation.report_reason.updated` | `{ reasonId, code }` | | `moderation.report_reason.deleted` | `{ reasonId, code }` | | `moderation.report_reason.reordered` | `{ reasonIds }` in the new order | +| `moderation.warning.issued` | `{ warningId, userId, issuedById, points, reasonId, reportId, pluginId, targetType, targetId }` | +| `moderation.warning.revoked` | `{ warningId, userId, revokedById }` | +| `moderation.sanction.applied` | `{ sanctionId, userId, type, source, endsAt, thresholdId, warningId }` | +| `moderation.sanction.revoked` | `{ sanctionId, userId, revokedById }` | +| `moderation.warning_reason.created` / `.updated` | `{ reasonId }` | +| `moderation.warning_threshold.created` / `.updated` / `.deleted` | `{ thresholdId }` | | `content.approval.submitted` | `{ submissionId, replacedSubmissionId, pluginId, contentTypeId, contentId, containerId, kind, authorId }` | | `content.approval.approved` | `{ submissionId, pluginId, contentTypeId, contentId, containerId, kind, authorId, reviewerId }` | | `content.approval.rejected` | `{ submissionId, pluginId, contentTypeId, contentId, containerId, kind, authorId, reviewerId, reason }` | Adding a note emits no event. Hiding through a report also emits the content type's own `hidden` or `unhidden` event. See [Built-in events](/docs/dev/events/built-in-events#moderation-events). +## Warnings + +Concepts and flows are in [Warnings](/docs/dev/moderation/warnings). Everything below is core's. + +### Warning permissions + +| Type | Module | Permission | Scoped | +| --- | --- | --- | --- | +| Moderator | A report target's `permissionModule` | `can_warn`: warn the content's authors. Added only for targets with `resolveAuthors` | With the target's `scope` | +| Moderator | `warnings` | `can_view`, `can_warn` (direct), `can_override_points`, `can_override_expiration`, `can_view_notes`, `can_revoke`, `can_view_sanctions`, `can_manage_sanctions` | No | +| Admin | `warning_settings` | `can_view`, `can_manage` | No | + +### Warning API routes + +Under `/api/@vitnode/core/moderation`, using the user session. + +| Method and path | Who | Body or query | +| --- | --- | --- | +| `GET /warnings/reasons` | Anyone who can warn | Enabled reasons in display order | +| `POST /warnings/preview` | Anyone who can warn | `{ userId, reasonId, points?, expiration?, content?, reportId? }`. Runs every check and answers points before and after and the projected `sanction` | +| `POST /warnings` | Anyone who can warn | The preview body plus `{ message, staffNote?, overrideNote?, resolveReport?, idempotencyKey }` | +| `POST /warnings/{id}/revoke` | `can_revoke` | `{ reason }` | +| `GET /warnings` | Moderators | Query `userId`, `reasonId`, `issuedById`, `status`, `pluginId`, `targetType`, `from`, `to`, `page`, `pageSize` | +| `GET /members?search=` | `can_view` or `can_warn` | Up to 10 members | +| `GET /members/{id}` | `can_view` or anyone who can warn | Active points, visible warnings, `hiddenWarnings`, sanctions with `can_view_sanctions` | +| `POST /sanctions/{id}/revoke` | `can_manage_sanctions` | `{ reason }` | +| `GET /account-status` | The member, also while suspended | Own suspension, active points, warnings and suspensions, never staff notes | + +`expiration` is `{ mode: "default" }`, `{ mode: "never" }`, `{ mode: "hours", hours }` or `{ mode: "at", at }` (an ISO date in the future). `content` is `{ pluginId, targetType, targetId }`; with `reportId`, the content comes from the report. A retry with the same `idempotencyKey` from the same moderator answers the first result with `duplicate: true`. + +Refusals answer `{ error }`: `self` and `protected` (`403`), `forbidden` (`403`), `points_override_forbidden` and `expiration_override_forbidden` (`403`), `override_note_required`, `invalid_points`, `invalid_expiration`, `author_mismatch` and `report_mismatch` (`400`), `user_not_found`, `reason_not_found` and `content_not_found` (`404`), `reason_disabled`, `already_revoked` and `report_closed` (`409`). + +The AdminCP routes are under `/api/@vitnode/core/admin/warnings`: `GET`/`POST /reasons`, `PATCH /reasons/{id}`, `PUT /reasons/reorder`, `GET`/`POST /thresholds`, `PUT`/`DELETE /thresholds/{id}`. A second enabled threshold at the same points answers `409 duplicate_points`. + +### Warning limits + +| Limit | Value | +| --- | --- | +| Points per warning or reason | 0 to 100 | +| Threshold points | 1 to 1000 | +| Expiry and suspension length | 1 hour to 5 years | +| Message to the member | 3 to 2000 characters | +| Staff note, explanations, revocation reasons | Up to 2000 characters; explanations and reasons at least 3 | +| Warning reasons / thresholds | 50 / 20 | + +### Warning notifications + +`core.warning_issued`, `core.warning_revoked`, `core.account_suspended` and `core.suspension_revoked`, in the **Account** category. They are mandatory in the inbox, link to `/account-status`, and are published inside the warning's transaction, so a rolled-back warning notifies nobody. They carry the member's message and points, never staff notes or the moderator's name. + ## Snapshot format Stored in `core_moderation_reports.snapshot` as JSON, `version: 1`: @@ -274,4 +329,6 @@ The migration `20261011193321_add_moderation_reports` adds `core_moderation_repo The migration `20261019161102_add_moderation_report_reasons` adds `core_moderation_report_reasons` with a unique `code`, `requiresDetails` and `position`. Titles and descriptions live in `core_languages_words`. `core_moderation_reports.reason` holds the reason's `code` without a foreign key, so deleting a reason keeps its reports. `db:prepare` and `db:migrate` add the four starter reasons only while the table is empty. +The migration `20261028212551_add_moderation_warnings` adds `core_moderation_warning_reasons`, `core_moderation_warning_thresholds` (a partial unique index allows one enabled threshold per number of points), `core_moderation_warnings`, `core_moderation_sanctions` and `core_moderation_audit`. A warning keeps a copy of its reason's title, the points it was issued with and a short content snapshot; `(issuedById, idempotencyKey)` is unique, and so is a sanction's `warningId`. Warned users' rows cascade on account deletion; actors are `ON DELETE SET NULL`. + Approvals live in `core_content_submissions`, one row per submission. Rows are never deleted with the content, author and reviewer are `ON DELETE SET NULL`, and a partial unique index allows one `pending` submission per record. Container policy overrides are stored in `core_content_containers.policies`. diff --git a/apps/web/content/docs/dev/moderation/warn-content.mdx b/apps/web/content/docs/dev/moderation/warn-content.mdx new file mode 100644 index 0000000000..e998e17fb3 --- /dev/null +++ b/apps/web/content/docs/dev/moderation/warn-content.mdx @@ -0,0 +1,90 @@ +--- +title: Warn content authors +description: Let VitNode moderators warn the author of your plugin's content, with the server resolving authorship and moderation scope, and add the WarnAuthorButton. +icon: UserX +--- + +A content-linked warning names a piece of content, so moderators limited to one section can warn for that section only. This guide adds **Warn author** to your plugin's content. + +## How the server decides + +Warnings reuse your content's **report target**. The browser sends only the content's `pluginId`, `targetType`, `targetId` and the member's id. The server then: + +1. Finds the report target, and refuses content without one. +2. Resolves the content's moderation scope with the target's `resolveScope`. +3. Checks the moderator's `can_warn` in the target's `permissionModule` for that scope. +4. Reads the current authors with the target's `resolveAuthors`, and refuses the warning unless the member is one of them. + +A client-sent author or scope is never trusted. The warning keeps a short title and URL of the content, so it stays readable after the content is deleted or its plugin is removed. + +## Content Engine types + +A content type with `reports` is warnable with no extra code. Core adds `resolveAuthors`, reading the same author field search and reports use, and adds `can_warn` to the type's moderator permissions next to `can_view_reports`. + +The permissions route answers `canWarn` for each record: + +```json title="GET /api/@vitnode/blog/content/comments/permissions?ids=1" +{ + "items": [ + { + "id": 1, + "canDelete": false, + "canEdit": false, + "canHide": false, + "canReport": true, + "canWarn": true + } + ] +} +``` + +Comments rendered with [`ContentCommentActions`](/docs/content-engine/comments-api) show **Warn author** in the **More actions** menu whenever `canWarn` is true and the thread has `report`. The blog's comments get it this way. + +## Add the button to your own page + +For other content, render `WarnAuthorButton` when the permissions route answers `canWarn`: + +```tsx title="src/pages/topic-page.tsx" +import { contentTarget } from "@vitnode/core/content"; +import { WarnAuthorButton } from "@vitnode/core/tanstack/moderation"; + +import { topicContentType } from "@/content/topic"; +import { manifest } from "@/manifest.gen"; + +{permissions.canWarn && topic.author ? ( + +) : null} +``` + +`WarnDialog` takes the same props plus `open` and `onOpenChange`, for a menu item or a trigger you already have. Both accept `reportId` to link the warning to a report, and `onIssued` to refresh your own data with the server's result. + +## Custom report targets + +For a target built with `defineReportTarget`, add `resolveAuthors`. It runs on the server as the moderator, so hidden content still resolves; return `null` once the content is deleted: + +```ts title="src/moderation/topic-target.ts" +import { defineReportTarget } from "@vitnode/core/api/moderation"; + +export const topicTarget = defineReportTarget({ + // ...type, label, permissionModule, parseId, capture, current + resolveAuthors: async (c, { targetId }) => { + const topic = await findTopic(c, targetId); + + return topic ? { authorIds: [topic.authorId], title: topic.title } : null; + }, +}); +``` + +With `resolveAuthors`, the target's module gets a `can_warn` permission. Without it, the target stays reportable but not warnable. + +## Check the result + +Sign in as a moderator with **Warn authors of this content** for your module and open a record written by someone else: **Warn author** appears. Sign in as a moderator without it: the button is gone, and `POST /moderation/warnings` with that content answers `403`. + +## Related + +- [Issue warnings](/docs/dev/moderation/issue-warnings) +- [Custom report targets](/docs/dev/moderation/reference#custom-report-targets) diff --git a/apps/web/content/docs/dev/moderation/warning-form.webp b/apps/web/content/docs/dev/moderation/warning-form.webp new file mode 100644 index 0000000000000000000000000000000000000000..5092065f4c82b6981e4ccc9c207c9bc06c3d797d GIT binary patch literal 74576 zcma%iW0)jcx^7jM*=5^Zwr$&8=(6oDtIM`++qP}nHuLuEnK@_g!M!(sWkhDI$oSS; z->WATB}7HViGhLCL1Bb(IryQYJ7z1zH|y<-6Yz|?it z1w>2Mbyh3hHhu|UlaI~24V}4>^hYvFz8IqOh&-n+#+`19-|>9gxq&H@1c5_F=w-Zkj$^*Qx>Al;>{LB4ix!_SA$ zyE*aoK#g}a0Q)2F(R-KfbZgnW);kM;4siHLYV=O_ z>iQUaS9^|n;M?-H?(FeK{bc+2{s!@4bJMlX_Y5d{T>5nXAU`{LmIHjMxAML6J@FlP zxpe}2n!aQ`Rb6`T0hRz3AFYqjpAc_H7eh}`fULXDkoUaTtY^TDcO%~#0PqO}K>gJF zxO+2s(S7P#|6R$|x9{(Azst4Ntz0(~uEq}cQoNk=~+Pvcc8=nThF9h@h z)&Z;D0RSO@#rqHd@Bu*fru;kt5Wm~J0Nw$IzlXZ}0RL3@!2h6oin`r;1dRMXwen;D zD7ph22VKwsvK+RSrarc^0B`-YZ1mi-x>zWj)t$-TRCr?5(O3)IW{|W}=e4usdEsjB05EZyVRfevJ&iivJHIb=AYVhkdFzrBJA_+G zE>+q^ETPw$CAR)n;1tnn)ROFH5v5p3`7SCt^ztfEU&tV{g`4R*{qvVuE+KlxB1*k? z$n!%iP`B$}ZlI2}CD-=B9s}xMePX#xfD)Y$KM50ym*9vItmG%QI5hO7!p#ay|HMWY=4C@$l@Co8JRjQ*p zC}_ZWR3!YLYx4k-bwb1|ax2>EQ8~Ve6gl00z z8{sKInFYy$3gPk;b*!kzI~)=`Q6&!YyW>?3?mc-~Dd=U1d|@lZq>%!A?4N785RjI zwB12OC>;Mxe4>W_I#|KdB0la|ui{|t{wau$`HrwcoM-(?)GSVXuv zY+32ChvyFv0++A4CX$Jo(3#>L#WhrRMzv635XCsvHK@=0!pl0ARq}Z#Uouz2YB$Dy zCQ^k)f=nt{U`;E~F#^UKm2Zd1q%jF9^Z>K*Z zP`j=FCN#^3jl0r!qm|@(pJTwS#V3Vz-1qHd@dt;%sEUDgAD?f6Rk%s|x9DDt8*(bn z#Wo)W(Sb`*+5v*AVfU}QmBcj%9p~ka*zo~OQx$z?qZLBx z4^K1)u+~h6QGFpgTj`ouLiu1}=f2!M zgbY3f!onvc>TtR>g-_}2s=es@g{G4t?$SNrW|8(6lPjuHLJ!>_wbFlY+L7?{hfZYfsIW(zlC zbofH}qFD>8GW$66{qzt7wk{@O17-f5PX@Q-3r_5@!;PKoexGx*dY*>K*yTYeY)=bHaZ6Vg}o-(I8p=g;=QHahx5 zC;Q(Wb3e$%oBeNR|GAHn<-OFw9!2A9aXN=*twEAjuw?#Y&s=j0!r37GTSI&R`Hy#7 z#O$q6x{-fR*{`GXpc&hOv484a@3?OI|2?oo$W4NC(Camu`9&0GAV5OYm$g08*ncy1 zZMdfIdi2a_Rm1J@j>)>5$?*xBW+{hrkKBR3VgT&2m*e!YyTiLETlvdHb z0ii(R{yjAM`xcyrW&5P>{a+hL$_P>Tqtq2npEc%bWeS88iUt)h7N2jXRnUuT+Ulu3 zQw;aEudsGoU~@?*N6|-+=8o&JdX`0KLGo^#Of^8XF+`22!WstLWlJH9bkSKTb5rrz zpFck$;aX2gO>^g?^xKHyYbD`=f;hf1DqYp`wYNRn^Vw`8Y09sWRV#jIb?AO-A3FEr zuV9Mu!RPfa;bIz-kG7seP`O-beX>nb&{Wsj|d9!b2G>oMD=;M_Mm5t;y z%|V5n-3ot4wSP=v;d<#)q~3PGBYLL8H5_6pa-{8CioAjC7);)VTxN`#M@sv8(wsQ) zPGiPb$<{0{MXgAWHCaMdp0Z-3qtYCAp|6S0&^ce!SH~KKr<!31b0S_ z9wa%`-3Ndz;5@cB5s+4YY$hisj~9|#of^7y2;9d=dH}x@Fs7*TD!G}3VWq3 z3^u#qib-=eg-`G3ja}X`uZucNtG69pm+xT~S(q{e`aD%r`cg~s*rtM@A}6a1u6t6I z9C@@7k&$_Nu*5z2ozy>qK@i*5{n>W^<4&QHCDR@uQAF)Xc`nRR+K!9R?z7&Q_Txw| z;7xrdugJuZY6>B#sMnzj1l4E+;%p|A5GnE+`6`)Zo=%7;-^dL_g~8aqy>%V;6Xrf& zYQB5y9@fn@(;Kk_$V^7IuK-y<_l3RJ5W=y0c^NkFD!fb(K-kWKjoZ&N2i~)`LwMi7cqz{E);HjpC{8tpb-~?Y=CTcluMz`Mu2(V)dTIl- z^S&ZrWbhljuvJ^vsA`Mm9);aXxz&E|TC2=*p_N*{BzNTT8o|J38HO36qKM#OXL$}# z*U3w{vPMzcx88Opg-`F@TzxE;?zGW2;3}9zx9dCV{bKd=KI=;-7#$==KlO1}(zqu| zej=qmBrtHenmiA^<`iw zv0b+XiQ_C@>%FCBwdVc_+8t#_fC#@fW!>)Mx1t!rl>ka^I*tkF^4Qcfvu%|J5V%JI zr&J4B3;UAWo^0v(^A;4gm_;Zz(lP>j_mqhyQ<`wVAvg-c;=v}==O&T~j}XOK*y4)4 zX%;U9HM_AQ%(PXpgjtzjh}1<+(wC! z{fb|6xm#d|!NBC;<<>w1zwIGG?H3Oo1`HC`#2u&_A<1iSI?m)9nQQ-SuQzTuyQl;@=xI{e6H^q*H22~m#ivy_r}4o|3sv}GdnHh608|boLgnC-^?%T=s&T)*#oRO z7K=8p9;wGnqGQuPW8R;~iZaCRi@J9Ba&OGj;wF@Y;KNNzGDVu=zGJQP{5`1??cge~ z<^S)XdGO%Z+#N{7WJQ5b2JC+%qkjkH%AfyDD*O%mxu9l@4PafN3$Pdy>gK?akVKZ3 zHMW-<^$mFGTq=0#g#HVb_XW(mVlo}XsG~hpo*@m+qF=IwUTZ{Hr@xQw%|1Y9OZnmx z+Sc~?8A?I@nIHd|D>RQX{+oLFlkkx}C(rWRgs#eHzU$>T)rt~c;~XxN0M4=JLAE?2 zsvrRm9KM&SD-W%Y#zH5aVV$g{oc(`%O@xG?8a)|OOzY1$_7^agsFm5nrS81Bbs74k zKooU-f#B}rA8Q!%PetJRhYroG1Yd4!-j!w2RH1Tzpli$H%wt%rJuB4 zoWsvl=iea`Sib#KXe}@maQOAFYw7zKW&jrqI!SR)WV3A9jc~0I2Z0<j^l+_syMV{>Swd#{}~YnOP7d;AE1PaI?UBLmXZxa*iZ{gZjU z8@p-9UxlKu0Hc?$phEmmSLc5hyT8Xt^Vy=ig;@%DkYb_<`loFD)5QOwcsnIc z9Xj01&eC5h!QB5{2>*jJ)LN%Rg%R(1s&&XjnG0qtmhSFpdk>6CV%W|+Ss52U(0^Z% zt^t!scLmV`S5qyDU~69(B*^|bkO05JvXUw=ga<|OFTKUzvcqA4JuSLe)=}VMMCkXp zHJW{G<>COyLgn&26ZI`&R?bNcj=l#ExDk4Ttfp&8>YH(|5EJ&{9JED*_h3%xgjf5VhoQUC(cvQaf289!dcDqc|C!hR?mSk=eO%JxZ>LnN zma#hI>+4JZqdot*x2mI>KKX06!8H5WtqxwJ*(aQ>quYrot0PZ9U4wFCQqXGSD?Za{ zfHTH4F#Wo!n9uIY3p3CS(3W_@ky81-1v{ryYZR7fR!gLTlbqKZXQhuqwK+%&MGB3*8}@UsFC5`L|gAd*UnNA%|MZUix6ts)yj>>BLmrG zwjG@-y>+2Wp>NAkk!eor(f+GV^Lur!w*L}faR;Y)8pN8(Z~Bvk^H;@!YokL!8jI;x z>@uXg4uTS|3%-*Po@s`0itT(SW)O08m>~?eAA&qeQC4v;g#|_`BCFLXv}J#$D)i`Y z2qFh@H)qO7nuJ^by>mdeWB*^YmGAwIE@R;=+|=|)MxiUKW8?`O`pJbQM7`;(gOk?L4&?v` zu19-58z{f!`QBVxilU(uK~4b!J5 zdZsqaSq-@kLum5aVZJD7^x>9QpY--6Y$PbC?{i_K08}_UK$0zdN^RW7@dHnRrzPm2 zVQCF35NeohIP#_(oTjCyzgn3ei0F|S!oZs>HaQSk>dt2?9-w$;xCBM4oJJ!^pwgm|XL;@i4Q>Huq!Bf-y$P;aC}s^b>7? zNgB>r!Qg~Ru1s_XhJSJ*b8@3D0bbe!yB8lxgsjcqzPmz`4AlK}4LCv-w4?BO=~Pz9 z#eSS2sGi9z}?r^F<-}Z+%9)wIX*VD+t12 z4zJA_FeT%ZtS)CUt(2p+C7*ns6ds3epfsdX`P%UYMxBf20?~FGng8ZWyU4=)7x4eX z!c#{<18IHdbbJ>WGAv`b^8 z>sWVlPEs>{|6>kY!gZz>I>aN&X2A=I!Ad##(V+b-8_A5Fkw9%uM%3mg{paOzmmsv8k8JO7 z(5H_o=_-kU85xvQ`^6;*^N%|6V{k_LPkhp!`&EY@F!YvsF5St2XVcyK&&HJG{Tew@ zb&EjiW?LWG8n~AiyxoI@-|d$0ay~ZEEIcAU+OkC1>!Lzh^%`){?5W-=!} z9@<%_zj}q|TdYL)18JH;OV4d;`bc?0e|60aMFl^jS^mR}Se}{_IADLWIag=6XmskL_z7rgvh*nxAUXa}Xu+Cag8 z)|&QrQ^oYD+`0u`%DlZCC9`;FB`62qQLad1_CvejJE5*0sq|P1RUVtyO@R+H`c-G! zz}r(oXs7G6`=Ym|9Qt;nH;fHj?QYQMrxbha`nd9$1o-fWwoK!l6jH?9z8Psf4*66C z-G!Kkel_?Z%7y_YZoYvdk)#tDz{fyiAbh^E!;R$0Vm6NKcWY>un*>!;p~W&M%EZR9 zd{~L{cyBE(2N_loJDU_Tw#08e*mHf}=Xkfc7VnS&2&g~Mn1{@w1&7uBt3|Q-Dk0XL2e@kXQ%Uy z!%J7SSg6cKa2XvRCDZ@TZzA9(Txy*VR3P;dCX{AtH7MHuq-2t{q#0terCr+;zJmSg zvoXw*4+Z{9TZSIrN@S&8 z^O)2wsj{$t##E;Vb6wJgKYCPE(&gEO(b8}>3Qd8O9elp#ZNu?d!y?p$dz@4m5D38> ziSk#cnG6C0&d=0SLe_-R$!2nWN(m;?y6Hp_twzb_^9w5u@uH1~_-#=(lvm)Ym4%BK zpTV5T6(fC!SIblWH_Q3kzA%oQ>-|lgtx11ny5w(3+)4OX`7p6f;C@&`(Vbrs+nH}U@kU~clKX+f-^a^W zQ=xKg8wom0zDP*n6q6S9Q#=h?U`gnN>(!a@{hJ)rzTdlGZ$3`aU_>vjJmU|dbti0V zK&}!X`4OA`{19K)`_0~fRnsjWx1RmnoNDy|sRF3(gyj4H5#wChwiqiP5(5_0@0YVN z!&lqdb{;vd7CT`Xl-uYieY!A3n04o=#gJAWRC-3>=K}j>cAZ8~SMtjk6j;O#eI>$W zM{%h5at5N>mt2=5o{fHQ!lGe?;|uId6Jd#e$slLDWq)Z?M@6<*Y;`&cr@qDl=aT$A znD}4bwussHU%Y}p%!$sMEAFS8+j!BHKSIwxbl|~%@cNq!DUAkIr4Y{cH?lEmS95(E zrxH1LoGzS2e@S(Fs~t0I|04_iCxHH56u8u>PGudqdW1T4GvBFcZyI2b?yKB@&(U1; zbAsR(6Ci!QQ8n|H7V_jHuHgyhx+y-1hvafZDNRL!ZpI$|+K;NpByoLfr zA35vsajknn0+y)OB6({)T<2LjKvkd-Uv0QkB!y9*LmwjF+T7;zmnTL~I=tIC)Hk7e z>$D(|qyAg@y28kWoL*f191s6dsr|TrlX?FG>lO+wb8-J00RNL`Q>kS6xDu^|m!oK- zjT`;QnRXia2B?)f+Yqb1aq9@6Z5hoVlL6IrqFP9+{g4c^y(c}mY-$Gej@y4#>xl7Rg&h_A&3f~YzqKaow->UGX%3dC(nElZo|jidQNRB%KfD;YcAXf^RF}3l${7(f2VOd23EoyQ^QDVd1}WcJf`m@SIu%;{Kd4Sw6KUl zpWB}zcetA0Z0RJrzyw!h+LhSJ;U{)|=B4-GvecX3u)Yu{74X_td6H1M&zWzfG(R%T z^Bw6764A%K>~`x3tO%DFW+TL>!U?QO!iVHj4GnNC$@ITx>gC$31~T{Z1qU?OZD#n# z@4=Lq?*x&N#wZZDc~?ARt2S|nZU{r|MGcTx?Jodrn0B2jsxypp+_*Qg$50MV`4&`%;@)VuR) z;)}YYr+nc^0`e^}LD63AY~jRRO_Ap#VKc1YVY8=7&E)2@rfmXvT4m?`>BlPxh|`n+^y>GgupAuIoi`>#g+OmpWeHoon~e&)&t2T9<7w8{mGzgLD_z1-w>9!M z38}_FqOXy@l(ETtpLv=*w#>i4p7NE#>k+*V_^yf0dfm#XZaBC=5ryYxk&D;{3Y`;( z>jY`?4s<=I->X=^YHpeEi49UM1~hKzzAh29Aljpa?2HV52(OjFOerqD4rJd_#PsQW zwK3Eq+fO#~J$(4me1TFd*WI!SI>a46F5)73R7GS+@C}HoJGxb@Kj*+9!%QeQjIF(a zjrg=kJF0_A%S=&8z$pRj`FDSNRmg6IUc0^_(o(-o+F-%ON(_EJ+YTrQEa@X$}woC*Esbv^e4~YQOYkKvZA0&hMnfJrbQ3syfd!bC zxlc@Q?Skq`#1*Kq(g2Z2Z!6zzB!i3VO2Hu)_Zf~MJy)NoPOJeY4h*~TGp~~xDlHf! zE=$whD)99z9AfRh#Sz4)2Y18CZYVAv2%F+%1GXII9oUM4dcHqDF+zUp4dF58OMz9F zL84d1ghI`h{w*(k`HzP>#TVA(luJ(?8LWWhPBcMSPtXZpDmDhM5|K@1TkuFn@!`5C zi?x}DL?ld^>!~bI6(o9>qU_1-uNmir*>cyz_J;||w*7KqXcu1{^&49o@+O^gliG_U z8om9V^g(fIaBuS0Ni#`7&pk(yx_8u6MtNMX0t0v2Oi@rKVZNX1aN$mkj-TDXM3k@u zLvP!1D59zWcf%Ofs>MiK%uHB3e}_M+NX;q!>>xyejy)S-lvEki0%MnFMQ@u{A&r_f z5JF82^I|RwUy1E6l=50~lahFk)Zioq_zSA|QyzbBz(psaR?H9RJ<#7@JFwmNJrgp1 zbL2+Y_(GEFA>WNK*+fv>%=n7_h-!r>z&Gc}lFGrLkcBTZ3WAhSeeXa$s(1?Kp9iXy zvq%{iusL@&0@rt0sexxNKCPo(Dr^p|+N~Sc16?0>gXz ziImwHch;*IdpY$zwr|lt91o;tEBad}7%WH^m)k@QmC<|?zgB98+ud1Q|ON%us$UL$|$(PGxZ({&FsQ&cXcJo&99-w=WZ#ky%Ko++yUk6{38h?#=J6 zzPCnZnH=^#o!utna|_%yZj}V}dL%vX7w?S2n#WC|5=AQ+dI?wqGkHihG?a zrG~Ry2o|{qlVT$uVq9-Pk^!r@qxH@BTRO>zVJBZCwOv)%uT$_CO8|b00RTfrG>1`f za|XJ4a3O?J3>KY}2Ae5h==z*a*i1fT4s$x1S8d)!bXF;|d!!WWPG<%eondV@I=T+2 z{oWmizHpCj9Ks>ys%Z)JAb4xyK$f8GC`9+k=Yb=bAUqT>ogdU5DUyzh!wc`#q^{4^ zN7}#&s-f+Wd)GvJkR}v2_v~XlV7~?{P3J9?NiI-CF+Ti#`Rh5%5V#{u8i1$+ML?#2 z8rialXK$ZBp5?nniL%Ch<2vPa6iX&}@n#ILduQbn?=*@8bjsnsn9vP?zK850yL8yL z*w{zyoqkOu_JP?8p(iL&-9YHm5MXv?^Im&bt1}fg7$(|UF#SMjS!0@?;om}DP+tx% z&}de%!+@;*2Cfdsi&d%Vw~IXs@ROIF?1_6!y&yR^W3d^DZTb@RsIYC(LENb;hQJm$ zsh{rhE#H zbeO-Xa@p_Q@XO6|jW4+^{H=;up>#Uy6pW|2Xpe$wZv5CQVL$8LH^iXf!EReIn;A3v zO%F@57v`RHscL5OT$MH4V4`t_5!MOTzX>#qmlP6>e0OoR3JLbs%pkZL3W9e%$$+xO ze{ixk4G)^hr0r-Cb~ z>~u?WXOahQ=Wa>a@rW8Dg^cCH)<6Upn7$+^4nFAi6-!8UW+inzNT7?faFX|22H%V#e-(oYdEUmLN*`!^UendJc(D(nA=h|9 zi9k>Qy1>|5E&F3qES_B_eZBW{?1PkJa8Qn=&w2!Xxafnj$|hqT>=eVaZPthpq@V_y z2cqBt92xX&a{5N1U}41m?k`vOIDl0f{`0NxzKLK$4C+YMj}(bfQ#mp*Jq4gX(r^aj zaP9iF(m0cNdz%v*HKjPk=123*pXe8!T?fC08KbT%_~BqPAzTZ#Y}u_7dLMA`@fkDv zn_P^9=ywY~X-J@gXT+y#14);N5jtL=#WtzJ4Y4x?aCJd!$+WZQYgO zS%}grMjJk(?5?3zO<~}z3g=v`vEi4b=r90{Y5GCg#ZBv z2xBi>>uKR=s-K9SJO~5&BtZsk!00u#3uKu()tDg5=US{{H+12~DdSfT(CNa8T=C-4 z=XFvhlW)0D3W1lk0B6t`mC^B6qNJ2^|=m_e6VGJ$(=PKMAC1PW461sVbN3!`+gNBs#U(2F$niRGVnHtI_)zqlNQ z994f}ATOllKV03n^Ed zUkKTiE;lx_Zx4(*kQN3gV+URDU*LX4mm5gfz|nfu6D>)#4#%;)h<-iBAM-Ry9PYP{ zy`4qUzT8a;nIgi8>Cx}Gg6=Lh*8UX&5sZmX;0bzw|DO_iNIvi zS?{^*+|o3gs1@PeYmMt#E`h%l(C-%lu%&YD4)K-nbTXzx-#+F&Q-;SQF_mYJAgj~$ zyic(-O&!o80NTdfUER0bA#=Z{B=^4MED-DrJzVkYUchJoc_X+@&Ao5Bj;#!G|7&g0 zTmg{ag1F?yB-!>kRJcg);>e3b`+QTVJaU+{V@E5HSS`3T37$?sV_0g#_boi@9T7i& zh_7SIU%w^6ns}ym-ad*=hH-fdW*oE)0b&B{;(sDkcg6^)Q0rG<`Hp4F^k^F@{K{F7 z;3nM4ZgZgYbqH4Bz6EP(4_>sVLiUo75C9S|*BNI2H+L(h`85dIGw+lxBRHR&cmGMg;C$;fAi0<)xVJ~#q z7x}D`_MNBaZ+H-j~>5A@TI!}3*dDq+Q(4`d5Vr8>~MX78;Tb-1U1r4 zdf#hum_vc!aI}o_YVWyB7KR{lcLR*1 zs7Y~jw$V^G>DNPXBDPRZQ(|rf5N&Ib5M%`-{wyQ#{O*N|l*F!XWf6 zTtbXWrzt4oMQ_rvi0m6rUzfT3fDWR~^ZsWh9r_KqeMlo*bO`lXX_`nb4xj6+)0cBh zS=C|2M5xH=!Yq zc%_uOSXgfB5zn+)Dx!Or)xQ%(JJ+7G1z5N9EI*OI-7`$4vJ>ZKeIUz|*Ae-3hHNtj z`hj@J4kjKGH1ADM$vL5BvRcCrP1WyC+I>9Rxt@A7-uvqJo`?T9)nif?O)-W^#|(HU zo3ybGUj+GlFLl|je4J5TaAL-ZR8yHqjNMlZlUYRx2)EtSW_^G~8H(~VgX`fV*wK*5 znH7n_x5G$bf|Y42df^Ov%vUaXOBAmGMn()&u7jx6@4}>jp;UT|Pi(L>k{bez`&9({ zyn3Dmh2{6ju|WxJRKUrU4_Vk764lwa8R?`yf|&+B zq7UUTSXovuxX5YzD#75o_^@{3Ln=)#Kd25Qk2HMY%De7MYAtKXHIwZYc7<0_BJq+g zR4KR2off&s-$6LJt;o3OR+yv#Ty09gqkH?k_S_KGUT+e5_fSR61L?5^c{iJxsq)Ds zMn2-=HLfVIP}B*D-+mL^dKw9u0is%mwaBrqI1iT=G{a?;Yl%=6AklGjM4y)Tjailt z^ZkVS@^l1z{2LpbzX0$lAa--wlT-hv{g;{nh3_lTP)$FM4-D;9rcQ9Y5{sj%Km@{v z#$U=toYIfp)Ju>5e_ zjCwm)49i+*#(RM7*aRc!mm^n=f3;$OC*cX-2+rQ65T9^={#O32woXSwrIc%1I0C83 zV1Ilh^S;%m75V<XPrl-&)=sUS+c7$6mNHxTx3C4cWSCit=AR94ETamtX2uP}(%nb=K(9_=YODEUo!W z3Xwz)yR&92$MFzERd-R7?g^*)6r_4Fr*bkvVxKoRwzDeWUK#9(e@(7Vqn!XRacje) z%mD#V(k0Dx&vkSW6IbcIBff~}u33SkkfP1K7Mh5eYH;3#}Ji+l*mqRkBsebM5O9{JrQY$WmM)Z$$>)QK%1%1Ph`a#PFUrog9U(C3)e`uk1j!nuV??hkcf~=h7C(p^GM%AXVxfRM z;$75je@n_DJjO`?Qy^&mnV6kSVv2>=_8phFK)x3BC4ARJL&?EAgc8N{xLQcS_Xnw1 z4Mt$DlQ<2<{=LTfr3yY`zfJy$kdGrjCpPD{l;ahyXfeTHzH_jFDo82F6M2!(8G|$& z6dd>7{h$ZOedIkk(@1TGRq669tj`aTiU;`yXiT9U;r_g(`eyB~u|^xWUty1e3SD_- zf;X;3PmIl+_~9)>_ZH(dcQ@*uG4RjN@D7*08Hv4o+?Ol-7-K(=!`2NVK*1Z&@Y((m zZmm`!RZ7!Qn4FXny@{)veaYO9Y}3eM6RSdUc?lCRm~-BLPLbR$153?czHW&CyNP%& z`QA*(!lynsq$H1?*=Gr>H{v&Mgsm!&$F{dsf{v5ziV|n~e8&F4RsED)5o5$&j5Q4C zW|a0aD(V%17nJlQ?W>Bauk{h+l9Ra5OPL5=2+9ZIP9U{b6V3UVCQU=wMy)7}yoy%u zKa|+3{0r>5rNr0%kdMDD*S!#1U?@43>s8mHAP0OrMgfPIuS`21no@W{fSzdvN1~wZ zRlXMmK~FqB5)k5r357s?T})Y#b$ID5cK)o-?A$!OupT|8GRpQbz9(kMUOFFqMhB(u zvZKOtYzu-*42Rlw;b%V)i;@A=093_5Q;Y&$d>MWYXXq92I#;Vi3Kv2MvE3N>`Mn*_ zSsX!?%uXstfDt*_P5|P{Xe5(X4*$HZml)oP0|~^tlCRQct2|94@ay(ShC(a?E%#7GVF}Dxszah_L_# z42Vrv&{CE86r5^Fq}xFldd~WnW_F~WUzrG=JGAL;h7vu%rzB)8?)#vNNkTXWUd|bg zNZku0T4y*Q!9wAF7hC#80izII2q*Fd+Wwg&J)w(rcW}%7xBGBa#m$#Q12JQiPV&?c zpy@Gv6ia8g3uJ{ab#-XB-4K61E#Ypj7m=u`_X8^ANUBuSMVrW0fNiwm{fz%HE!Z#! zxvnOry5UDMot^q9U+R{*r07R_yaz*o+jW(Xm_DSzh)(fcsWntkbJDCW6A9HTL`|m~ zubi1CN|C5HJ=9TP zNJN!w_*_~_V3Pb>)f)_3621~Ir^ud@8w<;i7z!`Q=~!s1L;V}%xzOX?Ge}=Y(9AE> z+4a3E%bQuWQkpPmAffwT&1F4S5Dy=Du;!wU2?)fh&~l5&4W6`&ZKZqjn<{la%ClCN zH&z>!-quSmvL8@~aezN+o(q&3td7`lALq(22i!+TRCKL?D3F88w@G(+q9DHC`la}` zPPUkporKE~>U(-!#0T(_T$pIRAVOBN;#X_~yGE6@^lzfP1%70q1%o-R;UkvGC`MlB z)+HixXwHb<nQS3?=4{j@*k*?| z%&>(T5V$bdZ$a_f!ti@+w0r@_W_vG36-{RHF7AG*ytTpD_!b0JFSuPB3}F_DtGo=| zhCAjU7Q5;_{e*FXk+VHG3PG=zyHfiQ!VF}%kx5C|B5dc%0b4j2R{c#Eu|Mj$sG`%c zaFL%oGpS_jVQU@Kuu)BNA*Pl7ksSgwi)wiRyUIl?m;vTH2ZBV&ZVeUCN3DJ&9yrtb z4ky~6O~!oX5;Cv28;mR+Hj;R8I8`1?$`t(;kS9~LqtnD14rt#`-6wMlInT-@N^2Kah(ziMpu^{(A$@D_n7gdBSWcj=rP+8>56)>N+ zAhjm$XSy;Cal0iar_x{)dG}6u{$+UZV{*CX=Mw>l9>2MU#i3TgIX=4T4nL%CTWt`J zG9BjN_Brs;Mn&2vjXs$eGTO4>t8g475(c_~>bTbfFZjWAJ~Fb7RNw<}D%J_-=!iJ| z7;d>D`J>U338nY3=`&#;QJo$7GHGfsFi_Eh;gGejaAxhOq+dHfLb4S|(GHb+Cp+PX zO_r5;^bST*K7v|K{i(0zPiZuckR7mg7#SAh*o|lTl3%`#DZT!4wy-Ou$o*KoZ2gU6 z8DDc7=i%#F#E$n3-6QiFUZbE?Xx=LGHo(_-v_M32XV#MBX)qVE+FfAL)js>ij#c4hrLn zO#iOfiH8^1gO2M)C;B&k?QJ$?rTWyP{T<_V_aie|A1a$C^UJ7RWteV=>YW<14Q<66 zSP0a{pDT<3H-O;FmwPwUbuIQ~vD}nfwr~uB+xW-J4?}D``HMUNe}YU8Ig@j!RvyGTWa6PjE`Ejwma zko56iRNqvnL9#&X=?BceVx0Qh$5g?qRa{|U>M8%!$hlZ#bAcr?x|tILsN#V zDl}`D%vC6`gJ(xif>y-sI<{WJM%DGTIC|CWS|WIvt6uJ-ALff%QV!Hk4DPAqYKFo= zwq0K5GuNAR6qbP*|{ffu?<=Wz%og7>st&6Ipd7X{5oXhSf zaS_;H#%4km<7n}U8abIhV>0b>pwas}&hSgeigX^YIWUruqgRI)>AHUwtFvhx^5wbF zK$IK=e}=GHJ)d`WyMj~hF?n&Tu!}uC5;WvSukf-=64BY9Tw8YmysDh*w#}J+r|*2s z75;HQxa;=H?YH3%PuP-Xi;ZiDwsdO;b(4*W{_eT~ZXUo@tNL82_p|Hkxx8~?Aip{( z+_Ns4N#={56p0ELETt(#INv5>PYc|Pei-!BAazd0chhqzD=J1 zC!vR#s~^FWYb6gxtt+~ysO|%twxeb$+vJ5{cB;2H9Rc_5SMwf!Ef!aiU44F7m})qojtlF>qvkPJ18D||g@|oV$J@6= zYg@;dJ&)xR+L+P~Jm8(;;Af!r2jn?Cpj@UARxaQW2_CEW$h?jYx*1zOhXeiO z2q49&n$PG&YXaxvdEWx!;Sa!{iF@}4a6E|Onj8>ku?`;fRpS)AX;@*t$KfyUhe1^^ zW%}QhK$o*{>6f#^6i$Tte>2u6K(C^zmhUmm?-I2^l?nf59~(6s?ustWaE(L=RbMNn zJB+`Rj*leV48bI)Q-eJ^u#ad}X3hotE%fmXFYvts_pQi#-R915%XggLhp@|*aW1?O zDG;`PDn{tA=UD2_B#@T0Gn6qn)_f3G3M-?%qDADn;U2PIKgzG_azPCO9}XK-ISBrJ z#s8_pJ^9NiX3^}O+?ikHqVyUXKk`dxoex{K2V}hPP;Gnf3bNwh!;kfu;_;bouxQjM zNScC*xcpK$C|<&8)fns(g&wAwbu#sL)QG5R6rtq;3->1^kzdAy-Pw=?MBP9(*HE#a zn>$V41d8kRErc{E%cRA|uep$aK?K_n0U`edOMP+u~ zPv6LHYr~L;8u+AEk@6}DgJTnc4bVnJm zIC9k4j)ehDXL(;z58A2b$`pYr->(D|wtnz;q5Di!En&V<-*~61lq9O+fru~7(S=&w zjHTBSo(;fL%y3-C1J<31fr4QQ=r1CqJBZK~>yy%xN1vUtg zNuCbh$Rrtu(l}X))gkojoL`cCdxY=$jzooV7GQU4vc+tO6H;YjLpi)@nyZJ>0047K zrk;@M^JHrjiCPJO;Y6Bwt0XWkz<~lFQR@+1Y;vl5|Bp1`F88w##GFYeAJYj%n9}}& zjNFSFKFHL7uyHpUjuVNY=9}#z%Ln*ozT9vS0A!?O$&Taub>X3%S1H!cS88E9S18H) zZB>G}O`mLZX1JV@+w8UkXel-mdYl&#_F_XK24uI)ih~qaLVdR*JX5$>=rems)g-^^{=41hL?1Vm%-v@~6s6@e$RG#*@n9!Z&3$VR9J{2|gLWR+ zzdJpG*Wva3rJ@>zTWJrLFrp(I#*i}5mz(It2Lqs7sbLApJj^OO`V!h2PH1P4odP`zTn4UA=y*|#ZNKpnzwHuD_9F|4sS;tB`(BW~y}9yiVSA3?<**awvfD6=M&w-M~< zI$Xe$>n&F(0nf>$FK=pse6CHj^8?FY<*j6K`Emo3jJk-Fj%jc&i^+Ld+plxv_wT->@AG)$$JeH<8*UqSc6m4*}`mB%X$fw?UBv09VJJH6UDmQv6$` z3Tb2ADrxISvv6u%c0*id)%9g9y66FEy!nwS?!c1g^m_v!T(UU#fFk%WH;^f4(AVs8 z5M7}B!{Al>W{jTA1Px*PBGRl1Ygx5F0LeA^j&W`RY+a59BPL>ey~|q}soGvAYaxODcFtEdwk2U_+E+MBx}Jn-0 z?rfFtMf)5=gk?gUbheV!0K*yMffkRSV`;~evK8stnB9DYOdDoKl!pPhGCXXmas+`8 zZf)d@B58lu0eV9WR`A~UPG7?-TS6rzCW`+DD>$KI4c~(>vIZ6veesR|Iy=+ye7x68 zE3sh`5Szn^XM_JMflUn^RFYR*%Cb)ajgun<(kePMOQm6kgNx=PCLD82x3;RmXIL6p zwW(VNP!+jsq3bB zr|Qi}l@)lwJ>K;U$+@mAjFPyJ`kXW4z|4l5xBv1FEEd%L$|0Wqg1%jLOZdCV`$j|h zF{#;|kzTFe1mkPEqNfvpaL)u1^5ur>1LsJiiEtq7NsrTDQsg0c3*Ug~3eZPv+Mj9n z2s(DB<@?-$%=jZ+@&t@Ej)biGB^-RtA7!u@(0P)9~j>2{5eYP?ZuL3MwT*8 z*(ItBxIX#li|f_7<5Z!Ufxl*|VUF)X+fO*l&YdUwqEPnPWc)Xx=2HZJ#!04_Kgoaz zsxFz8`A>4-I8pW#-ieI+5!i}I&!9Hr)~Wy)n)-k%uL}QpNxncy z(wpgdwM(vx-@fYZcjMQI(`&?h)B;4RNLg2e^oq+}v6SKpB1K=yk0iO_2T4uSXE7Qa z`ic#IcuuU&SThV)bZ-MEyCWE7Y_F0Lvt#n8_IrQhV0e6K6mQa!KvaTu@k9A?n!1oAr`-WBm(RmbUbC zLjjoqm}RSTemsE??lxhD1|moN`Id3RXY(br`esPjm`{S5UhVJ1DJd-C*EkPKMF;}WCUpLG*`C)pBN zwKq$8_NJSEg9`I|c}LP?vIHb8k}%vr`U~vN-o~+P8k_p?M)+V{2yuD%1TgvxkyNn%nUDM^UGvJNM^N4%*IX6 zI#mxYzNx_MF8e$d*ZI0c(l*vWcJc)0d_0^&Hmbz-k9x-M3tnAmzCJbu_K9b%9U^2B zX@pN5^*b)|1OchG`1B#6H zuR^W)QH}Q+56Dz_ITl(YOUugd+PogF&cPVib5!B$;nIAD5R5^|H5N(s?c z?Anw07Q6!$(`hgCjSCTp+POkHeqfkXb`*8lV)q^_x86=SFun*ge5UJik*xM_jH)60 z7UP|=M>5N6NZ7Zkx0&D>of8}ma}cAR2CLc&#B_CQ7y>>GQ#YyzRkBYydB60ofrQJb zH$OVTkta?8Udi}Qd$rAf0ofEoe`P8@AvRj>iu zq_(W6Ku-n?dE{9Mk|bSc1l7dK^g-r$=v@!vS@maKzUJgRY5w*D6I$FyrS^W_6_u*r zn+0Abttr2$ucgBv-e)?AvgSoII)vhX2Ev=M+=hX6N-4JdKeSN`a9$}9`gsB_rM@~u z)Pm3huW`1u^Lpy`f^3!jdUe2STF8%O`99|8MIm#^WF;BwV|Zf&*-LY^K5!UOBYdqB>S9@Z=VA z4Sy%%$hcC6MO2E%PJe_4`PXEn)G<9_%$PV~m97~PJ9)oIj)%HX*V$?vr_uXHD7HOWo^vEpSSh=!-6+zB`+BjJKw|JTl;NmZ4FX=57^Fwv(N3B-5i&XaA%zF^$GupfEYJz)7(USOtn-czR~19a zwh7N4Dc=0bawWzm)C>t^&zgl_yz)a%CIy!wLQf0j&GFep`eSA@x0q6=Cy^$0M)*1g zE|iCRDr#JiN1)D!!9Y%C}yaw(%LTZi@^rY{dNEYy}`@YFFGeBp=Ui? z9kuX;zy-SkRT9}LFAwYivH&I`tGT@aBB{uNqNv&sK-_SpP(NIK_PFmEShBQF?eJe- zP?{~^b;muO^hH{=^OvBf<2IX#8z}4LASzzA`t*JF{7orLB@$+3a_qZ7b^FYw|B!P7 z=c?@iU*OXEjLQ?{dCY6#yMnH&==Wp0N-B2DL-|*+uxJNhxxmhWX^y&N1}r9aSdYU@~vs6VkN; z99j~D`4o9J$cwxAQ9J6MHJa`eXy38+PY34BXym~yIf?*mS0DP&g7;tye~xB!4UagM zuSB^?ki^3IO(5*8XHD$FlT_s^{Obv~8X)MP)6i%j1dvKv2y5K>alHA7*i(mK7uM6b zd93CqYJW;XBnM&V*J%ugUwzmnaGP#0wTDrBEYQLXadlLTb5LFP+?77F((aX&C}D^V z83#_S*4|dVK_ajT9R`BfXeKnsagktmLWMnVJFp1ux&D4=Lkf3H(vF4bo{#Uv~BO?Es2gS#S_-3lf*S;;_rQs=?m zNttOvnS5-b{!Y&3000+71fH z{@g4=EEpNqFzX0I1QgB_?ds@X*#cp#phgTgIM2VO$(A54ZkvO5kY*29!s#(G5WLu0 zDCJHP7L`0M0iHi-fbaV0F}jJGf-#H^^LyE+c+f71IyFc&8$|?#qFt}`$mMRJ=Tl$;^+g0qfz3h4 zu9B}FG2b75c5Rf?rdxY@KDWS%6IYq7(*$j2Bl13F*@at-vIGQ_TUTuFu6VjqI-!r^NW)SkMCpnD;j10ozvK3i{5EXqBXiQzWS1#NF|&W`qi#A*-Dq zrceT&BF)pYULX^U>}a#Ij)C#v29Qwl9G}55G3Xcf@?c-6+QD6^MiVdFDjj+)vXtN# zFWhtQ4P~tg$dNLaJa5;5U=f|mo7~VKBB`H>VKK-Koq*=>OUHT}s8Ay`n`~<{rbBv| zZW{VJ+P=pecXGnVQUz70i0CPuPlF53%L9q>Zo#q&J&3Z1b8wrK{`La1uK|x*7B~m) zKP^rS9>cl%rb~ai6+I9k;}|7^17FXvnU5Axp|8nTFy5%b`e^)I;h$pRujv_e1@3u2 zTi=L|ygaSjw|-3iI#Ju{-w6OV+gMQ5x6EWdc+6;nb?PMo&m42LmJT3xo}~rd9B0HiLB4B6g@?0MGeZ zqvz?*XC~9?2sL6-ynVAS3Ih;2A1fLiKLJF{uk&Rc1%*$jY-8)|JX@%Vcdkbd-wb34 z{AV_0c!L6MZW338qY1#(i1Z&N8K3Iq)TI%98Hvqb*Ro^)Br*z$rpLp!!AjdhS4&aI z?x5`5hk+a~-1BM!ej}ZT;?`-}l#z@93mH!F$h|vUwvQGSg=w9 zDo659!)<}&E_Vj>m++#=qUQM_5;8?H9C8BbGjYc*9-hL|oTI<07Rm!DdM($^Q;d~j zD}d90TyN*sCd=T4AP;D@i5G%@L=&f-M1UnR?KFN$)9^uk9j!d-d)SOeykyzA=@Ni> zse}XN)S_~Ge28d##8&(W2`S3ne%d`-Unq^1Fmh=Wt`A+GieVesU!ge(o{RHrhfw6( zVNj5L9nU+-U7xvOE9p-K!SZdQ-GbQ7$kb!h30k)}5pLp+gr@jkdi`y--TpXn=!=y*Y(-WqIhwXUeL%TM%GK~=3<&nM|x?en^fQ%RN{gP!A9*2`O8X0u~n{R zR15j@7#{QR2>%qRAvUt=v!Rpd73%OOy{QbPjTIUg!1+G3P2bIGtzE~WKug3} zNhrK93f;X~b;c|+25jEVPJJQSS=jRDG-64Jsci#)s;Zt$)}{NgSPaUc0R1Ok(5~mj zXKw_nc&6h4gZI5PMYnT1^WZ>V zkIh4o2nP~rO|qL23r&Ciu%}&L2k!}wNWQ%%y6&Gl?l{U%s)W>{TYSlUDpS3nnp!w5 z9Jxx^8xE=<0KHO3pGvke-H~d$|Rm5;a}ZtO15bNDQcmq z`)_CdHLe+tF}d`SG|_6)`N=H>2Tz!+iXJD*%W%6D!oZskUVjfd#c_YS?YHlknnEKk z_ng&#H~?`JdP%>}9iQZ1;W^CLA|%mwxZpMpHP8(UCIz9r2X{T{WtRblN5st2>FK); z>L7;g^+#CS_~EjVL3^<;BMi2K-h|zVYx(Oqh?@LI!2UGrDiSyc9kpsy1M~LDmN@{Y za&0ZhvB>(QaaQm^XEyV7Ymo$Zal{%Tsh7XNkIftA6se7nMADek3OSiFdc(F&m>X)P z%b^$lg@w`BG`*Pz#lnF2yk%(76>B;NIUa06%aJF8!vmv~u=I-BVtE;h2$?3peF>)v zv2FV)b9obDH0AraU`0yI2&p#V%8;6FxGnSzH16(-gOXx`7D#Yh^YA+~hFm!Dmjoir z9g%!-OI8hdXmp-h>32AZQ-`4&D8wSm=*x$R8c``F10P`|(q_X^uHJMubcsH?K44pe zGvuhlX?Ph9e$R5CuP&*n*fEZV3qwZhVPFhdZjD4rJLOrT-#@qCA~K0{_BdhdI9Y33 zlCqOshQ1PgTI4DBdVyON1OB|hq%bBJ!GZhrJ`0BALkI3NgNq3kK+sLta}TVPre9PX3=C|uLezGJ;h ze_?t2z6XdYN3ViFGj2I54WBcmTz-hr2R=OR?P9tP!B1dFtuo!+fF;OU`LGc)%YZ-r z1A@&HXw727E^gau%u08L3U&r*O;5COY4jznEo3JI|{UJMD|~ zePI^CGiD)^s!SaSh0VYw|4CuTLl9TKe8(}f_%4j8UDaU?9cqo*pJF?NAmHWxQb#RSKw?y8 zDI9946!-+EhT4dW5(pNzyk_8#p&eQ2*xx9pXCg^UZXrU z>M9DH1FH=Ny<6^^4*D9? zlRjpUQ+_(Sg>K3;+pkgsE0QZv)fFlU=!Lwdto~{;jVl}sa;iK9M6QJyoBB8RXI2=5 zwmCa#3ODpog?_N}FW^MQ{tG_HG;BHEeKI1pmGMk1zC_IM2$BS?yqiXW{fLX4JXO{@ zqAw`Y&f<`%&%pFrm$K+-FeLy@_bBsUuH?&ETdO8zzC{fH?&o3wqw>t8X|w8coRB-K zI(%FURp@mRJcB65rqGFXgpjUVpc5-m>1fP+&!MK2ly{26`?_a^`{Ed^K4CV6?nfR= zPd18iO+(y(nn!Wfi1<9@JYt+0WE+VzcIOhm0hb?(csv>MFnm_LH@z6PSB^14z_Q^2 z&evD6AH3jaansVN+t0w=*s#P=d<4zo#m<`87~$wxwrq=G#vXKThGRx9&3s|rHp?7I zczcM&AiD(L6Ue2$;zPlyK!LtFs`C`~L_T529ebm_^Q5JKG2zJaJ38EnlJQoY?YX^^ zcMZpevRr}q$G=8;2>ac5&wX}1FI{^Siw88O6ZvF^eIS+bsWtMi*$HgX?XzVKWTcMYs)k=^erfGARN`4x}c>K%o7(xx(5mO)Tkt9YHR@L zAVX$woX*<8?rm$q>8ygM%9PBda)s4-tG<$!T}5p!D+DG+{Ip?_ zOZs7c>-fu_f?=4$58k_Uz@F}6wqWB`W~=hjSRY-Jk_hNf;x`OMcf_M`i9d2Z9_3Z_TvJm|1>hW(V^fDJt}>Pyf-|c%e9ahOQ$Q-{3w=a1ui%lzEmrSK{7hm$6sTqL3Nd|$?&ASS?4VzO z2IsDjBXY$mE0|@TOrt>RCX+54&8(4MLxH_}t};VnhY8`E?9YJk>(0^2<)()7z=wl@ zy!E%=xCQzG7dfSet0=S#NcIl4yLVFFBbF~5ArfkT2io5nSN28#<}}~~J5$CXa6rgX zEy@;qC9Y%#A-NMe=@u8|D2p>`6bDf&6LB3eJd*`hHyi4u`u7 z6tM>aq`C|yU%FY#a&|@62Eoxl7ZZT-@OM=oXWxL1FB&kN)}*J+#zrm{0~E2)|23}G zchgkS!==hJ7H#AQ;`xhRS*PPnBz*78YTH5;VoK8?xid0J7`0@<3Qk41Kv{U?rCGug zU40Wua0a&V=13n+s`$ypiE>aY)SjuzjQBArRAH@ zw^SnRg;gP}@p}wSKpD7zp`5C&mf86o;bv983A_HYQ!)k{lf|(W zmJ%;7`$*c!I!g4Bf;rsoK_bOemXshz$!<1;0t4Sp-1&lcxi{*`tNysFO-E`-;0WL8 zNT2Of%7UB0rc}^;_nFK>bElmaX zWGhv{M-3b#IVS>PN@H0a{l%ITi!@lZbNh9i5L0yuEo||0QtRdI(3PLnUp@BS_!*eb zWkrvs5QQdtugipL;MRG%Lj{P54+xWi6=T!R%9+{&1K#Y=c3|2a+cVfyX64;cTs7iV z7JATBEB*9H)QHTQa{>oYiq~xLC%riv1)sbairllWq=MvKR~USOMUNfO)5gg*L`qB-K%A>YwxM70h5DiPw@;xN#83|c)(Bnv!=(gMIUH$Mj4yNLKY zhiSP_;8IXnaBRA}W((9e4#%~N#R-UsBgss=DrX$}7pY@P4cqtTPzKRq7trFSK_DFk1ej$Kij(JHNR_$Au`7 zk@O;7=yPfBVwfX#0WUa=sO}qudhrqLEKg!d#5NDS#~yz6ag98U#CheJ-g6<;PpA** zh2V?Jc21EO48NEgVTN(GZAE)uX)_8I-Pzqy z#ZWLQh=eTmw-zb?57a+OukTVhhDV(;gyd!3wmTFl~L7*w*tU} zj?;m#lBl!E61WXT@wjCdD~hdKaEiKwGh#-V9<=}L!WUHAij56T3ZT=;b5MFJr#Z_s z|2J-Y9Puo?R3}^o0V;5p?FrEO>>>TluBF)gbUSK-RUFJD{Nx?#piq47CK}{8ThKlJ zGgT&@D+KtFZB>Q^g!a=j-DCZd_gESt=E7j+{=A`AYDjyYlRFPcm}zaQ54*jC&E551 z3V?fFflVi0=CSV1OF5((J#(;$jRdqP4=Hrc1?N&2lZ5$$DANAYyKmLF6ho*)XNTo< zN3A!FV5yIhp}QfP(aU?KTQEJG;mp<9PSKPIva)qqbQDG)JBTq0URp+spM*8}922(IDa85GuT^uEc2U z%)Ju#jvklV|27sS8}P%i5&&FOlU{| zTM-}C;)hZP=$uu6vQt|1Qlq51rTFm*N^-Av4RHDYc33jxV3s;V%)P}JJvPg@y!cAQ zP4=-I0E6`x>#emvt*JHx;hJWB|6W{cxe9QQ4`0s^a?41 zC-O~|?c!l$4wO9Sh+<<)A)03HliKmuI^x@@B>WVNH~a{le233*!)$D{F5k~=mYWemu=S^;ES0$1N1|z>Y`X* zHiXvmK0DK~yuryg${<)Yu`MY8wS)RBFuW6OqA5nkZ}%d(MIVnQei{}Ze!t!`BK^W; z#78h<*-{ef7MRYJJz%^~V8y=`Ow^PK+uS z=nxLMLD_=q&%+~M4~wqdzLyJ0yWIP6w|hBaEU`STdtl$nI}FC7*nuqx&S1 z+{hIrbiCCseYtu#hcGo}k@xxc9_;yQ6EdWUsxJSQ!+DsD1{2FU3 zB;EjBJ(S-g5l6JsNh*O;3S7m$26k|_f;%`MO83#iRH97N!@trWdh!?I(-V9Z1UPWJ zY$U(U{-Zx#P$MApHGl4Pl3Qu-rqeTn6m2f!V6*~d)RSK;zgzUvhTQnfyHtp(pwag? zAAmgCLwD7u4gAqhN}iy`b*z*N7eH)&c~#FUlFdbQ2kQI}mUYa8_+;x52_UyT$8)ic zcB$_O#!1dMD!li+U5&=2R+!l**yTaw>I^AK!vNIgi~+T*&w|OU(>N%q7y2RZv2Ui5 zM3J;{1%)%1;J4fZ?10$e=p-r~(y>b`OO&;p$(6ldHA0y&1VG;u$;{sIpR#<-bZjGA zkhU`Z1{cpti=041)$RcfeR%}jRn)j8Em(jw_F(fhuGe8$Lg$6cNGVJdWtZAWb2PH3 z%PP@RLt$q!jwS^9W^&FzwY(5VmpGOx7f|5@5bDl#s3|F$3M_bdjHIFx5L6^8hMA!BubpM5QpZ`FugAn5_#ZRbt6a z%@$ZoI7RFY<|Il((lO?_E%7OIKfELzT)eC&6sOk%|x333s^|riOI~%+V&=2XxFQ zWyxsu=MTRkYTncx7Ou{7DI@|h| zJr{xJ;AE+Bqzny25g4jH#tcK^_E6Bu*mr1h9y3vT+=;@cwJrG9EK zUNjeB_fH7SZ(l~*9a1q$kj<5;A<^&b|0f9;yEhNRXs@ z=Z#PZ;on~-<{cwTXMb0#R%b8LP$x0tnylKJrVArcc3`?IUfrl#Fa7{NL^OojYm6_t zzy=0+|GAvXX-lrpD74O&P~qn1tLR;tEUI2~StAJy9m}`>MeFSPK$`(jwc9n>E7>aD z(G5m#eVPuOcK!2bh}0P0*;H)Jj+>2r5tER{4eO}wOyZbWpKKdE?fw_~-HSevu}N?w zo@%i<(uzKU(6%HDOaj!X{^L@SBBM&vwlW77yCNO+RTQ^;g-yn`m z1hd@`XK4P0Nf-PG2Q_Cd#R}_NpJ~AO2D?f0j_jhaG|Uz(EpIn4JS4%2EJlgL#7Ys0 z`0l!3hp5Ag@--cif=_&=&=)Y_351XjzH@IgLInL4$6MHY$GV60KTWkv8cLyA621cD zlJxtiwDx^)H76R!gB`<4ub$iG6w92wQ0UMfLo?WZkiuP9 zVk8xa(V`bd?AkhvewhvZRX#6})_)8D0x0`dgaY2vPrm}x0%v*55vaLwFaaQ?#Ix-~ z8~daE_1a}u_#ZNZgY?wrDLniEiG5<)`x9ReqkkqDbw?8JO6r!i5YSrG-dK;?#FR1F0-Tt2c3&)*`jwl8R=6@k^SP;@=)i2PR<;jhK7{DCYX8mxp$+1 z6oP`kZj05=u*6;0+E@f)%C-ntA@mZ49#0AhyLWA`Pfm|t|5RJh!?0llnJrSN`C~>z zwfBTGgZH%Bl2{(ubhYl{X#JVI)dLjTS>6J@0puk)a|)WT>~04K%Pdo+D#K#|k-Rvs z;fIK~p@qJDibIQ|xL7Kco2OZoslk7y>4^XVDvKd$VHM86=bJiY5?9J(l&KZ{y>V2r zHM?vn$-fNGJQ+acI7N6<~v{jv$=B1!Qj4AUA_fANyL$cW4W0r#JA z6P`%n6N;*@ZeEaL8-YG7ZuhlB- z(YJ31er?*YG`7A`mB$vRFHDozu;F|PAm$yS6YUNcjl2kyQaLG~EpCi=iB(b6g50k| zCZ$fgr}Tn#2?0Y&?bq^9=;#(u=~MQTl9%)oR5e)SiBj%?jwY(s{y>H;KX2GhC%9Cd z0v0UH2M&Ml)f?@QPbrOJsv2|_GsEl>-Az|61sj5KasFNPu3TJ18sV|0Rmags!h2K< zD37~~#gQ<{geB9D|&bJWDI&3Cm8$Z$3$ejV#P}<;o!~pt;kh` zu0iu+XEV*cU-J*JypX0bas_QX!l$ERp*Uy zk8h%|GcVDabjK?L0XabOVUmM2;Xp21uS5X9rt*#5=`kR2{R_#;W=!bT8rS)RUKeuU8#`y)pfoq&2QO^)dtcd4OGDtg)MQaFQL2K>;B=>`1 z0^Ra^yVTG9DotYo z0Z=I**`dk>i`+>}KVgQ8=f3`=-ukf!%cGP*^<5nJT9h%8d&#{o6v}QrF_EQFzTH_wjj`~y{#e18bnRNgA zS^F&{f6KsypaU+eGq)*>8uoReqYF-5O@nM5h~rURp&PKq00y%$0jL)Qj8jeX{Vl2g zpXh0u4*ij`8=4+cY!I+^#R%W^ie>S6$n#HN{G;;y7j$3Xe|B2Ce@HkGL&Z)!F6*zA z25)+Kz$2C{Q!~o($Qb*s(CcPRBFKcL5SU%L{;U6|K9`%GAq%x%sQ4YfBHk@vfJa_Oh_afiYo! zuq|&`S)#(M5l8_G<#d}$nBEIEgvd!nl@^CGWX{QE74sFav@3g}gmW%JGgVx-O2E41 zR*sN6OKqCgK6S^~5oM(Bo#w;_PQnU2CGHZXwilO*Qcrif40eOvb{c0Db*{x24cdox z-PQhm9&O~4;wa{{B;waHOCQ4al(*Za<+koulE7Q8dqDZk9H7T4A> z1ph68&3LZ3+3h5R>44Ra?V2rv^d~L*rIH0pls)2<1X&d8#;PVkx4Ted#ov#C!@2QE z-Ktvl!PpOxH|MvZhsAdt=S;cahhBa8ajXe@%EBeSh>96JpqVQ>eO7rfl3H@|(2LNE z{y=7dD3$C@lMvgE>P5B(X)^MxnERa{b8S$fPI|Mqfta_t7CNd1@^fCZ6o!g{i5K9& zZ%{iB7vQNqDQ6QBCRXi$64rcMis0wQou0=F7Zk^wkhPd_u(WPh`Ajw51p>zQB=rks zz9;yd%vwm}t`skI409SdByN37no+g1TiIg`gWeH{SWa35cY1tDDa>m!n%m*^9=6Z! z=7SX%Kq&`@M!76-pD>K8TpcJs5gzSme<=zM#GJ)SGSs4lhqVDc2W)G zh`-hC-~qfXS8iuv!5Z%21J##3z$aaClZ2=vRNAepyC+3pt zIzV7~*mOD}7q>yno9l+}g4LgWypTWFhdfXb6Xyr(gHZNNL1~|`H}u+WTT>y}y%|>?`X@8%l9Dv1F#0XtZ^m{ZNr>DBq^vJnI z6dw2J(QQa6$z+AfQ$WVt0oqOcAM}-dMQto-Ig()Id6>cEjycdG##69hpsC4yMx0ALj)`KQ-6_SIwL!6)iYQ8k zD=T$C6LC7ANy-QJgT`IW*r_d|)gFV`b!!ByW#FxHHunGoYwZQ_bbj8S$xCB&Jzdt3 zg@4j%6V*?(#jyRP}OU>@$NlRyQE z$s!DWR=w4nwZWMW9R|=y4?Dg=3mCh;$)p+nBKqgVuH}!sHKW#~g8^jn;1PZ}zX2Ui zjg)18*2Wt7Ow;Yv7rD`oZ}%6*sn{hqXD9}~oZw?}3CVN)uJQF_d465s5raDm5_TgGA{9m~a39000000;U#6Serd&|M*4Eep0oKHxN(6 zPCz4Hd7)68Z=Rqq!dC|-Tqa+~TQE%^cd|w3zw%C`?baeX>i5*Q_(6(ENSw_3xu+mi zavznPa!#^0qW}lR z^1>KLn=7O`DW5^+3B7(wFVK6h*0eq$G;bU%#cI=R)2`EqW_nQe3~X-GvKKx>%Wb0_q0r?>H zNoWX%q}Edy?2*&7!$R&M<8wG*KPk z8l)46l0T&P$Zj=T%1GtluLJcDuB&VS`IgD2(swRTXqpg|TV^IS%M+fVY61#wae(V; zTWZhNeYM+$v@C)t@a5*|Z?HZa&h&r*24JPc!W=a)KD}^+81pqA(#m;)bDgzh{!ayz zEL%!6D?BU_Lrl>xJoXk^-rah7){{vBHAFFayc;uw!O^`jZN?YuN+S~Y?$EFkq$;pC z_>nKx8Llx+>9J35WNzNnvyIy}rT-Zk3vi7g7I{rK&=7}PXR&;`^vuC|!07s41u41R zNX&IaM~#;_+eA^us~zd{yZ7H1(I{YN{~gqN+=gfbqkt-QMrs3Hxl)zU$?{qq$h{c? zQZ@k_V{V5cr5oFi863B+j&?awaY|?;)C7{KM?`v_&B7hAxY`#JnDwN7RSWiybekvU zeAd|$7qN~XHnkc}80oFxSG+6P%3$vbW>-%)W~WtNXC5MGHlW3Zwewsqhdv@R!|{V3opvow>dS6Q8}0*VpXVq#P+sX94?Ze( zm!uPJ@qETo*`@Ii_lStw^RMstov@cq4LOIzzNWiyThy*WWf4JOqDTF=%;w|-MXWr*==i=m$ zGJoLA3097Wvwax9##Yctnt`=^Y8WKsmU7(Ne@8Yj(na2#CyKPvY|~NU>;i~q;u-Zc zr*OtNdyf44Lf5$@rw1Y<_}> zuheWNZtn}_8eKx~Nxh0`VoTdCj^F*2saQeO5DBJ}a$16VXovK8L<99d&NW&md~L=} zmbw(7o7fUMk6ns?rpfmIkb|b`WyHoOF)JqH1h$ZiGSETs0@&8kO_jkIdf)ASg=5W z|3Be>zb}DBc~dvd0AG4JI*HDrZ8L4p0|(ZrOz=fu%1_=`!3kKNhb&QPo8nSmnovl7 zh1WcM+<)qQ_8x9tOwB1WxW%;+&hyS$mh_`q!d<;3s)%Ukqa~d?kW~8%l()2l5 z-~s*T@IHAon^2fqi{nr$A1kzs$krB({OwhKN?WDe+e?G>!Lk9;7l(m_GfY7|@nX7u zI}sdoL{9bH*qF%V$=d9-6xP942+K0+AknBI*bA^k4WrzK&njFFD*40xczCf&7`lkY zGH2K}Bi*aR7OeVjf_Wug#a^GLt8GSHi7pBI3QR4*kI*=Hce-(#iMa-lN4$fAUf%3Q zj%j5RlAD=wfq8cX^b^C0!ygEpTfL|Bh8sF;^u*Fus!|8N)vyYrJCFIQ$b$89(S$4h zIbTFRNLf1y0B}09l*D_decr#~Kvzq0`*;MNB%oX=Ke@tAEFHxnNffCYr{QvAES}C# z^RjQ1a`u4DU+n{k)|y5qQ)icC*t`5iooMxH9zvq?YiOuJFU4@Bc^{zPxYbnye%C2|wKi(;t^g0V>c7ci9mff04OJ8K!myHt6#iK2^m_@kcIp?w4>hyb~@l zS_t=6PhV$hlNv8F~bcu%Sp4fxoMO&zsR=Nn~aTWv%b@7+ymsk=Lqt7^GcY5x3r zWFR3{Ik`bo03wU#Dj?VFBgQO_bJOv`L!HH_uoB43SW8tolRoQ=odkx`g9b!utMgc6 zmuEG=K6QBEN`x`#fwpP>4r)rd^Uj&0q|P;)y zTy!t{A~59iulOFa-y8f&tIFJ6Is#Kidg@hk`!MzO1&IE^y$coblj&#HYrj=saf8J^ zzNEoy_mp}+ zeuAG|CULxb*@l0nq5R)Dtp?d!7P_vH9vOu-KEa40778}kwW#O_r5evo&XffTxtQbl zJ%zE{xS-x2j=Q=Dcqpd1!cnwQQM`SCziqc!i#s;b;H3~bUj_hWuyNT#d(|-pF&f=0 zFT~gwJzn9K_#<%|KM6 zU6r8ybqs8I?_t*rO&V26unTqveb23T;vR<)ViRmu*d&7jHuKbUXBI@t_#16B`_-MN zb6>aIYlRYzGVO8o)(c_@C|>s2bei)b_5_J6B?-9U!!V!x8>ZVKJetI;Au50E6P_=W zVsCx}gDTcI>@Y||DL_Q|GECcv@eYP-D2Au5!TbdP7oxV~-{evvZWjhRjC!kKlZgl- zH>#))AKvnj7s9D5AZS@_vmf3^fMRSMS5*l9QotI(!c z;6XvK&zSlinJUD2ZGWu9&Mk?Rxbwtzy)o(xbqXNvq`nm^xb7K5(&gL%k1+kDl+iXk zKQNrH=YALeIX5&!?DgBP!_VHUc#8-XqG<}yY}6OqC08BM}*{5o29b`M#)*6VV3QiXc305wIzu+<<(9|FL38; zw*5;mdFQ@s!>YHFe+K6d^|u)hK$*Z_j)B4w)t=&-o&A|MZV?l@P=K6KKu$pj`r@fw zm+UD{IsC|GV)Xdm(mo>mN}QWZbtWw!%Af@Gms*TWPBo00DQy z`wl!|3MXu|cC}apM6||`=z?7F@IWW(s~6&z&%PffC~S3To$S2#A_ME#2>ej1)9o6d zHK-Q|eKCS(?i*&m<)(W2SG%U+?j4pmH$&Y5!hmQa9zMf5boM`*q0^R+N;cbkYvKhQ zGwSU+5DD&mnpY+9g}!;kPTi|p-rYDZT9`(sL?+tCy4Z`IjFNMKIE#T1-RNAW z$dT!)+;2W9r&rWyz|C0H720f@Q7wj8j1+Ra5JCNmiGq}X#~S#1pdvByP+LKbu&oeG zoh(5#7S~N{2-r31f2CT+rBAg8l0d)-2O2}O$+$wztcU)D?C4{4?SeCsuBVzdY&S(W zt;ioop;aFc>JLv@fK0qu4sl#EzUcnZc&E!xA7r5%&p+7=syTx%epOQy^FIOwxSj;x z0fcgPF+;N?Y}oiL6m7D>Gpsk;W@y&7I=$T7VIHGr6u(ME+gU6~3h+S&=%2TyvOseI zj@1yM(T=nb>We_&K>AJ=FP>)058Uwo6PBru4`W`@^1Y;D;Eqr3!wR<*p>MO^St_wl z>n^R%+*Iuxp-d~-J_ZB`t=hwDkWjQl2)p;ZccP#~FAcfiIZ2n0gs-880#MAZ3nx zfV7aCVROA+JY}|TGpH?YIHmM+0boUnTW$;u@J40I6|l*G^TPMZP*-D0?u?=+>NLIe zV^j7<)>S-|`IJ6t3*A}P6Q5diHPP0TltRR%zV%)@YO#9$Xm+s``(57n(32GZ@!2V;uR^oEtno}=dUaLJaW0(!=6c^VzuBS*Q#$P$Mffw#w|gJJ?lH^KSr2es2&xeLWA3TWO^T@+}7+~ zHwa`;#uK4DCxfetP~PW*OL-3Z>A##p)breJLI^Jr$L0PZD_6>hHVDdmXk+Vr8i*EV z_)54C>*f8B$SfbbUi(LJww`Ue_pjj7w`JJr*fl;IEimX40fwt0+bu_S`oa8P7s80U z_;(4z&XF2jx`DOOOxXQW(DPLde(=PVkbm*kh_$svJdo(f303{16*A&@0inFNS0=Nk zgU-n8HY&}st{syr6hs|N3}obWv!+e5^j#{bdc=Yn|Byw%OoJZ)c#dt4DaEHIZ)xEl z4b+JYwE};J-%XbAApLNcgr%JmrTH==(2B|2N=M~&Id@DgOPP!g zc%ba1;OsqGb^rG(u-|=A??(UjDY6ZHP6jk7^$DSvX;PXHEGVR|NQ3*MM~52{scnTX zeAb*qvT2U&rwc*zQ<#t0c$>A4Uk6v3Y|*D+2W#MVAQ%evPuOdGu#?sLU;2G@x)g5< z{-A($Lrv6E#<$T^brRBdhVJ8!@r3-d4eX*#0=tR@@<3m1G1xv4k0(bIsd&<40=AliXHR3R?J-!dbYS-%5&nUSu)us_wx=vGKJ{>L;rvd+DFSxuXbT_!pI#|?EZR`Q|#yY;+j)#!plJtSRg+z4d+PiEZY+C zZTN`Xmm!kl*M;bm>YcGE*7jn|AYhnN>aJ$cynzO&hF&4tlIdFl&9FcNh3SCR0m?vd z4%HYa-K^`F@Uoh0uH|1tgsSOF8&3z(Q#AqcN~G==;JBkaa0unfLa(Ag4>+-Pcy(aw zZ2Re_Gi9h2MeEdV=U!g|T(_$3R`@UnMhFn85i3Yuj=BUH4fU4cbsDf0$-2P0<%0+U zDAL4S%>7+GoU#j`jY=*CIrEgmYm~jUH}^Ltc5By`|8wI(8vVB00gp^k?~foxgDFl1 zxuggo zt;B*Hmc<bR&sn_ttS#ejj_GPx64{R?FrfkHmaXqI2kcpI~j{@?)x@LIOm1V`&O{auK$- zh02%V7==fdKYGtnZPV0M7QQg^o(eKpB!q4TnHOpM_vesw0hp#tF~3mG{-LLVJvIe;*E4di`mS`Zyn~=p-kUm12 zz@d@Nr0r$t(B2Z`oLV=(`V zFb8}h{96D&)s8#MAd;$XOiw{zFGWd+2X8ay+E7Pi4Z_~111o|xyhiaJNgyk|gECX# zWSspIqJ(|jIVQ-J%2yG*Rj?I+B5=GOMqxvD6MzHVAOo|JFMFs3zs9@truo+&4jgo! zWqHPa!GzH@Wy^47{PKWC#&sCK%DD(~d3;ijiusqlQTV1%h8iVa2imu@D{|HX!Ma;v z8TFT>zAc6vh1lr>a8yre#ud;B?TCR;=l}_1BYEf^A@XtdsLIwCzM(qx$gI;!u(8`o z>JwpkmnRG=KREQ8L8HxTvQqeMHxWW@mN~4Y5*bs=(7FrXPNPq@u^sKrwYq5^rm4j1 zPh~>UH#-1$)T~J#03hLDY##ssH2@f+TfvDm`?9yXn^sw2*`7DPC>xH&Xh#i*JomX2 zFYHex>x}MhK`7A2{^*wicQ&nf=vYBJ`CqfX_ep4q%EQ;s*YZ$w<4yhKuxo7xCjrMP z(qL$=wpl%?C%AoEgdBRISU~5DJpcebmd$EZ2IJ?Wb*2##lfOuEta;M*&eX08g~diP zDo3zyTHHa3) zNMKQSK=b*c-t$C`yBv=jN-uaAw7t&{TL1({3>MWdg|cZ!6#JN_+#fr-;YUiM16sDz zy4j27x7#L$MW#A9r(xlh0=RTA7Hp84D+Q0LgW7dnbgxEn7k{=PYl+$t zd_~?WP00A!I0e~jgg3qX&8naYD8iKb!hg_9R>LkCoP#d#ztNBERYHEQd2+2Y^RIcK zo}^Y|TeqM07~R*`fur)x&}=JPV^TtWK(;3}zo*?*P*On)T$ zxK3ggv7QHB< zq783*n^kiemTbejCk5=(x=_?gt|0(4;yhyYB#+DQvD@5sLxOq%w{zTV?2?*J<8?u@ z5vx{>X!gWeM<9B7a|;taZn$6$>6|29u$9wjH&%y@@3fRNqulB(7rIf|B`nS54;G}L zK;tV}$pGJnrs0#920tL&%@|gp=Nu;a;2gI9Qe;#=gDmYEz)Xw6W%NN;vlxNk8NdF` zIx0Gch=-~}6!Wq@N$n+N?RF26bls$*0=HG=jw#Ec-r06r+l8AxyWJCYSHZ3HzXgH}WDa-flaV9QL zR{!Fqob5`Vkv58#htI(nO*=rN`{Tfbm!$FSZjz_0tvUrc`syT6#Bb{j?sjQ@{8T%* zsKz`*f5Wr{z6=cQ`k%E^+(Hq7T@AblGtzj94Qp~0{HnedQo$PknckrJJs|~b>y(HH zlBqTOqzR%8vVXN6>Im;a&jzhFAm?^V96&)9D`*CKv}PndH+8hF15PBQg50ybA)l4O zD2*NyUaWr=Q4uXDjut|6Ftth`+ug2#BFF9nyV+;JZ8s{0#>^DwRd^>xJENL8pep{6 zN7P;HqHpKmPl_S#Onk~|r{5+LAPnat$5Ho(yYt-u z(t)Mg_~SIO#xF(02o%s;L*T7v9WYP7%6u2Brb-38O$t^->qVG-KmpB()&f~;)l*iR zIkD2uIQWcx;enbs(Z9M(+Zd6hh9=K(NDeKpjzZvwhgVYZ$vGgU3sj{ZWmge`CzYi7 z@M&`)Dn%(*IjPwG`T#>Wr83;Ot*D5hD3TI8-8Nch3$yZMPzS`h5eQMj<@l0|ceBF9 zvzq||`4pmHXP)#oH09FYyhUf-QfAwjFlzPe1$&}S4JA~)tJl~=mr{PWU%T)Q=u-^^ zD*Q2E&cNh4BiAQEaI|X6C8Z^Ai-UR9n(bG;%lBt`=GZ_1O9)ilD?G{UdFs?$3Lc$F z|31E$agR& zIKDQLA42#%iR(6uP6zcoL__Pf9>y*Q1*I7pah`A!gr2Q+IwH1GSTfIolcAZRTu7x3 z!e~^%cTD$x@50t^wA0La)K6P0Ytz2WQVTfg)X(i^Da7w(KHS&ig8#FD!22t{Vmw+d zMap$gNbhk38@N|C$oKIJ%oG;^0E6@QmrM)-$=s-oEyxc%U#@|=R~a7w5GJLSF1zb@ z{FQ0_UpoUbH3wjxsnP39Gu&RpKA==sUyy}pqz#?djd#3JBEqPI=J=mId2@wEkzcK| zDF1~q2sr;uMfE@SG}@C%m3_t@wR5=(fg?WBp7+QkC4PBo|ACzAlKDSTFl})|@+11cdKqd~! zeyUOu_cJ)1z6R6{dE)qsp@Uis2SR<75XbKi+$l<-MJ1;9j1kBKB7NT(Df4}*T!#B_ z|D12yrs9d+!#VPpIbyDnQy^IB={1sRa>%5~a0}s)Jio#qaJ6$6vlQ!=BII1t!Rmvj zXvVOysT>o%N_SpHOgFY?qeHhIN|?0EtulvLwE#HYszmVwOW4ZlLF4FqgQdIvyuQg= zf)$F4M?1e%Ym*tr6}~lj8A^b^i1qz3^#4xrPm0bQml9bAf5uF1HTVY!&Wm zDqub$`Fl?9XsDqn3PRqQ4vdyJArw*q*=(y420?Srod>W3gY z#(`!9^G`-iD@9va*?GosFc68k=gQ{O$~AN^iVLVhM38hLf0Q$0`-k=T^2SilvvrRM9;s?;y(QYxc{jRZ*la=nEM?HzzR^$tZF7IEr@4RJkMxQ;d zD9e&%IlS@8!Hi^SMa--j8A*vtd;W;w`7~Bc5dQM7uRRM^kqOJ_t9dlL=}1E9N0P)7 zH&#kl5FdsJ+$1d$W(Ms|=~&|ImaO0^w%G8)FEM2^5z8<G8JvQoK2C(ari%j z)?Dk$#<*}+@AmZzh1l_%02Ph)0H8!Du?LKTWH~WtwRhjBafW{b!!%MVrObJ!tP2Wi z_=W7AXl}Oz{puKoK+>+(y)davSp_ohL}6e_cLHu}-G=Rh9?v|t88wX!e>-($3L0Qp z3R}Cp8%oBJ(m(XYEB9LSXfV0uAGA=b#KH<8Ul1M-j0YQ zEv&CqUj@sy7g);*&VdY>(%Hb=ZEW3YeX&YgPdr7+8&&mj<9|d4T!fC8uL4*8c^Cq6^p@A5&{}ui$$bK zMig4B+fS8Y9kFGx@g(Fzw{-^fDZGe@teKjRg)0PN9ksk{SjSD-p@qYe$9C_cpwlHm z7Th$A?UAc|25JJGb(HYFJklq^;f`z!GMD2acq@sPpw1#ZgxuzF?FXtgS_B6Bdw{^h zfQlOCrsx(HyZh_H&g@K$s$4heq7@n_p*x#Kjd}3SnVpwP-b~#{Uc1pAXOZRPjR%9a zNP}$LR;&vfX@1|%$>|&4$6)nn;e~@pFCxV8?tb|K8@hul0d=0wOo?8kF&P1ZfvxBD z_|l=<8b5vUVP?&FBPR4haA#l1H2|9w|OKd<$&hUz?)9s z-2rIvOJY>1eV9&H5is*Vh?7#6{L9bpNo<$6?0@JcdBLph{yZUsv2GSyB{c`KNq!Wj zZ;N*xaihXkbU%bpIif2)9ydz8zn8&Q`f=Dc{N6nOtYmNbbl!|{S}Xlc-jsTDSDPUS zA-P}*g}Id31pt6F2j1|0NpAQcyZc@))-2&deO0Kq%9-20g585kcdlP`(>yg3 zDL}196*1_8HHxd8Fkp=B0&{JIrqdW>7XR>>$}t%Im!YEq0AspM5bQUT%Tex&W{d7s4O88VJiX`I2pzOo*CX<&5>XxmrPV=6Vudi(;kdL|H0=l0{2r)F?S0onBE+{+PeJh8`*lU3{8L!ocE{AaTZ(3fJ+~EA=N}t+W8FEqabAmU5 z$gjmDFFn2Y7RUU)Pqjq32^n5X8bK&k7hwAY)y3-z$iR`JcJ*9Y7pOb@d9D`vtmp~Y zCfoQ3tgzLCerIs^KMo_|Ehr?oRHx!&atH2XxEzvQjM!Z*mGf804m^V1)%SUH^-@3g zrqbTRgn?y-fo?6G zwwr^)yw9=lCr z;c@>+a8B!lXPLzoR)~kq`GL4r?>&1F@A9m9+L)^NVuMM(fkSiTgxYwVEmai&n$jM* zBEhnLdnEF1n{Ax&x$l9;D3)iirv7=L^A;*oo=q_R5nXJY*vPIvfA}hO(=O(P_eR@0 z8RYgsI8TmN@MdMWB9|Kbkbr^^pm__mS3yniD>yL-t<2}sgyQ+Y*O$+=H(z)PXXPsYsIy8i3E z3AlAfY&;LfUa2%8%P)|bZROHaWDq*n^|JsCGR-qo#p9M&X#JR$m1LH+ujHld>u_jt`8nR%q!OgSP)21n=oTM^n*5=s-OK3h)c% zsY~D~4qm#t!<#>X0_^ogJrA6J#?LP&?}0Sz(LfjfVCqC`e&6tT?M{R>Jomy-fU;8S z5?xiv4st_&OHGO9Tagkvktv#~iG84*wQ!xvOTffF>mFEs%S>oIq+YssrqdE(2xqMi z@t5B%5W+M?3HYU%rnT)`9#}7|4}{1y#`w~zlOaCYMY)e^yd7R7gUU%756fTS@^nVf z{_Wsv_xYR;2r+k}N*$ITa$8mT-)!H#|kr)~noJE?Gv{f&O=(@*s z##+2y^uMBI+oZp5fLBVM8Gs$diNRP##jEQOCXHMPb?3JC{?`}@8z$$FJ(S~j)2>ia zqlFW0&h($xZ-W=*Au@tlA<$7;P3K=e9~vacrTGjujy3f0qJ?S zZg{ag6oZ$S&qbXuXm*BbdG|`^3}-tBB?<-Fr^R(+vG>spLWtDUrl&>(%2a19g)4fh zXR|)(Vi+pC#s}t<2UkPuC)>+jyq4EN>F&4D_JIG{#Q@uyHT({VilBsrwRk!{Y1wN97P~cX)OerUN@PYYi>)zs=6}>>w76qMO#fI*%>@>dkD;aayzPTS4e5xuc zZsP45oimhR7j<|R>E=gc^0p2&2DGQUW{lfR^HjGB`b}GMvR#gQQ$FexD|0*e(QzdM zEs7F}F{lI5A1{$rfbxvv@8QfB&uZL);OS){W(mFzTtSsrJ;xGi_Ay0PbJu6_(IN&9 z7pNum8c=G5rw4k|b)(RbB#-pJC0KQ?$%JE2<+S;t*$8?~SE)^OhTS^%)7_O;wo3S~uVXFHJAjOShKaL^Pi z)ZT}Wgex8y7dDM>S5!@tb}gP8;mSOVs-dBk5bBOVi6*}vvPbpN{E{0B0aM7%!|+Q2 z#ABtKeCl*Bjk%*+z7jwv9T5!Z?cC`|()J`QhkZKF2kKu{o7goM&19x+3V%-(bPlWz z=-nMx?q}{8Q==28CzL)>Vl0;fc#O~!rgIx2HRb{T_1r12kMD8zvxc}#EHRm|j6esl zyskJoLr!g6Htpyu04L{KjX%#elaE6p;gFWBs7T^vIOs1;YVX3P*0Lxhx*p6X#q|OBJNJaK28!x=*?w8LEP_hqYL+v^TF43X^)rfyt+tI`7&~1 zWmcEAS3%G&It*qPwZjIHvInFLF^H8pHWJ0V?#_~%%U~n8`yr|a{*=zwjoRn3*|h;t z>;{KgT$^<7@50`j(uu5@2`3ot{`YT(YqIC7)rHmj_FJqIh*4YYUiR~Zv-Mb7TDEm0 zGjU+l3l)5ENT*0~TYXug3t6panCFc>dv6|dOV<46%r>Ubb?5PW z8cu`r7gLp9nt^hvNJcGSI)0&#(!RvHPWa?DUy>MupiDi&-H2P@PTzfCOcg|3(}R0B zmh1k0^?NoIav07|k7H|F*4c3gx@wbS__NuPs$priI=D?w*^pc}0;xE;p$a={Sv9@v zfy~JP3A8aY-8>(faDW3Vi3Q3>^P(IY9qCBeB^Cb^#npUzqEarl~SQ>3KSbIeliw=L1MM~O*+83 zo>K*}1rG_<yJLond2UVPa@C39H-K|3F-U%BPiw)<6#>*!U2 z&&g}o7opGaRvaSRdV>A(neCis%xa#!;*sT*QsyH7y6%DClOETF+<>r!_fwRinUlPZ$F6R_ zOFd3dkUJm}m?5MnhlNa_T=NX-Lt<02nbk_5d_xIsBqHm^->51Mmd`u=M9mGuq~4wM zt9_o@#c|41S#o%lnG)VM~> zEYxPTYheqaLjHc|HaLrO^GU>y-6Q zhq(t$28d-atX0nAtSl=j7DF)0VJnCTiGgHhGw+3aEoVjkncPKw_12uyL@eys5Cer; z{_t;K%x5h(zh3Na+Yb}|v6mywl0{;@$hCVMv!1xRuzSgj%~;jf;hB7{p}Mo!v^oYS zfxJIha{-$_81r!JEch|PqWvqfNkp?P0^MNYLd)X(nr^FgV<9F_+Deej#Wfk z%+y1l{Nq|^o~7GcynhP+k36_;3YD=eirGG@e>WsAk~*Cx%th$uBN>goz*a^W=fSRh@?LgrrAP(*AL%6MPajbSHht;@>p%fG(CczRYa-^&hOK-28YY`iBZHaUxp*)j~#K{V1zcQT4QeUbW{qIFsJzp0FceK#(SaSII*== zKl!^F6gFRmWKPZ00mPqOP~!1OcX6I=JcEBihuei4P~nv3z!k=(L2Uap(z{&mMGK=w z%O^A#yCJ003#OS7@7j3clIKtn|MzbMqo^(A`GG)y2HSXe)0c*liSZd{-TE1JddMv7 zwl`W_F#LMe3e`sbZnA|N?7zF1N1ea&0kS{{y5dKiQPbF_ZXOZ{xCvw@X{6*N>SMOD zAg871JrH7$H&2S;8-Z3^rOtJ1!TPP<+@7hXS#YlNu03Yz#5a%HokR&wo#Q1j0{&w! zvxN_Kvd3fTN!(=1m-aacDFfgz%#V^F6M<53QUHxP#!ywJA9e$X0qnN1l_qSLyjF3XaJT<5K<-Y~gOg9`Zjb^-40 zcJc8Rg+^?hSs#U~+XbQZ@`5(WIp@s{F0(K;$uhd0z(h?H=Z=zQU5 z4nb|yD|bnY@Tt#)w225 z@v?)o35@1)*!O{H*mnQsOYh1c^VA zbJ5mh;mC+@8nH$uh2L@K_4=25ZjC8I;_eQ675GU27wz$Rx!pIW0} zbNPwve8|dKXh)&p=HWq@)qGV`SE1)NJ4d+1CbQuW@l{+oAaZ7kuhrLA_iGcB3)(`l z&pRpt+$grUPu`2WJ#pV_O1)dN{K!gG&hEm9pXilhK-+4c6e`lH_+)6ru?X_DftYm( zNpJxw8Nbh^dxBlZ^6%a9o#%;P9xU>l(BSlVMMz>s4j&5};!^}g|tQPVc=?!N9>_YX#f#b^%4N6AE6Y`sN|&AnjXPw5=l z7KKA|n|wp%_onHUC<&1*_~$`VV>y!GS;Y*kQ@a>FoaJ3K_E17ws?k%-EHHa=4R58V zabG%$^C57ZU*n8S41l9R)jD@;Vxqpii$HhiQsuC3>M~nl-2Dc09FiqK1Yd^GYL38l zgptv$v-J!IKv8Qw^R`;R(Wy|T>k^b(hZtW-Y_5ctX4ufieB1OXwpk!4ts3L6?nZ{d zJQ6lp0uy@p;aD+itQ4FF>x`le?WUP54q2JTBOjy3p(e=~R(t$a+*AR&Jz18@i0}#y zBPKS3g%Eq_H{CX_$&Qszmxr=K2kf?w^Umg)zHnPOABsnw!IfNUGfHCM+OE|C(pWBS z1)mI4ZlK}mJ+1+a1VIzMt}1RDeOxoBu3Va=r=*+yzE`EEZb2?Rv%uP1XmT%LU#clQ zrGMrz2oy{_a43xX43F-JllX(=r-;ZfPajVpqmZ1WEvm!<&aZBdl(qUk(wQ~$K~fUK z&&O=~u0Z-0|3&QI^T|M5&EJpj-c>PY9CGn8TD}G6_ncXV)oAeYVaE+a=;~TE?fz2` zi(JH^0!wKd)&*=Oa&Oz;R#jgdu1=oDBd{a=-6R;}?At|Pw(0#d1Tt))qR_NM1vXT2 zjeKWYG!a${;wbg`EmC&vKhI>uo?o`Gmn`2aa+ENQCE;InCcKqDlYVh9B>au(ZSa>o z_PRS_>t_4}rkmI_myvAW`z4e`oFg4kIv#P#*H=bO*9b}@IU73{KHVn8Z43zi#Op%Z z=^0k2xoM2%&xI*O`CLk1x3nEDjdoQ_e@N?JnaBN_dX+=aDd`Mkcg3|CN!@W@Z@IBZCMDRkxYQ;Y$FW&#|8Fn1J>fL8v=?p@|nb&pT2=`YO>BPkTh z%C!}7P4d|wD&q+Ql5dOCOJ;&f7)!R?3z3TbOIm5)i0?kZaaRzpU4>9yMb&UKpG#>8 z_DZ+&8iowT-wXDr$hgy~YEdsnf>d5S(CcFu55#f|#2mY73VW+QEp8nSNI_+=qv@h5 z=SgI@(;7NP!7f*n^PV-km0pnMuU3l~c zlJa^)ub~XlE*pGJqX*AYr^(zgd5k!HWYn>@81LQ=a2l3(u`aH{ z-$Gg!Y@{ub*9K9RD(XgQV)_jE^3av%2kiY&KVPw>a-AK9kr|-4%{nn{2nmx`C8)`2 zFRfPuBsg@OhVu7aACyOaNI~K2FM)UeJDET%nqBF{7YsOE9n;dgZZo!{qZ4ZP{Gk9~ zk{nhEE~xVCGRpyU92%B0JArHZ0yl28ep*_J1^!N(lPX+;eiaCrDOYK+k1 z5NOiuYnH@cCVe=fX=RF1nx{#-dtSf_g3&b?wj#bIX)(?wOKD(L)IbP3xUnr2_!59t z87bI$IWj~+K=UZ6}6b#V`cmVixpt(O3ks9e_GUrJj89M<=I z#*OABo|-`xailWxIb9mQ0STZ{`<`&hM}AYOTw!qpK7TZj5NsZfw&*CxpNZ*evA~w{pvN9s66O!e}60>jN6`M<&OPy zzXAaJiJY=Na)2@Q$euqz;60HE&_8q+<X&}~OcYB9IQtf>gm;E|n%8RSw4HwTM)Na&0-HX)$;!bb3ly0 zzCcG28A3-0G@mL_Dr&RIJ%13#>I6I9ZiNC3&b@~JveiXVo5jbnfWU|rykwMqnVUoP z7KwjPkJi8`TG*bzvbu6%J>d1S=s_TjnOJS$`B^;M&t#R& znurV)oXh2RDO(T>3rl~#u(6Z2ePV80OWUklJw zccdVzJ-v*AYmF#Vwq+#we-Oa17-ncB~9pizO2`>qt+l5gi4gF( z1**LlFQWJWZnbO1$E%SW)sacj%)ldjLV|&kuUhOH#<)ZI#Vuscb;H-{+0{##m7DEE znrHhKhIUN`p7cOgR&X{(Ci3P31SorX>^wU3V$Fo{PckMv!x`CKeXUh-AgKWK52#nM zT}goAKyVGXtWo42#{7I-VmW0`D{@}tQD#0tctO&wa|*4WEXt9KMNfhr*h(_Zd!2ix4^X zEb=MjbG-_Eo;e&*W(s3L!}@cj^bAUVW*BL6CIy<7|1pp6fm|jL$X46^r*FE`du6S> zIUl0#ZfTBK6|&Lx58EI*d{jy=6=z`6^^qCKmols8W)S5HvRJ+O^J06BuZX7RBoQSl z9=!QaJS>R<mK=V`o3K#(-MqQ$xa5%^YOXVmI4b8~ zOS1ZUp8(+dskRYwPA%0h&Bw(qzZKB981bk&M&u(qWtg}f(-f3%fAZc}zao{I`GBp~ zsHW{4&Fty zEquDBn^!eU4iI%t+Ec^D+gM~c?-`0V6GOEmXihR}TYn8di;6Q7;s zOD+3C9I9gu;xI>jC6xirhEHi;)(5V@Gv7SX5Z&Nc2-~!6054i+rBmk#|KaiQ>Eh^V znX_uiB6JJ8Y|&i+WuBCz!qa6=8kEqe`F}S}O_i_)(8yRl>sWL00yyf3bK&|K@&s>; zrp)Sw!?{wj3iNQyY(%_WN zSilBF2UBFv*%&Mqx%qhzGEWjgpr9C1Ci|N4c|b4a$sJ>r#=B-31LZQ_d|N)hqrKjv z2=_&#cGXMouOvjv75*VyKFgCv|LnEHRyWd@S#O{k)Qlv120CE_f+meF5mBL^c{_nnv!q$>iWkpL80GK?-ksw5#f zRlC7>_g(_uzfr>%+4*HHYYYYq9a_T1<1(!twxK%iUv?_$|8R051;L_K({d_{sE9&)uy2m!ctB+Pxg@Jmh4%Lk0c< zld)s(CFjg13$Ei-Z;%@+!?=l)eDDE(wgi3@LT|x9em-BOmwO7P?L2aS zlV>qK8xhd3pVMEDBKr*|%h-v+)!%0HUUCOv4<#BffHwbef z7ccXiiGeJfw_Do7xy>1_Vd5cNFARBe z#H20(G!0e200Je4AAkS=SO5%SGfEkcAwmAz&JIa&Aa`nVdt#HVZfDoQJHy6B(sgIe zU{bNOF8cF2I=T{e9?zyR&z47kq^TDDeF=c$HgsB+p5f4-D{f0GXAfAyMeuOD^M$$0 z$$v&+(cvS+r6cWZ8&&STpUVJT=pnL6nsNfs!U1a|)WH3OZtshMKEsd@LZ@%SYzad8 zgngCRA4gagkz8oy-IP8Ggi(h`7p8m{`|d^TnewgE#D&cd4ZrH}gv6Vc17_b2wLYLg z_+P{mRkonN+IsU$;>JiWo_litNZXbC(7Ad*UE@_4+21z73DC!0wb-6_!E{4uO31#H zN-T8H!qiia_`kg-zG$G-)?ymsndHI@r?mg@?VvC_pCy|MytV!B@G`GyrfAHhLs>Od zK>ydgqt~mhuX2QM#u|?6?4*mkf_Uv(tjNH3W@YY18fTu^KRnl(aokfzjfsNhM zi?6@a2+?pyP^#&$=JN}XMDf@H-}z?&#<2Z_3%v|>yT#9dG0N*hvRO$M&SD5Jzjf`~dt)y= zNVe1!`mBxz;|-5LFO1S#U+Cp?b!y#!Ym^tgy;#AEno(vn1VDCvCMO!_@A~0UeQ{+K zB%UqbW|T)wqACNsEZ3fT7i(l+)l?5lrkiu9A<5$4u0138BQzCYzLyq7g!%zEZn+wH z{BzLfe;)A!6%1*TOR9Z3ZLbM$o#ia1C(CUdnytkQeCnJ^LBlyc*N-IM10U4J3SZtopt*&9H}gqeH!^_vvFi0DGPaGxCItvqV<2 z!W!+FI||fRWB_|XXx96%LAFp`$`ocV2@szDt_dNEq)HV&UaQ2hDFKvf=5cE??QLe+ zr!3Nii9!~UESFZ3aIz*7>HKoZ2`guW`%4etvk^+BV2NN8x8y6W6n>FPy%QM?g1Fkd z$@i#Xud-efqRsXTQEKt1v`v{GU-hByf@aW|JH57=&nru37-Xf9Yv4qHTZ`t)(u!O_ z+Voo6a(?-P!q;hMHIgl~m>$33+1q5|Xm?T*^m#M`X&pudYl$XQ*33kUt)-kUFWu|$ z+3f&tZJ0U|(RPdA^J!8(2uMmCMIbk$I@rT_R>5Ar}OK?|6iLB=kA%M?kv$cV8Peaw=dCy!)>t(uxfh zqRzuPinV5UZJ1P9JgYafCFb{Kxv{qdb}Yo7|4`RQIb-?)+FXYr22Ex7ykCy$5~t{c zfGWq>?;x*X0}lW`hOWe?hak&u>?HAa#qdknsC9uj&0(#P9HQxH1gt)m&dyzT+<=HN z(B@hpMq+{eZNRRS%-j#aUf&94C;&ReoY$tbt$21sNR$-QatmpQg)|c3LP{^x1deJR`bm~cT zCGAJMuN)G-;M7kmt;V4Qw4-b;7z6NZ!xqkPT0uwc8i0y2)`oy@aGTCuhpn~ODRzhc zc*2LFD;rHVs1no}Uo@=2q{c~~FZm$Qk%n4gQ@ zkGZE{IOrAj6UUb!bw1kN>lK@|pA|6Z>i%pI4@ap4_bqe@=7|K$*Z^VQzz^X(+u7sQ z_I*te5Rt-+uzYy)^=cSG>4;Up#FMHjmO8>2<^4Zsd%y(1`G4VyHh-;~alx-iodz9u zIey$3cEpvzMf-*ta3eCQK|IO2Y6*d+^ba9C$UTBr^;hKo<+f%I zraZ!~0X0(0S3P*{q3x(}4(+?1a+=N4Kp{==Zyzb?UFL*n5M++#Y~^8vo;toj_OWqD zW@>tvR5Y3kt7t~|JU>yzguY(a`HE<1MXom}DI!v}SXX+H92(Hc+cy#UART8lW?r}< zW>6@H`2m=Y1Nh7Z3a1fZ_TW(c`bYgfEAyKg8BgZ|)Yjf8Qr5L@1kY!!)`isK4?hE9fY=pU{q zB5z?}AMAqOoFQ|O5{aWV?lTk3e;;vIknQ5;~e zC5Jf3rK;0H?(?eaX+gIt6=@oxw2{vX1r~pd2no;=Sy_Hau8WIe5SEVi)(>=3RohL< zvjDlfw{4S$xfz=(945{xbH5dS;rkDOP5h^V$j@HFC4wN*!&9_3mFiqUav^lKdg1lw z>f}F}`IM5dZ{yUjtXo1A0e`A!;r~H@A;bP06<6xp@w*I$Su-MNHCgOvFimTOH>LKM z?T97Twfv_KZ^p4G*>{Pfi8y*Yi=$Qv2`%8*(a{6Xhd{Cn%0%70<_v-MnCMB?v*)t6 z;#KX?*?{vt8LIaObkiHC4a-P$&~CJZL=}H!v;Z2BV~5h*ehV(i$8o$n8> zw@h^BbTjpcUofTIO_f>e-Hgi0M|D3}$`cP_1yu0qN^8B>_{l(0G2qdAmCxRV&Fkmf zppGXQ6CGSnlbD90)pm^Q`AhGEi&$yDSh+5!6w4q4C&FegoKINnjO7Ub4*b7nO$&4b zq85ZMjrFs{{7Jep;)K8cS1CY?J*T1p#o&XCxn~1XtJp+R&OiSfv!*%rq-`X5%1EDk z4!6750D=qt*`o`-Tsl7JcGZu>TJou*^2wKyB&NK-3O{XbOE;cnldCMKL$*uQ{Qn}o_`B_~nq9GO4q=eBZ z-b7(Wtm=GV`7bVrNmQnSok5olmf_av+bg+JZXDFU{Wnc0w@c#1ad%5*FfuZsI04EXu zU$*Uof|DNdxIwNdzrS~4Mw_CO0OHZYrMgrgii*3Ww|D`$BCAVMHl3VAE#q7l!>N!zt*UyXQKXWN{Ubv)ZjFz^?Ete7Hw>rfIb<2V9o4=thzR zi|sXQLMVrGS;yD_UI?}Ut25c0a9W^ouA6h+k(E;v2YfCodu`r;VovQc6=on6msXg( z7w7TLX%n-<6+;1i19Fj8T^ZJRUj^vFd_ zx&zLJeVfYIJ3+E|SOk;g)AlXp<~J-;{BWVnR;oax&1mnC+Z*rs(QQRH%Dm>>^)wl| zf%A9G93W>d48US&`+*Vwo(Pb5?SN2}{)C>TnOeS@m%2P(lJYY>!Ili!@uijt*!DzG z9V1EE<&qHRbxMl^?Hde{I*p^S^}yxVY*(d2e_KKB^+E_jxU>A7=iJ>udRbZLwS`q4 z4*^c@UHS@BN|Q|wNnH~J7~2*RcfdVCf^rSCQ0OfZLW|^B33v>z<8>b91k(>-6Vm(oHIbH5 zPBd~WlIBi&OwAqYu7BXSnuzMO&9W4a!;2o}#BzXB+2nQdX9wKsclw&%LlF+-BMEZ} zF%wEGs}vpn;7iCAHEy{u{rgfd7iGW;8=@~TbK%4jJm-QI!vTce-7 z$phQv;kr^6sE1dpjyNqgO4|-bIXKCSz4E#qe|Y3+pI$D~ae{0is>FzXYz|W%lfc*_ zp)r=||3kp-KI0(E_PW-u(&ki8Lcn&^L4QSFIE7nJdM%cvQ0YUI9Vo`nM4K6OxM~kB zu)47t`ptcCj4MG!Pzf4UQWfmbKK)CY(!%s}FC+X4t2ZGZrX5r;NBcpGZ3-T*jei3% z8sAhcW&FAVG2_p(Fx~JX{&QM81L#$LdP-9~BKhWMrdQ!y*T9Q`CRnks-_tp&9d;H~ zwoaj&Iww!=sW*}6vcXzU0^WRNsq<=Ds{iO3Lrwno$mXJOe3|tDM5*zG!{4=cxbem? ztex(p#0Um5gVF#ZQ#Cf=rIGsYW%y)&NA>d%Q#2ijAi9-T@aUWhrmxpZq_CN-JTB)I zJ+E|-C>qS^YHY3NNMFHzkHY`G`+z?^o)#w8X5cOX=rdp@qYTs1s53z^H3odW zYfn=;?`)6OtmgZgK?tMfHo4(b^83(8Q?<&vWQyh<-n`q8?k%& z5(>{gCM9~=({O=2yr7M_eyuLv)TO#oavq%*RTwA<&rzO;M1xr+JD|z_y6)~sjGp4M zYtm>-pHZPWlkA+{dM!`VqJ}Kpl-p8+S)SXNM3fM8CNgQ_&8-ZqJU-SeiG#!(F?b9x zu^3$n;_I14MP|3wypGRQ@z3JdVV5UBt$o5w&0gO2)uW%ii@4Oj+3x`|D?8NS4~s?Z zEExzyU=N(n5)a~XA6U&;z`JX9t7V+Eu^uv64BItMnEg>R$up!(Q} z4)9k>5yBjIbtg)$Benlg%7JjZo0!qA>)vV+<0UT4Tm_lH&QS}If|~o+@|CL;m$Sij zB9-2?X8O^F#l#bj#2`@e?q*dMc6HaGOk@8gWKk!f0>3I|V$Dp34WeFbW&eQ&rwuNe zHSKH(TC>vpo0?qEPuJnqVMh2vrfh*Ftfri6pBXj+QH#BNoQ-Q*x1K!=(}*`JQM>bda&qd-Oc6S@ycqz>{hGlAGYy#LF|x8q(vJWamen-EG%JzB}X^ z$}00aHzUG8@{x;nYB!gp$8*xaxha$MR{=tf%n^S}k%vEL>?(wjX$+Dx@iO=tEGA6% z1EoJ6aaoROesk|t8I=n>qyPGqf@#ZQlH;*5Sl+deG^1$iH<~{;yU%bhW*rtbi!)%= zgSB?YG%AA}UbWDr$g0Fq%$~)pa3q26%w7raiy|ib`E*LMvQ|ES(5c#np`ngjUuhg5 zZD>%eQzljX?ekeLy0~Ub!FQEg{L-EBQ6@fXbG-u}-RajGE0*ZB-G*az24e3g3edx} zj`KL%r1Oz55mV4@?4>yA)x1u;?Lq|4o(I>sL6*Ss{>%wL#Gf&oLG!NjWO6Xw2f0Ss zNIzJ7vIzZgDc-|fMg4bMq$9k;b2K+CdYI9Aq;O-+sgFq^)1}sI-^gD*-39y3BSr5C zd$p{)G`RW4#-RiwB%>iG{wSma-4Kz|II=DM?Crre?f8ZQ7YnskHYJj8qn<$rMN+WN zAluoMXxzA?hDnKY8|rrM;pXn$d0KAt%upfeFC59OzT;mr)owDq(F}2WuTAuf^i@?# zwvv5KUJ3Rn&KjL*DVO&16+1fnX9EasEqOhy9_=J3kBt??7QDeJ`Pm`*6J_Kx@WZvfE1m)gEx94A@+tdb{+U|)L{~s| zUGNx00l3}-;CeZox2f99F*3RjPmvqJdxd~6ZeDI--B;UCD>J<pmFYc?K8pxfs4JekVqBLBJ6wG}Uo;h6vR7<@T6|zh$G54&2wB14fR`g6!H0zD*6| zEhuskmpiS#Q7Dw}VXmV8b<+T`igeUrMc7{bT`#l$)vphmKxb$K9bt&;rS#jqdu~?c zwnW>~?7t!ic|t6Zi!?Uf9=N5byhmW81vU4h9S3s#OsWAe3XK{aM_M14tA?YX1+Fj>cZ*YTm}XElJnE@YG0pi(vF_g)I~5KR9igym~D2Lh3KzEU(5T^ zU2V*3(fYq;cdpggaM=Ns44?t=_^|nG)n;4po)-Xsq#;=oBypgoOd`u&LX?V-aAFcW zX-KfmZUwl3Q(PUY-a$X3(C)YF?2u}_6nH895;@Lf21OnV1fOyRW#Elb*6bUo-A4PL z15hc)G0Lrznpl=3dlg$a<0M9Hud^mmU54uk#QF#50A3zF)j&RHp`gq10w@_SnDIrs z%JL9Uo7Yg58;hKLU#v|f5rn1b8Sw)N?)SfXln12XC!7*~2WPaKjx{w|kfdryT?7B7 ziX2)j=4x6>S;^8U73U<$x?(+DEcg|QEqW&jVuFTMPdws-NION`v3g78f!U;yzPp$p z{^&q6%hAeeh{)F}ltbic{5K{gFKWHKC$?Wx@%2htg`PbMxnEPAxM5}!a@HPS7BSPO z<=qQ&jTJ_mFKVBIwo7^-{uUg=a)K+N*&8!*MTXLb*Wo7?C5M|Ppd zrb1(NguqBwxgN@jkM`x!#U1(O;XE1&8|NSzYK9bL*}P*RKvz#z zzb5}*elD)_l$d#zTed|s?&*OGJHJ$W7%lNJIZeF6P$zfzhNRWS^ z*w}&&xhv`*s!kkuO$YwWuh-xon>v+jxV>ONB&{lG2*mHhEMx3kwOPvz7u2ygPjX+a z``vyRo-y|(@yNq*;C$-hv*zQtD-h|LTQ^he=pKK|bsAq+thR3&{GA%U z$fwgT0yUkSZ@;@|f3x`lmtdqL+sedns>KmTxAc)J#h95N3$;X!TMg3={nzMvd#BNSh`a0 z|IHos8TFP~++xN}|EF{1wz_;GGdW*)nk_IYoL&OFQym5z`}QDAtnq)l&Q?d&oLS z>5ynD902UG!o{{0sU6sxPQu`$|E-ZmvB*uu7!^B!xWIhtA)<_2BiG^1|GQrT+HvSW zl0FJYI^1pfw}`G3_htbV*Bk-)zhTd4Ck=d!Jls+}7itTH9>){6;57+S{wvWc;ph9R z#wQVZ{Pjb9$4Fz|ZJlBPx8l>fI-rGcd>&>F0zJQ2I!w%4_B?Uzvsj z$O)XIsY0)2y17ICsFNW?Ds=o|8R;l?Jfs6inFjfJg!fxQ8w?O*EjW#5YGqin?sNq$ z6Wi-}rL}-nXCASnupfDJ-s*^njvE1!k`QX}=l_I<{5l+KlJpMrN7~?L(WX2VQk@6= z)t4#=WBYpnGl4!S>{`!DBwn>qfPvdP7HMd@KvEcuVi!L@IUd@z&FwO#xX~Ds!(|id z$Pr4P0HeSzK@((j3n}}FOCXZ(_tRPLs7f{r)Wq~a@M`0~S^kWELi z-tL_JjGnM({+?WE%Eww@Wfg<85WbHla7aaud`-N2WPVKKN9K=%Nh#Iy@bHjkqj9lO zuO0oC4Noua{eVxg)hpZ5FBLj8KkZ24pO+ntV&{&-miM9UL9Srxp{>fw&fd%UK~bU} zhKLEXH?xf*rRjOagTso|elu<~@bE8}vC((+B)-k-gLWKMBuaD~54>1Cr_6{gyZVT@ zWp==%=!HO&#e`O3%#jFP_#0sf!pZ#aT|E@dG5;zlDjxTE?6K~b1rol}DrGKlZ3Ucx z-~qFD^sb^`AK-+7WsFYMGu`n%;Jycoa}eK)N9MAmHH@?WA!VT(`o6+3Re zl|)wu=NPE}L2SY{{8>NjlXgE3>|n&|k#00%u?7`McMWxCrT%aIJT4m04Xv>oh+kBK zcWPfajXD4r+14RQ99{{2fjmySMzd9wC|}7Yz?-elSkbl z<-TQ@sNiG*k2ouupITQ2<-lLzoOwDzj}qv3_D8l1a}QOorhWozcaA2M_@XOC@u~cdOhRJgcsu%(JP`@6v+rMV zr1$<1H=Au2aKR9NY5sJx5MAng*no-Fq{_%9LUxlBw%Sm(zNnqzfTmoVnW{S0YEpA} zqdpfm5^yOUfj=d$sZQ3nq}L%is)&vNCJkOzv4Tq8A31tdp$IR{b0Y4ZW4ptR6eI|0 z*BsYscKh&96C#?v?QIl>5R^wGZbgFCc=!TnlAeYvdZ*Be&ojS^QG! zN2A_=3~ttQJ2=nLc*ps4ZbJ|?McO)& zoo{qJf>>b@9o7I6|HLfjT5#!K-d=k9P*t=KLUX)J`}b-#sadK6EEka;fHcWjkm=}| zLo`f5>C=VyxN!Zsf+$4?rp|m5sik*R<+@3mbTUCpHVo&}xzp?*svd8grIoC)5E(jEv$-DOH_5yJqS<($|3@#)tB}he4@-P@ zZr2}z5kysvyKXlj;80;%ERAW@$qe&nsQ8?enlyLY=|Qd!*|6ECaW=N6Pvy@4{@-Cd zOWZ3{sGzSE-ItG5WB6;OfwN!klCH3>arg_l+|CeyoGIxh+BW4dm|PiT95;c2AlP@SK-bbsmxugE>IwxZYzXtN03DKZ z$ks2beoDRA$+CS-vaUor>Dn8{_#7P2y35KK-Idf3!tKWCX@WeYkxO@pv*J)J>UqUQ zT#NfCmR~z#T_;ls3T}&omOWE+e&IbmZv}MKo*{JGK6tGkrb@JLJ0w$VBY=@w&jj-N zfZ|%!;Ho320()=ZH{G?irQe!~TA|mpVrV9ywOFa741ApRd*8PZ*EG&$6tMbZ{Jk^$ zl~=KIa4Z$w9Ix!{R8sm~QPr_Nl7!4@GNnqB#e+-nQ_esU*W;(QqKlTK9DEK(xM-Hh z#|JXWlaqz*e3m$O%xq^Jy%~&L9<~z!)nib?yfs9kf9wOw4QpZk0k8|MQyD%Sma7(sK(chgnA!(?#hgGl%&_-5KUdK zT{2kZCo|JDPSC$kh#@_-5IXDgthfH>#6%r0Z$N_j1q{AfzIt2fr|SViRUdIRm!wFr zG!7T`r9FV`ILw-<|z`k2kt{D48vjvxof+9%&A2b=(aOmxh7SvqkSJz9eZ+5S{U2yUycHZyYV>-EusfyoLW@k3Bx8jmihd8E;lX}-EnW01n*6$y4zD30gmd>i?#<$hXut}#c~RA^2T5f%g!!A1 z9mv9o|B$_vG*AtN^!9QYAV@YyrUqgCRX(|^eN=+v(x@`pOl^8)2RQAU?)bh0HER8$ zKy5>4KL7{roTwrbMwC#>y}{J#)BkUe;L(p$AAk%UWHXvMPX4AI-1y##7ZXRt3LoO- zRnICe%bJr7`*A;(-)ym(q5%!Gjeow4P5Lr{f=TzUwg?S0(DKTj6yvP_uh~qz!>^$i z@OMbUvq2@0AB&`XVVsk>f;{AxgG6nO>c3qROfFaz}w_>x^ zQkR7ZuC6lihTYqyjN9RN?-Y+waIzJ^0~*W%km}-)AIj1$T>Wn|yk*{G29$lej4l4- z^#}lyz8sQ!%1(dWaG(}NK$%zdg&*_J`W5rZ71##!Rr=;8IwPU3U+Z5Yq*ehTr_9(1 zmpT?F-IRIJqvzqZ&%#^(ER9c+arzNsn6hcGDG9cq!L#M zNYT@8i~y*ec~_f|#jV)-@uZ$7ny%M!&i=$y!T~7_sb2I$ngM{*jeKdw94;;FSRH^VP}7(s8qPU}`G+Uny>u8S zPIZY<`bMVmNsKt?9w5ZRzfD*uj7Y}KMSq%b!cl2Jf6=&-RG%~sG6MM z;>3C)#g|Myo4!V{GN^2&^tuFz0PkZ~th4ZP32DlYNhPyx*24kdK3X@vqqY`6UFco< zA!+84gBeJMB?nEwBbvM=^;Z}KeF#{iai+oV#7Wj1H*p3X-o@gCYn6L-JhC9S_PA&n zW=rVd)QM<5ld0qY=Nc7x!W~e+&cBwI4uE^s;QA6iE`-TYk&U$B1f;wAyV~+azD%oR z=#jvZD?cecn3tCA5Cj^jDrFFb+|IdQ+m6xPv*&<*W1)su$^~(o-L2>^=?ZSw|=O98HUnkti$SS1}4#*BJO`E_8Im% zPVt=zJMO=n7ah8gPi79KVc{{Jx*J+Nq*(Y{D7WE!n~WlwtapeLni+KY_429U^Az)N zv}tO2&HO&Q0IcISl@=}>K%M6MQ1gx?%GTovzaHV^M0e)DEF!x4r&=lM{mIujLDmyx z@@#KQ*2wm(Qxs0eoW1((cza$QN~#f+Qt*`br2X|`2EgxEzaS8_iNY?~@bpNf#q z0P((OM(e^f`A~-$mk>=yJ(wgl;Z(Lr^sjOL0p|^U(W^yiOyFfi8KFQ{Q9^RJ%Hu4^_k#(}y6Da#lh*!^e|20{+nT?tlW zc|xmCyxZFI8D3qZR)!jUgKWI69~I}s2KQB6uj8d*Vb>}N@;qG*Vk`QTE(!(dt9?c9n>xI3-|P9;#}4`vqD9zetRg4C|Hew2|K$ci<%bYw zN~_4Ofb6^AF&b%VjARF(wb1m#k8Aj}u5qt9K*+%BdwHV;3&IqiKOUf9>Tkx0B>uFt z_M5+ecofTr5w#XGU)(0Oij*~d)t8&Qg~ZvxKIkB?#CC4`vMJrJpH_Y!>}(1dDV#9w zHy2Kx4LUt0|9L?#+57(xqRl+{@5m+A-;)i2MH-9x+Ihb=e}>z}47buh$n7a>ifeNmHaiP90ZkZ5>@+~LBm{Ed=+l<)jceD!}lxkS6&w(m#Q7+5#z=VFmf zog0%RJ8-aQd}M-_2z$3ZNG?k3scH4epqyiP&0_6=PTPXsAY{v*adM|~VWxRam)?~3 zd*FFHQsw|Xg`gxi;Tz_?5hCp=;i@1xqNq(GA^5_i-Yw(2eB*%STXd56lyHfsP8z|8 z;g2G7s7rcpMZE>*blmQ_Hs1e-fIGITD*H=^ni|d~L^>T|ax6T=cQ`xd7d?URU-z3M zk%s6!%0P9*9Ejofi1?(ovDoczpwbLGZwoTY9O@>K&HA{aV#nnDN)iMjO#o3=ZpryR zf37QWQ&yu9Jq&gUho0-NC z7-Y!A%Be6!l7M@0W(|w0g5NpJ5(v1+>Bhw5i7)p8ndZw$aESyC0$9C6*-=O#w@ELm z1V%*@0)<4$kE$E8FstBsmCa$aT>D0={BD z`%f%h32Ub{%dFHtcf&>vT(^}cR}Ys(lYV(vnM}Vc|CBDUi{Ra=33dZXGYVqM^{A}h zbR7>W`;-ez70EXoV4!}492Sc?s44i>vwhj7#4`kbNQZL})v5Ey@ra06?EJ#qImWAP zM~4xuAiLg?MJ4~&^m5}t#+$#p$9(!VLi5KUISiq~JMq;!GpMW1Q~m^C5q`a14!AO*|~ND^_er+E7zhF+fJLXchg z%ro|URy8tL-72(QwJ!bYFS_6cXaL3H|6C(l$7gQt(MQ8nuPL=()P42+JrtPj+0aHJ zlr$7|-&P^!^mPQl@}6{HLC**;C)BO@nz~_L0iaxP9OKq@#vS?;^CF#MDxkjrm0FP?a)pyagL;^E)MU+LC85N0_1! zIiFwz)Jg7SB&2M$qEdq$3p_I#dze#?&;xf;ILyCRCR$oo(u`fFFx%b&fTj zU5?Ho1js_IIdIt96A-rmFOepL>NWzj%O3X)SR->=_+n|gPJkaj@AkcQ6+YBl02F0p z<6{t}soHRD;qtr`2!_7C`Ss7~P}LKuO%XhMdLrp(iGrQdK57%NN)^Q51<0U8_b%hx znEKf7Uw`*~00o5L@;I^L0E19=&b7o8vQFkyia7oeIa$2Dkf(G|TCMGb|R zUQyecRfQyB_Fy!Q>5ObrS#u&f;x??MmG-}-i)yn(Gc=P-F85Ku zS#%km&J|4AIi9T*<=NwXdk5Q2&6SeaO%R0mzm`1}{63-xZr#ILtn>n)G$z*T?DjX1 z*!?6g<B>0)dQGeQ&IPd<~q@h49}$i!#U z_yl@9SuaB*Ldo;}=w*oB;ditqc56`}9);LP>|pQiKLxg)L>v_(k86 zo7^J+6YMlCU;qG;`uUs3sS&}^m>n5`(U=_>fzg;98G+H59T|bqm>n5`(U=_>fzg;9 z8G+H59T|bqm>jag+d`s;0000003*i$00QU(;fLlk(zU)l^Qf!Am>kT|e2X(FD~e52 zx8QK7&#Z^JQ5>yt8R9#F28h$UUp%+GaJEnI-;`P@t=^z&HnXGIbFF&QC$~-a4MvO zy;Kd0ppQ7l%tor#i0Bv`%@dz6H=9{TV`we-dGKI&?YSD7cO_qGAEIQ31Y~#?le{)p z7o-n-BHaSPvz9B3mXdnJ1@(f{G#p#d`GP)y>eO0+X!fy}YD&bAoQyumnuc0VC|tBO zXwx-*%J0K5d*bCj`qGE=?!&wo=9r038g?8#C@JwHp|{CQ3Uu1c$nie02s?GGKjl%6 z7p!IiPjm!O-KZAm`(u9VEGG#kd0&l_ ztDbI|c^o10x-BN6$!W*l%p{3_RD_#@_*9P!6QF<#l(hNS1g-P}ybVMW*^GqErw?T_ zI!71gdBz?iA5I{<$V9{fo9kbfT?bcwf(pN9~=u=COZ4R>mqE??I{pnwdLX zU{-6ANUe~E+@8$V{|xmNl*c|~_iVGMt+-oKa3lRQ^!` znUNG&*OG3%UPC9udD?CH_~3Qot?)TtNcD1{Lz`)IX><(aw%K^&(aFtFGLyJ*&(>OjL1a51% zd>eMw(6|5rHqF>5c|24h4|18sp1ta~t;(Pwe@(XyL{+JU9ub?k@zyf)#Hu<`q#K6H zUUL9)4_P1$1L>N=Rt()Rh)JA@YSaaM*lm%pkVHjnkXD6+qw$&wf7WU+OiPr!JL_?v zGVERS3zq=xpH)0RsP>zM_$n(L5}e3vPqZYi1zXkyU&=R6!{WOK4XMo0IPeY!*`w4%Li4AG_h~@`^R3e(!d(l_hj?vyztY;=GJF z`y9UjBf5CpFydfh|GX!?%p5v#t4t+5TJq4(*|ZJkh1sIE^@K{>q$A}UO1r2ae>bo% z8ZMVXeh{?AC`G{ajJHKnwZ<&xyQOZ*IC^3er9;bxyOk}r8TUKNtq!^_!MWk&ep*NV zBW7(#4^Bv!#3KPv-x3i0HqNzJ(#WI+bB86?l!UN@-^qtstd(Un`9a!Z&0@$L6r3-j zUSXZYs7Kq{0g%2}vFlAkghO`JwH{81NNCl&qz9V5RK*;&Y#!B6=Hs`L-EC3k4#-$} z!FQmw(Af?TS>pJL;^N*}EYrW|o+_}UrL?-n;n+(xg%++Q)4Jd|16@l{W2~yvj&tJ5 z5Vle#XjG)nOm7hdG*UFNHD3tnYNm*>tUjb3H^aWs_l=Yd{uMAND14ZftHdt;5#dcl z^gyL|vNIql_S=pd^~d~)WQqTV;_U7N$^wye$(C#piA6K*4OuWT@vHs(8YA)X=qWCOPsY={OneQlIE(CR3eaTvWwo2FX`mP4gt?RL!0{MHpZT}_CAV^ovRenqI2c;8*fU{T9fTf>>V{TI?)-VA7esVp|7 z{Y!vHbPa2Y+Kx^AZ%tIJ2rT8Bqf|4@CO#KHKA{7#?lnjK9v*R>z16T(In?nn2FGfWYc$&Qw!)zj1^w*{&K?=c9_1goZyV_Kg?h6~QwPYKQM39E z#0l6d$on8%MykAIF_62Jw%-*Ye`W-jS$iN=JCk5Dm#5ykPmQr|P%m1BxHBpqzIhZP zLjJ$(~x!dV8 zvai~Bo+jika}DSD{zRn@sp)!Oy)aP-t?zRokV_DG1>+FY zqVduvW22X`j-2xkC5#b*TM%My;Mz1GT!7WKve-32!D#*9F5+h~izR5^y)1%gIIPeZyEO8%2@V}Z%(I`jGmoyW04af}I? z2iYDCpIXb~fvB4)31MZiFdP=Y)RnfV3hL$o;eD&chz7Re`31>GES@PSB-}t4dC< zqYiMds8M^(s_6_k;&X3Agnf#)&GFjId@kTQp(eFA}437hoI^Zy3D^5Ep!hj&Fws2q0yD3D1Y9W^+ZKY+vIn@zzERT-vWzKM zGzeO0_Vr|ieLW&l$R8qme>by-{PNi`wo7ihMP-bTyuaJ!F!VoKM8;_sa)75k5G@35wvo$IUHoL@3|@0Jf+A z01Fa9N7SA=Mfh}cK?t0HBjm2wT1AA>vLB#KP{VWlLC80y10JjswOND5CL$i4jy_yN zE+Ll?%ZO#fGU6F=47i3|LoOkg5X*>vq|AKUoYHh187;@)$tat5Z~U+{ix=k3<;=wl zPShq8@EFeoNQ!jEw4e0~2N(J%YF>w)gP^R4q&zm3RHsWdbCThRWv zN0f8s(g(Nw#WFy@4N@oU@%l08Zy=|VF})^Sx2ofFhAp=%E!B5OHHQN@(XH|y7Ir{t zc^$>l^?yK`=_e-i?E2M+TSOv^JWb31abSh<<-(^&;>pTwwm<+27!U1Y*_o;j^Os?D z#Pp9Rc?mjcpggWql+yaV87vm=pL+m5*b-s#5azmk-^P}h6DyA)e$yEp73YJ;1rgB^ zuTq9NBw{;n5W(+T9{WLNMNRxCjHgIX0Mc=gZT!?4@xSEa>jbXv1##IwGlvxzt&Ie* z30`2upkfc);4BvAs!}4MPACt;=a)M`*cpwXFy&u{8@PhD#-OP8<;yv{DGpc@zyk$8 zK`@X%c33f(@l6cwmu$)Mvit1!7b;|$O^~<;e71cruc^9H#0z7BGa|_k{_78%o(?hN zs5-2GMxmMziKY(OB~3hxFFOLd=f(;Vx#5XG{#(KV={*D37jMm}S>GBXlo+XI)y14tqkENhveT)!7I&cje+0%y`5q7!EN;q}~rNGa~gCU&A-v5*|aZuPBwi!sXTK z1IeK76m5BLq+WaCh#b>wKDfH24{`?o(I0VttFCHRCB!n-5x4hZ00#=ZH&y+ zgELvT^+W>N1?6Oayy*0jzKTDA>XxwN#E6sd#wS`?=jB;&3BE@BJixd33~cnwVs1O4 zCYBo&VZztXX6ftR17pw)7jEG*#9aX#ubr`ryCfru>X>QgC%{JX`RXrGnf^wzIdJ;4 z%pLfm`-A{|KP+wsB;H74VK#OQJ2|`R$ON8+5F{?*;*cV$01(nQj2^e<%D!BIY}Um> zF!$kt9ISs1%eu%_IS1sF9KmAP4@Jb6#@bkU=w|)_i)ffQ0%6kGpwK`R3{!iSkPpIE zKq4vu0-ttr+BDKjJH2QFK0-(h zk&R*(>gLDh#mC~v0wC%G-lwZ&f*z!09kNg4Anb;aA$|NjVvNuLwL*viG9H)?U-5B{vA>4_dl@dhn%MM6%S%YuE7K}*u(god-A^{I9jsrvd zN>SYE;7umtebVJwn3)R;(_piB_#_Q{sUXh{59{qN#nM!cV%vr{-6p2ST)kRxkbyl9I@?IU$T_55 zsv14WwVy~wuJxx8Rh}?vL8~IG&usP=U5p{KfCA)xz@C`Mo^>Mx^y8cqW1dEOX)G=2 z#4>-O8{+>V17}1>LFVCq&>Q)N$3W#SxxRb^x}SZx3|lNnFC~UpWT&XoWQsmhd#U!~mQ-0@Bbc)+<}!bw77mGt zq7*funRQyd410Ex-1m1^W_Y8^C&8Qf6c_$UZoX1Zz^rRl8?dTc8DO}&u(qiUODkg# z#LmY`D*jLK=^@;Y$@N~N>HH?l`60o4ho`QjEO&6xj$CRz*jJ`p6~SOFRmg`=??Y^~)9y(TS<&=Dq)HNwQbz+t>P(_4nO8S5UE!@ri; z4DcYxFv9EC&@()`pjeD`GIq7@+e;O%R#aW`)st-nZ#dXP$>XTzUJ8H66ypQboZ(lh z3VLC85N9_N(HQzZT?7LG&03r_Yff7^<<@BV#t5 zeo*90l(&`2K=bfKFd?yCX(s)X2Qsf4`+^b@aqV_mM8>34B9#t@dCcVMichujOYq8@ zLp36c@XDD0i@x=6_^I8>C1C6qxPF_`esTr~zL$9cYSDG{O&oBP;+%z(z(oAv_UF(6 z<0s&n&C5$q!$zG?Kx#5ZdIer!8u!Vr2J>fg?C8T?oqZ2uBmFb$ki|Kzo*_XI)vMbiw0yt!n2nxQ29N^ItC1V*`Sf@j;ta%szjU^rpczAx_ zFgCp`4f^(Z=Ka=4(+6^2pk1jBSCfBIrr5ahk?pIqFZJKt6Rp0E!oPFAfaJ1-O*!oR&^)^~-_gmd z8S^F7)g*5J741mtYhPW9#tAi~2;ZpCE9JEj7$86jO9Yq%XcY57B(@K@S<^elvHZnJ z4oncbC7)k~iNwJRqFMF$Se#4{x+R}qg^9$$3!+)|_*k4w5V|FwUxkUp!3&~0HQdI7 zkD(2;{t(F0UJ6%UCy6j4dYI~2F-8CY0000003i_RApigXRkCyd0*+f@+h2aCS(8QN z9YvqS&UE<&`$+^(cUI(O6uimoC3KN>m0z1E2m7gr{HWE{)vE;8R@KOIr7KpvEYXG# zSUhdJ*Z=|;R^fWM(y9ZEkxDV1k=(u0v}*2UFAu$fo5(X>gB?M1UWtMZOgMdpz$M; zPLAyzpMbd{=tEGhB1&$ze_|SZ$HewFN^S%0eJut(EWVrNZ7w#>Q}5^ z3{{64lhhd;qw6}QPdlhy50rY88vE&q(JN7@pgTORp`|1|<%qk*jBg074#Q88#3^u> zK>`;MNCv)ciG-HE7my<;t;z)b(O80bsi3S8<&kc&I0FNF$ns}MJ$2dpeYl~8ce4U+ z@$Z7zAjjyL4V-NvQ}sgG0mP*P$OBjmTkQ4nED!lY$)i0gmQ0>bLSOe z>DFM1d~nx@wghU0JXoMsSMA;iE{BY3B9J3lP=wFEjBnWT#3ti{)D*rSdEnDbokUvc zo>r_Ib79~$_%|vLd?^MS*OV6tLG^t)ojttBaDdld{y6V*^2n1_5wsOp6g1){^r3yy zZ*aX$lLm;pd2eSk-73~WR`b)GAeo3C)_?Bl$M{^N|kGX(i!#}Mhki^}G7cZd=u{Pop30!pp{6TAU}-0(gE zV^o?w^1Zxm+{eDf{iqk|K!PQ6hTbuyTbDk2MORAmP_rWy{=Q`HH5XJm)CO)&q}~-` z*0b5z>Ka}KD3KS1k7|g=ZMxnnWv!xpX4=(Bdz?DqR>8B z?68L9w^tYeq80S$m#J1?l(4h??TR9OE%R)Y-foDd^_E0*W7{xds-%HkG`7sSIAcy@ zsB4p$TK7ZQLisU@Kw@LYl^-kUz=UJm!Pth$Oj4qycv)7T78UY|gHZskQ{964ngUtuguW|*L`LX^ zDlQk0g7r$MKnN3bC5LeoV3cRKBH16xc6GW2sLSM1_8PrT^xO%|P9uE&1aIxTK)u84 z2N6cuxq(I;-5&W&aTCR=XhDDLV zs7)PnEHj6)yIMV#>=~N8G{}D++3pyv?=59on&= zJKU$$>j$;N5NO6NBiThK(7y$NU}gh!CUYw$@=oZpT+8J+{<=miCutJ0{K?cp%YO!y zWAX93+gWnzb6xk^;w%p(=JjrzubIdC3X3TZ56c=+kB8tBq!ScJfu0Wb0`juyc1KXRpueZw4O#Ct;ph%UkhdTU}Q40OoOlNkm-%C>kmuwd3{fm)jvcvxf zb#=^0KSwRY0N?mBiEayzC?Ozn$8C&!ThBClJ94*$GEXzpazM;M*_sZtJSPT8>lodtETFdA*ka8i!HY! zKmbN3%5yG;y~wwf&xU%!683mBaO%!KF^5u<9Bv9#COdD0<1<&;UAT5{C&WnyfR#0n z*Z^|u44|N+t^M1jz-=!C#sojNPzcN-C{$*K?VDxI7A6$nOev%sM?GlIQO6tVk7}m9 zhBE`t^16_>1;5-yM?WSg@+d~;A$!1VaQ25?hIAut6sww=p!24HV!617P^8T12#M)O zKn7cqdooN`KMF+TtV24dV!3WJC%yUw;Ld}xLr%mO{aNF8vHHC4* zFt7ez!&fTtR|#QWibcwki2Zv1XKO=Rk&LZW#Z%?Nr?S}9VVyC-DbKdiI*o%YY%B-Z z`g!U#+z*f?t#_E#@6X3jNy@3!YV{Twy3;$*s;5iyWP4$R!d}AJ)EzRVPQLO5=0;57 zJ4p3GC3yC~%E7WbjR-PY(u*>L9521Cef#-qJ{!hKW23*GmLnWzf^YssMyqIDB4Pd~ z9l8+mR{j1Ud%W-)GyXhKGWvL}fj3GR$CMhk(*zW&h7=9_$=%YH>s25Bbd0GtW_w=9 zBvEPT$=h!fK-}L5-a(Nnn=c@GG9ScjkCm4Mzut;|=l%2@<7cy&>)C9zzvpFrHDw9& z@pgS)&PAfp9#8+DcuFxh5{W#^=6oihLBa!0h|0eRS40r$H36;7pw!+^wJg%{j5m?Ms^llVHO0<9W`aYHw!!i7?>-kMb zz+h!Yk=#(c4RDg1TL%8hkqPrhvh?(VbUy({TCWRQ%P1|4;EN@L2`VMeohp0Yi?3uF5Odl#I9^Z5?Wg^_t*)Ay7HYerAD7IPN|@t@wr^)L_zNa7~zgvJn!ba z1w3iFX0Tpa<})kmSf>9q$~*Ip5vSPmNmj1+>y`8l8T?B#H#EUAW&`7888@w2*Ho`| zlYDJdo?If$u+@GCnr;$HjktcnvS2Lt{fG zs4Uf?-TFznlpi5c6!yObv@Z^384yuTDE;LBc+;vctmE}Fw?8sDs_*8W;u(haW(UHO zPl8`o97i?KgqwtCIK)Y;T>MrE{0_+o)i)@h=oUK6{HVW#_vbQ++@!xNgXyo)63f4Z z!Rhdav!$@X`cD%#XYB{mLuSmFEb%ZAEq{Ra{-~BO=*WIhbSK2bSOXoSxkN6X18QZy z#615~h0L2eE%&&Vwa!h#ZAKRR3O~A|P&2rW6s?%DHWrdJ`<0tTf3|r|137Wj25wq4 zYjif-*j#711u$$U1@EB*%!JOb-l5YnCXuyFoJ#5?PQed$n|6omY7~{PFR|`?C%MAz zsoFYJAMFcQT@~g~=H-JActO0GxvGLjIx<9*=PiN9)fcVL<4C5+yoQ#3fcYULjr*O0 zc-3+Yo4S%1p65w=MiD~IOP9fT!iOjgnrnop>1AhnN^hmRO}}~(SI|R-$`!+1wa8Q3 ziE4)KeUfpTIptPA$_b4lhNC|Y`j8wGPeW#pu=z)GfIftR5H$#h5*bn){SlPrlnISB zNRUY^6wx|68~GTnjs#EY!A}$R_GqEy^(BE^S(0(btNb?=s*S@i=+j3IFFVrnqq)x z6;Uac3DFvjGi#qVk*iKRJ8HFrrh?H0#S36`QmZ99uE&9R@73t*2owC-d)u*Av> z$)g7d*K$D$m@Z^9!m%=1V}zdNniTK84%XvxD*$6rv>dyIS1`9n^K}cTDxJKA%{v#V z>`0p`3s4|p-{8f@9(?~b+I)^(`m6zFy!3g2O$n*!ytazEr!MXuYPMR}4>bXI|LASY1en)#isJh+ zfB=7eVBSq->TneGorXhE5aEZ=000RecgfH}2zz?nWj=%r_7DpRUjGm9X z0+6e_F92f5n|sbn%ra;@#15Ok(07O(H-Dh-5IS!ELEa$55?_br-17jJ<=1Qqnsweu zk!F;vz^Y59+^+O#GV*$Z!UPyBZm(8S{OeYQKCHXC+yF5{yt&E|VI{NxfB*mtTdC{n zU4oHvbrP*QZ0xDIQxw>Y)__S)BbPp`MEj`A4VzS_Vm!JUlwL);)Ec;SV_0vr$3vmxHvc00001t)3L*{pnMcwV@T?XCQdPMD?!o@-VzM h-NL~K#}r$n*%#X6SXl|59@VMlL`-x95lTP+006h;p41-b?f`250AS%M@O9xnAk+e4qKq04R^uH|wW-TYzC-Gr-J zHURBO=oa=q_agVGXSrt>5dXRaSa^|ZjlEiL037sm0{~ab(>=4k4FIYqfhWK<_z~Jf zPoJ+V0Pl6?Y3kJXQ|LwRCf^uf__YTxvm-YKXz+~!VEyvlTmgVry03U!xf4D804o5- zFA*pOoIU6{0#X2UFR^!e)23&7hrjH4ohjjqp5g^c$;41=9_+^7?U#2ec0Dx=nRlw0R z;8Ea72*7m(*x$NY2cVsp{_etl5-=Ohe{pV=8!ID&_<08F$)b>y-jEHkRSbi@R= zljL^ncu?*{nXxSy~qs9{c z{QXtpmVp3eYIRkV9)IM_jonMoN5&%Jb7iirEx#oFR#jSe*g6`?)UyeMQxRj}8VJ*Z zB^B7l*j?L*|0Y5(<%5kKeOQ2AKK7>Sam0l2bcb!|p$<6tog}EICk9*JeHIc7dRDd! zVf!g;p5?k(k+OWlUv%k<0;5{kymVJ+`(<(G?;8tW@wgXA!9~FEr947n_UukQ^O$;ys04C87_% z7O!_CMXU8=-QvjU+_+OX8|9d{k8b&a>cjYy;|7R zv(n+L;T&6oR}B>V11{U-#=xm@LNZZ=l_@wTNyk$rCHtXTB|L-im|p!QW1EpA}sv?hC03AiVq<^qSBNb2YyAG&aasiotRjDv*XYERhr1PF)n z{#!OoY$wumJa`P(nB;LH5`NM9wQB4yGbVdOHLroE6%N4Tx+}X-TxXz6S}d0R?-LcK zdQ!KsLb8C6FcG%Azt!CzpQf_IMk5&hQ5?qyAE@OmO@+J+l7j=+;#uO5ipRknF_=Ep zPvag{>`z!k1foo`Dpo;`^4VKn(jD_x5z1ljEV2s35T*n#wdNn|3fxYne-z3h{*j7} zpO#{;xbxMzL(zQGrh(+r!HE7GcL-x+{LvKxf8+X_Q_w7kSH~3LODA3@&(RWhE5Gfj zA5q&n$WR24rluu?xy)Z%D{f<`Yik0;mo}jk&&Z%q3tD;I5sjJehdizwH-TqOURYgh zwB2AdlB&B~8B1%GKdhT7x)n!-~5Rfk)Rt?3F3%iz{$sA|w#W`Hm2ht;!y!!CJ`%#w;?x^c16> zW20aej^6qCJ~}K-(NEWwcbH!5bg1VUiFQy@W{k^`mwtahyEZo{WE6q{6jfqV>uL~fwOHB zvP4#J2rhzQ6zwU~msdGiNq>4ri)%!rIGn6Xc2*uW(SIm4wlyz{ z$7+(X&HQ8Ql}$xLMJP!>LLJ6PmmHFo=#3~eVbmpvl?sGBhXjA5t8NZ^?vK3fpav8( zXF|mWZ4TA(O|4|BljOp`wz@yggI{R{)q`EIc{Xv)k&y2SuZq{G9=wzq@q+leLhKcq zvZCr}(xE52K~X8@qrJq05K|-~=#KLtOXuW+R@h^^kMx<@ByF-z-9IRc-O^@a?dxcs zV^@*>GUr^UvM0hoY#LA~MFA!9IgZK$9QfM1_&_H4kx#Tc`xiM6&{)DtnH52Tta+~l zERc~o6`f{R&GL}6*s0e^uoiuu)u;I&&S<9oMHflc!;zobxS$67PAQkdMH#*KgRrgP zkNO)zn2Dgwv0Do2C5?4cDlLl8sr@yul3GC}=Ms0Jn_d|=o_N^(KD?Bgqbo|Vs5CA~ z;3qh_$XQe@n=cczd=L=|`Oe}mnc@p?12dXk(*#Pt0p{$0!SBwmozi^6M0?Z7@$p5} zi=gV`sk0rDYlyD+Im_;uD*un^_@cmhkYmOfn5k@-!@hZ##Q3VNBU)0L`x#bmIR3W! zl&*hSwg=)sO;-LMWNbtXxxf1fk{kHP)V z67Qe!*^sH$bn$ZstT?@Nuq+n-mH(zJf3uXaOTMkwPX=j-AHzFdgrnEeH()V%K7AK~ zUY@XOy{_1t@inCxsTPipRWF60R0aHz+ZL%=-UtD z5lIeK-7rn6RIW{S>WH3LG(`={t|!HMnf{l+^apy8GRW)^JAq5SIvM^H(Fd7 zz4cU(h7M{4Q}TYE*MIb{zqjl^l3Zng*W6e(SCDFzG}G#IdhwR40}y?sYI$zG0FV?S z*qZ!?X#EDP?_G;u3ipbfUwjOuNaQysroRI33eM0(VYHa+1}`aiE+v#_E7hFe$shww~#f%6b zqEcY3_(ZE~T?G=M^$Mkj2;sl_k30QMgm#c_MR`&P;2}fwK~XT43foRoMVmK+gt!p( zY@42Ptygzu@_FXR>$$gmkT|K5>rGtb4G@=zu=qN_H%Nn!wJy{3P(Gf1n^shn76eg_wgK=`<}mJO6Gv^Pg9Le;tch@wOTHuDCjb zGNsaXMRlAUEbOd>a$dK$XB_Tuefv;IbDPho5OGPxjmw~jyX(SrKtuG$DF%n>W0Vg3 zVG;Sl?V}S zB+L^_f^C%b_!q1COX1@H*>d7U^WVxG;VwsD;THOa4u_MSlUVlW)B=tAW1;gf9NiZ8 zR}<&U=RqSn7$Q;>5{)2k_Esuw6qIh_o+z3gz@h6WLc%^}urq```FYJOU_+KC~jt(9Td$ zXkUde(FaYQ71@HRoPzp|J07rM4(3_JaAU^en$wIIETvRIe%p=zG>E@b82FpiUv@m* zI~4N{5rf9i!6>s>2Zy^b*GSHDi6<-C^22qD7=rikAh6P1iUdxKP5cqG1vciY_)*4` zJ`XEQkKU2Reg<((xMHSe)>)-75r9NuM^EW_kUi58qHbXQHGuYDe89w*19W?pZ z7Ta$)#_#a@yJL<~6pyxniXbbo;So;1{rAfd^hmdUK(j~1Q5{Q8C7c_|L|1tgMltJu zXzl&$=l&@u{J#9o_g*t0##q*4{yEma$?^Y$PFz5y9kDF`z8A#Ql{KNw#QrKo{uv*9 zo0i!E(B)*9ZxV^|7Oc0fcI>y^9h&CCvTRWSD{$gbPyu>{sE81?xX_8y*ao+@nJA%IQV5Ve{CE(B4rBA>b zd_9f`@4;!*0QjJimGMJkSwEO8BF)6byZ~YeE=Za6jq?)a0&$7fc|$xb2L3Ha68f4+ zs=l1{(FoG-2_x1!JjyjMbfHK&CI7n%T{c$HN+R6Vv$(@KJ)LoKIRzWEJ1L z&*cNbxQ30f2S^Tc$)?bsTLmP8vnY8|P%7*{@EZKPc**#5zjZuPyR4x>Sk&!vSc0QK z|8`*TG-?Shp0O5q&FF}qTXA1NGeN&tmY2&X93;cu^BAp;_66w4`b*6xA&Yh!?4ENn zV+p=q{Ig+>zB%bDDCL>0B93boUkSYBL5eM7gQL|rYXw&rpcx@<;Xj`}{F;=;A#Vp4 zYwC22@>Sihav5Mqwoo^3HtJ_#{PvR#&^Iw}Svg;x*MBa{GAP~Ou>4=xQ7zoyuDv-% zmj58X&C=l;(eO8zQt8>p(F$g*DpA@}%}*V_0?yO^Cjxt^_^nbScy(r7O`|3>NQ!_y z2d7(TzHs4-=BZF%PCC|p8@m5%;e^W5j~IbzL%8)45kkEYso}C5;9Yr>@a1R90Z4a^ zM{gJLH!{ck$%pEUCsS4Sm zpyQG+WE41OCfA1u_7y7e-Cz9#a{IcB-8_IO;o2|S1K`)*V~B*uA(U)?_P zXJTwo&K}{%y{K|=8m%a80hgF4k8={vXsagz$I_;pEUnPU=ML>qmE42A2#ROZ-Zq7tEfwaSb{~ad+?N58-1VMpVnyw8YLNQnHC{=`>IMl`|cF z0q;hzp0>R{Bw9siAc4CZ6bb>vcI>H=$?rDr`3*?|mw9!YD_&l-Z&|saS8$E!>ZpI# zav9?#*{D>J59eR%gZZIQQa&dmJxgwV1{Di%%ew~EhDt=88qsiZaZKUY;f}H%d!$#( zo6}7vUqlVS1X9cZmo1C07Wy3_XGVJ?pwg2&x>ndcnReb5!=H~gp>B>(bI#!~f0pil zsX}a$DNoJ!Om9>CPsgG>LF2$RIDNktBV;^&ie|Jyf15%5#~#SP<-QbU#Jq$<*)v>!Tu(#|8;l%JHY#Ka>7?Yks<=0I~3#x za>(hd-RxEAT$xZ!LGGVLtkV}MvsJxQ^;liBgw&mTV&;Hr&p<1nMoUQeQhDfqG;DmvEc2Hn6$O*0NBZplTDPn?)t9gE;4Us4_gdXp= zdRA0kQOvk3j+Fghknu38uz>%9g8%Wb;{RRnab2Mh$6f$(kpFw@@2_d!jKuu@s=kSm z^^DW=w@!0i{e|WqL+I~8{qJcrLKa04L)iU0%ZZFpz!C?=S@7T4)XdtPF*V{E>F_na z=~F*V-HerfZ-B;tOIN`~?EpwSB@J;oKIFh#ouQkoat!kTrd?Z7C?&J;nUl^{ceoRr zy`AJj4w>yrE2=fg$C}&=L5&#g!*TxRqi!99Ti@OfKnF?#wwIq;FC>gbN}R&4%Xq6H z{))?CXDNIDH`fMYdUOa+jnF0B76S_zPWaOXZw0dnX8nxwWA zU+a)tmJ(q(AAIVbgUmd)jlPQ>6EVUAGqdLSuCEofhLXhLauAC&Z3I<;e~1^*XZ++{ z6%W-S>{r~l115tTya_4)7=Zu2jsB$p8FtO6NiKvsjSOI- zPe|kl(KqFnO06{&$5kOd`JqL)d_E66h()!*2V0$9A|@M9QPe5qi%j{d-JK@s-ey#} zyprh(cMrM-rybA73DH!_|J++46X%l55cg7KTak6xkz%UsUe6iU6zD-@x{OqD5afnV zyQxXPro3h#;x8Rr#R)`im;e|WfiKS##g-Nk>W&A0(+oSS-^TtA`wRbGZb(a1Gli6*U{G$yFVioN@%hP-uIC4hbPTQEAOg-a`68KFh z$0W%qUB@%$Nk=!Hf~iY*xO*e|{1j%N5$^(4MYiyq&shF|a|>|O;s@I|_{Ex+gHeA& zRC|sQz+9wC1*3Xo%l;C1Z(zmBlmwpO^5bjY^~d_$$qWTX_p))JYhauFc^%R>{Qdtt zrV>2VH|^cu z>6e}T-YZIL398)pf=~;sYI49w#Ed*0m}vAhwEZS~P1f5I-30h!nnFMT|JK9Hil!yc zx~6`rB*ih-{#fk-zWbkq7#tKY!eH2#9iWZfHH8e{BHUTG&F)Yhd@G zK>P2T@oN$E+m(O9yDftRyD#a0>~$jO)u^jcasMYnJSOV5-?95Ltt+_>-nzz8)>ir7 zqgFr1)D!x?&jcY9s;saNG%5@fBPr!^=p3rkwK7}LBySQY5oW> zdt2mx2qC{(y5Q2hZ}hnPX1@bNNenr(!{>qx#-5Ks5l+&3$;Vm~cQbZIPVZ^76q5)T zUdGTHgccpH2GgH!gBj~x z1p3C?Y6aRHmbVdkhpe(BT}+N_;wIEL!uu3FzN!Z(d-t<${BuBx3w<+2BOMs397Esu zqYnEgJ=|;YRcj{l?P0#H#Y9$UySV?tnl}_{8r9?Zo#VuTU`}4cT}prwG5NQ__P-Q$ zuK(H02u2N;S0?U2Jm+_shIA_n+4?FZ()GksWEYhvi&?A3a+!|49DB3i`;+cB_h9io zxDRK`w~^zNV(exuJ`0Vy28tX5A|1C~1y?ZflbAH?O^4)yBhPOgF#47|*igYipX!PK zkBEQu3~w=YX{D%--S8VU88Bi^S+Dvh&eT7F`@goI1Kfuw_@lgpfFgkRYshbsWlV;W z9;#|hL*5P=f!4F#leD@g`A;%uQ1L8SyhdS~SojWt*Q2Vg?<{(m_ipnWs}kKW`)Qld zONpxKPj?8~pa4t4_VK-ieK1+QtvX@Yt8u0!#7P&bY0J-aL+E*`CYNvtA%M)O)4YF` zDA5vvl$^K|LL6GuKP`*FpPfNB5cDo8r?}xFqQBB<^qL2TZGM9qhMQu3zbw1GdS4oh zZQ#SJPp0_SJ_Tt^?NMC{rgcup>O6Z)pd`dm7^z2+&zI@dynfUDXY>De!t_^=`MKqr zd`!tk=CYF*XdE2RfO+Q~Dtb3C;BZ{~jdDy(9*M)Ho!Z=vWp|8x%4o`}AzRg;PN*8x zeA`LHNvv6i=(B=lp@F9~%v||d{}&`d@4%t-n(O0qS-b{{&7+4hBX1X{ZPO7`yeQrW z3Ku?VP{FpkCDFT2-{8Iws|?MY=Vv_koi1F6#j$#%e;(+l{OmTwwoP^xD55mkP}|Y!R^k*e6$>rHviX5t$$S zDHy7fFl>6D+={Glz&^G$2sU5v)r!X0s6Pq2jy=abxVJmYBmDMxcp>&Jp8tlh_b2KJr!Ja4pP@Z1NCi5Z3Xzk&v~HwX(a&MjgkaXHJIT#G8=bGA z--Gq45OdX}?zAvQGp=md)sc+Nj} zM50jp$-f}e26lX_%<)AZcA@JH`ydtXrir-;__D;2g&G00?+#&#N3$fWP>}S3;*45hW5u$GY z24*WF8)A1^iwC9DpN=OGLd+Gj`VE6X=E0(qh;45Eb)zlwC$HS`om6Dllee?6tu}p*O%ukiEqGfr1M@=|KjRuE&U+!9ghujMU}z)i6t}m>0^X9K zpPJPK_>QwWaP?{qzMoz}8vo!p+KdvH)lQ}YbomRc|ECi=_I*oO&Tpn7d85iuKbt?a zq4s~dZDq$#d;NPF`P&M=6%@qoyoe=-j2=f#L=HzKAYf58JelI~VT;&yx~-9JQp?Og6} z`gh#O_O&a9AXw>+&HYl1A+dd3IZVgvmAVZNG8?BKEFiL@|L=_To7FR$0Rg=NdSZ`e zsBpxAetAg^>-nB6U)F69@$Rsvqpulo0t^I2iJpqZH(mu*2@suIwxHGHIb5#J7&qYo zJVKVLXq+fuKWsiG4wS%+a_|<{1b^*f2;cB#5zsjO<#-hJ$x5*8hotuo`^oAUA;4sk zSg4^;U%zJ;RKU*>vKQ&zqZ;$mP1I_E6f4$Jity4Kzv5JXjo(xmM{wc|&dD?4xJW?D z*pUy3z={rF0DeO?o?Lby zm(lii!R(0jZrH_-tJI_0yNsS3doL3U-F*$?BeM)CA+_QdR8p0(`9KJ1btxBz?G`V` zhD0k$q~<#y6`Yjet9-$ zAsi2LE0potOka~r0hyh6nt!)CoRzwfuyGsFa-qr@16JekL|wV55Z3%rTb$;+9N0^| zxkh$CLzDN*2h&MeHRae?aY@*G@{6-;nMQ&|Mww1YU$JKOJc%M{I?!SxA4o_13~^|L zN`MzCwPty!z{$rvdw(kA^{ASrG+Zk3=yrlH(h1M)GR7!UEC|T-li(CTbQLucj3}`y zQaK?yB!_?J!TuGUMh-e(cRe`so8JYpvM&<&03GjicV%xPgI zWLR(x!NHSN^;uDyL}&BVw5ezBll-lYr_El3=VRn-_EkAn+luhIOd(U@DJ+;Op)N;( z@xZO2Zq2Moo~NqkT`JaEpTzDG@ZDNvcHXNW2XT3U+^CG^nEi=*!eDB?6{Ff2h7L*S zrqx^!wCI&1LhA+OMnLF6Mn58Y@z!^H$ZsXZ7ujOo_c$9uqhfHR-<>SKC3&`U5yKV` zII|Wd*|WqNj$aCG$lnMqAYM9}5`R*3Z#Ha(={^*^JS?>~rgo_i?hfvAkED1o4Ly+_ zuj)kUMb(>W9^A=byGNG|3Q&p^MJ&1&(_e1BiK^@;9&H@6`ds~f{|9J?#dzMCc?^Wd zNv)HBkhg>#QTQOwStXBygRo#Op|{#W86}sLV$V($d8xd>RTQ>4D(@!UGn-`e z(Ozw6CPTSfR4v`VT8Q&fLhxNGmn(MOf5x8ubct_Zq64b`(WR1!puzkC7(5tc!s!<=jFlKMrr>>3ec;`1+u-ARFBa(iIO>A21%X+>iy z_*&{`lXi(G5TFM*FK5gVoH;1$6+CWqI}l3}DMmVrv5WYSW(H4pc;Rc%)XBrSSi6~J zPzvLYg>=6}t-x0qt7?X^(S+Jgx@6@OuxZtarV^|g%;yLmiklODh>BcyD zix=71qdTszjeMCM!)-cg`>;#+f>`v$Bs2ERw7Vl#Og#D(`>}U0D0Bct2(^c{T(FB{ z_GE?uf(~;DL;r=6ntxhi#{%?&YgI2}f141FzKMJn9o1^)hC#nk5^~y_ST*0(O-lzJ zi7kgTi~Yf^TGZ4uCHyvULj>KkBrW_i6Tpmxc@p#MOV>W%gz{Gng1qCvHWLFlh?KMz zuh3i_*iz<|fDao-#-qS#K9GqSo5Pb>7FAetAsLZhp(`c=Dd$RQ@zDe^fx zu-x{{w_ti}P^eW3_NbODxoES`x4wr3-0klz!7-N*H3wxd94+SQb#1g5Df{eN`|l`4 zl6&_-4u$CZtHgaWs9_3dSv`}R>Iy~gU>Q{P1s91D09ueX zPNm8~c=QZ&idt;D?-3(v6keWIdPH>5M7pkz)K1DJLk84&nEfcy4aYTUU>O@`DW%Q& z1IHg`Tq-zh*Y>`?o_SB@g4={tSf?16#xfW-W!wUq>mlOBF8(6|nDb<3qYFavNv)u- z8G~#=t1Fw#r}c2ZvBj=d$Aqyp${7immZ$~s;e3ITL!1&+NLoYNK1}(hYxICk#Z+UpdDP^*)E#b zNG;tS2Txdrv2K#dUe|-Tzb2+x3CwiE(=d5<57;pStrtrjEudggG9L;KcH#UB^3+B5hYI~;i1)wn(NJ3J3Q{sq5N6?5^$6-~ zHky3Ifkki&!b1!P+)~PcvuQPP4ReK*&=2(x`(B$uHlm72RYKJ^@Xo*jjc}kE9)3QJ~#`7?Nq`-L03J#2W?0iBNwJR!E8bt zpIl#NqMFaSth&|p5EE6`cIB}?EVpC;iM3GQAF)A!2 z{u>%+7W6%b@UZ5l&fE=+7WsncB^X-w>8!#>7)+9?x{i|I+R;H+cRt9w`ays`2#W52 z^nfmZkCz;H~pRl5ZL=>(kBX%=ybUF_9qNUC6 zr1_p4Xo?MDma8tb-`yNwy`(Vf!qpk93UaG^n~_G>3*JSC2K*p7N7J{cKNid}NydeC z3-Le`kWf#tnZEo4BZg7^(s2;Tmc%sU<8Np_N5try>-J%5i9E%Fg1c|D@+4#*w}v{X zhsdmtQ~R2RPpW5c3~y##Iu#Ut-7abLr+tlN{v$?`* zNn!2NjH@KT+&jRZO#bvvpIMxXkVDAT#R>IN2WgHlp-O@2W&TxR^WH>U9SlA8lkZ}z z;XHcJk6=jtZnML}>7f|)_uxZV&nduJrAf#*koo{aUe^qGW99t%la5}KdNI*7948StwL<` z2RH0DPBSDa!Bep;7{cCDWZsR)PANHlM3yWYcWEWR=j<20>KJP~8eeex^0e_H|IWEM zfFX$7{;t#5fg7Eo3j66g&7&6aK%h6&%SibO!B8~=cPrgw@*NA2PVT^Qa&WVgy4U&L zZYCQ(w$4;)?G?3jGaOBG3#tZd-slkqh1q8?D~HK?6>Q$6C3~bShj&peg;5hSpLW9E z5z?cw%N5(?bzy5N)7g3?rIo2GzJNW>Z{w*9XfwN^tMUuZoQprJyh_fblht~ckk{*5 z*DQ%hLQ8H8XiP>=Z&y!zstKDTEkjD1p1px?E}|CYGtvYfS0hRqqXk_FA=ad2kd2(p zC?VT<{*rpJ$OxQ-rI@(uCp>>IPg(Q8V&noNAN4_lO{ofIjJTe)#)mG?1>*-U99FzR zG3J=e(58OW>BBp)6z+}7FXafuUf||qqk{oMUHUL1)y`F*%r&~fled-kF+^I|yWxYu zlvgJH%M#Sd;pj?XI{_nUN+*y6RdY-&p!9~JQ}tQB{jYQ?_B&I!T?9Pv10SOtrR4W2 zS94_(b)n1sU}DiGqBqwlBrHdcRP+hf<$n=RAQm*czCgU1z+MaD;;U>h_os)q*M$(A zT6M@7UUq(79frEv?*aWtIlL7y)oxHu!e4De^uPMBh@rc@+GXy`ib7aX!i*+M)3W5e zSig(UQjU|(rn~Z=tA%iDmqwQ_^>fm3-uEg9)_%X}FR{=o0|87qhrEQxkS8)E-%Bd> zAzO-b6v*dGUnlu}9Zo>~T(5FK_CpHn?mKH;v?B9#ZKd^-^Q*N)7*jgf$&==bxx13@ z;Q16s_6?D{6 zcw0+t>tDx1$LvcKd=p3lWj3BPI}=w={rndJ`RfaApgM4SXI*cxp}uY| zI;6^q#n7O_+?9oZC&&-PFG4pN-mX4)qjkSi&@2wAfQaP^D#5mwQLG9CSR)@{0cYrA zg9lftZ^`DQ+-!c=%(UfI;72zEH6%)oor3MlwM$Qi1Sp}ggd#z2^jL1l+-JR1pzj0T z4{Z4qQCJp5GBHn@5|ZjzT+>6e*{kaXa{n}JllMT)TKKm`zxy6%b(c#M>fOTNDM*Cc6w3AwR0EO{g?Cpll_FJ2rnra)OhBI|0C#a`)f zT9#Y3aaGI(mivyqY0!IGerH*RFT^KT+-s!BRf_7<^yn9mrfEo9_QlvEAxt*49LC^x zjbDf>ztJ2bPUwnLG5NpFGBwZEjhQggZHG@v@Pm!{V{7T?f#q40dM_^pV@zn%AFZ?g zu=VcHRmB6v8Yk0_uM)98+07VS_~@>JJN2k9+{>MP2`UcC%gw=Ah&hJdJ@n&9Y_-s_ z0bUXvshSeaO`kV zPn92jK5PW%$j_9%mLFyEX=rL6MO%|DxIujbBvkh^S74}m6*RyzAnxb|r&G?0lVJza zNArORr6jC+)W)O~6T50aMzpX~AjoH(KaD^Er6 z(YSM5*X2;$Xjq{s?e**=bu0S`xz+h9V4-yt7cza0Hz1yLOvs15}4< zW^ji{v^mp-Hdb_*4GP1$`_(fxF*}lQNllF5J|+s-%!RMTbQEXfH^xwX3&A8q=Z^P!ux!6yrTSvEhgwju$tdFhyVJ>lP8xf1F_71h-iza4)Ad3Gi=FJehaC1#6 zTs3?%GCUIBMdG}wU(w||kE7IMbOsy>TzcM1eWO63Ck^Ri!3=mN!5#ia0mq96l=m0s z;~=ina|;JFE9^6d8Ly7BDrOp^!c0UVC@U2LBq-XcEs)1rR~9s|&XvQm3r8&q@|2qM zq=C4q^S#P=UYu6fZ*`cay?9!grhDBnkR$Cf>&uNBrSWh3zN8aHYoR9k(Qn1za*`NPO(iz!e$h_iR)qhxBRur%iRBhzp&^XZG;d zo3`YL!VmXs5VW_8NR;I+Pgl=&<>jn3T6pAPUy&-BAfyh(L0lY^Rf0*w7p%RT`rlVB z)Js|%`;SMmj|q!B7GO(mb^2Ib6=;6-p7aDD>s7a}_KPmaS?R>Pk4)?v1$)pakf6L^W^vS4= z(eV4ZXK--al`$Qh8y?rzY*GfXA;n%{ng*Ipq(>?w+WchhyPexjzcjIc(Qh(1_`AhS z(l--8rS#hY4X+B-pNNvI5M*HoOCF4X4klNo{dbD5LJtvZ>YxXk=Xc$dUme>!PqH_^ zdaFcKnKnM}s(F*d=uNWOhxfd!pBbf_VW`48IB`N`ZZ6K`D?!!woy_D*H*d}ie800z zqWLtdxM-{FLhfxMx-QiTYyDy#p~kCq8Zv2s5Id;5MT4b|t31r~(&O=g1*Y zc0>BWluJ%N@+WW_RvcQy(u=K9rla_nEj@v?W?ZQdlvnFl?i02PG%-(C?LHu-zFJ;5 z0bjVbF*&$}8o8x`j$XxfPOE74yf385pSFg7a8mVK{IClY$C}vbadQz7(rU*iAa68) zGwA)nJXbYz1V22}y4O4$HP_%X4T=o*!3qvNYe+-#7w7d6V_Kt%AS;U zO7j-G-OHi3y>54$)en(OA%nZZPRYCbuqU@EX5j1Ec};HUc7LIDWG{X#*Sm;%f&gPU zds=iJF~ZYt6j^p9PmVOn~y<6>ofFy*!0tIVfP zve&uE+Y$H_c5{BiIHl$M(yy1qY-@jsfY?*pR%JWRhQay@4lwmCe zroEmx^yW!DIM47gL-fR1qO=YborUeqhPo3(<$c!o6)jQ`axs}`e-L<#u{acpY}TzR zOMYtLcRtvf&t{*c^+)N5pdhRcY=&?+F}=gS*WK4)cc5XRZ*Z$85q&L`=hbev&46oBqjjLD&644YV_WlDNz;m z`~wN}bj_IT+2h7bRKoQ{G^4vjQI;he4{W;ZmIfBli6JQ#(R<7v6Z6~#%MU`9mU3*I!RaV(VmFFY(AI2yZ^hY@< zx9K%9#4CbQYK39(rt=<0cq_|@NmcubU|p)DzvXb{&^O8HR>||p>+-iyYhd@nb)O>3 zED7aXl)0J&EH*1dM_Ph3IpFicjrVA|FINPGnyOY3y+*aNruq zpNr$Yx0Dt2_qJ-|o6yD1*lGb-MVon$%X$+d#@SG7shGwX?k{K~%6`^Um9J=hezZMi zBsLK5vn8OA{NzcXZO#rVHLe`DL?TN#^k~@f-Q@Ti{O@O z*k49)>LEwa0m7c-{26ZA+mCw)r?OJoxR-EHrdaPBoeRIk)2E612Ui?GtKG&6 z!|u*H;2+fAx7<)ATh*%+FKx8kM;PQKoEM^_2qWid>_Eqc;^@aWj0ON zCWg{YV>;mdS<^ZLInwi;_c519ofq&FPPmbA#at0L47tb`Sk)(DHyJa_AopsWL(Ed8 zK_0d4sK@8L&xwAYRdC)V=f+s)9Hc-72vdPD9XEH~-qz15dF)eug{I;KZW39YYU+ro zk7Nj1&m&yNLc6yjf2nMTj`a|AuT2lz_DWj70^VRNNkJqfxzkEazp+{LSMpt4n4SkE z@(XK>c{x;QoU0XxP7-2${9>D7X%D;sPrXvWu&D5C!N6$vc;PPvS!0W<)PTQ4-bM3&?_u_){|%{X9pa=U}dkgIq*vp4&-BKttC-J>-jIgdvvS$G}g*moVUi%#vDDp$C($T-Ko;1BZgBHekSy z?v?UP*9Q?Du=VQ``CKEuMgl;SdM$FC!g_0Y*2jX!JtGu8jv{zHxV?zeBV$Xlg(G;Q z`K*%ppF6;Ao86FJgXFNE!?a@Clb=bg(V15l1qu5b(e^DgEgjqS>^!B^$9EwBt)z?4 zP{vYP%GdLPde#s=NCeH*edG8Hj@wUrgbf6Jw|$9G_c$>}W;IvMZXj@Jw?>A_@5X{R4tzo^F>FlK7^pN zVd-bIJ3DM2jruAhI%cjA#UyQI*s=SJ!KyO~p6*=TnfYYq@A|V~45X)LU)(xo6XfL1 z?|pYv49M(=Y9=ihZSWK6&d5a68$k?#8$#XC)nni2u!w^f)@0HQSD@V`$d${DIA{K@ z0EvEQ&Wc(!-xlB?)}?_swEw;7DXZ;D-1bTCCZqfpa~BxyonY+`ghX7l@b(R6MUXmx zNJ|Fit#8;#(^$V1vLA}dr`ExoK}&QA7qNJj&JvWt&4v-0ZvL!$h#p-s%M)QAsK`AW z^AbP5RP~t82bdZ81ztK1wN1@k5i?()S*WH>`s3nE(->cfoA+wEFXSZlng$zQOg@0; zbbCvINlW<4q7^c@d}j5X_HZ!c_XNGf0aiu`2EDfq)WL$@C<$}Nq$3NZ>4Xu7aV<+; zyaw`Lndz9*lJ?#6qohV0YIBS!Yc%LL#5KH_6wJjAOPRM+T9tdfPC!afzmd2@R zkA%RM^k9{7H=}2>W~=omCxttQkWGfOzJJ1ms3G=c?}}$uxyCW&)wDr?)7nwCYtI1> z-G!e9Q6FAEiFA*AbtQOAHqZ@3mnuK?;HAAOZ9dKg;XOIu4DmIrwfoGtqx{|j?I_Gr zG1#)4$FW>w-qZ!Q8yC;;heE`t3413ejZ;^F{!1Sa%x~u_28GqU`bbM;dY>QSQ7*+h zr%n-0fIoOHoE#~2k)n@j-FwELtCxzjO}WOQP!-*uuHM)&Ba-r!4Gg*lv3)|AkTr=K zC|e?xSelg*7p4xl^u*XNFF21QHM}Y0iCk`GDa@{Q&g-ei)18~tWru6lW8Mi-WHh?_ zq?G$z9n8{7i7I9|Zmm}V({=cWu%{KI7UQHfd9p*rTi$YWuPSAfzET?mm2O82R zCdem3Klmc__t+2?Qe}*)1v;~nGEf2y#7>1u1`GJm8AR4eA7MeVa><~|nNXYbccz(0 z#K7p6K3dN7YD@_#Q9u}gAmQ5NWyJv@InX48%xy)D4XN%aP%n{f0f&Rl%ivgWQu4s! zwFFpXnB~w67><0-y?|*ChZ9x)LRBudlt?u~7BizO%u~9HO$!RPN$P^p{uu;-%ZO08 z-NY`kGFmv6+>DT_qF8w5N?KlA_)ueWs5>g!o_biTJqJ zw>sMlF6)bl9a-3P)tW3>>=8$}2Lr+IA#Te&X&1;yJLANwUS>EOCG1f3IXb2^)UvSq zI|~WtG-+Os4oX7E6%8oe3Fgh5a2sUH&vwF5lmW?>9Y_|My8sa2cR-TpOYp4-czZ%c zoOt;jlDC0lrBtIS2;WA8%CEaLGqAf7A59p&m7h;TATIm?$(E&WFnT$FLw>jKCKJb787*)>;yX_#mX@e zZ4EWuBt?KSA*h&0rhEai+XlXQ0k7+u-O9}-9XB?RReI2CNndcVGhH~_m?T# zd$U_-fd{FG>+A;jWXExY&3H%JwhGpgjd4t&X&`!20^r4*KAC>s|@ zhAsc|zkP$ZIo_C)j>JfKfq~c~7_}#_ z(Sf3<-YV|{l@KbVP!%TcVC~0_Wz;^3qs}ilij{EUi&T@%xFj~e>&&%4=4|tr&DS-` z07E!fg!#uZ{4rVu%vQX=)I5IKopEw=3re>P8ok_ga}2`w4o*Wa!wMZ;!Wt?p9=^4X zjAmU9^r+hyAk9#pS-&TeUfnhI5Mlbsg1`$-QEwlm3@cNb)-xlF1Ys{|{Kckn@~u|sPZ+W!yA9g|qKASf#>1d2q9%ji-%Sv}cZHc7Oq70? zSdi_p$CCJv6xLT^nf+q*yOPVI#Hj2g-n6TQ0#GQS&Uv5|aT5Ui$lTLg5f|%y0pCyd z9@TfW$4Mat5kMo=w~W|RmFa|>ga^A|7R-#5->@uOUk?Efn*|Qsyh+}zUkI{9f-4sI zG`2;kW|5zca5^myciCL@7E9+wM#k?}Ginql!u3uwrjV&hy=nlHLr z;O;sm)oQC`lo=;u6lPtq*ivaWG5F~smXFIufz$jpmo)NYaE?%Q!R=$UgDNHH)cgK| z743>gG|-M_Jf;!7RH&>*ddH*2(15%Do~ZdR$0NTI;+KNYd zK`{$B7#|)5Z~i{W{vb3~ARNOsTye|+`NchC$t*<0sLq`%q=Q*w1E29Cn!6T)S+fh^ zz)Bk)9kb9Q3N?S26CoMxICq>>C{A;1bZ|g^M4|eQcv+7VX`T%DDR{+$XGmR;VzY$4 zh~Y^uUHyl5+7A;XpCRwo+lus%FFJ#VncfZ0y)=SRvYO!FjXlOMK1J7FD_!>*@_6fo zXM9mQZgD=%Pxq3&mUy9NK#_%`S@Vzd!u6v17eYKsYaq-<)eeow z@H+q;6%a!q7)NEEO5OXdiiJw&^jOf+t}v{pyCggKtdec(Mj$)}_f0`vgl)tHdc3A) z%g!O-2pXT1$@2LX3=oUgCuHRxRN6N1!Oe}Wr05hl_>(maVax9v`|JY*M}y+`6W_q| z_#j_6RL$v!ySpvTd0wi2G3T7O@|4{hm_Nktd|=)|HNj(mhQze-GCuNjT{ndvplW`0 z%0f@u$nYJhMvGJPbwY{^S)o&J!9_U0o^qSz0-0%6%5EXrG|4N(J1D1m>6G-=g!ekp zT;qHE{s%ZAry=g_V3&+ODc_2nA7jLhb&NdLf~)IS&|9PtrvOPBRg%~+9BRNJNCcS2 z(Eh>3!iXZvLPI8{Vx48V;kgM6oUwP1C-}PtRZuR8%+xd$?&DxWceH3AU0N|ggzor< zw?vagSvT|5B`?1`_?WV=6IcqJM+9uYW^M?fpH5 zqro9z&2U+5AI41FSFqA4z%dcjff-|3mJK&8&D<+~=5rb>>~ z$st7xWjK4{vw)8wGrm}4->;!`)6o9Zg8L4J-MK>kI^(VpfT_++DEGt#zr0-j;s(Wr# zwYLBPjMK%kOG=ewqJP0t>Sn*wMouS~*s#as>z~ec**N7H8zD450sX0+GEqqwK7mZ| z)Vdz3?(!QDznP%b|62f#=FZf!@Mcn{zborkw4NMZKFPuVXF zUGw^17c|(P%bz1BsJw6G&_p6cg>7Y?dLb#2XWc$Ofl|MXdn8RLbbauPXYsVE__$-! zK0Cr=5wwwm)VK7t3G%YZ2PLF&!>g++pq2*Rk@#-|iwi(EkvZxEnBlIDAa{4QUzMjZ zURggpy;@_vtpPzZ7hX7|H}wxs>{XXo8>FCjf*Gc_b(c^9jDOBb2C6;w23PlV$P+mB z)B%W^`>3$yN|&yo`G>wa!9#>jDGpQiE-8J~vqdKj`^N7ItT_N_Cvp}no}Xsc^2%7H zO~|NglL3bUuFXf!&c8!XpvO2e9-_EN15*N!-ksMDckd;-s5o2c#io5ODu0AGe-|Pg za>{OOkmu`-#^)T+dm0vrYb<;QIBQph6ozy5c9~|Bg(AJfq>SLLfKa3XKLbHzXMi_px^l-UPGDg^`9F zv?w_1hgO%FB(0z?hWV-ci|g~(T9Cq7?HhUwGH*DloO7?KRY!!)wRkBWBJpx8;As}P z$h8*eV+mk)UgUNo=*8~EGx-K$)W12$$&?rruk);^gM(r)v<;C>RS@{uU~$<5?;L`Z zY_ONJmhn`Cq+dczxn9aZx1jdzrVi{Q0Kb|2;hW)Lrx$$`1D|i45Np*ftZ5bHhEnA@ zfI3LR{5to0&l1*vA3x`W4;%3U)^=d8lu3ZReU_@Xu-2|)<}XILkWJ%CrTXRL2=D93wHQI zuJrV!Pq(4>$F*Ju;1O7oqN6EJYGpVT%l?gWWScd%fQc3{AEi5W&fVFc`y5@Je8zDk zE%8#QmEt4femtt7h|a5Er;9%S1LRp-Ps!pH5`d0MbH>_}WDkg3X*^K4l7q(-_%%kv z<{IK6(Riq#8M_V!$VSNwXzY&lVE#G)jmAvZLpi`%FX|7By~Rda0Ens`{BUTVkbsid zUj)|K<#jYz&O)}=4-@yk$R}nO-YL~N2z`U;!c(3s-84B=lK#c}a|1ZhP{&uHaIhPa z5!GX(fQd~rdW4yRiF<}^jx&gab=e4y*%0EVwx7ioJxjE_iw>*UQWL8J!gt@)9~pa! z-F|585jGiK5LKbn2kO^!*y2xcyC^2|VXeS_-jy9nUdBY0`4qp6)0e`5TVT27x=<{^ zL0LYKlc#kIG(&BWv}ITkVq}F7gNYa@6lgqLVBr1N_kMac9;D?2%q9PO|xt8)O)6Wj_@lEaa>=F@!D{+-^_lJ zpr8vMRR;V(YnL~0Bo~q{w*B|<%xHbCPl&tlZ`*fBw?MQ9NGS-?=i*uE`lccYfcrQp z-q5E-)gle~`)XvqUz+GN00a_SetwX@5ATo#d36-IRm2gwx#g zaT!`n62)AA*S9L#HR(Gl?eR7)w1#O3^p}oD5bj z?)6X$-X{nxN#KoDv+N9VE#YO|ZOV;OB-;yo4}?X$R*q3gRyioU(eCfs^AFh&;-|Ks z#R#8L?Jpv_uDEimQGg0-L)3b(uto|prTyTPHbzF)&6Ix4mZr?67njH~v`aSIg~b4x zb%xJ6$mjiZ;RVnnCn{ZU?;FHx&@i)w`o>D7+B|w^y)oXx-TZuyQnK zCZd$pv*t-YHqA@gMo2>yyOtX`l_Jd^6yoR7>5@B5aWu9IF)PaE+DhyI>7wUwgLcrG z?f5%~AUM~9=cawgrF_bEX|U(&q0>v#qzY`LjI^=1ApyNqXFxfS8kQ0U-bY00002y}vX%4ejpha;HtV;?ijm0&&%0TeEc5`Xx~KW1Nqx#hN{UqkUB+;INqY}G+KW`85tJ%R*@&Wx7# z*&fSyXrctcCuuxy@yh&8xbh)ly!x%RF=sn@>PwQ}KVVew_#KG@_=4cV)Mm=ps6xLX z(2Z7**w6$Y&6F@2@1+~>Jyy|u7jx6wc{SZlb5S(>STb=r9uzKVueUebI@PKs7{2=v z*|%Vyu@#g$y|XCSg*B!2hs&*PuBmxZ3$ZU8fkm$v$(jmPT*a*wYpc^lvNFB3ZoQ5( z990G~CzXwxqySRth5A)aU4On};wbeauvt%IJ8G=BvLJm_@`o3(&3C>M8yvz?>nRW@ z|9IE_lEZ0##WiA7kEvHprQ2qseHon$wpHNi5YgCjP>?~3oeDCkm}%app+;Ynon~|$ zV0D)fcVDuF&?i2AM3t!_ZL#sG8-fE~tPU-K&IqX~u0V*jn16NObClsruI3$#EOl0= z(-2^2AF&0F_utQB_iM5V#_WEM(GZbELSDw^s!Ri??+KD1k(m%~f`a0$DTW=1VPdgRW6(W03)?B<*Om*8 z27?kIxwh{9pcu%;(E@T*US3RbCnLY<}1*? z-H3uQDD`2Q_jv_+D9Mn)h8AM(I7~avqjac|xXxFfW;ygQ+O7Y<^&{$^XTEM17@kj; zcd|}7T`C~%x6eDWRcuAfs9IT1Hy%3)RUrn&vYBWWOXgX>=!W+8sg9?R85RRK1MHhi zyQ%_L-8F{%Z=g}4Whr?!!Y%ddbU|{*MuAs;pqDrZV4c!8oWw@te4-O&;7pDd4QjMk z=~r&%GwBQGQdk6fYH6o| z`#fgh7<-cIJnp{n_;zAQhLAZtLoIgb22%qy8wpFgTya<*8%vqd4{fAj?5peF52`55``bKedE2mnbEvYt+$}&h? z%Ph_jGJjm-IuVHnp#}^wcMJhhfLpp%g6c?UEW(7SB!o4$=mc!1^Wb%n-Ep0g^zd|j zk()O89ggqR=JkfdbUzU=B+&R3{YBxX}B^->`Xl@pPgKyZn?x^gyNIifNH;h=L0( zRr99@`Zfdw_r>89vNIJ!wu}5x9;tx_H=|KBQ-@a*7Tq6KqqQz?ANG8jnYbb~6!JE? zR*3V462lWdJ);cH?KzrmwBRlQz5VZhgo}WWI?xg|A$C(;KCh9c+NVinoP>!1Cql2&Yj;x(78L#9g#s%rqQt8X=E0B>|?X zkUHKBVz|!p^TW)(vV6R_7U=p&1Pzicxq1wGktTOUxuFc&Kv&)tWU+k{3(iAAYXo)e zk|ntBUHs^5SD@E&j^HuMEjyE@bzGTgIjNEpe@tuy%vzd`PL=mua+mo?d*y3sNf1^y z&+b_@wu(V)+)VxH?Pr*XoeqAG2_|6jHGHoCu|4QkwZJ?!Xri77tu*;)DXayTD&<#9 zfQ4EPr@;LXm0C5!<+D>Lh>q<6SmtE3 zQOinhciz~*{wf18M*@}SW2HbE$|h7@OE}vL?Q=nGCjLz7sM!$gi$X7KEJM41NM)>= z0LDR;h^rr`t;1(aOBg#YB-S;sxWap1z{YP~2`7Rh z-{RKw1Pbjk{)t3x$=fpM60}Tr7}p6N`wA*)8oYlLz#{9i!;?AZKfLMXCmP06ZO;n_ zHz)1y6Brb^BKdLy{oQdx#A(F9Sg7gSCPs0~snF(PD_bHj7DSsPoB}Bs#^((O!5Q@~ z*HF6h%go$tZ{4Wmjw~$q_C##5DnW9hgHii#k8!tdMpi%&d@wygNa*0z9_!^}{UNWm`-SU(rw^ z(<|9%)|=Zqb=wd)(3z+t=VJ^T7g(%9gs%zoFG&wmXT{f^*3#QMV-uY*D{jZJ3o%vj zzp6Y~lsH>F>Q?biq;nls24yR?A6L3_E)&#?u`%10aC+V(T|Ivb%27fGcM|Ppp)x2) zussRTassMjsD@P1@nID{;CXOf`T+2EiZ~2-u~&cj~cN$GyfgGG12E#gIGiohojPwEP- zHY@xkg8td9DFTWkOTwn zx^-8j!A8qap7EFp1mzNBuK=r6son;|vO8t~Lvn`!lWg&0T4Nyq)olVcm@{AYKReL8 zxiPUJ!R7WgzPVUPyDPOK*^*wqrbZ^iTFlmdR}F=hjv(C^SEqJKG3YNsuOmj?>;@QM=6nAK? z!QvcZ3>Ib6mfdO=A|i1f)8ZUwJ;u1w5+6gn9S~Ptf9L&Md*?+-W;U)X6iGa`&@j6q z%N}TL-dUpU#L@k{cu0p7U~mbYFh_|`VrEWWaqx=qKaW)F`AxZY=;xdqSYXOdX-bQ& z(h=^P&~x}8nDU3Mk?pQW=I{ffS{c_a=x&Fx)qDbxRWdc6boPcr=>G8hKwRrqp`F@Z znLR-De2l1d4Av&E8MsV`BPX&xZ-L=xnt-og+~CbXH~D2$gGkBcUuO%|XvD_Owwe4V z-fRbaK~|4al5;+$Q`s*30C=Ne4#^52n)%(NItxx~&M{7bKklP;jSZ?rwm@rYkZzDR z>CD$_Tv{hx767`mtX9$R*B};kjcTOn|6s#qLKLdUfJafhib7dJG|&qa%~eazr{t@Q ztLlcqVhBv^j|r&Oulw_%1h7M=PMoFaj@(M0ZU~XLQ{<&(%lPm(W$5|fc+)fmV90VZEI{akYuNVSLqfsyJ;4-@==w2pvPm7^l6=lr zy1=aL<_GNbd;X$JL-gx}_9Q~@Sr#e;K(>D8l*CIE>?XM^=YkY^z7$yl_-T+isDL(? za%O$Pm-@u#1i>##YC5zm-xPOe>CJz*Q-l0Oxzle(8XJx>J#014#`9%a0iKv~fTsfT zZo(;%jjBiOm?EjBQ=n&RASN&MGBC48IgSBCKo$<6N8*oR0MG6Tmn%5LLWD73J#md>hGBO~-we@stai0_ zdRtnC64OZ$fR*K#Q}M=(bFHd^Fv6bVLo&Y{6JwY{ zc!Kdkfy~++=WD#H`D418A?9B;aMB#`6H%2LXnd9G5G&HJcKf#((Fx8gVixBn_H$Fh zF}54Z$gnBnuDuGQuc?Osg`077T1VVX!^IkQ^j^#^ge@$sFCaaNHqcq{xyk)wolNuS z62VTv1jkhl^o6{OhPwg20xG3FHN^RqwmB-~c7c~w<5j^R>-0Uwvphwo_hAM)FKssGl!$>zCZ8ZR;+X-3y(xj+$o#7ag%f(MG z57W@BBD9cX+&Z714v7vmsOIQ5L7cCv8<@S0Hl+!gMk9B^YL)_wN^7Wy{h=mTUtyXb zTEhZU9smx&YAg)@usfW-rHp6o_qhA6O(97P#KdWIG4XZc$V8pxT0n?3NCLZy&LRy~6S|s!qvx$cH1D#TR0AJXowZp9zQ#>n zHLEk>ROfD)_bOI(+ZW`*mKPPH*j>u+glNV2`Gkf^<^<~WDFd9Z&RPW963SXQ!(my> zfF%N9>LmW2dL%P#DrH(HM^?F~`7vx53-b>%W`5moCNJC?dW}b7%dPnQ_gPszh z6iKGn6qANo?Fn|w@mLA~AdjE(qK~z%)4F+tM}TXA-{e^-4V}3&gkj`{}_eS%$oEt2hCX2`qkdrE{fZj2D!t zWKpU(g~_vn+p$f{+WOSsaG%yHW9rt*PROf46E^)iQYR9n#CYpmrC=Ge^#_!|WqNs3 zm*F{lv}V0lkpsc4_jCD5Y;_}xt6zP88 zVG%%s+kaBaDh1p=!G5_ze37VK#Pw+MbABvwu;VZ8O2q(33S3xZ6l71e=waG#gZ$w# zT|%c0TMsJnv1%qe(NHURsXA7%Qm|LW*ux!z&RFE+tD`pp99Eiqfvc&9bFp~T)=QjV z;<(V39@QlG)5ZK?JW;O?_77;-8y*5sPA@g5%@BX8?_7BAcWATqM(k3x!LK?W`rQe1 zbfUd}UeHGX00a%Ty)zHdFHj#Khq{gErzwZn;(I30dj2NBvRxaKKa?Byg7)NM3;^5} zp{#X8^0z00Z$)~ssRTPs!UBl6deuPdQ(>)Ln0W!eW82_vBpKbrpw)ZrO?;8n-SzQT zr&{SqNmzw?O^f|+pF-t|a2}SHj177*DIJAdC4vmxn#5n&1LsBU8H*kDf!gAC2*011N$!uXdK(aAbM*OA15L|l_c>w02 za6m0QV`UsK?O_$ZHK#TBrslt}3I~7$G`X8qU1Sc54m$vnpW@lt{E4)x7D?7ZOR+2f zq+)11NMfNP*sYz?s${}Y#D1NV)f z>1`>W^pfmvk4#}&*^6NMiDAs~fViWU4}jPg#8H?``#Q`{*?s@n(a2?OVoI$#KW=xk z;3v*8-3+It4b$X>Ak~01jzj7*2TcK;hd{(D-9W1E=^b7yc8ig){>ZJA4*D6?N}9vg z0RXJfvcx>p(zm6zisSV?-iyi?qBN;&&Z$8GH~;_u03N^4H~dt}Svh_)5F@wK2JK%0 z9InmlcGe#;EaTbLUM@Ncm1bP4Dm+!7*Xvw!@>_FF)^l+_YaKT0H>E5wYbRcfT!v3x z#7~8~$v0nE0e2IPEAP|0qP4oiyT1w3pem_ZrvKDP-k?_u+0m}i$)Qcyg&{4vAMMH& z(qz{~&KGzSZxx~xGG^3CY_unMGie?TQ0^!r708QM~hZi7`T;GFfWibMWgQd`&xlC_p%G)LqOF9=4?NA1xj*%8@{$ii;~R zvrpnPzZ7JX8P9qZaWqnq9~L8KM$3kUbR5`Zux`xUNF<~A{?}E#M83~p zMjahI=LN11&DKg+y;%_>dmoPFntt$R${*777srfZ+B-!qO(FaV)G1({Xm~)Yrx{X4 z_YZX*`gjBK!?YFqk&N#Q4#0G~(@QN}fr!z}RO^*^^SAKYvG_60$C&Kcu|hZswL2Ks zv?R7SNvB8NP5r>?T;UR{wA6~yfmW_bBnAofE5YT^g^wRlU`Yf)0KsD3B_%}E4hr1D8U z27H(`ST(u}-4k+jrYkLIvP}vc3XnQ|=d#l4v;#Khw!g}Nx^-D>5eq^293}a5&0FQl zMY~ae^k-V)Geg|rcF6O}VMBn|Q|sv^Wx(rqDq$>opli%PN|=M9FBAduC^9+t6YFBs z)6oDee@6B~p>aWzS2OR<0N8K*6CZx4<435fIM%AU!R#|Shc*lARfGy1H)}CF@&z~{ z8Ck1`_w?wY$a;9Z|GhVT$MrKh+JveM_LO;w2N37nIi+B8rBIPMHyvhv@j^1yRVTMS z_-Ia2^<}}HWa;LO&g@%K00000XchS%EG0(`1$if<9it1U9lor!$m+^4 zK;}b$-adf_5&4NFjMHh`p}}JQXcGQ}ezH0nOE#NrWQPSO0gJX)2pA1H*%c)^9~MwW zKFcW4+y2Jvw(OcLk~xo#jyBa|{EZs}rV}CZ3ig`?Xk$v0A{#{!yLVdQLdfK}coF(B zY%Rf9oASi%fIoUzZOv_nkN^Mx00004L$>r^;^kCR*cUktO4U{gVuINr?`??Ss3d>@ z0162fOvtikMUyftnU7?NikyrV+r%WonplNIQicFnzib7p7qxLvh(#JEXlNWciERb8 zYOAyW00001s9tF^h~cbRWGZlm##R9Zq)3ui!^HPBVNh~R-h^nN001D}*N0%I7|P|B zw!5Swxq&5y3WaV&{!{=<5hgUb9tjr>$%sHWOUDZ4(TebGBe@@msi&~GONTE6iuC@QfJna&9&F^4neE zdMP(-IUd9B{0gf`kw?IsXkz0Oocf~Ms(YCw-cE^|5VEv`TPT+(i7d$R4jTc~v+2*e z4$tW_-`RcaJXPbxY|Dd2DNc=kbe^immA5y4V1%oYRBRd@YB5orYk2A)*6eUZK>Xz$ z^jvo}fB*ru>k^Hf7IpmW%Q8y8kL~$7SGf@SKgF`ordcHnCLRD9<|^~;Zd+_OgLlTO zb0Ry=f-_Kbl}Sy0kQe4I89oXb*UE0@>qBx=A$A{kp2>aoyymV}+tyk;bpnEISg6%X z`c0XRJHT(ZL@$FN6}*$~nAW0;4yn+MM5(!t{C5ay`Mg>&AWAJ`x98duZ^*v?-#G8} zRpJ^(W4rXrA$GW&0zp1HNHSFr#nXey36k9TpP7Wmf2LB@W#3}Y(4^-~&h50YEqol^4y}ov(#MIUJp=T1_5Z=$O z*aZ*6ZmG`P?rw|AtI|rZd3zj4XdE-Tg{+Is3OJB@_>OW7^sNX=*sxxCW3K7kyq6?0*DbuYwak&1I4?nUeJKg~AGv2SnrkLGO{lC6;sZkQi%=?N zkIF1y;V7|zOu~r=-UFbr z_(G|X=w-)1KbmU*4Xh{(#{47|STm0kEGjgn2dRMW=R%#oqaX(-j79>4bjvWU^vzKm zNhYq3G`!P$JpFFv5AVKaPiI~wYa(tY$k&QU`ls2aRltY7VUz*V&ABI%uvNa<^J8H$ zFVSm9-o5v*ius%1sJpB~RpVN356unn@qzS!I}Wnn(*SvGU03HGDC!-7h&GN~L%`VT)GiPfs=ragCRamOQiVH^?Vpw(+;6rY&5 zD@N_MhD^Y-E9m7)hPv2oP$_RGi4Fntss714;482r9>O{Za;D` z++ImlyMJ+DP`7rYeB$hv+4RZGPLR$Fn!1!0{0foqAfJKqO+6;{sU%4{y8$V&%ua)R z!RAjHmf}^Ks(-cKd7witQsJXo&=U~HlC)6GBxjQF%@oz}i> zKyki@Nf*-Q_ixN;KzYIx%(de5{S=3uw+`RKMd6czX4;lT`xYogFk&kBi2h)(i23`k z0Ai~TNMoMSM)D&xwcz)Z7nu)cPp2Fa&P5~_bjNfVY@C*wy=fLV|FH_%i$u`;oy1XQ_UyF@Co~pdLuKzFRh;xXFQfO%z z6=j{?=cclu<~eLI;u!SWiV&d}JCJR~pt{wokfeY)rU3kP$LsaQp4d4QD(*N-E6aS# z$y=b8&Iuart%O6JSDS2M0yE_;7{DUIi>-tZEPW6fbCd&{3R?`NPJ|M`y3skU7+tjngJ9@(9%C9VxmQ)91Q$RQrXh=Uo&D5hN7=!z4c&!wJZtb7wZ$OlG#OMW zNUls@l`4hHDNMO+SkvF>-QIFAEqg%*Nl0X0Amr@6IiUJMnEM6el|4mkPl` z*Sg)|bdYuPHSaW_Ig2k%x5BDLq(+9aT>H^kTKLusXF<+bHm+#*_Tok4z%1_xc!e#6 zc+_b)<7YRbmB0F7cF+5fz+(w}X~bA6I#aRqifeIljR)56sjz*N#ivDhX7oJ*KUv2_ z*DW|FCZU{{lOEuvTXaBg)7W`DJfp_j^!L{!88|rY!M-iuJ5AMx)D7=e0Z`o$UDjh6 z-00(hPV{ns%>hw6XLP@e&5D%~b{$uQ8-MYo5l=#y}uPKOW^!@`Ver+@A|F-K_%;cD=>SkM)6X^Gb8=#}W!>JH*$FF1^z)56!i6L`U{w%c+G~( zh2sw{Q}VnxS$EdltyN^|uCjU<(B^|$T&FHSy#HT(z$ydOmT)-&m7HqUiQ9$$YglY)aAf93e`Rbj6SAIhUBS8xjlV#l z10}X1E}NQ5{B*()dK-R8Mud~m&P;)&$rruQv`E^wOv6Jn#9OXB@N7Qn41nv%4)QElA46tgf(`o?;#n+r#GcV6 z2rw`B4a3obN5|t8$usciO=Tyhg0}Xn7P~5$!m=yvvrX@~p_HlK1(_^Mn?aU+G!hV^ z_mo4QjM?E98$hgb(qPU8SeheRRKs_Jn?z;?t_KhOz2&-#j7K<7L-D|#*il#67*j?l z!xP5%RgjjP%cb^`$Dmv6SIeeX*W6^rbrxyT&B|Tr5t$NA;Vymf4BCb{PI9^3VzbG#jt{9;-*@=ZIS$1N zb&dlh4sE~&UT+V@Mj`Cy?{!>#d3}dP4L?)MF7X8>pkreMzW5IXy+_Cl%%w{&?v;EL z4e&GmkkYm!ak64oLnq8_eZa8>q?3{8P%Vu~pYmIo)nji`&&eF52je2mCoZcEztGTxE%{J{wsVg! z^z18-NtwY$#Ik=uzXp97$g0{Hx){QyHZs-?pXA=VR$uH^;*Oo=1ZvOYuIG(a1Y_eP zJU_2YR_- zsbH|I&+W=&VPAT1XxESupmZBeZ`;`*0jJ#DUnDSIi}-jucx4A@ zvK@5ST}_OJiG=BZ63zG*5PztSh1*R)Lyt;6v`ysEg4TsKJrI=DKAR=js@xD2vwKHX zHCGc6@;6xkFyUX6w6+ z2Wv(_oopbRpw<5&o!@jF$!w3aO;(IIn!3Z`LVr1ZCw(ob;px5jg_6JFL>-9f0M}Gz zi_31pM#FeR4j&#!r@zP6qRfYW&{&xNKw%GMZdaE1XgEEO7SF1()xYcIU0I*|&EKzE z|9oI?DmfTsBBf+Dw9Pk~JeFwYmXzZApB2BUWb6UVl>5Lf$;m5-$e?Vw(vQz&!TntD z-D2L$m3p8;PnaSdM>dcFL6>E=!~-|yZK~*4Psbw# zrK?r`Nmc$`Y;B&HfjyCYwTg6%!)Lg{1xK?*3vqHtM3F>BK?`jwthHR(uDFO=#;Px2{^J$FT#O056?E;S9+^{MhAPuhOd{N2*@6q;T?}uxl)Vp(( zO^qM}rBZi|4Alq2Pi1k?)_Iokn);ef)r3ksoztE&Bp(u9V|OU;(9EpSFS2aG6pq=h zMWY?X0&4rd(Fr20C9F3-P!0DRpZAR8{>5sD38KRUL&f#e1Q9br;I1@^GG%7zIr2H^GYDG)x5 z2849x60dHw{Vq3pFai6dYhh!cH+ zE>N&j^cq4brLeLGa-hHCxC|Ri6w#%J41Us0VPK`Vx(y88c z-cl}Xl%Lf%)+xxq}wafrK@@{)C^E`R$^x1-nzhcskXRRxUFN7{|K4%cu0DaMe*@q(llL-Q?!$tJ&-ta(FRh|24T-8n2hMC6fEoeL=_wGU>_Me=0%J0)#r4kp~Mwhmm~NkY-5|~ zhP-jw$Yj!es?`JNbj5;e{^P zd={7dpweryH1whP$(z-fS!OnD`=Q76#syND+{jL;*%k>q7|mnQGb%1zX5iO(N%x`UGzib&QI%(J~$OGk3(4Bt&a`gvGz3>d!>l3jaV= zve6D|JDj)xy{__)e!%fA+I#hK@28+F_OZY7=WXx!N*%aq!3yC1^082^`V{&Wt)Rko zbSmeK@n_9KJOr8+LAVH4ErD|}5uw<-9X1{~B45Qdy{h7k6p$F$UV53pF^>Yelq&~6(e!W`7McxQV?#eV7_gN0lyy5b=FyU|zFFG(06d=qj z!KqrN=X)-^%4=&=E$uA(e9Ha2zO3#-KMGsSo5p^l%FO^fMPH=0}zFKT_@Bgvce>iYajQXX>O?#RP96VK}*bx8D2Av-6JhEII}aR7@?pH#Eo@!g!Wgst~Kt7U**_jxi^~ z_w8BSS{gs2xT!tTouy9msc@Dv4=Qh&pYZx&&)$VQys1jQ@kdJice#I1%vKL%Ffb~a5Bu*apD zjyd{ezL($>kAzWFwc1n?YLL%fhgxcw0PmVE5h^x03<2oG1z5b6-064)9M(|}V~NHS zRyubASF1TSujk16&Dx?Hh9K2C#7AO2uY$tRz_uFQlZdniJ~`E;ktLXMxPQ$kZTfP| z_0+X|GCIqvmDQQp*F=3cVz@;5v z7J_@F_;R%Dx@>VNvWl1bknRss&K%9>6ZrY~7l@ZYF~?d&KRUlAM3%msnZFC<0O{ zl|ql=%OVvUKEpV;-SJsQHJhX+BSz9_mfFkF0x!E100Cm3Ec2xDW;ppk-?iyU=8>#6 zB#kbJ2oDW#udd?S7yVyO+z*YE)HZ;WT8^idx3l1+knqab;o1d4ZnJdf45cIGhi~H? zD!5FiOxZwebmZOm;b;p7@aXU0ee>A?n=B{V8EYWiSSUT3vH}%Dc~kRsQzY^Do0$}= zW=Cvd5@xfxq(8{!^vXH1+;9TdQGeh?%+tlOMp<_BIA$a$u)%f3#0LCb+p|+mGi()U z9%Ctf0xfzd71r^PqUb3}DBR^VrC!v206{TF#Ye3@RYfo5<5(_#lRiG*X6fcNv5XgK z*p15l1Ly6=xn)shuB=ZNkBR`a4iFCxEezvDj=3DB5sbaDYp;Kpr_{1~(*W3v8v%0W zx$9DG1T#j93<^>;J6KN$k!7cm?6Um)t3|I~G#ObYg&-@&zsVTnQMhOo(Fd?@&Eu0JxfbqA1XJ`H%N2xJVMpX><|{R81$}a=Q22W`zmU$ za(@YodJ#-CIM>Wzyx{tvnNf*92`-+QEu?S3l+vX#qrhC1r67V3mNo}cF615qwx#~V zehh#@&_*}y;(r9f%`u;@Vl>niX7!5jo31L?#psY3fjx3F`y@5omORbgh>8XR0l?@d zc`GqHDxj!O`j)jQ>wY;dH71o}Z0c@w4!WI>I(_Yl{xz-9Hp!VUZOFtv;-rTSRE!AGRl;q4uv<`!Wpk9$VkeRAt`=@$O_^t zplmxPhwF49niEI_0DRgRI@md6)CNQiS}kI9uFyW}o8(5)o{t`Mw<<9>2D# zwOe(xOu%pO;Mgte?CXRni$7!6frb#_WLSBgh;-syOOkBKWJFzKFE=P$3*0IdC8=&x zjBrwEb-rS%N$+?8mSh_XJ`5a_6M`eBd-Oq%^#>H`u4Dr)bs5Ay>8N zkSN>XGVwItYC*ab8%}FT(oC9eiohcHV1!W=t>745yYoSlk@D&G_#~kvwtVK!3MUZE z$1X)9es5uFAK3#PZ2r%mk$9iqHo)4wrg}Q)2Z3kJ_vK`c_iKe}_oNUj04@~;Xh_PB zG1?y4ConD^uNzsJp<~5FjU&q{cC)Lp3cM3cy7nt#Kx!Uk_U~Quh@}|hW{#lb(Uz#)Z@YntDnP}&wvygXeo4QG-zRHfkyDQ}Dt#n?f|b&w_H|{K zsMpC|F~6UdZf9}-G#uwxxe4n`(%!Bf3O)c-^i0hBr#&mdHSCI`lT348z9B4Bgi6I& zMW~8xdKAYoj7QahVfWTKFA%=o&&7u>zzL-=iHv@YlkE9Bx$GgE@v`>IpP%6Iy!>76 zS6ez(i;b|dtx<)vC^M_i>;5$fGZ4iLXwF9pRtEAi zs@)tsDlCCh-MOp$lGIfwFoTFP45Tgib^SBNqu|lqjBq$b`Yep;WteunS?R&Q!q|Ju z2fVns_Th=}6d*NowNefreLCPGIy)+H{F2cZbOjq`4x;0$;58U<4@yoTUHu&e?cOS6 z0pkr(G9qc`nEfJ;iMVj;c~7{AVueF?(sSc?(V3L?ZR1~`rkRCx! z1Wf@URu$=wt=&P%Q3|}U%-TO{K(H6PJSJS#8@4XjsQAZF_)6C|x1IAN7o1UHTWmOt zQ^Dfp(jTJyIoGRud=~?M_t;ji^ZU&>M^<{uludjm&tmFX;7XM-@xhgFM(XlXAvxU% zwd&W6`34(>*RS?Q)yZfE{L{~fM6XIbyak1ToRPm< z=yo&Tu>a+`f|CYY@&(|l^`PXn6R$lfKsNFQX{f$g3JQ+8!$`?|#4;fBM?OnnN6S*)h0gCn(Bk+*k^q3YVnN9xV#qdZ2ogXmJWV zi(`pk1UBwWZM!0F#Na@g>jLX?E^%xS4T3dLW4E2?FslD3WCeqYNpyk!?fff`p0_bu zU-z%ei`<&OUk^`cP;U;a*%Ch0;s$0T3?AlGNpBN~7)N;W2KP(@`k9sYlAt!@X{mC~ zTSgg4X{f?exZxmW=92!5Ev1FcL!Wx(reUYo}{L{7;pw&N*Bpl^o- zAOTWbELgU}XbC1&jX7Q5!SWvW2Q+JSBI zxT3J|Xw5%A zRF;oYOx{YxC|S(3c6lWRT{4B-xU7~%4*=gN#G-HZA#DYfn(d9MQuh#VELSg*Pcz2{ z*uNu83^SC=Gb+*~Ji?{QGn3@i@6`LxUHorjhx5m?g?dH3M3Q`a$1|m_jFZTgkM;ZTfZTD(@)K0scpE$Y2taZjO z)1S6skW}HPg6=_}^ZAO|b4OyA`~Bb&aUxHDwCJAZwlxU=<%QGc4-24>-+m=y`n*cF z#|;r1S|5QSr2(Jj0acOD3+8D+R{?=VDV303rc|Phvra4%`yv(zb|EB(uTg5o>3FkH z*5C4qAi57i>BX-4Y!|DxzJ%XwxBkkzs=wOt;Km_^x1;hhtjBQaOp4L%;Uw1^Yt14jH(lu#UR9;+#z^uG*rRnOcgKK;hO0M~j1lY(S&8EMg6wMI1!#Kt>5A|G4t7;zPz z>{KFy(yfFxjPZUoQ&GrdhjvSmfszL(tR^USvs9YAI$}<G~@^Ak&zshv}{WuSmo{w$O zHC)mVe{OfQ5*MvKjC`Al$4=@t<$;d7<_fE=9<-7mbYeb#G<7Jsqs?ysc86;+nSta2 zY3E&4Yzb3qnmHsIoPt~V**<%|Sg*o;yRs7~1Wg!9S~k2Pb@>!DPD#U&?XRP|3>u4~ zc}h@pO*r$O>3d7m6KB7Z=nOd7t81GH3fROV9-X(dtQVaZ*Z3ANPdEdJuULoUtK1IN zDk-5rkjLyYhTJ6)o>nR1hb}t3+y#4Nql}d|9X2GWuQ!c(t`K9d9n3Gdt=ZImt$1#r zVxXzYR|}I+E%4#3mh%(xr+@br9wHr4ld%-8&?_NViK-GNum+#a#|qow@`zBpwFUig zvM7*-%f0@{C4XGU>gBHGuPKZ+2Y0Q5k_qe5zC7qp-gX{BU2+Kfmxq#_uutPYNaViJLU^&HTBbg}^ob(ej86iNi`8;0I?hY^Xr8ooVonMwZ1!(v zW4Z#X1{mOSM>Fk<`d|6s`B3yx6Xq#}SP}pvt0`q)orUmikv@c(cc4`O67WlJ0WBOp z;q0TUHmj5xi#9Z1;a=^rEg^0u_yX2VDlpz3JR2eDrFKEwEPbgsa7-U^Tqr{(N~yS2 zDmvupW~#640~dr}(che%j%?&%KmsKhB5ihd|g-!Y&c&J4La!DY*tRy`AS zhLDRRdK5CK#H-z^Y=PnMU5_W3gxCQIrR^93+*Il;yHY)3oQ~8gqVGG@$%0F>x)|Vn9OA-|?};qO3KovL1k2c8?@t4f@>I#*n-g5x1nm5)4=k&tx7~tbi#WhA zoIIZ0;U9=Og86`*Mr2&#=Cst5;g9K`{|px0oS)@nczR!ighP5(jEDVSdB2En%r7g| zIA3+x2!2VX;T=G-T4xuj!X>iVaqbv~^jZ9M!NSfaD8MC#Kq-X{y8e+{7?(md%0jG& z$g_}?T4HjZ&llZjrYJBKNJ{S^3n4v@861mN2ry;olqQRsQ^sq2py&WZ>yRV(A40_h z3xk2!oTAOSPM`WF<|OyfN4#&Q8VH2asS_A`hG(^@%PRDs(ut$TDdy6d1%V5GxP*$y zI0wliK-;ZL8e+l-*-g8FNf5%^m8N(W!qKarAV0OrhTr?n!b8DW%o+ZLu--->4J(AC zhjv9wa4K#oMd+2!cF5 zpM>4sw(u)odxDVK1b{DG?8iWALN6tq&o@gdp z*W03d^cn&`g;==@8PhUR*{%8f^K;AH#{> z{ZT{0^tnu(qqELgM&?6|qJ$wWL8G~*PLCwU&_7wa;jAo9Ra!kL(}+5)iAQU%}Qli5;$eXpW}%X)gn^+YyKIs~sT@ zt))>)|HXJ)oZS}y;`sR#64NL+gViv*5wW5G@v^a&DP9;Ipb6+v(h&(D{G# z@>8PQ9=NgV@xevTdW3IA@Ud;MJzL0R8Vxd8DZ!cj=@pLPkCeE9Ra^c9R|-Spkbhs( zj>Q&5@my>`0!hXvALa4+N&h0nTvxdI zNzkeF^J{j+JZEYYXo)_B#c=vc`*ar2aPo!beHKST9-P4bq1880GxO7v-fe~B+0fxD z{&14(iiSx)gtt`Zw||CPwA#`3guw0*rqXaJe#}@|>xF9v(6FyAzgLcqrJ_liu%#FY zVLUq=4RfHi@#Eg#{Us_;Du)iXGbtAb>WSe5yvk;Hs-4BG59p|)j~oUxH->LNQSzlD zdlOF>a(u}QP9T~vuO_qe=8+GCRl2;|*U_&khtVMw(3+kFn|TS5!4EJ9+tv#E_jZat zKIed@B-5c%0l8HCPl`8SA={iVG&D%Ql$lW`tx`akNZ+5O!^|<7@2~DgEe#Gc$vhDc z(=)ca?ykw9Ye`+J^IQ(u3WAF7=ezd=pHz!1qrtK-xO&QFKv>V=_DX6%URc4b6riU8 zz<$YDo%pl9{g{M;-5dHV?8%lwFkTon_;>-kl(=v`cCfklH(N zX9NA_f(_mCYQxayHp+DpOl7pD*yAT$urAw_1i@UoHP4zN^r@j9Wc`Zf zKsj*PZu?foA{&4h-TaJKY;Uw^IJ0>@D+FNLf+sS1e&3z^n`y@>s)Spcp36hNN{K8j z@{w_bA2)Df>Erk`I%q}}0qwqWXP1y8C3+bF5mZzy=%xg!GYXC~zv!hWw2UTBem}t5 z-E#1hw(o#`N6u0IC3JtR%Td+8he$(&oAid`X8dDv7&o~kB(Yy8_eJ0`8ccVlpWK<{ zOe>&Dg9^xZkSv^&AG=e`Iy^fhcx?5tb=v@&XdPe??hNK{Z^Di$tB+73a-9KiPsy=uD_7=9*Go#`VFqBl-D z@~l~jHLwHvOKqUEw(=~8lD-Rw3PPfMPl%|#Vl+QIr9GPR*=RApL`NdJ#`fS!OT;R#LXxB5t1<^q_b$tgA|<#r<@l7SW8hMpj=%W+mxTuHXI$Q zd4y#xYzkOkSW!-Rr*sD4D531Wa)DFf1zIgS>UCZ%CG~pFeSd>`{G@#JP$G<_qz+6R z^roi{4-boG?bMwE@RFkT;A`ED^<)8$mB$iCCC|00KW7^wz-VDE(WkDHtDzv(^Hm$^ zF)k?gVUSXL1ds%UD7knva-Yyfrw4!rZ`QJHf*Qs(}Mw zwuWuhJJ=cSg&u_1POf-hQ&f<%fg4NKb!tU9cRHWuCeQtbafnS@vR1*0v|WYeJJHGL zi{yPn>h*yhhQ<=6Elm&wWWxj6{4H{xY`s%O+XLMsG=Le>1f%vTmQiiJH@XY9{SMf?$knDr8eu7WqI4!xNx$gYq+>A+KO1p#-~E z^J({*9%s0>E%^e6FSaQ!i(F!gl~azKOf!>XOx^`h%8XQ|@?hK&lM%GydByJ`2?IzW z-gkjoQgx-JF@m6UJsr* z^?Wv@JjEnKfip9pV_kV3L_LD1_YA@aA7V-mJ5bLYSJSsGgN^g!cI&G)lC$L$>Bw40 zY(2@{MTO;uIS&LNs zoPxeJ3HzHwi=FnNL$uze6}vmOup!|!Xh}%~LecdS)G12_{)O6c`2||#sw&?2kaAmC zLlz!n{4JuUiG7rzXI|B?4+AvJa)&Q(*fqnCN0S9+r|&Yci$FXDFeiQrkQnds`;MT7 z{&&5wk?~KC>0XcH643Ou!sFelM0=~(f9y0Fh}zf-=&@{g+o5GTC8MI~bNuJ!nGy{T zu6H!#R}s6UG!me_{L;7-QJoS;^5RvMcJ7FfhB8zuAVa8MZHQzC-?D_&Htwdp$us*Z zvJuEYi0yQiqf1c;(%yY?H33DMkTqUCzDt^*bc&@s+7XODw0TR-T z&m_U}gPmNlB!rK3=hmixQ4jU-iUfvKtC>PrysQ`=Ka4h<8*YJ0Eto7e!oUP?K%8R6 zCfxc`LDU-@-K2|W;ZI!!M`nQo?UhgXXc86?*CA*R?^w-n;{gIwI-(Jl9{>PEw0;M; z=#Z;%qGQPkUmiz(cp-Py<%S2S2`%7ZWd0z5&}h9g~9y<-^=b-u=U8e$Vh1jek28j4lDK#KbS3*ZsLo$r9k{~MA?;Go=lon zL3Z3hSy)-_g*VQBo(+9BBBW?o|KnApH5-!)rh^Q15|*co4;58jO2YyL3P8+6|d zJ9qv8wPKK6S+LGGf^0$Qt3rGZi1-!_7!IOZUueUFj;5QGjLuqZpigs3;ZYK6N1O~T z{rEUiNJ)-08EJFfF@L>?t7)TkAq23c=I5Ou+v|JSaN?8KBg!8Y<<4|I7|+fK`LI&16oIJl3f_f{%y&qK-ncW+6Kjtfs8=LTy3o+nhdw%(y^0A5oc$DwD zJ)Ca@zmQbd*tAo@hx!8H2TYswp`==|9S|kvw$;QaPHlcSiuU=>Y@Op2>k?}q?_;Eg z|0SpED4ow*tg85cp?!DlYSJkYWV|S9361R3{d)Cz*t+^I@@cSajTz=mIZvT9ih}Mh ziytHvHnzzC;%G#+-{=+-4-!)j0Y2h2y-9M$_6oC<-dwG!zg1T{${y_A)-|tMkU>s; zcpXNS7ypimMsQ4POPj#)j(1V3)y(b{q1zfBg?lycsRc@Few33*C5VQ*ZLylaSqKST zkU4Z2Icab2W1gc{HUANS2!g)v-iJG<<*wB1C&$zMSpGGF=OM@2Engn3i?$=@Fd^}K z0F&6c7Z8@Gsn_wty~}Uq%-bti^Il=RIjNc~&p!$qQm!a=UKN6z+Y4;(y$a|S3=?%y zSX=!1i0^w1zned>l}9?@<*(_lL_ za*1BX?5!Qm%Ydf7xsaqKZ z79;t!hVjiplxxSS#_Tww>az@mpEKDGBHYxKSnyS-1f=)@zg-*#cq3XY&rl-MH}duP zG&k>MHyxcv5v79WlFj??Y?2A1lIE%kPq&M=m6Ah?%s6C#-i}?^6u-99cztnsWmwtT z3htoR1w|&#GaJHvSOK|I631%zP8!){($JSr_$rwi8`g%u1#zAN!UMQ8f+PQ^qTl=Y z?^N;^(C}-(Bvpm{K&Dy`bs&5aOBCpa=rVO@2gOQFaRJSHxb2i+q5%#2K>y|XpADo) zj~Se!@xKl}KA-$;gME%UpsvqCH+DEW?=D~}A!%KPWs3Aqp5{a1A_o`1@w^|;A2(&) zCf~&KKWHZaZ=Upzl{*JdAeSkn`V=-BHp@a8BpSkU7`JB{x0@MKi~u1dS42h-^1k9( z@nhk|N-J*9UAZYw)=t9a#nKV@C$+f&iS$L4fqa4 z4^PKhIYmvxmQ*BWJ5f~_CMHnYGOsWs2SkYV3Gc9g=1#7I$S^hz7zI-~udC;Dm{6ixtJaqzxC708Li59{V4u#+cV0__;#xc8by$gJocUfKl!8B*G-4@3A z{C*_UcBUQZ@I9BR`s%92`7dMi67izhtf6eFZZ6%CHM zpGWqkD#IBh8$A7A{K@!Vj-k~SE?80aYS11N@d!t}8qM4=!TsaubS{V0>!~iNy&w{b z0a2X_b9McK$oEf-xK?>uJ(|C-VioO0_~ApVrO$O6u?-DPIDKX&NF7#8$^4r&K&SjF zd#D;hGVXQqW-%VXqw^4Ts;k>r**u}khgGXa+?D-_?1z76c;x`IN_?8SKH+a*$d{{|pbM0QqM-kkyh zsmO3Kt~ST|%=q#Kn2znDkK$K=`$cm0bg?-~)Vg$Hew* zs8y_!+{bNBPonHFW06qc@>v!!Qii0jtkB#Xh!W@ibb=@hRzfrcK;}IGKxF$?Y#auf1&!v8V#Di@;7Bq<~ZxC{+Of2qA&dYNe^wqr`NFD5T~-T_y+(49UA3 zut-oXq6U3mAJ{?2J=;hmQnz$((|b?@Su4xNjQ$3|VrHUJ<|VOLyIayKg|l3Y-|M}d zZ2gMJYB)j#3&w;JFS+LD8t^BHocjDCc9RXDRn_e*93pnZQO3oenhfMNR6hgT;(fS? zHxEk4)Z~38=iKezj*z{}&+N-yo(4<>Yj{c^*{&Q1ABAQ6R00cwvA8lroDm6BX#*oi z{r8H}Aj7|^kHBciOUf?o&Y>9XvYgvrX7bw7dA|G0k^_&b?$fu0lW|GD_{OTS6(&SE zW(N&{aP~lbU!~i(IWzDTQJJr*ZM#rBp@I3Mm?_Gwj=#HVrdKmW zBCWqR{FAq6$(@nMSQu-Dm!0CbAt92|cho#)!<@)j@oCBw2e9H!=bU+lu5-sI6?_x$ z!?$fztZyBW0BQC12Pr+9{G426Q$N?kwz56Gt}P{MCmz-U!(`sUk#Zfcqk^gkv$w8B zXE3hgt@m+=jBDNNK_2(X4ny?&j5Y*5yvy&fmU% zS<@6|Ca8cLR-$#|E+bpcTajf7bfp*=4}Ezu=~uG<2**nAuoWkH z>o{$sMM)y2=9K#r0-5`9#r92NdY@dAw|Yk4i#!2KVIwvd5C3#8kLV=8229|tuxJ+O zFh(c#cX=eZU(iwJ(@p6|fb@e-+<>v;k3M7Mf+zYqwEe{u#biYB^kinNVBnX@p%OHB zSYMPbl$>H(X2BsmCHGysm!@sQFWd%wJ#zG8(;0Xi@5&B<40WO#?iA4 zAfH`KL>9_m5mhJF(beF4c)-k2AIT!R*S%91EMHp*loUBYEt7`-wl_(a90}(eM9*np z4gtW3Df$R>=^h)czXUMQk>ku4uxV-J{`gzB>`ISO2?#=)Ownjl$ za52kR#t%A_a9mxW!~)rLCuUz0P)PI4#5904fLq^AKpt=~x5sv?Z_G&1VYl<{8aMs-*7Dzp*^zf72JA3e&5it{6YZCQd z#!K+8?u>AN^J?M4qoaNBwl3(4rmQgI5WHspQbT;)dJmW5O*L5q0!R||6;-o~#_eIF zxLw%C$iKlp_YuEeT#OnR-C#tBFFy};ylW{;JNH|DujD?*BXybH$+R3HGd8o4P_x%Dz34KE_K&x8>qBhB_zO?&o(JagIn+K*fmIx(24i`!~sEZOio?o9QkEXSTpNb;C zVkE3u%iH$BHbF|!Rf7Bu(*r(O%@XxMjMT| zBN_l4C_58jS2VQSh+};;_jRlH1|_dYEYM<4GKLUd`-~tEMMc4u@9+m4d-r<$bw+O+ zcW|l6;eaj)@mVTgB2*BW=_mND!QNpPiGXcpX;>y`phhPLI4t|~aKMwKyEb!!X@2+V z%?%2Pt0j^sqC?0kJreKIZYfur*#5uMki~YWop0*;lOT#9T+N7DrpzuYu`z zi`1mmK%d$FL!jz4?1a()2ia-l(g=*wd^xyv{eRvmOLjLKYK8ZCIqZ@8r<0eB8w)bO>Ty^mA* zIWDaK!J8j`jH7NI-;Ed8Y|<7nYexlaEE6q9CuZ>1)b^}gzoC^lO~U4ykioE@*kR)_ zGeaMZXM?lK4ePEytGP5KOMW}jvt(B*l{Ex*`y}=IDmR?p>wh+}BZyv9K4z$^#2Xn) zI57Y%ZekNtEy*X1qx=eoDSC70#^wS-P)7Mju7sE*R7&axqlR$+@wrL;2xp6Zu$r4* z5!tBEbnY=$Oug5bfQm>zQd!^J09XP@+i8uL>MzON+|@#X>OQis&o9w?wSs z&oyNNaXn>Yh+jbwLgkH@Ie`ptXf~nsbn|VNvXs^%Uew+p0ZQq?co5Bw>eXd+EpL2& zyzv!UQ1FB8Ftf_3WaCBk*3^W9``a0WTBspgB$>+hTbI~T(1R9mznMoH7DGQ$heYA1 zz~rCBkQD8g6bDYWzrby>jIH`<-(oh)e0Ww_P$G8<)Km*utl>{x{Jcl8A_)E~C(2fb z{aU_~k{wi7!j(K$E#JpHg!_{iyv0(M;%sG%XtwDL**UFOd_f`rT_I=Z_qHK%1rC4b z@-wupxPN0oPp6g8k%>For9cw^IkW|xkfyro0X36bPVC6azR#>x>+w`v zJ8HXw^mkcHkV#G5Nqzjp+D&L^jtP={h}{76*mt}4%twy?;m!2~Iabn1&rESmyV9*~zpY$mE)sS;2r_-o& zZ|*wLh<$&JA2lhx`z=cuf{plb@vHmrY7T=iZ?RMpBkaw01E?mJ!%5<4)Ew?b==l8^l0308+RhgQi3{J zq4*}FM1Elyj3%ORrvi59WOm<;NgK0UvV9*4l+@kF2?KoV=k*RrY8vpY$0cW64aq-U zfB;Iap*U6fZ7LQ;w(}&5d*yBTc~#326`@{Fss3?KYiD2LQKra9UUBNOtZKm2f52J_=m>?rC`%i%7Wo z1%K0}7jL`svM`~cChz9=d7R}xiFs^o9l#ZtRl9~~fS2Svx+WxcEiB_nB;-e<+M6=6 zqJc{93FR$~x=vE8LrC@E{s`Rqh5$PDT{i+IWBKhU{jMaA4m*h$VnN2g64!e90{J3MQrR>78}=>sT^cOH9n&s9$s&Yb%Xun`B<0uX`IF20rwLwqOV{-) zQ+E-bNeVu=#w50|TSBd>zV@G2S;Ha?L01#RE8VrtrMV7pXyNOxKE^W+HVwx&n-$-e-)e##NBL(KTvH(DW=Fp3*l z*f|P&gfVibTG73OcJ|E;&8rP2>anKysm>c=+A0%`!V_nLlb*=>66IGaQF_Sxk>xB( zDNCunSyvV?93CW}Z5Z*yOD$yzFWsh^lAht6YUV7X+i}J0ptA{ke3nDBrfsGvKLo)e z-xs8N;~Uq}oZkFRc4Ei8t$Hmu{C|{;2r&UjJKh8R6;_^HeTQ{&*u0&~(Q)z7(a-Ye z(zGgbuvoT~y}TVfC9zE7?S6SkF~|iaqtUdYTp%N?E-zEKW(TeCWR=uRPS3q5&((#DY1i1OQyn~PZ$q-jlPrDdm7pO-%cSHVS#XRb%3Hj~UrWNcag+FYYt95J z=5}F;)b1)=R?5L!hP-GHXFXGpHMYz4`y>XOa*l;;%@Fbn5gI2=7xv^;1GjNzdtB>Hm+eOO@cuvwOxxU)y7L?!o66{Q>U_y(i#(O_yFal;^$dC$^;l?}|xfI7Ze72xk*$;&=FKfdA(voWm`(!6xYI&GI>T+P(2 zgjI#kfDmV8#vhVl7NB8`4v=cm7xT4DkC8n;r1{S{kSwN1?=BN?B{L-#cE>kiHYGr% zjQmK)B)1okIkAP92r^STW|3B0oewhqIgHLOSQL`S^aX(exFQ+PTd*(Ev5ABQ5#MKD zKp;{cQffQ~Msuc}Ko6g!X{+H?t<}KFz(49#H~MJ~$0F@7kPM}wZP#i~b%_&m{TJj$ zF_kE^(@Ky9frKCs+Vi6qOVAb7buYX*`CYJEa5*;mtGdTj|Bi(4`;?}C23CKxA=nNW zr<1z$8UfMpT!CR|-8~g+p!<#Z2t^VdC}usd*w>|kzO4V1*qi;5jr)F6>4w+Smil87 zWFBFmR@-6oTk`cG&j&@PYVki!Y|pL>6+K~Bg8HweIbWsfgeO?H*_UoIV?_zV2~NqE z8|UQ^@c?czwJrvv9C&b)#(bLD{l5=cJ5=_0)Vgg9)&i>Z*HEFjs0VTF`?w8>2aJ61Z%dF zgg^&)hhShvgrrf5H8kfU^_t}CO@7gJf9WIO%w))HA|~%XZ(q%x|2Ca_inU&|wSE6x zS?61(;G-CD93sBH-u*o!FnLwU6!$(p74FnyE5ju71CWVI&_5`fjwHbWs3b%mlh{QA9cp6m1PaPlrP9YaG!1AzdrT{u>KzTKlO;`&RT=6dTrV@ zYShW&M-J`(&7GDQ<-L;NdW+x4|JM-7P8AdmdrEL!Eh;P}sJ zVNXwcqP)xf9kP*HNvL`^#@4m7CVopFlrMxceiRmLU^@wlz2|la7?3j%%#>Rb3I{@O zGuE^Rrk{98yx4aC6J>i|llLZ{i|k7CcmiSsLvP1^-`g>ZZS&O`qI_lN{rJula!C?z zWq%LFBICeBDvdR6Xd+}-uuwM&EXP4(y^~oCnH0n*=R5s`#++qz7y)jn6jWv#o_|E*D zZvvkmbtjLfqB5D087tWEqTwgxYo4TXWb_wzjwVGptXx^%B_GjD#CbnM*LA%eWe0jG zpFpNdw`wz0$k2XePleY-0xyN7pA~=A^Qh^eXL32by8FA8+s#=LOmz;@u;faZ;N%2+g2#`(ftoM1hE~e~1E+iOz|# z$nJ#oNkENCJ2;NhcUs{RJFl8)I>Y2nEn_n&XlAOFeAE}dUsr=VOF@W~^v46JIAI?1 zJtJQczzpc>#-E{J#^o{sjcW78))6?SpsjP{Afkl|fLOeg_#ILlTqx+ajU~BR1x5L8 zc1NQoXB#rTw5<<-#SJw>pXjd7fmg?#DE}92@6;Sx(}j!1PF8H&wr$(CZQEI~ZQHhO z+qRRv-cxm|zWoDsU-V7)oHe^%;iG>eHrNRtGM1Fj1@6D(mwWf|eb~f3ghwKDQ@-KbW8oevtF{c-Kisl)mSdvV??# zPR|7G?z~{1zJ*)1Pl9tgx@Gy`&5D}a{CF^>=66APN~*G0c5$~^uy0td zH00CXbFU|2ZhGBfzQyPv49}I8H!eEa@FP0a3u983F|GB7;F?bS)Y#9`3>aL2P+t%- zhyf!#nz;!p9G8PD2flqwYMKR~1y^Ce%-Ti7#Hu^|>0ftZHN=v3Nq?6-Av355GF5j0 z>6MP<4fa?L()9P?VVl{uTKxO{rB#MaDT`0f2=q_Uz+g-d5dOO(q?rmNBz{qc&h2lE z4Dc&{XY|m>D4J^;nPvyH?*d%AoW8RwAwZowCGr~eS$|VTt9nM3s2#t-Mfy0A{U|_slm8aK|0(>z**wYAi>KVZmy(d^W$R8&!xO~~{N<*Mi6S$KYHL2`I!8^k51MEmG|*isAv;onwOquTP*%>!_;1=3XfAmO5J~7memuHxN8Q+Z|ES=RDkURFh?LI-%OnA zEfqCaJML~ge0C~3~EMf zzw{peyuezT40p(?sLqP{X3Vs3hvIq6dNCfiHc!B1=5CEAe!4)V_9xctH2Du~?}NV9Gw z#PIh`Jf-MB|7qHkI2;v2YlT^b8DAm~c7?uslN_@M%UfJF7#HWa4p2hrr2f~W|G`dC z0Ksza+;U9Z2h$k_xgA+jRQy)t!j{g^pBxI>o(B(XdFSi}B&&$ug zIb8)yz2Y}wZZ^pn1NZ0dU&Ckts!VLHg3vudT9|EcX~9i1Ki$lnhM)qb5h=B?Uz4yQPY^X)AHa3%xJU;ePfooq#6 z>1}2%zJZrNi|$UQ5Ca4;ou=VCOE+0;Y7_<(C{aHc(hM=yXDkPx>oABYO*-ME~HeIT`K4tdE-+ zbs@b$&1O_?J^BSK->l$`f`83L8f~e(6xaC3sX!jTe$`|5D^1@fUkESIopC(7m7MV znNTf%W+9Ye9i!lP>u_xa4-BGDnM|{%YNU;e#40X-^atcM(4j34rr**akdB1;;fsZw zL@qgkj)_#>v7#vI*%`S*eu7ksvw+0b#{DJB2b!&tuB)-ZW2{t>Z@d(g z>5jAdco13%0(%D&*0szIx}luRp~iOV7w~9ov8TA1KAj=kT_?n!8a&50o)S8CjR4^# zSE}%nGEJ4=7gs@m;QT6c#{p=9s#;drv0c3QYE5B%Bdun2|;XGLKO* zi`1=G0%QU!z#d#3z2=y4cdep_Rm?n|_*qpQCg0@O$r7AjgTN&&1uY%seS(IX-?~Zi zX}fx9=lZkuiKmu9D|_O+CJ#mxCymCUkI~@6TRM#qU(z<&{&J?TpUO~SOVJz$MNfGj zfM3R>+myv2Y7mwozS{ZKB(rkYtfaR$?0V>BN3E7cj~9<~<_7m*OkwqPCKh7(IA<9K zac)#@MGFn+xw!O^D4P-sokAGpcQZAr-SXhq);UV_3p-mj`8xs`+~WOi_)3YSCgUQR zUftFqgOZUUg+40!uDzr$Aeuqi@YS>b^t6WDHIM!K!gbQXh**oYz69Cv8gA;--K%u= zV!JzoKfinSHflD=t`J9hnfdZ__Z#c{OsPbJzQ?cLS4cj=G*cXxN+l9xSa9ppiXE({fq*RK1j zQ_KgKKRA=X>gdtAR?UjZ(FCW1$PX&rtcpMx)v82^s{MMX|81q1FCJBl4G!oBe@}R_ zO34M5GK$y~255;*S{V2AuP}{%sWg9+JWp%S!CASjn8|}^Mc9>(%1&m8wqlHk);g-% zVkG_>OOFJwZCUZrW=&Wx&Sx6b`OvM?9_{=3$o>I+LawX}dJ3uO2e_2Q%+86PwRNv{ zu4HJq1nTn6a@Kt~q$ixGZdxTe2| z`^PuC)I|wUEG%|eeBMsR(}nX!vk#7XP7jSthjZin&!!2Q2(iqd=w$@s43Hf?FyzX# z$@sc93h;9qh3ntclh$~bgzP*vojK*fz9_dg{>@3q)XGy>y6Sti@c|iLhXC61ru_5n zah4P>&qe2Zd_3uIq?d^KiTGXwa8t=N;q2&1oaFjBj{5tJ@T|;7W@d%8jFNZ1Ou3kJ z?~qJ^-@p&rMivZ2Me5QPd&Ufc<`4nvn3RvR>8zzeT=#bdJn+u^S=zAs%j|3o7_8MX zcmT~$Q`C|*Q`qigt?+fHSb5Og6~Zg?3$U;nxkz-;75}^Rui##>JiemsN*^TnV1U3) zf7)}|@o@4c`qa+|O0sy2QucX;*+@!2J%sd$G6a`Qwn~)S$m30>t_WfgKWR61vm4MF zR9OeeBCr}g<$SG}Phk4Y3y7K}aJ_94sip1XrHumK%`wf^k4%Y!6 zISG!jXtP9pAyN63i)hn5=7JOFLPCn4eGkcsLma`>o9YryT7M{7Z*W_R9|?(KQNr09 z%~$ZWeR48N+{cx@s5aiDD@ZRk)0V4yRC+uLCj&>=PqZZeHL?5v!FP1n1rd}plt?#~V(?r62ZxtR0TuyXcvpX1B_i(Peae|qKaa}(|C+;0t%^d`b}uP<&y*3Kl9IA zwXma+7pvF?O5iAZ0TeSH)YuK0+-1k~cjCRO4Q+zjZP2QQw{D~NWq&ys2WpW@aekiz zbS*(cnuXJ)Cb)?neIxiNrwg&u#u_}Gu0TJK?`kUU{*jaQ1Ql@T=s23A9W7&g=N~W( zpUZ_2g$-x)+P`jpcv`Lk37l0(UlmMM^mR8bqYN;}DBiVa?ggO#?uD9`?5M)xjL~F6 zZRad?(f0_0RL7>olMEEk)8=3{1PKPlxk>?c!8PC(8wB&w4Mihad-1dkw`kPX1H75;zKOi{^-NHmrU0U`Fmw@RpF8@$$sq0ML31rp$ceLXm^z9cG#k* zcp`R<7molARvano#@K{;GWB4K7~nDZKAh`2!=WQ3H)o=*3DTx3+1R+E!#G_`8qCLB zJ22CkW$5s=KtaGt0`PnU||;S1M@M! zy`DjzXvL8je?OK@VMxF7C?iZ}eX2A7?Kho&0wsxK({+XHn_AsUJe;bIGqW!eL>PSt z4))Lmmv~H)&i(~W)Aev3HV$BQfnky~ZCGBw&EmCr$Y%3U3}s_D_{6ngJ^-3anOOd7xEl({vHENMKmR1Kgr54aZJz#fnhJ?R9A6 zHYQ6LJUBFx28(@I+ZcEGLJOZoAAUtTb37ON%(!PL(CVfEZGSXC%RI%*`QxD6*Eg#@ zvkP$Jdy?R168yz&A$f^zUbs|ZOb)JGc-zkeQ&_1ByDVio`!ql%Rnn0=TD~3}Yp3rTR5YRemJ5f6zNOW!qL> zZU^VM+CrTjlwiCTk{Ps`23UIl$8^lH___eq;173Z5$90Hy%ePm*`|IEU#&+2yUw2! z&Ic!JqWzH8JANhkUQ$FCc~R{TumHJXg}BA%Nt2Q~aAQAr$q3%|$CI~eA_Dv&MCUH&hBmx{Qso7fPRUcVJ^_hovsEILr~ zOO0C8-+~aWxn5mA60lVr$?_n=GJTu~-HOEj8PiGjzWkxQ&8Pkygy`Kn&n}-EE`Dyw z+>?TYgOA>i*CUxq$I^9Dag+Y-v*On(9_iWnu{a;MIeXy78@$ygOQgwp_34=1f!WMkwL$}*(x zBFQ7kLv7kCOhEmfbH(aOZmdrkdr^v>s~TtTKoifP zA`#d}Ko>nbs~SA2Mdm6>iInV_jAHYLD_Zu!%TiQ04Bs9XL@OEhWhhy2IiS=y;Y7)` zvUtQ!9Mi5}2gW!r=^=e|y`Tx5(Ns4rjNJ{Fs(oQwKTM5ZMVFM^P===-_A76mXs7sN zim}xlfg-g*Kjr9auBhYUQ>>26m)?THg}?cxGHCw}0b>TsTcv-5|CLYB-4pOq-N<00 z0E+ZBT+nt!ro^brRIjLh&H|oX!y^9+xWY<)0w+JHX503r(*p<@&;oB<3!YjEMUUQG z8o`uN+n&a9>)LZsX_5Z5tuz=eq(bZpncnW4%Ia0}HG*9E^)K*!Ks(I#{WQPDCu7hjvj z6G=tJucF>cxt@d-Gn}JJ!GVCNG3aK=@~|?sY09o^y;^WvzchhtcwKCNP-D3^1YM+ouJ>hGiH{4Ra~0F> zUHB02-$G7fu!G=N;tOd*g@AxhVg*>B;QWQ%Cx0{W|0@SXNE*Dclya8cVosjsNzev* zA(vS+n-~Ps5v%XS#k@rCPLKk7vUI@N6aeV+z1M^-ffq+Gkj-VokQO&Yx(9&zSrU{8 zR;L~KCdEjDk1SrM3>ZnPkp)aC*~d0qP3~Y5v+Z-_6TnAW1akK(h2Lqbjx%u5JEF>9 zl*C#F_-KX7J%3`x#!Vgs!H>7PySaI+>eY3`WoonuDr9fMkzzAyMo~s|NdUN_rjeUX z#5tuc%(k@0Sd|7H;~=56z2mM++WU`rN5XQ8Co7A$Tab_dE<& zqRB4+C^@cR9A*B9Y;;3TO8>3Qo5Oa_9doz#F!c6p<9OzECm``hRu}Xk32_$Hp#{7*|B;BX9!J#NFdsqizr)4hO z_(EDoe=~Z_@LnD-Y7#*uJA!JJ2-q8E5)rMKl8ruK9t+#sl{b$dQjsp6Jmz;Zwh8`% zF6{oRkW+znzSyZPC(OXj8A~KXU$Goh5EMinJ1SQ9Pe8XaAX;)C9a-79?V}D)y?~V& zPcs;xXNSUoA4j#SpT{Cz@5IjD{ar*NW-Ew7$USCWv7Y_6HOp7GCq;YpjHPi$n-G-- z{x}IDlVyWe8q%s!qA(CKIY0kCYK%>_`|l?YLcA!lUab|!WKCdbO)Tdgql2tju-7RY z!FqCn)#3Mu8?o58`@vNKYb(+!xK0FFddB`gMxSc;6;j1Fy!w+}>#jjz6aK)pOMxbS zQ;k*C5a-`YgO4!4=*w>O1)V&;?AYZFD#_E8T8*TeKhmo0*v>%UhEVpQcQ>ziR0Eu& zY@j{hQ-6wAJrc-qz5wLWqX7`|i8`>cEx%jo-=Xr_`PRU8%wMEz?GsSx05V`-!h_yRV7{>q<-87lc6^x2 z1(8bb!SY2Db2F5X;F;>}sYPw6xewV!o&*^&NMwdL#CB0Fc9mtq)weMigD2jQ@Nk)H zb@3{7z0}-w{<=ue+WZ}(XI~{^!L|i+MRMS%7tyQ6bNpFqjcdVDZFhS$A*F49svFOq zkIo+WE+hvov9C;w`_ll4i4?ZTvyj+mcp4*$zvRIyyRiAS3k**nTknul{(A@ouren9 z0#mv6Or?xZ0~q_~UvL&4{BLl< zqgAN-b5^FzaB7{=43cX3K)lV6$cN6~>+4@sJ9 zxZof6=uhm*>4m;iVY&FvQXSeZ`sCuC>o7GjY4B!CMb&rgW038Dl-hdK{2$|b8F0aM z&sy2%kx2r34|Dqvl;DU(-~=z$vB%I8?ANj=$<$_UXU2yX{pY)Yuw7ung=nlLK*oEt z)2gM0W5`b@OHJK3c1Bk1XubEH@9TIai7Qu1>H*eAP_T1Yz_dJ(4eUXz<4J23;Bp3v z?1V|)jv=v;;+f}NfkRx1#KOp?y1^WenuDF|_9L;PL!4&aZe)#>d!_XvW3W9`^c4sR z**O0Z!k!3Dtst24pSBm%g{+S7a_4BUNtg$}lkxm@LX`B;$>$5rFL-7#BdqH zC=xo~ydS5`Nd0fh{k48OJj4|k6fGJmR*p@7;6ZIwAdJ9F!AA9LCN`VA?pQfH*sUz* zGQh49OFN25s?L5T0OAolR#Hou)RBLLk^iee3jHLz++^;nO%5=RQQVrQmWy&cmQaSue!UsO~D@7fLM|>TRku(2n`!kWPngwf$9~U073hcgRpC zq*LiZz*$}bb6KipZ##|jQhsNHv?3$FPmtp=S*&M}jQyVVEo|>$_dOsKXs6b>H+NYT z2EL+;qXPJ6Ce`+8`iHGIuWA|Bf+sXn$RqJ#R5nr~D!ENI82Q0omKYvuezg}b zuLq}?#xOp0t~{1Nw(#0X!%uJ`ZMb;qPFW(l>DoVc5w~O|=mnpQOsv0&ptd{I5F0Tp z_7I)$GUPMG+1;j|AAl9E$yphJE0qpok|+tV-?_82NwinqbR?Y-|(-Gy3hOag~aH73~= zE%mW@E`nC#5$`lmu-`gte1wW%P9r0k4{m8#f zwUZ#b7-&HreFlZa)Z^umhC1t^{(n}^3330MO|Bs9YGWB z*WXseHa2bN08XI&+E2AG+Y3zZXL;9&yz!GFN&!%C&o^HwLg?NT|Mcxn#Lhdteq9^? zTMw;f_Wn5kQ!%yit-aqC4HsJ=;y1MJI2g2k_9#V2ghWXscNY4f#~SR;k-H3lXX{4m z2(6sw1wYqOwbz_+K3C0gy=@%oBko%c78SnofpmK|^1Lh2UPsLD@u89&&6^uOEp**f zXCUvWP%9sCIj|#n+d5yOQ>{T0K$ZsX7d-aV=PQ=+%ST4GEArfGokl?utI6a+?2?j$ zrU2z*3Y{V^>Ey`!d4#dHM`>C{Qgc>Zz)fAqqnlw)*;de)>El8S$ya0jVH=&NKl66e z+%yluCc&Mo3zfA%Dy~`aKXQbk6F%@<-gsM}Gv;CSkYez$$bN4G%R<^>O}EBTcP&}_ zu?Th_H2C;hpwjbA)LCRIoD(Nt;-AS5|HR?%s+@Zh@;xk{3F1;}`_phds+MTAf!YVp z{ieA{BBAdTrUxnNCGMc?enbd8pM9bz5xk`A!LcaiyD|L2>S!I)pfT?b6B@8L z%DDVaN(=+k%c`(~A(}irRudG}2T5Y73A(GK77J}5u6ZMmvFj!^t4w=l5Xldl6P`QW zjPa9RqTmI=0HC5+lrc)0Be)cMgO{coQN4 z)tjU0Ox1G^=5(8A2`eR$zl?`F6%E^N{P94Fh>bv;aD(FhFDM{kY%I7lAD4S-V?W{W@{NHxCK>;4#3k5y9A^-3zy zw11ufa^&e~TVLiM?t1u|%IJ2V^t@lJ;QnPG4QJvDZ2E3QSr6VPM{*Uw6jyM7evPV8C)e3 z>^?=IjEjdP_trnJSEzLwv;zikFV23jxOEMK?F-c2oe<8~I8v4V#% z?c@hiftF`uZbwfG@L^4fH%6oMx>d33ffIW0Ed)S)j?OQZeCiAh#2!lrVR-7ktwAA; z1JfZRpmq4LdHSA6onJtjY7D$ch~57o#?SiAbFbGn+M^ou2$qD@2;P#bs((AwJXzbw z%6CzDyp)p6yHju~KTOp>`cHFabXaXyDR z)mJmlmAVp56PDB$YJj7dBjo^I?7HVIce&$7!QL8gK`Z$I0P1@d0<5^!UB*)MQaK4` z0B0}tC@I6KeF=QipYakYF!V%hzEV3az8OXky^O{k68Xr3j)kO+!I2+gBUrV}rAYXM z@XJZ6Nc0ii#E#~1nag244les8PF828C`)~5sb`7zD zZWynr68i5IN@pKPaAJUZur)Iap;Nq{!nxh}C(8ksV+|(a0RNP0J+U(HYc?CJzi>Ka z;|*#&NOa z-qF0GI1I^=T)%P2hh}x5rj2z>l8iQ6&c1wI*GoRV3`7o9XO#f{#Nxwi$faqVPZt(G z_?QE-W$%bvb)tYFCRV#ae}_*;RTW1e#%{-hd~aC7baUbCIS^nm;Xn#p8zSyirCW6( zSef;E4D+4e5uxwX)<42MmKlv4_wfi{pU+~_1ROKcWpgg)|8#uWv+YTdL9j;pzlSrE zo~^rvD?;;cEA#C9`9v(8Xw0Vup)wn@nj|Rwit7FNk|g4saYl#sO{&kBB);14d|_GI>X@@fGy9Ww)= zmR70v4Y-{d!u1PA>L9Y7twn48p+6$aR;ZZVtz*!sm%8{{{=M$&cq0B)U{j>nR!m;L zL$9c+JXr{a3Ak0e6u<|(2}%xu6x zxzx{V*+uVhY~p2yt`AbZv1|hV8>WW(k9a?TD+wp>_Xb_s%YJt;Y+_3iOo0pkfZw;_ z(N<4*x$SFp!ZRW>y;P=OM{;7XZtHBnlVxyz-uR2`u(n}d%#wR?b3OH;|BkRgtVdjf zdUj_5zQ&^zUu!suK1HC92%JW6LY(ldHPAt4p#HeQUtudW4LTmi5xI#=(W|ifB3dmQ>iKMG2Dr!?T6UH;@*7+!?vqm5c5UQR~LiHQqCGP_Y487Rm&xw;7)dxhFprbk}|rO@e4_^<>* zOvL38Xbf;BbWG28O|>!u>7XPWNe+r1U;A{?AQ?QWh8=oY!XwO=+D1p4VT$qw02=Pl zeU?FrB*%zSB7xs=m+-=eH&DS1EpZvn%uq)zQ6XHN=4ESCZANA($W6ybM)(d8vjS0O zaj2r-LVo6~4K9vuJ}bI#{QAp!@k!iFv`}NY^C`uURi_S>OGmc+*Z-};LKnCWS1X#7 zewrO7BgPrKxV9-ge$`Qay2voQpq`Iyh-+7RYd(z*-y5wv(Vtmr?w2>X;U*IBWW}Fb z%ZT2j9RS^8sVZU&cwjq6XN82-FA*S(cwyo!&w2$$lES|DvN+@~?Pf7KFU92iH8rW6 z!B^4(eymOT7B1(*BP4@Uzp1+|t2@BLDqoA!?eRYAm1aB6ORRBN)r zn!CcbA!Bzi4zhd?7jq#&YcD4KXql-ls7i0+I;8^`jJUFO-1VObW4ERpZQ$#y!C9m0 ze-gw3vIWaJrN_>YkCeWW#jJ|#zdTb{6n5Vke?zU=_LmGMQUbOnESKfAlU zH_vSiz|_-~I^ML*&M_%&lQqVa9q41cPi~B2Z-MN24b9j*4HSjFCYPgwmD@7Y!B91wWww4wRNhuubFWdqo&p3vHkxOV*B4j?F+g{S<%9Eqc$o-<^U zH?O>*UTFql@|&ZV&0jG3FnkQNcnnm`jAWe{AZJ;1*}JR8cgr$l0uARYOsk6UR_ns8 zGktaoF%^>G+F^-&*c2P{vovZ7AEtvpBcNZYFk>U z3m}1mk#ud}rGHgIw?0R}96jpPExB+m5u5HQ*+Z0&)Sc>IU*oniXR1>hYv*5dpG{!T z5?>b#3MG3bm}OJ~^x|^jQySB1@2;mxQ}UYfd1-AIchDh>NXP^G92Fq45sv90-A1)l zP=(cqxX|3@FGcIRBg=AXbMC+uGvJ^QR`Y?mjf?B4kfgR%K-yA966dch(qgT!8HGX6 zwUQSvS2v2olLdai`Kt)kGrqpWhE}|)G2z;D^w!^mh*0ss<&E;pl?`L@NmZqR zQ<92F1;c6r3~O{gb|cG`nSNAI0P4fViXVYU+f9`s7HG*M38*dtG38^{3k+|Z@>~i- zPtrn+Fj3(y2Gu%7)kp&8=4*}+U!~(-%boczm8#(-NXksc9zMDOG1J+mD%2AWHf*@x z7NZ60GSB=&+>&Ki9G?SyG^G%P1Yc6Swwxo>4jZ;QM1rpxkCx!d`$!JIU{X|{P;U*` z%8thvzMwW22_cNsmsQ5cM9PjIvFoTlpC6$xN06B4d4C@@Q{S5)6XwK!q*P50+NT-n zwL*lFYZds6TpTA6p35oJYs7l#cxj+cLJTxmg#k6|l(LI$I2+8^l)&3bw{r(&;(D>W zTQC3=(6UbOe2d`AvQQFIkR#c3wMS}$%PCX{1>kGXoFBHE zna7m`u!c1e(>I;yY-ALhK5E=#vj;dve8C|i4(?c3{+uHKcbxnQELAbjVMfqIFBC1B!(DIku3_D&_GS4zFW2GS1ub`c=Q9`!j&3 zMTT-%xn4tdIrbBw=hCvBFF)^hxNv&>| zPD|BDNgcs>4p?1`pj>@J>ZQXh*`{fIydGy?Y(A`H5SEP=m@F!NmX>IQ5Kue>m;)}=bvyC$SiL_-_<dkEl01MDPfVv7}kd zDim~Ju&otcCl|mv`3Sc~s*43)>?S>-`q5hAhzJyx`;b~Q@Xq39&nDzx_q_+e%nOH>7(u(G z-f63tFI}`UtwMosgbIWXzvI^X3-@|LbrM!zL*nz9=$qEIVuV7x(D5LnrX=EJ;7YQ^ zWsH99s%{sW&f!E+Z|S%yZI^BO?M-)=i6dfW(bi(Sp{Mn#F!JdYuXQAqHDg z+~)+#zDIvJ&}G3cPceFa+hBOPL;*=g)@*E`SJ zR2Sx~DH(&?OzOV-c!w}&xlRP|y z)bJ(ko7XL>gF#RJC-to$7Y?PYHg{JD=i|l;)+QQXUpPrZxjg{$Ozai;(*omvqGs|m zkNi%VG~e^@$Jsn%wm?mOO@1Ka?=|d^e`^+(i0e$u1;~X#gbaflHc;UD|BS|b?BJ{V zY305}qa+%sy7Tky!z6{qe^S0Y9~-}i03f`Vk&i(9_kLiCE6{`{ZCWmybtk|1G5R%H zm?9J=*Qy{amo7LV2BB%o5Fi_nB~XcAff=nS76Am*smLlE8&)=9oj27AyIew6YGxH>^tW}3e12O#1 zYFH2H8JO>?$`PwdP5IpZsuZ^yMhcr0cOwn~cH#SJy#?>H!k63uXTOys+AuOK^1*9= zHBs&M8{P2CF_ixD-#z)JKv7WV=7r-sHcG@(NwbZ}PF>*%pZju*(bzo+)O z*H>zoV@O2s$3@YzPI|oTC_=f~MGp`6CeV_+*sKNXI827%C?_^Rc%x7@@xsH;7NQ8{EDyYG2+!#2|a&jNeDYcF_}_++Ri{R-w`ms%*@t~7UDvc*Iyb)?7inCEfBm7pI;Fd! zd35M4Z$QF@Z_3uh1NUFR_5pTR)z)*^D6xcbRMFO772wt)X$KkEi0F7-%UHL!DkuR0 z%qqMnN|cld7)gOu2ca5U#D7XRN7r#(J=|uWJ_nY`;a%hsfgK{-U9XDHrA(6xTFk;& z4$w5(XoT6wGa;o1F1sMkR5x&%R@Lg{8Nw!cN{6Ilq>AuiaeQPLT4m1+3cQ9ibH4_p zE@(dc_3L4gTR71F{RW7+FV>E5-6AtjHqyJ!0um}cp0Fo09@08=Y299X2SMm}^qLKt zoBF7?U~#VllUdGi;NOF9+&GeGCTkL%d0D4I*Djq+%$(v#FBicreOG!ivu)_muZpRD zaXM3)$dD@8I9{FOkfMjuJ%UlPC6c-}#4_)pM+1GMqbgg>r<$<hKe1_z(V>)=r!`B&Bue{`Gx-!8S0F%f`-fZ>0 zAhFndSlQemJS+bE7Zb?6Mxn(ug5JcMHS%#6mSD9++CgXQO(F*e?~z`|Fca_@S`8de z>?|?Gj(L6_VMC;qH_$ig*YvSz67XkvEz3d80ElT>Xz@c)f9mCj>$u3-U@t>1#^l|z zb+U6yW9br-OeNO@=_SwJb#09ANq){s`W7M)lEEjE)FwLQQ5;Q@Yb?ohdGml~(B8PD zq_T-vv0TqJ@NX8B7>Je%rEf$;?_(`PjfXV9Q4{dAbuBSuFx-Gb_(Twz zU&jPTH|5<&z}eMl6R??qKX~0}U+vOrgAF3cU>f0G9%9*)(2YX=5-4BS2aN3LgH={q zw5bI=OmYwBnw(GrkNvU&H`J#@G@l&>;zL+;DIoA`Omx2{ybZs0@r>j82i(b{pH#AU z?Fj_$cc*L;Y9p}UpMBTAGwDJ5234sPexfFTjJyc!gLcoylHy+vlFszWNb}H4EHGN& zX0!SD?L|26AwW};j`8eloQo*Upv<#@AO~!9zOs9^Hm2 zG;$598F5Hs?~;7{=Ld%qbOAI7`gLM&pR@Z*3?5MYTd8hvd)i$o*Pdq}sw;wsnkmkn z6LWFiW@TuMXkk|pe^4B66so<|>~emUv5s;#`qU%Z>fekF3tkcIh$~}z?$)ta=`9SU zVR6?2HC+!bR0D{^K}JrTmsbgvCF)^^VteyEH!=MLD|`Eo4fF5VpL*16i1e7(?bw8z z?c$lJcdivGynqKH%yD+SJ?HkQW}4jgUq1i1>fpD_-`$*jg*I z7oWd@T&Rl3YC5U9kc&2%J8+sYO!lEZ28LAPHntqKrx6=hlhHKg;|_|!(c~DB*SG~! zeNjvpDjn*oL0y5Z0ulK9QUtgx0&h(^4s;PHhE06xei+uhk=rLb{Y{)ne(dUh`hcsByO+?VfhF*{`ur9v>tNAT$fimD+TZO5DP0}?7#Gl%Uk zCVf%EbZV^fO!bydMt=?@hNnVyX?a!Zy}3uVX@%1EYw1ZuA>t1RwOxF(*+*S7cf9Fu z8I=bbXTL-R5iKj$O+3jVdC=ZsC=pESo6;^wOAx}HVy@W%^Acq9@1JS8N! z9t3NID=G%6)`-+M%e_UCq{vB0n12UU#KLM9O|u6$0$kqxj$SV5>4NG3ID!%6g=EQs zI{vT(xf=>v;8J{UXpj(%KE?H_J*C6fO}ru9>-Zp&NQ9!!@Qm@Jz3rv>Ye~zVM)}+l z&*7J6e=0K>-c0L4x~S>_SQ1D9%*HFU_0W5kCjrG7^t8aEa-eQq0T-I-5hKA_ru4~R zc__!m=PU|vg64FRNeoZERqxNHM3LHcANS*hG=p!SlvJc3JMt$La`Xn(_ z$uHtJlh^V-ir%Puc6JV#9p$Ln5mHt~aP^QI5vmU)1+y%k&g1KO@af-)mxI!}$k1-F zO(`3Bb;6qh?%jvLKb!QDMH0k^3mJOzn$zQ^k+gxb31&(gC6+fZQvA1MYyYu4XI;w@ zo;J_OVtSK9fF4%`;~vD6O^qsCo#X3^74*MJ3abGNO*hYgtkZj8T>KW}vmAg|r) z2@~XCpOv|4nl7X8)}JuS)o)QbW+rvK%59C@J4?rpcF_Ox2bH5|k*dRk!9dmgbToT1 zf0dR(+raf_5nPBxnLIEw`>1Tx9R_ODMr5H~hpjsf# zg2+Gv^8HOhdDFV;lS>kfd7is?oH3u^LbkpZP;5N3xH0gRA4{?32(=ZV}Oce;NQB zN=+YhaT*l_000yM1se)|xQ7T1!LybbE3g~} zJohsDliwcGsFi50=xVE`n5UYwJS zU1|CMzpwqD3MAF24=71gX@C*5@Sb@BvI&}=L3fiV64HbdWDNkcsAOg%5AXR3b&v1V z|KDTqc^M(=7@K`R^Q%S}a)Bg;rW*PJ_CQi??eqS03`JCSDla1cx}lfINGbuxiBHU< z0}`Oe@?mT$7jM09F0Q~Hf#-8Ym;N2ix72`cEoDfH+`(B>#33+K4TuKy$onxSdU40)%(7l~rJ$&Zqfc05P&R!8Q* z=jh?Rg&J#1^dn5i_E3VthaV*C7!KDgoz2mkR@rn6T-At z!v~Fu)2i<>qFB}kyhU~j6yG77NkyT6$qD^9G5592&qp@vfR+c17xr$%0p$S^O2ox4 zqxA*=gxT@*IvF;9cR(2m`PpaJhz9IgxT9|&&<4r|C5t4x#~nj5>B3;5S5$WXyLz{DhFBnY5Z%BLmX+;(1d;)P;L#o5Oe zN00fNs8oqNtd5l%;MMy-C4|W|=CqV?egH*WV3MZJOSGi8Nv6>(f-N<=XjE3wmv7DtfJ&W&xFJj~&FM>Ug+??OJ~{^^FnOn1_UHb-%@sSZ3E4k;qPh-C z9Rg`Wn)Q7lhcXp*4NR+pFu;QOg}4N*daDA`rW{eq=x9rzIY>esaCm(4kIBuDZC)W!cSvxIQxrH|^+_-E3!nu#xD zBqV-+E(Y-bPQXus{GqAkojBic)N)G%!VZA#Rko9})nEN1&w@3kEcO_XH$Qrkf3DVl z;2K*fIP>EI{`C5xxxR%$FWb!faOa@luKwe>SP-`qCZ{Gy5dR$#ecl}D6Wk;}<^kQI`&Xb*{ z3t#^#%Rd6%q`Dzh|7-nAuU}fiyOL@1fd9_X){BQP?eAL%caWd>?t6TpaL(t+Kt`7u zC5Sn0*MZsJLvGkzMhz6ecyrJ)^X{G{ha8sLDQ4(-%LobAyGuRxF70^uPqMr-E&02m zJu$Re7tx}_OpU4?It1s|Zl1O7i4Z-xwRILH6n^k`tEgIj4iDv892pQ9C4147A8om< zcv8q8G_e~y@(N{GVk6DT%v?%@|MMn(~|OtdaA07V>Y1_tENm3TW6rE3r%_K+Nb`^TQmRXfbL zp?d0a6KagMVFA}Bzq@EB=EB(}PT+9hT!#<_@|#CNh19iawFy0F7SN8ZmaFf?3j9jb zIr5KT$J|CR?6LQM^mRmY#5l}P>6RgukW3omw0Q~4Z4j3x!R3p=&p6<&H=2;wc7V8< zo$V>-OeZss*eOGkD~<$$LVJSd9xz)K9!5OB;v;iitp0j3_m-e`HY2|_I59~VZl~;w z;RFtb@3m4SMDqu_$?&O zBKAHIb^5elJ#vEkC|`xof#`5wjx2TCpU@5*gcS`yger05b51Z?=DtOzSwO9VlPHq= zs@@a9qBiim)|qoueEp`A@}$~Wyc(N?r@iC0@N#(addhEb&$(o~g+Oz6G(fmMWt7~2 ze)k{Km(l@Q1a^tWhY8d5HB1rqi4zJ9+W#Cb9rS(r%U0&`a{RJ|EB^_ah7IT2j$hbO zIkrtjslsQ0$t$GTcKJ@o3(ku${FoFlnaK{`-$#eUUz_CD>(EQzD|gr3$~M;K4{ooN z)IAhjZHWfAG<3ISb+e2dl|nzx7;dv^!Yl69((%LK-1p79oc@?EToF9R{G26*E*UD^ z&XQ)HE1?aI(sI?D28rJ)A&o~8iBGZU!sgVSmJ;?vViKz-1@)hH;Iq^`Hzleorw;W% zw0~CE=?ridv+m7pcsiE-QBXGXo*FLxTNg)=#|Xar0frF>-@q~xO4yg!$o4B#Sv~d# z-sDCA-cD~VR2kD%1HWdA<*g<;rH>}D7YYuz-%4ys{6khQ5=Un`vahbRq;z;XNpDaY z@(z@s-jJ6rr@HMPLQG@%q}wp?{IM^dAj>7#Z-k)bg|Uo=$`b6R{ozRnUk}71LplGo z_V>J9(m1r1rblo6M}>d<3-s*NQ6mQGaUH6UgVDG+&;OSMvKfUl_BOrzUix_s04cJ# z-j~O~#tAV55JdaS5=N7p`+%yT5n}7@t@)uNWI)KzVLWe)`tqftcrO!$P3*$ydqN%( zYD0tLv5^xCc%S;h3j5F=Wa=y698h2XeIb0QzcPS{;b(*+J`Y3@d(`RVDpqsV=$KdG zOk87S7jerjx0eZ-ow)PUWJ=fb4^s1g%99yD(sQ|T7I0U6e%zq38{zsfw(x4@ILRbe zcx6gW3&kjc`fWr#SHf%Q9+@z8I#^2!=qs-BuZ%LnS>uXV#!(PJ2GefjCX_Z0`@!J& zPdNqNe_)U@;_~QFAOXZ}CNyIj6ycdd8P8aV|L!*+g6*%wg+jrWD4eL!hxhYN#*HE} za)#|zIYVPAh|5;6cCuXK;v_mre^0$Z+X@tHRH{7DlEBh!y2SO|f)~!x!r`;<8(KhE zK(x}u8v7(Q0$5sCul9?FoHrxB27gUGBwc5zP-@kzBn;A9(!g00ulS)qZ-6*u;6-9s zTF_T~%0d>Dl|T`=vMj)ztg3YitACLTuErnW9EF?!bek(vMxt z{$5*w^vjRS@X9!feRxj2Yd&U|Y98tmN#Q7)#t3iS-Y)oQiLx)*f)44A>;v2#BREbG(x zd~Mah0TPOzCM)T;_*u&455j6Hscg0SBP5-dRP`KNZ?%D`cfNr>LFJ2u%^d)p-5gz5 zTMlx!9>aBjiBG52^JHK(@5DN?w`<&jRt}p;;Z!l>6m#Y$nB)u4TejAYp8(|km@B5k zg!o?1-yx8nHK`kQ6z4nT9vI%51$OLZXl1~Ny2L0^o+6U!cK=f|@8CUI#6JKkW&L-^ z(entEHAVELlqtiv-{yj#CY7$Au}1-${7NAT1R&8b_dbTJM(ve!m1#fYK_OGnj||&K z`!u=ftSTY&Dv&#(;)&qS^=D7Nm`m+iOIsJ=DzlB*ARr;Z@z&Fk1r2zrMbn|0%Vxf^@_dM=4_~enb$b7 zvLTj~M^fvjW{zH03?OKLWVnuG0Q6Mc-Je4|2#3mqt)|qYSbzL;lAu2zgJFxG7)vA8 z!}S8Q5jw3u;u@Dx9@Lq-DF5G8ju>7iimFu|Z1{R4GocUgY|L{Jq^U2LGNEG-3pm#} zQw{5(;}hK!%5hcPjC)nw4T0rajKOJmk#$};AsroidK1ADYl+A7hSIBiM{rGUC6-2l zu`M}AokG(lX=)kT2$~RB1CUP4uzEWCBG_nOyyk~-W#P&LWp|dNB9jQ~hxi}=3^pe= zrK_&=sv6Yfw)ZQLTmiGluj!vPDADzH69@##0%v>`x7}>Pe2Yl+FD=mWh_n>Md+O_J z!%RXW_!FYPo?x>Btro0*#b^S9=@l{0lgnrlRJa``(6c#=-?_=E#f3~=u`sR1xT?GO zGF%fOp1dmwp@y}BJSM?myQ;v&%(k$80NA-#M_b9HQ}c~*pThMh*A5S|r4%#%I=}hK z}p1i1KK1)xh-L^7f*Xd$cwzH4X#j2* zX*`WLn|DX;W(eOr zSL8iFSx|PXho4hd;X;O+c^{jQiY=p_UVfa}fQSE=Ozsic!E^h8GWR(rIHly_^|)2; zDFa$}tQS~sbOK-rq(Qcp3}TC_6M)pTjyazGQyo4yJ<;XjO(~+@)6;r9$SH)FD^jbf zQLy0a2tK(k&|c6qaa6=zs&LHwyI434N7Lr3*`VrOVr*(&1rPy*mjmWU~ z{XpLNi)h@F_mZU>q=EnZ<$FThJ~}6gm)tBdW@wvrS6O0bGNBxBs;`6-t`shGAMd@Y z3wDprG3$60uPti#hDNM=0(0tyF+IUG#zNu^t{B-p)_cp0C^zed8Gx~YV9_U;5rpwk zjxJ=`^V~f2;DjH}Rn1jdo8K5>=LVR;&&M$+;v|Tphh`X9Wp?7C2)Qy5aN3^h=DFzPPY}$kQ!3Q;4aviO{UKlYNjaQjJ3d1ohssL|*l}cOs zJ&jmO(Q19s2-vGifmgoR*O6|x4NF{< zCtsr+0ciz3vTTEv3S;YaZ%5Cs4=mRoPTHnfz&T3ICF}&}erLWqmnBy81eP2@JfX%6 zh>35!s)B;?wUdG)1MTMrbPd0ZKXT%rp=&tL_>Jl$9$Umn^8PL&dOeOzkKOIfa@tI{ zQxHat^ws1Sfl?7k2VreyZ@-KvbG6atPMr^z)PW=kzM_D59<0m`%-$K}@r=N7F{>9| zy%1RHZUu%5f6#Q|{#6SCn9B77OkQPhwhgumIuZ|1PKou1=b%VTL%xwg+q$=oQHe*p z)APXP9rBmZ!?1)MXqZvbjbi)~ug~>PiV5H(&Uxd2?h2pBovQZtB}R(J_}h<>5o8Gf zo&~&v%atU1hD`hkF7rVCWriJvls=k@_P}I2>6t5GjHv!n?P+EH!QAWU6`~AN)Z+{M ze%1j!ahGsQ*PhAggUFCPl!^u{8JE+{Q5#sc%Gcb~DgK`h)6x;zNMlE zpi9Mv#3tE~NDQCwJa55g0}$iKo!c&BBS zI?68__yq&mTFnj|Rr+<5neh!)0etp&bn^yJE;2Fq>XV<6mPVtZq#f&fHQ2eZVnHlW zr`b!sE~@;*G{ci*!((34cg7<(YayA7qpMHTFM=GcN@axzUSM;1l&UUoWO6lzr7a(` z3^Sq*6(~c;7Kt}L2{XZSt4mU28Y~1086Cj}24TZD5B9o=#UKu-B3kNF8@5XZ%AJa$ zV*MINyiNnc{}@Tic3UzLjSg6D>s8E@I?wRG>#rglCz$@8vd*uw6n9#2NgT4HmvMwE{IRVTbTR+_$5;3Yz^7my_eJskXaL3yJjN~k{3EmOWy_LUxQg~*=cq{0A8HIpNlMjIO$>{-UeL^E{|eE;VZSl~U4?s$mU3b#8Ca4Xnxty(spvKXpAlwQ z^*r7_qwJ&cn(rLZncfok@!1NY1txVG{)CcDvNP%R*`}+Gt(n-tklmv}R!iJ~VC^Y^ zp;iproc)Y#Zy|ORQaHim2oVHSj)26pK6E^3o>(h#0^*w5f($yPAm{adA}5LjTlWXk z^fD9WI8>tZ^`m4P2jetN&+pQtuT9%K#J;UyMJ4^dE;YRe7v8+~*6nMZs|>#|&{ews z>>o70kjEa6v1+_AaX>mJp#Ye@_qI)F;0#*@)!iek2gk$K-#;$b~uMWm^Tp&6~J@ zLJZM&#?T-^JHD}H)=Vnj8JnyWSAN=0ZKY@rG7KnC$0BF-^x3gmX%O70_f>`3pOm#E z>7fGc*+>i9m&5&PDk*ydsSfN@wn6XzqX>VN_CKIn(I=CDKBzIGvsWAba?hM$j=!K^ zVxy10R$l7R#3y zLx?l;mCCBuwndPLiJ&U(X{A)>%BCy1!!--mX|b$m=X!qvR)nNlAzORoZ@cPF9b9dH zphVAL{axUa#v0m^Q#4#zxHIhI?h}mbMv>Y@pQf5@@Jk}RwmO3wsV%1;*{72rGWTR_ zBR~V1-6Mii`2;zqXTYntIy2la20}qYW6@k+)-gRCt)f}_7?$!#RT#lW$fwFee2_s8 zAM`8M-K{GYrDt?Nz-YpqC2mVdgqQ5=88F&N$ISF5kYT$P{ zxyk-``c2(NU%-%W7VMj)Z#SJ>EXEyM6x%wjzhXud;M>AB1;OP8OeqO-U9k%FFOaZu zgS&VWeGi%&xa~OkNzFkYbCU7`FrKKSureZTB*%fN(~p<=+#s5ozdEqi;SCmqYtnSr z{v&jJI?Uvqgp4%LgEC66d&)&|8BQ{{62Ja0HDu_SI??^CNDj5B(Y7q+}tA- zT^hFGwhHXiU0wT-cR)GFY5HF~(O%C{Y7gP)t3J>&p5YAvCO9#s3U$=CW)wnH#-YlY zn0{wilKN%|guUeRCrkqMen>lvs?%!S*6~&zXB(zM09kHySQeih#$x$6KCs4eDW@5j z4PXEQSLfzx>K%pV-=cH zi0VfN&3($bmVHOarj9_R2rvjD=XNv5I%i;Xw3io@fTcStmZ-EPU`$45Z^lirt*52j zSo{aNqIa5=L?IYbc-!r&C^wbWm^Z_qB%(WM+=qUVI<)GQR6&O*0FWK2=G@PY`x5+~ zHsU@t3t%qwN=tWA`dd!JgL>yE>bq6zV_~JcuNCP`W+p}km@V61VU^B&U!_M7Zm2yd z$m0IZ2P+j21#611=A~-d@gP9(yh=l^|AXF~)yY06AeXQrx3+s=TyQ&!FzNdUo2FR| zS@yb^{%N9^7$-wa%MY_BTf4bM=LlHC+eR#J>YX(!GxkLrQ25)b>=H!C8sruXY_J=k z44Fw!(F7#=ruw4mv_0n~4C*bKm#vXQ>Uv{>S>$uzvQvk68|tFYMzDKgV)$cxg{TlZ zhVi2;aOeEHnB7u@MB;u*I!ay^Kr5=pO(4Bm2P!Rivx6~Grb!i+1Ry6@%@KR4af@#( z@YiwP8WHl5)NCOe5rm~r9Q9_|{A5ZQ3;-to83c4U%cf>e$}&o;Ea~!|i%SOXg~20G zCC2T^{Ql_+MK1%jz)WTo1@ZHe=y^|xkAjN=XpG=BmN-pZG)LFeeg~RQOpzO(-=Bex zUR7h=suRU7n5}w1O;L$BOj)^ckGFHP;pz;htr+5xMf><*Q+Y z-C@NECaRna(UjTvY)Oy!-UdkduCB6gdHn(kIP#?Rv)*+;ZwTL7 z&;ntTgBp;lTtz*>3{QWBj2OsJyem zuD=1wR0wqcX@tmZ(-QJ~4jCxp5R>*phoC1t7H7B&%s_4vj2V!p9zUH#!DJIuMb#+x zGc}JhZ1p$t&So83U@YI%D5CQ&w&PQN?P5Ek{9g76EN}r1IDSkfb#On@KU0S)B`(F> z+9|Uv-YJl9A|p$5(|dpUk<=DUk+%R+t{Gl`2xYg==8V1*TXACa!F{A)DA{hcuB#S2S9t7E- zG$yS|?ctVlP#+xct2CGqGME4%sU?79dHpavogkSPGi;qfka2&`)!ViiZ>s40) ze}LfyVubqS+!J%83B)$cMn8}Lrebhx5iq4v8R6qgW<^>&TapED(uFmL!uIeI4WGb( z0008c000|cX9Ho}lqLL`XmcmJd_M!5KvbQN91p$z*zNXkv`|95j7KAYGJrGs8Fgmc z{cRX4lV8+%JrJN+Nb;gjd+t*MFA&p}0XE+lxt^-6`ERAf{5yU0VN=GMS|{%D0-~+` zZ`7!;^_!SS9+cp(yV@-rQ=2ine;sO6Pq&B??H$HdWU(RCD&_id(&MzCp-^RPz$>@q zZ8#w5Tb$)uM$rQ^@AkutEs;&XCs$yE?~~V!hPNG!Dg6iZ=jD1ULmYP1;SK>+-vWLk z_jcdSm+FDYic~9c8GX57_E+wMgg93c@%cNqoN5%n_>eM7rkv3{R@~LiD?Q2X#cjYs zk}R!{6P~}>@8z+)I9O2=rYK70!i=$K)7_L$p!;3T+_oW0Ll1`a7JL(;-h0H116_hN zR`WR*3kQEmNa05oCR5a#Ik77IR_dNqdb@?R23=g<=Fmom5&wJ1lwT2L&MU?n$f-$1ImSod*Q9~_c-%5I>vI0v=P)a zDv#wzUT|?;u}hD)O5|H=C^)CluIc+Ra1gFv&noVsNqo}dfS|qUbZ3I_q|ZMMCt)M# zwO#AMsHTQ5lAKtk5CK^!z#jY=Vxk|fXbuYmUpIAYn?N$Gb;?=iohUUTrUF^{3wAr? zx+mHgmx9MxTDRz5h{97faPqF;ox(^HfkpgA4)2iK9&YRYiDmI^Go|Uj_!Mfj za^DZ0>4d^oG;N6m!e>_4BxSTdv5#OfSHI7>qquEdIlF61mg?lk5mQ03Vzz-wp5A_S zVw-m(cl_)*#sq;6{n5W}|Nq74m5Q(Ca(hJ^_(*pTFEyoqAZhprdmV2Z@KO8~sfGMq>`quRKhh>}!jm*Lgy{&M0N@dh zoFLggK@CWrIJVII#`#DZ04LNiZrq0(W<%5_e`W^-hQbRF9MD{UCSnzC{*v$KyjVZ+ z^oj)Qu{bW#$L}>z{bgfa)c<^RdxWdYU*vLg5{e{7;>y@FjlkiTQFtS%|Gl#6@?nNo zGd-0Lvlk(?qX&fTC}f2dyc->p_(OpJ00I3_;D3-4!}R9+jA>1I$_IdC3W78GfV%4V zRE`bp-LjOs1Rr&lajO7>*D7k1?Mr7h=%;Oj?3ZBMzKb0X-TY?E)qCwleH33uOXPk1G*OP*n-E4wWF)b<)Uc0<| z4EVaN&=t7LXJ=;tTkbNy2sA{Jd*uccVi4OXQp{s74B_vF-e6ZNTvKsI>>W0Zf|a}? zUdm`Jv3vyDRHUwWrmZXIRVHwqdUvF7KvE+(f!{y}BxcW2QUr@xqN%VULoSYkLMtHv z&Y3^5#+g2D8*(704Sk0j$#cLlP?MAT+)EDoVu*ZOGxfu>e_n^cdxO6-r$=9T`2MKb z>NM`bqMXoB*i5|I&vvb4%KF}=z})iN0ErhPM?Q|pe8Ww4LkO4RE4Fb32@{OCsxBeY zxQxqk8waWL8gY6+@@0$#^-~t3__%|z0vZI5VLKC$fHflC+E+eXZ@{@E{VF9+_3>{PX~?M}u? z<;MnyqsVy(%CM*V;I!oYwQC9{Q;_ir*_e8Ht*-H#j?|c~J*>O>St$!))`;Zf#TC?D z5hURGOgqZV)%ISiDy04w7lueIswOw)mC{RsA*yf0sgXG0B8hVyH?R& z^1bL+!%D%MC5`t2*TUYT%@!6Hq9DuFQ!EQb(Y|C?K*+?@MMEMyK+ zC@2pqcmQIFXH@TO>#F#I=;Q09-g(C8G7xGg^H*9}Xb0D^<-M|9fjp~sBO z*5;`xq=&*}^r74_J^t99$ZhxI2Mr55+g{p5tbGqQ?qSF|0W@Fz^Ckip)a+=)@{p#4 z0;e}c^m58r6m$8&(-P_+vV48c6lxF8s~te6Raq|5f`WdpVmu&1Zznh<4~c-s0z}sv zpJ<3G@HrMjg-$ZQqq~+zEfhu&nEdn0Ld0b?U3mXqGB{EbzB7hT9uGAYADTND{9J0sStCpqfI_~ayLwy86(>M_zSQH3i ztYg3Hx4EyWbePfc)|4A0UtWc9Mzfj(wvm%w$t=}eVh|!*P<`~E%6hueS~k1=nx=@h T)1EED$+SB@Bu5JR-~a#saimlL literal 0 HcmV?d00001 diff --git a/apps/web/content/docs/dev/moderation/warning-reasons.webp b/apps/web/content/docs/dev/moderation/warning-reasons.webp new file mode 100644 index 0000000000000000000000000000000000000000..d9c1186e698e03dcdc2564e31facff2ada1f8c8b GIT binary patch literal 41202 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 YQk}jyONCw*g7uft7zO}fc>w_b1G0sm&Hw-a literal 0 HcmV?d00001 diff --git a/apps/web/content/docs/dev/moderation/warning-thresholds.webp b/apps/web/content/docs/dev/moderation/warning-thresholds.webp new file mode 100644 index 0000000000000000000000000000000000000000..dafbfe102e0c39d9962bb2805bb675cbae6d6f4a GIT binary patch literal 42626 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 literal 0 HcmV?d00001 diff --git a/apps/web/content/docs/dev/moderation/warnings.mdx b/apps/web/content/docs/dev/moderation/warnings.mdx new file mode 100644 index 0000000000..6384b95078 --- /dev/null +++ b/apps/web/content/docs/dev/moderation/warnings.mdx @@ -0,0 +1,53 @@ +--- +title: Warnings +description: How VitNode warnings, active points, thresholds and account suspensions fit together, who can do what, and what a suspended member sees. +icon: TriangleAlert +--- + +A **warning** records a moderator's decision about a member. It can carry **points**, and when a warning brings a member's **active points** up to an administrator's **threshold**, VitNode suspends the account automatically. Warnings, points and suspensions are separate records, so revoking one never silently rewrites another. + +| Concept | What it is | +| --- | --- | +| Warning | Who was warned, by whom, why, how many points, the message the member reads, a private staff note, and optionally the content or report it is about | +| Active points | The sum of points on warnings that are not revoked and whose expiry is still in the future, computed from timestamps on every read | +| Threshold | An administrator's rule, like "at 8 active points, suspend for 24 hours" | +| Sanction | A consequence on the account. This version has temporary and indefinite **suspensions** | + +## Choose a guide + +| You want to | Read | +| --- | --- | +| Set up warning reasons and automatic suspensions | [Configure warnings](/docs/dev/moderation/configure-warnings) | +| Warn a member, revoke a warning or lift a suspension | [Issue warnings](/docs/dev/moderation/issue-warnings) | +| Add **Warn author** to your plugin's content | [Warn content authors](/docs/dev/moderation/warn-content) | +| Look up routes, permissions and events | [Moderation reference](/docs/dev/moderation/reference#warnings) | + +## How a warning becomes a suspension + +1. A moderator opens the warn form. It previews the member's active points before and after, and the suspension a threshold would apply. +2. On submit, the server checks every permission again and locks that member's warnings for the length of one transaction. Two moderators warning the same member at once are handled one after the other, so the points add up and each threshold fires once. +3. A threshold applies when the warning moves active points from below its number to its number or above. When one warning crosses several, only the highest applies: 6 → 11 points with thresholds at 8 and 10 gives only the 10-point suspension. +4. The warning, the threshold decision, the suspension, the audit entries and the member's notifications commit together or not at all. + +A temporary suspension ends at its own end time, even when the member still has the points. Expired points or a changed threshold never create a new suspension on their own; only a new warning does. Points that later fall below a threshold let it fire again on a future crossing. + +## What a suspended member sees + +The server checks suspensions when it reads the session, for every request: core routes, plugin routes, server functions and WebSocket handshakes. A suspended account is treated as a guest everywhere, so it can browse public pages but cannot post, comment, report, moderate or open the AdminCP. Open WebSocket connections are closed when the suspension is applied. + +The member keeps their session. A strip under the header says the account is suspended and links to **Account status** at `/account-status`, which shows the suspension end time, active points and the warning history with each message. Members reach the same page from **Settings → Account status** at any time. It never shows staff notes or which moderator issued a warning. + +## Who is protected + +Nobody can warn themselves. A member who holds a root role cannot be warned at all, and other staff (admins and moderators) can only be warned by someone who holds a root role. These checks run before a threshold is evaluated, so a threshold can never suspend a protected account. + +## Limits of this version + +- Suspensions are the only sanction. There is no manual "suspend without a warning" action, no appeal workflow and no posting restriction per plugin yet; the data model leaves room for them. +- Suspension state is cached for up to a minute per member. Applying or lifting a suspension clears that cache, and with Redis every instance sees it at once. Without Redis, other instances may take up to a minute. +- The audit trail is stored in `core_moderation_audit` but has no screen yet. + +## Related + +- [Scoped permissions](/docs/dev/moderation/scoped-permissions) +- [Configure moderators](/docs/dev/moderation/configure-moderators) diff --git a/apps/web/src/page-cache.gen.ts b/apps/web/src/page-cache.gen.ts index 8e11435303..2ccdea3b66 100644 --- a/apps/web/src/page-cache.gen.ts +++ b/apps/web/src/page-cache.gen.ts @@ -36,6 +36,8 @@ export const pageCacheRoutes: PageCacheRouteTable = [ { path: '/admin/core/debug/services' }, { path: '/admin/core/debug/vitnode' }, { path: '/admin/core/moderation/report-reasons' }, + { path: '/admin/core/moderation/warning-reasons' }, + { path: '/admin/core/moderation/warning-thresholds' }, { path: '/admin/core/staff/admins' }, { path: '/admin/core/staff/moderators' }, { path: '/admin/core/system/files' }, @@ -57,6 +59,7 @@ export const pageCacheRoutes: PageCacheRouteTable = [ { path: '/forum/topics/:slug', maxAge: 60, staleWhileRevalidate: 30 }, { path: '/login/sso/:providerId' }, { path: '/modcp/approvals/:id' }, + { path: '/modcp/members/:id' }, { path: '/modcp/reports/:id' }, { path: '/news/comments/:slug' }, { path: '/newsroom/guide/:topic' }, @@ -69,6 +72,7 @@ export const pageCacheRoutes: PageCacheRouteTable = [ { path: '/login/verify-email' }, { path: '/modcp/approvals' }, { path: '/modcp/reports' }, + { path: '/modcp/warnings' }, { path: '/news/new' }, { path: '/newsroom/browse' }, { path: '/newsroom/embed' }, @@ -96,6 +100,7 @@ export const pageCacheRoutes: PageCacheRouteTable = [ { path: '/stories/:slug' }, { path: '/users/:nameCode' }, { path: '/solutions/:slug' }, + { path: '/account-status' }, { path: '/admin' }, { path: '/blog', maxAge: 300, staleWhileRevalidate: 60 }, { path: '/desks' }, diff --git a/packages/vitnode/src/api/lib/moderation/registry.ts b/packages/vitnode/src/api/lib/moderation/registry.ts index 389fdf3a1d..b67779c4b4 100644 --- a/packages/vitnode/src/api/lib/moderation/registry.ts +++ b/packages/vitnode/src/api/lib/moderation/registry.ts @@ -30,7 +30,10 @@ export interface ModerationRegistry { } export const moderationPermissionEntries = ( - target: Pick, + target: Pick< + ModerationReportTargetDefinition, + "actions" | "resolveAuthors" | "scope" + >, ): PermissionStaffEntryInput[] => { const scope = target.scope ? { scope: target.scope } : {}; const hideable = Boolean(target.actions?.hide ?? target.actions?.unhide); @@ -52,6 +55,9 @@ export const moderationPermissionEntries = ( permission: MODERATION_PERMISSIONS.deleteNotes, ...scope, }, + ...(target.resolveAuthors + ? [{ permission: MODERATION_PERMISSIONS.warn, ...scope }] + : []), ...(hideable ? [ { permission: MODERATION_PERMISSIONS.viewHidden, ...scope }, diff --git a/packages/vitnode/src/api/lib/moderation/targets.ts b/packages/vitnode/src/api/lib/moderation/targets.ts index f9ce8dab38..aec4d7a7e3 100644 --- a/packages/vitnode/src/api/lib/moderation/targets.ts +++ b/packages/vitnode/src/api/lib/moderation/targets.ts @@ -14,6 +14,14 @@ export interface ModerationTargetState { url: null | string; } +/** What {@link ModerationReportTargetDefinition.resolveAuthors} returns. */ +export interface ModerationTargetAuthors { + /** Every user who wrote the target; a warning can only name one of them. */ + authorIds: number[]; + /** A short plain-text title kept with the warning, or `null`. */ + title: null | string; +} + export interface ModerationCaptureArgs { locale: null | string; targetId: string; @@ -84,6 +92,17 @@ export interface ModerationReportTargetDefinition { parseId: (value: string) => null | string; /** Moderator permission module the report permissions are added to. */ permissionModule: string; + /** + * The target's current authors and a short plain-text title, or `null` once + * it is deleted. Server-only; runs as the moderator, so hidden content still + * resolves. Setting it makes the target warnable: moderators with + * `can_warn` in `permissionModule` (limited by `scope`) can warn its + * authors. Content Engine types with `reports` provide it automatically. + */ + resolveAuthors?: ( + c: Context, + args: { targetId: string }, + ) => Promise; /** * The target's resource id in the `scope` family: `""` for none, * `undefined` once the target is deleted. Required with `scope`. diff --git a/packages/vitnode/src/api/lib/moderation/warnings/access.ts b/packages/vitnode/src/api/lib/moderation/warnings/access.ts new file mode 100644 index 0000000000..ff50c1b435 --- /dev/null +++ b/packages/vitnode/src/api/lib/moderation/warnings/access.ts @@ -0,0 +1,181 @@ +import type { SQL } from "drizzle-orm"; +import type { Context } from "hono"; + +import { and, eq, inArray, isNotNull, or } from "drizzle-orm"; + +import { core_moderation_warnings } from "@/database/warnings"; +import { MODERATION_PERMISSIONS } from "@/lib/moderation/const"; +import { + WARNING_PERMISSION_MODULE, + WARNING_PERMISSIONS, +} from "@/lib/moderation/warnings"; +import { manifest } from "@/manifest"; + +import type { ResolvedStaffPermissionSet } from "../../permission-staff"; +import type { ModeratorContext } from "../access"; + +import { + getStaffFlags, + resolveStaffPermissions, +} from "../../check-staff-permission"; +import { + hasStaffPermission, + staffPermissionResources, +} from "../../staff-permission"; +import { canOnReport } from "../access"; + +export type WarningPermission = + (typeof WARNING_PERMISSIONS)[keyof typeof WARNING_PERMISSIONS]; + +/** Whether the moderator holds one of core's unscoped `warnings` permissions. */ +export const hasWarningPermission = ( + set: ResolvedStaffPermissionSet, + permission: WarningPermission, +): boolean => + hasStaffPermission(set, { + module: WARNING_PERMISSION_MODULE, + permission, + plugin: manifest.id, + }); + +/** The content a warning points at, as stored on the warning. */ +export interface WarningContentRef { + pluginId: null | string; + scopeId: null | string; + targetType: null | string; +} + +/** Whether the moderator may warn the authors of content in this scope. */ +export const canWarnForContent = ( + c: Context, + set: ResolvedStaffPermissionSet, + ref: { pluginId: string; scopeId: null | string; targetType: string }, +): boolean => canOnReport(c, set, ref, MODERATION_PERMISSIONS.warn); + +/** + * Whether the moderator may read one warning's details: with `can_view`, any + * warning; otherwise the warnings they issued and those linked to content + * they may warn for. + */ +export const canSeeWarning = ( + c: Context, + moderator: ModeratorContext, + warning: WarningContentRef & { issuedById: null | number }, +): boolean => { + if (hasWarningPermission(moderator.set, WARNING_PERMISSIONS.view)) { + return true; + } + if (warning.issuedById === moderator.user.id) return true; + if (!warning.pluginId || !warning.targetType) return false; + + return canWarnForContent(c, moderator.set, { + pluginId: warning.pluginId, + scopeId: warning.scopeId, + targetType: warning.targetType, + }); +}; + +/** Staff notes and override explanations: `can_view_notes`, or the issuer's own. */ +export const canSeeWarningNotes = ( + moderator: ModeratorContext, + warning: { issuedById: null | number }, +): boolean => + hasWarningPermission(moderator.set, WARNING_PERMISSIONS.viewNotes) || + warning.issuedById === moderator.user.id; + +/** {@link canSeeWarning} as SQL, for listing. `undefined` means every warning. */ +export const viewableWarningsCondition = ( + c: Context, + moderator: ModeratorContext, +): SQL | undefined => { + if (hasWarningPermission(moderator.set, WARNING_PERMISSIONS.view)) { + return undefined; + } + + const table = core_moderation_warnings; + const conditions: (SQL | undefined)[] = [ + eq(table.issuedById, moderator.user.id), + ]; + for (const target of c.get("core").moderation?.targets ?? []) { + if (!target.definition.resolveAuthors) continue; + const family = target.definition.scope; + const resources = staffPermissionResources( + moderator.set, + { + module: target.definition.permissionModule, + permission: MODERATION_PERMISSIONS.warn, + plugin: target.pluginId, + }, + family ?? "", + ); + if (resources === null) continue; + + const ofTarget = and( + eq(table.pluginId, target.pluginId), + eq(table.targetType, target.definition.type), + ); + conditions.push( + resources === "all" || !family + ? ofTarget + : and( + ofTarget, + isNotNull(table.scopeId), + inArray(table.scopeId, resources), + ), + ); + } + + return or(...conditions); +}; + +/** + * Whether the moderator can warn anyone at all: directly, or for content of + * at least one registered target. + */ +export const canWarnAnywhere = ( + c: Context, + set: ResolvedStaffPermissionSet, +): boolean => { + if (hasWarningPermission(set, WARNING_PERMISSIONS.warn)) return true; + + return (c.get("core").moderation?.targets ?? []).some( + target => + target.definition.resolveAuthors !== undefined && + staffPermissionResources( + set, + { + module: target.definition.permissionModule, + permission: MODERATION_PERMISSIONS.warn, + plugin: target.pluginId, + }, + target.definition.scope ?? "", + ) !== null, + ); +}; + +export type WarningTargetRefusal = "protected" | "self"; + +/** + * Who may not be warned by this moderator: themselves, anyone holding a root + * role, and - unless the moderator holds a root role - other staff. Checked + * before threshold evaluation, so a threshold can never suspend a protected + * account. + */ +export const warningTargetRefusal = async ( + c: Context, + moderator: ModeratorContext, + target: { id: number; roleId: number }, +): Promise => { + if (target.id === moderator.user.id) return "self"; + + const targetSet = await resolveStaffPermissions(c, { + type: "moderator", + user: target, + }); + if (targetSet.root) return "protected"; + if (moderator.set.root) return null; + + const flags = await getStaffFlags(c, target); + + return flags.isAdmin || flags.isModerator ? "protected" : null; +}; diff --git a/packages/vitnode/src/api/lib/moderation/warnings/reasons.ts b/packages/vitnode/src/api/lib/moderation/warnings/reasons.ts new file mode 100644 index 0000000000..62e9d28878 --- /dev/null +++ b/packages/vitnode/src/api/lib/moderation/warnings/reasons.ts @@ -0,0 +1,119 @@ +import type { Context } from "hono"; + +import { z } from "@hono/zod-openapi"; +import { and, asc, eq, inArray } from "drizzle-orm"; + +import type { ContentDatabase } from "@/content/server/service"; +import type { ModerationAuditType } from "@/lib/moderation/warnings"; + +import { core_languages_words } from "@/database/languages"; +import { + core_moderation_audit, + core_moderation_warning_reasons, +} from "@/database/warnings"; + +export const WARNING_REASON_WORDS = { + pluginCode: "core", + tableName: "core_moderation_warning_reasons", +} as const; + +export type WarningReasonWordVariable = "description" | "message" | "title"; + +const multiLangSchema = z.array( + z.object({ languageCode: z.string(), value: z.string() }), +); + +export const warningReasonSchema = z.object({ + id: z.number(), + points: z.number(), + expiresAfterHours: z.number().nullable(), + enabled: z.boolean(), + position: z.number(), + title: multiLangSchema, + description: multiLangSchema, + message: multiLangSchema, +}); + +export type WarningReason = z.infer; + +/** Every warning reason in display order, with its words; `enabledOnly` for new warnings. */ +export const listWarningReasons = async ( + c: Context, + { enabledOnly = false, ids }: { enabledOnly?: boolean; ids?: number[] } = {}, +): Promise => { + const table = core_moderation_warning_reasons; + const rows = await c + .get("db") + .select({ + enabled: table.enabled, + expiresAfterHours: table.expiresAfterHours, + id: table.id, + points: table.points, + position: table.position, + }) + .from(table) + .where( + and( + enabledOnly ? eq(table.enabled, true) : undefined, + ids ? inArray(table.id, ids.length > 0 ? ids : [0]) : undefined, + ), + ) + .orderBy(asc(table.position), asc(table.id)); + if (rows.length === 0) return []; + + const words = await c + .get("db") + .select({ + itemId: core_languages_words.itemId, + languageCode: core_languages_words.languageCode, + value: core_languages_words.value, + variable: core_languages_words.variable, + }) + .from(core_languages_words) + .where( + and( + eq(core_languages_words.pluginCode, WARNING_REASON_WORDS.pluginCode), + eq(core_languages_words.tableName, WARNING_REASON_WORDS.tableName), + inArray( + core_languages_words.itemId, + rows.map(row => row.id), + ), + ), + ); + const pick = (id: number, variable: WarningReasonWordVariable) => + words + .filter(word => word.itemId === id && word.variable === variable) + .map(word => ({ languageCode: word.languageCode, value: word.value })); + + return rows.map(row => ({ + ...row, + description: pick(row.id, "description"), + message: pick(row.id, "message"), + title: pick(row.id, "title"), + })); +}; + +/** + * Appends one entry to the warning system's audit trail. Never pass staff + * notes or override explanations in `data`. + */ +export const appendModerationAudit = async ( + db: ContentDatabase, + entry: { + actorId: null | number; + data?: Record; + sanctionId?: null | number; + type: ModerationAuditType; + userId?: null | number; + warningId?: null | number; + }, +): Promise => { + await db.insert(core_moderation_audit).values({ + actorId: entry.actorId, + data: entry.data ?? {}, + sanctionId: entry.sanctionId ?? null, + type: entry.type, + userId: entry.userId ?? null, + warningId: entry.warningId ?? null, + }); +}; diff --git a/packages/vitnode/src/api/lib/moderation/warnings/suspension.ts b/packages/vitnode/src/api/lib/moderation/warnings/suspension.ts new file mode 100644 index 0000000000..fb18b1211d --- /dev/null +++ b/packages/vitnode/src/api/lib/moderation/warnings/suspension.ts @@ -0,0 +1,114 @@ +import type { Context } from "hono"; + +import { and, eq, gt, isNull, or } from "drizzle-orm"; + +import { core_moderation_sanctions } from "@/database/warnings"; +import { isSanctionActiveAt } from "@/lib/moderation/warnings"; + +/** The suspension that keeps an account restricted right now. */ +export interface ActiveSuspension { + /** The latest end among active suspensions; `null` while any is indefinite. */ + endsAt: Date | null; + sanctionIds: number[]; +} + +interface CachedSanction { + endsAt: null | string; + id: number; + startsAt: string; +} + +/** Positive and negative answers both live this long, unless revoked sooner. */ +const SUSPENSION_CACHE_TTL_SECONDS = 60; + +const suspensionCacheKey = (userId: number): string => + `moderation:suspension:${userId}`; + +const loadSuspensions = async ( + c: Context, + userId: number, + now: Date, +): Promise => { + const table = core_moderation_sanctions; + const rows = await c + .get("db") + .select({ + endsAt: table.endsAt, + id: table.id, + startsAt: table.startsAt, + }) + .from(table) + .where( + and( + eq(table.userId, userId), + eq(table.type, "suspension"), + isNull(table.revokedAt), + or(isNull(table.endsAt), gt(table.endsAt, now)), + ), + ); + + return rows.map(row => ({ + endsAt: row.endsAt?.toISOString() ?? null, + id: row.id, + startsAt: row.startsAt.toISOString(), + })); +}; + +/** + * The account's active suspension at server time, or `null`. Each sanction is + * re-checked against its own timestamps on every call, so a temporary one + * stops applying the moment it ends - no scheduled job involved. Cached per + * user for a minute and dropped by {@link forgetSuspension} whenever a + * sanction is applied or revoked. Server-only. + */ +export const findActiveSuspension = async ( + c: Context, + userId: number, + now: Date = new Date(), +): Promise => { + const cache = c.get("cache"); + const key = suspensionCacheKey(userId); + let sanctions = await cache.getSystem(key); + if (!sanctions) { + sanctions = await loadSuspensions(c, userId, now); + await cache.setSystem(key, sanctions, SUSPENSION_CACHE_TTL_SECONDS); + } + + const active = sanctions.filter(sanction => + isSanctionActiveAt( + { + endsAt: sanction.endsAt ? new Date(sanction.endsAt) : null, + revokedAt: null, + startsAt: new Date(sanction.startsAt), + }, + now, + ), + ); + if (active.length === 0) return null; + + const indefinite = active.some(sanction => sanction.endsAt === null); + const latest = active + .map(sanction => sanction.endsAt) + .filter((value): value is string => value !== null) + .sort() + .at(-1); + + return { + endsAt: indefinite || !latest ? null : new Date(latest), + sanctionIds: active.map(sanction => sanction.id), + }; +}; + +/** + * Drops the cached suspension state of a user and closes their open + * WebSocket connections, so a new or revoked sanction applies on the next + * request on every instance that shares the cache. + */ +export const forgetSuspension = async ( + c: Context, + userId: number, + { disconnect }: { disconnect: boolean }, +): Promise => { + await c.get("cache").deleteSystem(suspensionCacheKey(userId)); + if (disconnect) c.get("realtime")?.closeUserConnections(userId); +}; diff --git a/packages/vitnode/src/api/lib/notifications/core-types.ts b/packages/vitnode/src/api/lib/notifications/core-types.ts index 1fd6ee2490..7a6c5937f0 100644 --- a/packages/vitnode/src/api/lib/notifications/core-types.ts +++ b/packages/vitnode/src/api/lib/notifications/core-types.ts @@ -16,4 +16,130 @@ export const adminMessageNotification = buildNotificationType({ present: ({ data }) => ({ body: data.description, title: data.title }), }); -export const coreNotificationTypes = [adminMessageNotification]; +const reasonTextSchema = z + .array(z.object({ languageCode: z.string(), value: z.string() })) + .nullable(); + +const pickText = ( + values: null | { languageCode: string; value: string }[], + locale: string, +): string => + values?.find(item => item.languageCode === locale)?.value ?? + values?.find(item => item.languageCode === "en")?.value ?? + values?.[0]?.value ?? + ""; + +const ACCOUNT_STATUS_PATH = "/account-status"; + +/** Sent to a member when a moderator warns them. Never carries staff notes. */ +export const warningIssuedNotification = buildNotificationType({ + id: "core.warning_issued", + version: 1, + schema: z.object({ + message: z.string().max(2000), + points: z.number().int().min(0), + reason: reasonTextSchema, + warningId: z.number().int(), + }), + category: "account", + label: "core.notifications.types.warning_issued.label", + description: "core.notifications.types.warning_issued.description", + defaults: { email: "immediate", inApp: true }, + email: true, + mandatory: true, + present: ({ data, locale, t }) => { + const reason = pickText(data.reason, locale); + + return { + body: data.message, + target: ACCOUNT_STATUS_PATH, + title: reason + ? t("core.notifications.types.warning_issued.title_reason", { + points: data.points, + reason, + }) + : t("core.notifications.types.warning_issued.title", { + points: data.points, + }), + }; + }, +}); + +/** Sent to a member when a warning of theirs is revoked. */ +export const warningRevokedNotification = buildNotificationType({ + id: "core.warning_revoked", + version: 1, + schema: z.object({ + points: z.number().int().min(0), + reason: reasonTextSchema, + warningId: z.number().int(), + }), + category: "account", + label: "core.notifications.types.warning_revoked.label", + description: "core.notifications.types.warning_revoked.description", + defaults: { email: "none", inApp: true }, + email: true, + mandatory: true, + present: ({ data, t }) => ({ + body: t("core.notifications.types.warning_revoked.body", { + points: data.points, + }), + target: ACCOUNT_STATUS_PATH, + title: t("core.notifications.types.warning_revoked.title"), + }), +}); + +/** Sent to a member whose account a threshold suspended. */ +export const accountSuspendedNotification = buildNotificationType({ + id: "core.account_suspended", + version: 1, + schema: z.object({ + endsAt: z.string().nullable(), + sanctionId: z.number().int(), + }), + category: "account", + label: "core.notifications.types.account_suspended.label", + description: "core.notifications.types.account_suspended.description", + defaults: { email: "immediate", inApp: true }, + email: true, + mandatory: true, + present: ({ data, locale, t }) => ({ + body: data.endsAt + ? t("core.notifications.types.account_suspended.body_until", { + endsAt: new Intl.DateTimeFormat(locale, { + dateStyle: "medium", + timeStyle: "short", + timeZone: "UTC", + }).format(new Date(data.endsAt)), + }) + : t("core.notifications.types.account_suspended.body_indefinite"), + target: ACCOUNT_STATUS_PATH, + title: t("core.notifications.types.account_suspended.title"), + }), +}); + +/** Sent to a member when staff lift one of their suspensions. */ +export const suspensionRevokedNotification = buildNotificationType({ + id: "core.suspension_revoked", + version: 1, + schema: z.object({ sanctionId: z.number().int() }), + category: "account", + label: "core.notifications.types.suspension_revoked.label", + description: "core.notifications.types.suspension_revoked.description", + defaults: { email: "immediate", inApp: true }, + email: true, + mandatory: true, + present: ({ t }) => ({ + body: t("core.notifications.types.suspension_revoked.body"), + target: ACCOUNT_STATUS_PATH, + title: t("core.notifications.types.suspension_revoked.title"), + }), +}); + +export const coreNotificationTypes = [ + adminMessageNotification, + warningIssuedNotification, + warningRevokedNotification, + accountSuspendedNotification, + suspensionRevokedNotification, +]; diff --git a/packages/vitnode/src/api/middlewares/global.middleware.ts b/packages/vitnode/src/api/middlewares/global.middleware.ts index 21271b598f..88905bf3b1 100644 --- a/packages/vitnode/src/api/middlewares/global.middleware.ts +++ b/packages/vitnode/src/api/middlewares/global.middleware.ts @@ -19,6 +19,7 @@ import { runAfterResponse } from "@/api/lib/after-response"; import { DEFAULT_SESSION_COOKIE_NAME } from "@/api/lib/auth-cookie"; import { CacheModel } from "@/api/lib/cache"; import { MemoryCacheStore } from "@/api/lib/cache-memory"; +import { findActiveSuspension } from "@/api/lib/moderation/warnings/suspension"; import { AIModel } from "@/api/models/ai"; import { AuditModel } from "@/api/models/audit"; import { EmailModel } from "@/api/models/email"; @@ -295,6 +296,17 @@ export interface EnvVariablesVitNode { search: SearchModel; /** Uploads, reads and deletes files through the storage adapter: see {@link StorageModel}. */ storage: StorageModel; + /** + * Set when the session belongs to a suspended account. `user` is then + * `null`, so every route - core's and every plugin's - treats the request as + * a guest's. Only routes built for the suspended experience (the account + * status page, sign-out) read this. + */ + suspension: null | { + /** The latest end among active suspensions; `null` while any is indefinite. */ + endsAt: Date | null; + userId: number; + }; /** The signed-in user from the session cookie, or `null` for a guest. */ user: null | { avatarColor: string; @@ -739,8 +751,17 @@ export const globalMiddleware = ({ await ensureContentLocalizationLanguages(c, contentTypesMetadata); } - const user = await new SessionModel(c).getUser(); - c.set("user", user); + const sessionUser = await new SessionModel(c).getUser(); + const suspension = sessionUser + ? await findActiveSuspension(c, sessionUser.id) + : null; + c.set("user", suspension ? null : sessionUser); + c.set( + "suspension", + sessionUser && suspension + ? { endsAt: suspension.endsAt, userId: sessionUser.id } + : null, + ); c.set("admin", null); c.set("adminSessionExpiresAt", null); c.set("log", loggerMiddleware(c)); @@ -775,6 +796,9 @@ export const globalAdminMiddleware = () => { extend: c.req.query("passive") !== "true", }); if (!session) throw new HTTPException(403); + if (await findActiveSuspension(c, session.user.id)) { + throw new HTTPException(403); + } c.set("admin", { user: session.user, }); diff --git a/packages/vitnode/src/api/models/events.ts b/packages/vitnode/src/api/models/events.ts index 65f06552e1..b275d838c7 100644 --- a/packages/vitnode/src/api/models/events.ts +++ b/packages/vitnode/src/api/models/events.ts @@ -158,6 +158,52 @@ export interface VitNodeEvents { code: string; reasonId: number; }; + "moderation.sanction.applied": { + /** `null` for an indefinite sanction. */ + endsAt: Date | null; + sanctionId: number; + source: "manual" | "threshold"; + thresholdId: null | number; + type: "suspension"; + userId: number; + warningId: null | number; + }; + "moderation.sanction.revoked": { + revokedById: number; + sanctionId: number; + userId: number; + }; + "moderation.warning.issued": { + issuedById: number; + pluginId: null | string; + points: number; + reasonId: null | number; + reportId: null | number; + targetId: null | string; + targetType: null | string; + userId: number; + warningId: number; + }; + "moderation.warning.revoked": { + revokedById: number; + userId: number; + warningId: number; + }; + "moderation.warning_reason.created": { + reasonId: number; + }; + "moderation.warning_reason.updated": { + reasonId: number; + }; + "moderation.warning_threshold.created": { + thresholdId: number; + }; + "moderation.warning_threshold.deleted": { + thresholdId: number; + }; + "moderation.warning_threshold.updated": { + thresholdId: number; + }; "notifications.deleted_all": { items: number; }; diff --git a/packages/vitnode/src/api/moderation.ts b/packages/vitnode/src/api/moderation.ts index 797d8ff71e..5cce7251de 100644 --- a/packages/vitnode/src/api/moderation.ts +++ b/packages/vitnode/src/api/moderation.ts @@ -15,5 +15,6 @@ export { type ModerationCaptureResult, type ModerationReportTargetDefinition, type ModerationTargetActionDefinition, + type ModerationTargetAuthors, type ModerationTargetState, } from "./lib/moderation/targets"; diff --git a/packages/vitnode/src/api/modules/admin/admin.module.ts b/packages/vitnode/src/api/modules/admin/admin.module.ts index 0c25a69cdd..ef750d1c87 100644 --- a/packages/vitnode/src/api/modules/admin/admin.module.ts +++ b/packages/vitnode/src/api/modules/admin/admin.module.ts @@ -14,6 +14,7 @@ import { sendNotificationRoute } from "./routes/notifications.route"; import { sessionAdminRoute } from "./routes/session.route"; import { staffAdminModule } from "./staff/staff.admin.module"; import { usersAdminModule } from "./users/users.admin.module"; +import { warningsAdminModule } from "./warnings/warnings.admin.module"; export const adminModule = buildModule({ name: "admin", @@ -31,6 +32,7 @@ export const adminModule = buildModule({ notificationsAdminModule, pluginsAdminModule, reportReasonsAdminModule, + warningsAdminModule, ], cronJobs: [], }); diff --git a/packages/vitnode/src/api/modules/admin/warnings/routes/warning-reasons.routes.ts b/packages/vitnode/src/api/modules/admin/warnings/routes/warning-reasons.routes.ts new file mode 100644 index 0000000000..10f41be05d --- /dev/null +++ b/packages/vitnode/src/api/modules/admin/warnings/routes/warning-reasons.routes.ts @@ -0,0 +1,282 @@ +import type { Context } from "hono"; + +import { z } from "@hono/zod-openapi"; +import { count, eq, inArray, max, sql } from "drizzle-orm"; + +import { buildRoute } from "@/api/builders"; +import { + appendModerationAudit, + listWarningReasons, + WARNING_REASON_WORDS, + warningReasonSchema, +} from "@/api/lib/moderation/warnings/reasons"; +import { saveLanguageWords } from "@/api/lib/save-language-words"; +import { errorSchema, json } from "@/api/modules/moderation/lib/schema"; +import { core_moderation_warning_reasons } from "@/database/warnings"; +import { WARNING_LIMITS } from "@/lib/moderation/warnings"; + +const wordsSchema = (maxLength: number) => + z.array( + z.object({ + languageCode: z.string(), + value: z.string().trim().max(maxLength), + }), + ); + +const reasonBody = z.object({ + title: z + .array( + z.object({ + languageCode: z.string(), + value: z + .string() + .trim() + .min(1) + .max(WARNING_LIMITS.reasonTitleMaxLength), + }), + ) + .min(1), + description: wordsSchema(WARNING_LIMITS.reasonDescriptionMaxLength), + message: wordsSchema(WARNING_LIMITS.reasonMessageMaxLength), + points: z.number().int().min(0).max(WARNING_LIMITS.maxPoints), + expiresAfterHours: z + .number() + .int() + .min(1) + .max(WARNING_LIMITS.maxDurationHours) + .nullable(), + enabled: z.boolean(), +}); + +const idParams = z.object({ + id: z.coerce.number().int().positive().openapi({ example: 1 }), +}); + +const LOCK_REASONS = sql`lock table ${core_moderation_warning_reasons} in share row exclusive mode`; + +const saveReasonWords = async ( + c: Context, + reasonId: number, + body: Partial>, +) => { + for (const variable of ["title", "description", "message"] as const) { + const values = body[variable]; + if (values === undefined) continue; + await saveLanguageWords(c, { + ...WARNING_REASON_WORDS, + itemId: reasonId, + values: values.filter(item => item.value.length > 0), + variable, + }); + } +}; + +/** What an audit entry keeps of a reason: settings, never the words. */ +const auditedSettings = (body: Partial>) => ({ + enabled: body.enabled, + expiresAfterHours: body.expiresAfterHours, + points: body.points, +}); + +export const listWarningReasonsAdminRoute = buildRoute({ + adminStaffPermission: { module: "warning_settings", permission: "can_view" }, + route: { + method: "get", + path: "/reasons", + description: + "Every warning reason in display order, disabled ones too (Admin only)", + responses: { + 200: json( + z.object({ reasons: z.array(warningReasonSchema) }), + "Warning reasons", + ), + 403: { description: "Access Denied" }, + }, + }, + handler: async c => c.json({ reasons: await listWarningReasons(c) }, 200), +}); + +export const createWarningReasonAdminRoute = buildRoute({ + adminStaffPermission: { + module: "warning_settings", + permission: "can_manage", + }, + route: { + method: "post", + path: "/reasons", + description: `Add a warning reason at the end of the list (Admin only). There can be at most ${WARNING_LIMITS.maxReasons}`, + request: { + body: { + required: true, + content: { "application/json": { schema: reasonBody } }, + }, + }, + responses: { + 201: json(z.object({ id: z.number() }), "Warning reason created"), + 403: { description: "Access Denied" }, + 409: json(errorSchema, "The list already holds the maximum number"), + }, + }, + handler: async c => { + const body = c.req.valid("json"); + const created = await c.get("db").transaction(async tx => { + await tx.execute(LOCK_REASONS); + const [{ position: lastPosition, total }] = await tx + .select({ + position: max(core_moderation_warning_reasons.position), + total: count(), + }) + .from(core_moderation_warning_reasons); + if (total >= WARNING_LIMITS.maxReasons) return null; + + const [row] = await tx + .insert(core_moderation_warning_reasons) + .values({ + enabled: body.enabled, + expiresAfterHours: body.expiresAfterHours, + points: body.points, + position: (lastPosition ?? -1) + 1, + }) + .returning({ id: core_moderation_warning_reasons.id }); + await appendModerationAudit(tx, { + actorId: c.get("admin")?.user.id ?? null, + data: { reasonId: row.id, ...auditedSettings(body) }, + type: "reason_created", + }); + + return row; + }); + if (!created) return c.json({ error: "limit_reached" }, 409); + + await saveReasonWords(c, created.id, body); + await c + .get("events") + .emit("moderation.warning_reason.created", { reasonId: created.id }); + + return c.json({ id: created.id }, 201); + }, +}); + +export const updateWarningReasonAdminRoute = buildRoute({ + adminStaffPermission: { + module: "warning_settings", + permission: "can_manage", + }, + route: { + method: "patch", + path: "/reasons/{id}", + description: + "Edit or disable a warning reason (Admin only). Warnings already issued keep the points, expiry and title they were issued with", + request: { + params: idParams, + body: { + required: true, + content: { "application/json": { schema: reasonBody.partial() } }, + }, + }, + responses: { + 200: json(z.object({ id: z.number() }), "Warning reason updated"), + 403: { description: "Access Denied" }, + 404: json(errorSchema, "Warning reason not found"), + }, + }, + handler: async c => { + const { id } = c.req.valid("param"); + const body = c.req.valid("json"); + const settings = auditedSettings(body); + const patch = Object.fromEntries( + Object.entries(settings).filter(([, value]) => value !== undefined), + ); + + const found = await c.get("db").transaction(async tx => { + const [row] = await tx + .select({ id: core_moderation_warning_reasons.id }) + .from(core_moderation_warning_reasons) + .where(eq(core_moderation_warning_reasons.id, id)) + .limit(1); + if (!row) return false; + if (Object.keys(patch).length > 0) { + await tx + .update(core_moderation_warning_reasons) + .set(patch) + .where(eq(core_moderation_warning_reasons.id, id)); + } + await appendModerationAudit(tx, { + actorId: c.get("admin")?.user.id ?? null, + data: { + reasonId: id, + ...patch, + wordsChanged: body.title !== undefined, + }, + type: "reason_updated", + }); + + return true; + }); + if (!found) return c.json({ error: "not_found" }, 404); + + await saveReasonWords(c, id, body); + await c + .get("events") + .emit("moderation.warning_reason.updated", { reasonId: id }); + + return c.json({ id }, 200); + }, +}); + +export const reorderWarningReasonsAdminRoute = buildRoute({ + adminStaffPermission: { + module: "warning_settings", + permission: "can_manage", + }, + route: { + method: "put", + path: "/reasons/reorder", + description: + "Set the order of every warning reason at once (Admin only). `ids` must list each reason exactly once", + request: { + body: { + required: true, + content: { + "application/json": { + schema: z.object({ ids: z.array(z.number().int()).min(1) }), + }, + }, + }, + }, + responses: { + 200: { description: "Order saved" }, + 400: json(errorSchema, "`ids` does not match the current reasons"), + 403: { description: "Access Denied" }, + }, + }, + handler: async c => { + const { ids } = c.req.valid("json"); + const db = c.get("db"); + const current = await db + .select({ id: core_moderation_warning_reasons.id }) + .from(core_moderation_warning_reasons); + const known = new Set(current.map(row => row.id)); + if ( + ids.length !== known.size || + new Set(ids).size !== ids.length || + ids.some(id => !known.has(id)) + ) { + return c.json({ error: "mismatch" }, 400); + } + + const positionById = sql.join( + ids.map( + (id, position) => + sql`when ${core_moderation_warning_reasons.id} = ${id} then ${position}::integer`, + ), + sql` `, + ); + await db + .update(core_moderation_warning_reasons) + .set({ position: sql`case ${positionById} end` }) + .where(inArray(core_moderation_warning_reasons.id, ids)); + + return c.body(null, 200); + }, +}); diff --git a/packages/vitnode/src/api/modules/admin/warnings/routes/warning-thresholds.routes.ts b/packages/vitnode/src/api/modules/admin/warnings/routes/warning-thresholds.routes.ts new file mode 100644 index 0000000000..92ec137bea --- /dev/null +++ b/packages/vitnode/src/api/modules/admin/warnings/routes/warning-thresholds.routes.ts @@ -0,0 +1,279 @@ +import type { Context } from "hono"; + +import { z } from "@hono/zod-openapi"; +import { and, asc, count, eq, ne, sql } from "drizzle-orm"; + +import type { ContentDatabase } from "@/content/server/service"; + +import { buildRoute } from "@/api/builders"; +import { appendModerationAudit } from "@/api/lib/moderation/warnings/reasons"; +import { errorSchema, json } from "@/api/modules/moderation/lib/schema"; +import { core_moderation_warning_thresholds } from "@/database/warnings"; +import { + WARNING_LIMITS, + WARNING_THRESHOLD_ACTIONS, +} from "@/lib/moderation/warnings"; + +const thresholdSchema = z.object({ + id: z.number(), + points: z.number(), + action: z.enum(WARNING_THRESHOLD_ACTIONS), + durationHours: z.number().nullable(), + enabled: z.boolean(), + updatedAt: z.date(), +}); + +const thresholdFields = z.object({ + points: z.number().int().min(1).max(WARNING_LIMITS.maxThresholdPoints), + action: z.enum(WARNING_THRESHOLD_ACTIONS), + durationHours: z + .number() + .int() + .min(1) + .max(WARNING_LIMITS.maxDurationHours) + .nullable(), + enabled: z.boolean(), +}); + +/** A temporary suspension needs its length; an indefinite one has none. */ +const thresholdBody = thresholdFields.refine( + value => + value.action === "suspend_temporary" + ? value.durationHours !== null + : value.durationHours === null, + { message: "duration_mismatch", path: ["durationHours"] }, +); + +const idParams = z.object({ + id: z.coerce.number().int().positive().openapi({ example: 1 }), +}); + +const LOCK_THRESHOLDS = sql`lock table ${core_moderation_warning_thresholds} in share row exclusive mode`; + +/** Whether another enabled rule already sits at `points`; rejected as ambiguous. */ +const pointsTaken = async ( + tx: ContentDatabase, + points: number, + exceptId?: number, +): Promise => { + const table = core_moderation_warning_thresholds; + const [row] = await tx + .select({ id: table.id }) + .from(table) + .where( + and( + eq(table.points, points), + eq(table.enabled, true), + exceptId === undefined ? undefined : ne(table.id, exceptId), + ), + ) + .limit(1); + + return row !== undefined; +}; + +const actorOf = (c: Context) => c.get("admin")?.user.id ?? null; + +const responses = { + 400: json(errorSchema, "A temporary suspension needs a duration"), + 403: { description: "Access Denied" }, + 409: json( + errorSchema, + "Another enabled threshold uses the same points, or the limit is reached", + ), +}; + +export const listWarningThresholdsAdminRoute = buildRoute({ + adminStaffPermission: { module: "warning_settings", permission: "can_view" }, + route: { + method: "get", + path: "/thresholds", + description: "Every warning threshold, lowest points first (Admin only)", + responses: { + 200: json( + z.object({ thresholds: z.array(thresholdSchema) }), + "Warning thresholds", + ), + 403: { description: "Access Denied" }, + }, + }, + handler: async c => { + const table = core_moderation_warning_thresholds; + const thresholds = await c + .get("db") + .select({ + action: table.action, + durationHours: table.durationHours, + enabled: table.enabled, + id: table.id, + points: table.points, + updatedAt: table.updatedAt, + }) + .from(table) + .orderBy(asc(table.points), asc(table.id)); + + return c.json({ thresholds }, 200); + }, +}); + +export const createWarningThresholdAdminRoute = buildRoute({ + adminStaffPermission: { + module: "warning_settings", + permission: "can_manage", + }, + route: { + method: "post", + path: "/thresholds", + description: + "Add a threshold (Admin only). It applies to warnings issued from now on; members already above it are not suspended", + request: { + body: { + required: true, + content: { "application/json": { schema: thresholdBody } }, + }, + }, + responses: { + 201: json(z.object({ id: z.number() }), "Threshold created"), + ...responses, + }, + }, + handler: async c => { + const body = c.req.valid("json"); + const table = core_moderation_warning_thresholds; + const result = await c.get("db").transaction(async tx => { + await tx.execute(LOCK_THRESHOLDS); + const [{ total }] = await tx.select({ total: count() }).from(table); + if (total >= WARNING_LIMITS.maxThresholds) { + return { error: "limit_reached" } as const; + } + if (body.enabled && (await pointsTaken(tx, body.points))) { + return { error: "duplicate_points" } as const; + } + + const [row] = await tx + .insert(table) + .values(body) + .returning({ id: table.id }); + await appendModerationAudit(tx, { + actorId: actorOf(c), + data: { thresholdId: row.id, ...body }, + type: "threshold_created", + }); + + return { id: row.id }; + }); + if ("error" in result) return c.json({ error: result.error }, 409); + + await c + .get("events") + .emit("moderation.warning_threshold.created", { thresholdId: result.id }); + + return c.json({ id: result.id }, 201); + }, +}); + +export const updateWarningThresholdAdminRoute = buildRoute({ + adminStaffPermission: { + module: "warning_settings", + permission: "can_manage", + }, + route: { + method: "put", + path: "/thresholds/{id}", + description: + "Edit, enable or disable a threshold (Admin only). Changes apply to future warnings only - nobody is suspended or unsuspended because of them", + request: { + params: idParams, + body: { + required: true, + content: { "application/json": { schema: thresholdBody } }, + }, + }, + responses: { + 200: json(z.object({ id: z.number() }), "Threshold updated"), + 404: json(errorSchema, "Threshold not found"), + ...responses, + }, + }, + handler: async c => { + const { id } = c.req.valid("param"); + const body = c.req.valid("json"); + const table = core_moderation_warning_thresholds; + const result = await c.get("db").transaction(async tx => { + await tx.execute(LOCK_THRESHOLDS); + const [existing] = await tx + .select({ id: table.id }) + .from(table) + .where(eq(table.id, id)) + .limit(1); + if (!existing) return { error: "not_found" } as const; + if (body.enabled && (await pointsTaken(tx, body.points, id))) { + return { error: "duplicate_points" } as const; + } + + await tx.update(table).set(body).where(eq(table.id, id)); + await appendModerationAudit(tx, { + actorId: actorOf(c), + data: { thresholdId: id, ...body }, + type: "threshold_updated", + }); + + return { id }; + }); + if ("error" in result) { + return result.error === "not_found" + ? c.json({ error: result.error }, 404) + : c.json({ error: result.error }, 409); + } + + await c + .get("events") + .emit("moderation.warning_threshold.updated", { thresholdId: id }); + + return c.json({ id }, 200); + }, +}); + +export const deleteWarningThresholdAdminRoute = buildRoute({ + adminStaffPermission: { + module: "warning_settings", + permission: "can_manage", + }, + route: { + method: "delete", + path: "/thresholds/{id}", + description: + "Delete a threshold (Admin only). Suspensions it created stay and keep a copy of the rule", + request: { params: idParams }, + responses: { + 200: { description: "Threshold deleted" }, + 403: { description: "Access Denied" }, + 404: json(errorSchema, "Threshold not found"), + }, + }, + handler: async c => { + const { id } = c.req.valid("param"); + const table = core_moderation_warning_thresholds; + const deleted = await c.get("db").transaction(async tx => { + const [row] = await tx + .delete(table) + .where(eq(table.id, id)) + .returning({ id: table.id, points: table.points }); + if (!row) return false; + await appendModerationAudit(tx, { + actorId: actorOf(c), + data: { points: row.points, thresholdId: id }, + type: "threshold_deleted", + }); + + return true; + }); + if (!deleted) return c.json({ error: "not_found" }, 404); + + await c + .get("events") + .emit("moderation.warning_threshold.deleted", { thresholdId: id }); + + return c.body(null, 200); + }, +}); diff --git a/packages/vitnode/src/api/modules/admin/warnings/warnings.admin.module.ts b/packages/vitnode/src/api/modules/admin/warnings/warnings.admin.module.ts new file mode 100644 index 0000000000..70a5b012a2 --- /dev/null +++ b/packages/vitnode/src/api/modules/admin/warnings/warnings.admin.module.ts @@ -0,0 +1,28 @@ +import { buildModule } from "@/api/builders"; + +import { + createWarningReasonAdminRoute, + listWarningReasonsAdminRoute, + reorderWarningReasonsAdminRoute, + updateWarningReasonAdminRoute, +} from "./routes/warning-reasons.routes"; +import { + createWarningThresholdAdminRoute, + deleteWarningThresholdAdminRoute, + listWarningThresholdsAdminRoute, + updateWarningThresholdAdminRoute, +} from "./routes/warning-thresholds.routes"; + +export const warningsAdminModule = buildModule({ + name: "warnings", + routes: [ + listWarningReasonsAdminRoute, + createWarningReasonAdminRoute, + reorderWarningReasonsAdminRoute, + updateWarningReasonAdminRoute, + listWarningThresholdsAdminRoute, + createWarningThresholdAdminRoute, + updateWarningThresholdAdminRoute, + deleteWarningThresholdAdminRoute, + ], +}); diff --git a/packages/vitnode/src/api/modules/moderation/lib/warnings-service.ts b/packages/vitnode/src/api/modules/moderation/lib/warnings-service.ts new file mode 100644 index 0000000000..b25b1697ed --- /dev/null +++ b/packages/vitnode/src/api/modules/moderation/lib/warnings-service.ts @@ -0,0 +1,915 @@ +import type { Context } from "hono"; + +import { and, eq, isNull, sql } from "drizzle-orm"; + +import type { ModeratorContext } from "@/api/lib/moderation/access"; +import type { RegisteredReportTarget } from "@/api/lib/moderation/targets"; +import type { WarningReason } from "@/api/lib/moderation/warnings/reasons"; +import type { ContentDatabase } from "@/content/server/service"; +import type { + WarningContentSnapshot, + WarningThresholdAction, +} from "@/lib/moderation/warnings"; + +import { canOnReport } from "@/api/lib/moderation/access"; +import { findReportTarget } from "@/api/lib/moderation/registry"; +import { + canSeeWarning, + canWarnForContent, + hasWarningPermission, + warningTargetRefusal, +} from "@/api/lib/moderation/warnings/access"; +import { + appendModerationAudit, + listWarningReasons, +} from "@/api/lib/moderation/warnings/reasons"; +import { forgetSuspension } from "@/api/lib/moderation/warnings/suspension"; +import { + accountSuspendedNotification, + suspensionRevokedNotification, + warningIssuedNotification, + warningRevokedNotification, +} from "@/api/lib/notifications/core-types"; +import { core_users } from "@/database/users"; +import { + core_moderation_sanctions, + core_moderation_warning_thresholds, + core_moderation_warnings, +} from "@/database/warnings"; +import { MODERATION_PERMISSIONS } from "@/lib/moderation/const"; +import { + activeWarningPoints, + evaluateWarningThresholds, + thresholdSuspensionEnd, + WARNING_LIMITS, + WARNING_PERMISSIONS, +} from "@/lib/moderation/warnings"; + +import type { ReportRow } from "./reports"; + +import { + appendReportEvent, + authorizeReport, + reportConflict, + reportError, + updateReport, +} from "./reports"; + +const HOUR_MS = 60 * 60 * 1000; + +export const warningError = reportError; + +/** How the moderator wants the points to expire. */ +export type WarningExpirationInput = + | { at: Date; mode: "at" } + | { hours: number; mode: "hours" } + | { mode: "default" } + | { mode: "never" }; + +export interface WarningContentInput { + pluginId: string; + targetId: string; + targetType: string; +} + +/** What preview and issue share: who, why, how many points, and what for. */ +export interface WarningDraftInput { + content?: null | WarningContentInput; + expiration: WarningExpirationInput; + points?: null | number; + reasonId: null | number; + reportId?: null | number; + userId: number; +} + +export interface IssueWarningInput extends WarningDraftInput { + idempotencyKey: string; + message: string; + overrideNote?: null | string; + resolveReport?: boolean; + staffNote?: null | string; +} + +export interface WarningTargetUser { + id: number; + name: string; + roleId: number; +} + +export interface ResolvedWarningContent { + pluginId: string; + scopeFamily: null | string; + scopeId: null | string; + snapshot: WarningContentSnapshot; + target: RegisteredReportTarget; + targetId: string; + targetType: string; +} + +/** A draft after every server-side check: what the warning would be. */ +export interface ResolvedWarningDraft { + content: null | ResolvedWarningContent; + /** The reason's expiration in hours, `null` for never. */ + defaultExpiresAfterHours: null | number; + defaultPoints: number; + /** Computes the expiry against the time the warning is written. */ + expiresAt: (now: Date) => Date | null; + overrides: { expiration: boolean; points: boolean }; + points: number; + reason: null | WarningReason; + report: null | ReportRow; + user: WarningTargetUser; +} + +const loadTargetUser = async ( + c: Context, + userId: number, +): Promise => { + const [user] = await c + .get("db") + .select({ + id: core_users.id, + name: core_users.name, + roleId: core_users.roleId, + }) + .from(core_users) + .where(eq(core_users.id, userId)) + .limit(1); + + return user ?? null; +}; + +const resolveContent = async ( + c: Context, + moderator: ModeratorContext, + content: WarningContentInput, + userId: number, +): Promise => { + const target = findReportTarget(c.get("core").moderation, { + pluginId: content.pluginId, + type: content.targetType, + }); + const resolveAuthors = target?.definition.resolveAuthors; + if (!target || !resolveAuthors) { + throw warningError(404, { error: "content_not_found" }); + } + const targetId = target.definition.parseId(content.targetId); + if (targetId === null) + throw warningError(404, { error: "content_not_found" }); + + const { definition } = target; + let scopeId: null | string = null; + if (definition.scope && definition.resolveScope) { + const resolved = await definition.resolveScope(c, targetId); + if (resolved === undefined) { + throw warningError(404, { error: "content_not_found" }); + } + scopeId = resolved === "" ? null : resolved; + } + + const ref = { + pluginId: target.pluginId, + scopeId, + targetType: definition.type, + }; + if (!canWarnForContent(c, moderator.set, ref)) { + throw warningError(403, { error: "forbidden" }); + } + + const authors = await resolveAuthors(c, { targetId }); + if (!authors) throw warningError(404, { error: "content_not_found" }); + if (!authors.authorIds.includes(userId)) { + throw warningError(400, { error: "author_mismatch" }); + } + const current = await definition.current(c, { locale: null, targetId }); + + return { + pluginId: target.pluginId, + scopeFamily: definition.scope ?? null, + scopeId, + snapshot: { + title: authors.title ? authors.title.slice(0, 500) : null, + url: current?.url ?? null, + }, + target, + targetId, + targetType: definition.type, + }; +}; + +const sameContent = ( + a: WarningContentInput, + b: Pick, +): boolean => + a.pluginId === b.pluginId && + a.targetType === b.targetType && + a.targetId === b.targetId; + +/** + * Runs every check a warning needs - target protection, reason, scope, + * authorship, report match, override permissions - and resolves what the + * warning would hold. Never trusts a client-sent author or scope: both come + * from the content's own report target. + */ +export const resolveWarningDraft = async ( + c: Context, + moderator: ModeratorContext, + input: WarningDraftInput, +): Promise => { + const user = await loadTargetUser(c, input.userId); + if (!user) throw warningError(404, { error: "user_not_found" }); + + const refusal = await warningTargetRefusal(c, moderator, user); + if (refusal) throw warningError(403, { error: refusal }); + + let report: null | ReportRow = null; + let contentInput = input.content ?? null; + if (input.reportId) { + ({ report } = await authorizeReport(c, moderator, input.reportId)); + if (contentInput && !sameContent(contentInput, report)) { + throw warningError(400, { error: "report_mismatch" }); + } + contentInput = { + pluginId: report.pluginId, + targetId: report.targetId, + targetType: report.targetType, + }; + } + + const content = contentInput + ? await resolveContent(c, moderator, contentInput, user.id) + : null; + if ( + !content && + !hasWarningPermission(moderator.set, WARNING_PERMISSIONS.warn) + ) { + throw warningError(403, { error: "forbidden" }); + } + + let reason: null | WarningReason = null; + if (input.reasonId !== null) { + [reason = null] = await listWarningReasons(c, { ids: [input.reasonId] }); + if (!reason) throw warningError(404, { error: "reason_not_found" }); + if (!reason.enabled) throw warningError(409, { error: "reason_disabled" }); + } + + const defaultPoints = reason?.points ?? 0; + const defaultExpiresAfterHours = reason?.expiresAfterHours ?? null; + const points = input.points ?? defaultPoints; + if ( + !Number.isInteger(points) || + points < 0 || + points > WARNING_LIMITS.maxPoints + ) { + throw warningError(400, { error: "invalid_points" }); + } + + const { expiration } = input; + const overrides = { + expiration: + expiration.mode === "at" || + (expiration.mode === "never" && defaultExpiresAfterHours !== null) || + (expiration.mode === "hours" && + expiration.hours !== defaultExpiresAfterHours), + points: points !== defaultPoints, + }; + if ( + overrides.points && + !hasWarningPermission(moderator.set, WARNING_PERMISSIONS.overridePoints) + ) { + throw warningError(403, { error: "points_override_forbidden" }); + } + if ( + overrides.expiration && + !hasWarningPermission(moderator.set, WARNING_PERMISSIONS.overrideExpiration) + ) { + throw warningError(403, { error: "expiration_override_forbidden" }); + } + + const requestTime = Date.now(); + if (expiration.mode === "hours") { + if ( + !Number.isInteger(expiration.hours) || + expiration.hours < 1 || + expiration.hours > WARNING_LIMITS.maxDurationHours + ) { + throw warningError(400, { error: "invalid_expiration" }); + } + } + if (expiration.mode === "at") { + const at = expiration.at.getTime(); + if ( + Number.isNaN(at) || + at <= requestTime || + at > requestTime + WARNING_LIMITS.maxDurationHours * HOUR_MS + ) { + throw warningError(400, { error: "invalid_expiration" }); + } + } + + const expiresAt = (now: Date): Date | null => { + switch (expiration.mode) { + case "at": + return expiration.at; + case "default": + return defaultExpiresAfterHours === null + ? null + : new Date(now.getTime() + defaultExpiresAfterHours * HOUR_MS); + case "hours": + return new Date(now.getTime() + expiration.hours * HOUR_MS); + case "never": + return null; + } + }; + + return { + content, + defaultExpiresAfterHours, + defaultPoints, + expiresAt, + overrides, + points, + reason, + report, + user, + }; +}; + +type Db = ContentDatabase; + +const lockUserWarnings = async (tx: Db, userId: number): Promise => { + await tx.execute( + sql`select pg_advisory_xact_lock(hashtext(${`core_moderation_warnings:${userId}`}))`, + ); +}; + +/** The user's active points at `now`, read through `db`. */ +export const loadActivePoints = async ( + db: Db, + userId: number, + now: Date, +): Promise => { + const rows = await db + .select({ + expiresAt: core_moderation_warnings.expiresAt, + points: core_moderation_warnings.points, + revokedAt: core_moderation_warnings.revokedAt, + }) + .from(core_moderation_warnings) + .where(eq(core_moderation_warnings.userId, userId)); + + return activeWarningPoints(rows, now); +}; + +export const loadEnabledThresholds = async (db: Db) => + await db + .select({ + action: core_moderation_warning_thresholds.action, + durationHours: core_moderation_warning_thresholds.durationHours, + enabled: core_moderation_warning_thresholds.enabled, + id: core_moderation_warning_thresholds.id, + points: core_moderation_warning_thresholds.points, + }) + .from(core_moderation_warning_thresholds) + .where(eq(core_moderation_warning_thresholds.enabled, true)); + +export interface ProjectedSanction { + action: WarningThresholdAction; + durationHours: null | number; + endsAt: Date | null; + points: number; + thresholdId: number; +} + +/** + * What a warning of `points` expiring at `expiresAt` would do to the user now. + * Advisory: {@link issueWarning} computes it again under the user's lock. + */ +export const projectWarning = async ( + c: Context, + { + expiresAt, + points, + userId, + }: { expiresAt: Date | null; points: number; userId: number }, +) => { + const db = c.get("db"); + const now = new Date(); + const [before, rules] = await Promise.all([ + loadActivePoints(db, userId, now), + loadEnabledThresholds(db), + ]); + const counts = expiresAt === null || expiresAt.getTime() > now.getTime(); + const after = before + (counts ? points : 0); + const { applied, crossed } = evaluateWarningThresholds({ + after, + before, + rules, + }); + + return { + after, + before, + crossed: crossed.map(rule => rule.id), + sanction: applied + ? { + action: applied.action, + durationHours: applied.durationHours, + endsAt: thresholdSuspensionEnd(applied, now), + points: applied.points, + thresholdId: applied.id, + } + : null, + } satisfies { + after: number; + before: number; + crossed: number[]; + sanction: null | ProjectedSanction; + }; +}; + +export interface IssuedWarning { + duplicate: boolean; + points: { after: number; before: number }; + reportResolved: boolean; + sanction: null | { + endsAt: Date | null; + id: number; + thresholdId: null | number; + }; + warningId: number; +} + +const findSanctionOfWarning = async (db: Db, warningId: number) => { + const [sanction] = await db + .select({ + endsAt: core_moderation_sanctions.endsAt, + id: core_moderation_sanctions.id, + thresholdId: core_moderation_sanctions.thresholdId, + }) + .from(core_moderation_sanctions) + .where(eq(core_moderation_sanctions.warningId, warningId)) + .limit(1); + + return sanction ?? null; +}; + +const findIssuedWarning = async ( + db: Db, + moderator: ModeratorContext, + { + idempotencyKey, + userId, + }: Pick, +): Promise => { + const [existing] = await db + .select({ + id: core_moderation_warnings.id, + pointsAfter: core_moderation_warnings.pointsAfter, + pointsBefore: core_moderation_warnings.pointsBefore, + reportId: core_moderation_warnings.reportId, + userId: core_moderation_warnings.userId, + }) + .from(core_moderation_warnings) + .where( + and( + eq(core_moderation_warnings.issuedById, moderator.user.id), + eq(core_moderation_warnings.idempotencyKey, idempotencyKey), + ), + ) + .limit(1); + if (!existing) return null; + if (existing.userId !== userId) { + throw warningError(409, { error: "idempotency_conflict" }); + } + + return { + duplicate: true, + points: { after: existing.pointsAfter, before: existing.pointsBefore }, + reportResolved: false, + sanction: await findSanctionOfWarning(db, existing.id), + warningId: existing.id, + }; +}; + +/** + * Issues a warning. Serialized per warned user with a transaction-scoped + * advisory lock, so concurrent warnings read each other's points and a + * threshold creates at most one sanction. The warning, the threshold decision, + * the sanction, the audit entries, the optional report resolution and the + * notifications commit together or not at all. A retry with the same + * `idempotencyKey` from the same moderator returns the first result. + */ +export const issueWarning = async ( + c: Context, + moderator: ModeratorContext, + input: IssueWarningInput, +): Promise => { + // A retry of a request that already went through answers with its result, + // even when the report it resolved is closed or the content is gone now. + const previous = await findIssuedWarning(c.get("db"), moderator, input); + if (previous) return previous; + + const draft = await resolveWarningDraft(c, moderator, input); + const overridden = draft.overrides.points || draft.overrides.expiration; + const overrideNote = input.overrideNote?.trim() ?? ""; + const staffNote = input.staffNote?.trim() ?? ""; + if (overridden && overrideNote.length < WARNING_LIMITS.explanationMinLength) { + throw warningError(400, { error: "override_note_required" }); + } + + const { report } = draft; + const resolveReport = Boolean(report && input.resolveReport); + if (report && resolveReport) { + const manage = canOnReport( + c, + moderator.set, + { + pluginId: report.pluginId, + scopeId: report.scopeId, + targetType: report.targetType, + }, + MODERATION_PERMISSIONS.manageReports, + ); + if (!manage) throw warningError(403, { error: "forbidden" }); + if (report.status === "done") throw reportConflict(report, "report_closed"); + } + + const result = await c.get("db").transaction(async (tx: Db) => { + await lockUserWarnings(tx, draft.user.id); + + const existing = await findIssuedWarning(tx, moderator, input); + if (existing) return existing; + + const now = new Date(); + const expiresAt = draft.expiresAt(now); + const before = await loadActivePoints(tx, draft.user.id, now); + const counts = expiresAt === null || expiresAt.getTime() > now.getTime(); + const after = before + (counts ? draft.points : 0); + + const [warning] = await tx + .insert(core_moderation_warnings) + .values({ + contentSnapshot: draft.content?.snapshot ?? null, + defaultPoints: draft.defaultPoints, + expiresAt, + idempotencyKey: input.idempotencyKey, + issuedAt: now, + issuedById: moderator.user.id, + message: input.message.trim(), + overrideNote: overridden ? overrideNote : null, + pluginId: draft.content?.pluginId ?? null, + points: draft.points, + pointsAfter: after, + pointsBefore: before, + reasonId: draft.reason?.id ?? null, + reasonSnapshot: draft.reason ? { title: draft.reason.title } : null, + reportId: report?.id ?? null, + scopeFamily: draft.content?.scopeFamily ?? null, + scopeId: draft.content?.scopeId ?? null, + staffNote: staffNote === "" ? null : staffNote, + targetId: draft.content?.targetId ?? null, + targetType: draft.content?.targetType ?? null, + userId: draft.user.id, + }) + .returning({ id: core_moderation_warnings.id }); + + await appendModerationAudit(tx, { + actorId: moderator.user.id, + data: { + content: draft.content + ? { + pluginId: draft.content.pluginId, + targetId: draft.content.targetId, + targetType: draft.content.targetType, + } + : null, + expiresAt: expiresAt?.toISOString() ?? null, + points: draft.points, + reasonId: draft.reason?.id ?? null, + reportId: report?.id ?? null, + }, + type: "warning_issued", + userId: draft.user.id, + warningId: warning.id, + }); + if (overridden) { + await appendModerationAudit(tx, { + actorId: moderator.user.id, + data: { + defaultExpiresAfterHours: draft.defaultExpiresAfterHours, + defaultPoints: draft.defaultPoints, + expiresAt: expiresAt?.toISOString() ?? null, + expiration: draft.overrides.expiration, + points: draft.points, + pointsOverridden: draft.overrides.points, + }, + type: "warning_overridden", + userId: draft.user.id, + warningId: warning.id, + }); + } + + const rules = await loadEnabledThresholds(tx); + const { applied, crossed } = evaluateWarningThresholds({ + after, + before, + rules, + }); + await appendModerationAudit(tx, { + actorId: moderator.user.id, + data: { + after, + appliedThresholdId: applied?.id ?? null, + before, + crossedThresholdIds: crossed.map(rule => rule.id), + }, + type: "threshold_evaluated", + userId: draft.user.id, + warningId: warning.id, + }); + + let sanction: IssuedWarning["sanction"] = null; + if (applied) { + const endsAt = thresholdSuspensionEnd(applied, now); + const [row] = await tx + .insert(core_moderation_sanctions) + .values({ + createdById: moderator.user.id, + endsAt, + source: "threshold", + startsAt: now, + thresholdId: applied.id, + thresholdSnapshot: { + action: applied.action, + durationHours: applied.durationHours, + points: applied.points, + }, + type: "suspension", + userId: draft.user.id, + warningId: warning.id, + }) + .returning({ id: core_moderation_sanctions.id }); + sanction = { endsAt, id: row.id, thresholdId: applied.id }; + await appendModerationAudit(tx, { + actorId: moderator.user.id, + data: { + action: applied.action, + endsAt: endsAt?.toISOString() ?? null, + source: "threshold", + thresholdId: applied.id, + thresholdPoints: applied.points, + }, + sanctionId: row.id, + type: "sanction_applied", + userId: draft.user.id, + warningId: warning.id, + }); + } + + if (report) { + await appendReportEvent(tx, { + actorId: moderator.user.id, + data: { + points: draft.points, + userId: draft.user.id, + warningId: warning.id, + }, + reportId: report.id, + type: "warning", + }); + if (resolveReport) { + await updateReport(tx, report, { + openKey: null, + resolution: "action_taken", + resolvedAt: now, + resolvedById: moderator.user.id, + status: "done", + }); + await appendReportEvent(tx, { + actorId: moderator.user.id, + data: { resolution: "action_taken", warningId: warning.id }, + reportId: report.id, + type: "resolved", + }); + } + } + + const notifications = c.get("notifications"); + await notifications.publish({ + actorId: null, + data: { + message: input.message.trim(), + points: draft.points, + reason: draft.reason?.title ?? null, + warningId: warning.id, + }, + idempotencyKey: `warning:${warning.id}`, + recipients: [draft.user.id], + tx, + type: warningIssuedNotification, + }); + if (sanction) { + await notifications.publish({ + actorId: null, + data: { + endsAt: sanction.endsAt?.toISOString() ?? null, + sanctionId: sanction.id, + }, + idempotencyKey: `sanction:${sanction.id}`, + recipients: [draft.user.id], + tx, + type: accountSuspendedNotification, + }); + } + + return { + duplicate: false, + points: { after, before }, + reportResolved: resolveReport, + sanction, + warningId: warning.id, + } satisfies IssuedWarning; + }); + + if (result.duplicate) return result; + + if (result.sanction) { + await forgetSuspension(c, draft.user.id, { disconnect: true }); + } + const events = c.get("events"); + await events.emit("moderation.warning.issued", { + issuedById: moderator.user.id, + pluginId: draft.content?.pluginId ?? null, + points: draft.points, + reasonId: draft.reason?.id ?? null, + reportId: report?.id ?? null, + targetId: draft.content?.targetId ?? null, + targetType: draft.content?.targetType ?? null, + userId: draft.user.id, + warningId: result.warningId, + }); + if (result.sanction) { + await events.emit("moderation.sanction.applied", { + endsAt: result.sanction.endsAt, + sanctionId: result.sanction.id, + source: "threshold", + thresholdId: result.sanction.thresholdId, + type: "suspension", + userId: draft.user.id, + warningId: result.warningId, + }); + } + if (report && result.reportResolved) { + await events.emit("moderation.report.resolved", { + reportId: report.id, + resolution: "action_taken", + }); + } + + return result; +}; + +/** + * Revokes a warning: its points stop counting at once, the record stays. + * Sanctions it led to stay too - lifting them is a separate action. + */ +export const revokeWarning = async ( + c: Context, + moderator: ModeratorContext, + { id, reason }: { id: number; reason: string }, +) => { + const db = c.get("db"); + const [warning] = await db + .select() + .from(core_moderation_warnings) + .where(eq(core_moderation_warnings.id, id)) + .limit(1); + if (!warning || !canSeeWarning(c, moderator, warning)) { + throw warningError(404, { error: "not_found" }); + } + if (!hasWarningPermission(moderator.set, WARNING_PERMISSIONS.revoke)) { + throw warningError(403, { error: "forbidden" }); + } + if (warning.revokedAt) throw warningError(409, { error: "already_revoked" }); + + const revoked = await db.transaction(async (tx: Db) => { + const now = new Date(); + const [row] = await tx + .update(core_moderation_warnings) + .set({ + revokedAt: now, + revokedById: moderator.user.id, + revokeReason: reason.trim(), + }) + .where( + and( + eq(core_moderation_warnings.id, id), + isNull(core_moderation_warnings.revokedAt), + ), + ) + .returning({ id: core_moderation_warnings.id }); + if (!row) return false; + + await appendModerationAudit(tx, { + actorId: moderator.user.id, + data: { points: warning.points }, + type: "warning_revoked", + userId: warning.userId, + warningId: id, + }); + await c.get("notifications").publish({ + actorId: null, + data: { + points: warning.points, + reason: warning.reasonSnapshot?.title ?? null, + warningId: id, + }, + idempotencyKey: `warning-revoked:${id}`, + recipients: [warning.userId], + tx, + type: warningRevokedNotification, + }); + + return true; + }); + if (!revoked) throw warningError(409, { error: "already_revoked" }); + + await c.get("events").emit("moderation.warning.revoked", { + revokedById: moderator.user.id, + userId: warning.userId, + warningId: id, + }); + + return { userId: warning.userId, warningId: id }; +}; + +/** + * Lifts one sanction. Other active sanctions of the same user keep applying. + */ +export const revokeSanction = async ( + c: Context, + moderator: ModeratorContext, + { id, reason }: { id: number; reason: string }, +) => { + if ( + !hasWarningPermission(moderator.set, WARNING_PERMISSIONS.manageSanctions) + ) { + throw warningError(403, { error: "forbidden" }); + } + const db = c.get("db"); + const [sanction] = await db + .select() + .from(core_moderation_sanctions) + .where(eq(core_moderation_sanctions.id, id)) + .limit(1); + if (!sanction) throw warningError(404, { error: "not_found" }); + if (sanction.revokedAt) throw warningError(409, { error: "already_revoked" }); + + const revoked = await db.transaction(async (tx: Db) => { + const [row] = await tx + .update(core_moderation_sanctions) + .set({ + revokedAt: new Date(), + revokedById: moderator.user.id, + revokeReason: reason.trim(), + }) + .where( + and( + eq(core_moderation_sanctions.id, id), + isNull(core_moderation_sanctions.revokedAt), + ), + ) + .returning({ id: core_moderation_sanctions.id }); + if (!row) return false; + + await appendModerationAudit(tx, { + actorId: moderator.user.id, + data: { type: sanction.type }, + sanctionId: id, + type: "sanction_revoked", + userId: sanction.userId, + warningId: sanction.warningId, + }); + await c.get("notifications").publish({ + actorId: null, + data: { sanctionId: id }, + idempotencyKey: `sanction-revoked:${id}`, + recipients: [sanction.userId], + tx, + type: suspensionRevokedNotification, + }); + + return true; + }); + if (!revoked) throw warningError(409, { error: "already_revoked" }); + + await forgetSuspension(c, sanction.userId, { disconnect: false }); + await c.get("events").emit("moderation.sanction.revoked", { + revokedById: moderator.user.id, + sanctionId: id, + userId: sanction.userId, + }); + + return { sanctionId: id, userId: sanction.userId }; +}; diff --git a/packages/vitnode/src/api/modules/moderation/lib/warnings-view.ts b/packages/vitnode/src/api/modules/moderation/lib/warnings-view.ts new file mode 100644 index 0000000000..98b5ef64d1 --- /dev/null +++ b/packages/vitnode/src/api/modules/moderation/lib/warnings-view.ts @@ -0,0 +1,223 @@ +import type { Context } from "hono"; + +import { z } from "@hono/zod-openapi"; +import { desc, eq, inArray } from "drizzle-orm"; + +import type { ModeratorContext } from "@/api/lib/moderation/access"; + +import { findReportTarget } from "@/api/lib/moderation/registry"; +import { + canSeeWarning, + canSeeWarningNotes, + hasWarningPermission, +} from "@/api/lib/moderation/warnings/access"; +import { + core_moderation_sanctions, + core_moderation_warnings, +} from "@/database/warnings"; +import { + isSanctionActiveAt, + SANCTION_SOURCES, + SANCTION_TYPES, + WARNING_PERMISSIONS, + WARNING_STATUSES, + WARNING_THRESHOLD_ACTIONS, + warningStatusAt, +} from "@/lib/moderation/warnings"; + +import { loadReportPeople } from "./people"; +import { reportPersonSchema } from "./schema"; + +const textSchema = z.array( + z.object({ languageCode: z.string(), value: z.string() }), +); + +export const warningContentSchema = z.object({ + pluginId: z.string(), + targetType: z.string(), + targetId: z.string(), + targetLabel: z.string().nullable(), + title: z.string().nullable(), + url: z.string().nullable(), +}); + +/** A warning as staff see it; notes are `null` without `can_view_notes`. */ +export const staffWarningSchema = z.object({ + id: z.number(), + userId: z.number(), + status: z.enum(WARNING_STATUSES), + points: z.number(), + defaultPoints: z.number().nullable(), + reasonId: z.number().nullable(), + reason: textSchema.nullable(), + message: z.string(), + staffNote: z.string().nullable(), + overrideNote: z.string().nullable(), + issuedAt: z.date(), + expiresAt: z.date().nullable(), + issuedBy: reportPersonSchema.nullable(), + content: warningContentSchema.nullable(), + reportId: z.number().nullable(), + revokedAt: z.date().nullable(), + revokedBy: reportPersonSchema.nullable(), + revokeReason: z.string().nullable(), + sanctionId: z.number().nullable(), +}); + +export const sanctionSchema = z.object({ + id: z.number(), + type: z.enum(SANCTION_TYPES), + source: z.enum(SANCTION_SOURCES), + active: z.boolean(), + startsAt: z.date(), + endsAt: z.date().nullable(), + warningId: z.number().nullable(), + threshold: z + .object({ + action: z.enum(WARNING_THRESHOLD_ACTIONS), + durationHours: z.number().nullable(), + points: z.number(), + }) + .nullable(), + createdBy: reportPersonSchema.nullable(), + revokedAt: z.date().nullable(), + revokedBy: reportPersonSchema.nullable(), + revokeReason: z.string().nullable(), +}); + +export type WarningRow = typeof core_moderation_warnings.$inferSelect; +export type SanctionRow = typeof core_moderation_sanctions.$inferSelect; + +const contentOf = (c: Context, row: WarningRow) => { + if (!row.pluginId || !row.targetType || !row.targetId) return null; + + return { + pluginId: row.pluginId, + targetType: row.targetType, + targetId: row.targetId, + targetLabel: + findReportTarget(c.get("core").moderation, { + pluginId: row.pluginId, + type: row.targetType, + })?.definition.label ?? null, + title: row.contentSnapshot?.title ?? null, + url: row.contentSnapshot?.url ?? null, + }; +}; + +/** + * Turns warning rows into what one moderator may read: rows they may not + * see are dropped, and staff notes are blanked unless allowed. + */ +export const serializeStaffWarnings = async ( + c: Context, + moderator: ModeratorContext, + rows: WarningRow[], + now: Date = new Date(), +) => { + const visible = rows.filter(row => canSeeWarning(c, moderator, row)); + const canViewSanctions = hasWarningPermission( + moderator.set, + WARNING_PERMISSIONS.viewSanctions, + ); + const [people, sanctions] = await Promise.all([ + loadReportPeople( + c, + visible.flatMap(row => [row.issuedById, row.revokedById]), + ), + canViewSanctions && visible.length > 0 + ? c + .get("db") + .select({ + id: core_moderation_sanctions.id, + warningId: core_moderation_sanctions.warningId, + }) + .from(core_moderation_sanctions) + .where( + inArray( + core_moderation_sanctions.warningId, + visible.map(row => row.id), + ), + ) + : [], + ]); + const person = (id: null | number) => + id === null ? null : (people.get(id) ?? null); + + return visible.map(row => { + const notes = canSeeWarningNotes(moderator, row); + + return { + id: row.id, + userId: row.userId, + status: warningStatusAt(row, now), + points: row.points, + defaultPoints: row.defaultPoints, + reasonId: row.reasonId, + reason: row.reasonSnapshot?.title ?? null, + message: row.message, + staffNote: notes ? row.staffNote : null, + overrideNote: notes ? row.overrideNote : null, + issuedAt: row.issuedAt, + expiresAt: row.expiresAt, + issuedBy: person(row.issuedById), + content: contentOf(c, row), + reportId: row.reportId, + revokedAt: row.revokedAt, + revokedBy: person(row.revokedById), + revokeReason: row.revokeReason, + sanctionId: + sanctions.find(sanction => sanction.warningId === row.id)?.id ?? null, + }; + }); +}; + +export const serializeSanctions = async ( + c: Context, + rows: SanctionRow[], + now: Date = new Date(), +) => { + const people = await loadReportPeople( + c, + rows.flatMap(row => [row.createdById, row.revokedById]), + ); + const person = (id: null | number) => + id === null ? null : (people.get(id) ?? null); + + return rows.map(row => ({ + id: row.id, + type: row.type, + source: row.source, + active: isSanctionActiveAt(row, now), + startsAt: row.startsAt, + endsAt: row.endsAt, + warningId: row.warningId, + threshold: row.thresholdSnapshot, + createdBy: person(row.createdById), + revokedAt: row.revokedAt, + revokedBy: person(row.revokedById), + revokeReason: row.revokeReason, + })); +}; + +export const loadUserWarnings = async (c: Context, userId: number) => + await c + .get("db") + .select() + .from(core_moderation_warnings) + .where(eq(core_moderation_warnings.userId, userId)) + .orderBy( + desc(core_moderation_warnings.issuedAt), + desc(core_moderation_warnings.id), + ); + +export const loadUserSanctions = async (c: Context, userId: number) => + await c + .get("db") + .select() + .from(core_moderation_sanctions) + .where(eq(core_moderation_sanctions.userId, userId)) + .orderBy( + desc(core_moderation_sanctions.startsAt), + desc(core_moderation_sanctions.id), + ); diff --git a/packages/vitnode/src/api/modules/moderation/moderation.module.ts b/packages/vitnode/src/api/modules/moderation/moderation.module.ts index 390c2a50af..afc46dc58f 100644 --- a/packages/vitnode/src/api/modules/moderation/moderation.module.ts +++ b/packages/vitnode/src/api/modules/moderation/moderation.module.ts @@ -1,5 +1,6 @@ import { buildModule } from "@/api/builders"; +import { accountStatusRoute } from "./routes/account-status.route"; import { approvalFiltersRoute, approveApprovalRoute, @@ -16,6 +17,16 @@ import { } from "./routes/notes.routes"; import { listReportReasonsRoute } from "./routes/reasons.route"; import { showReportRoute } from "./routes/show.route"; +import { + issueWarningRoute, + listWarningReasonsRoute, + listWarningsRoute, + memberStandingRoute, + previewWarningRoute, + revokeSanctionRoute, + revokeWarningRoute, + searchMembersRoute, +} from "./routes/warnings.routes"; import { assignReportRoute, claimReportRoute, @@ -48,5 +59,14 @@ export const moderationModule = buildModule({ showApprovalRoute, approveApprovalRoute, rejectApprovalRoute, + listWarningReasonsRoute, + previewWarningRoute, + issueWarningRoute, + listWarningsRoute, + revokeWarningRoute, + revokeSanctionRoute, + searchMembersRoute, + memberStandingRoute, + accountStatusRoute, ], }); diff --git a/packages/vitnode/src/api/modules/moderation/routes/account-status.route.ts b/packages/vitnode/src/api/modules/moderation/routes/account-status.route.ts new file mode 100644 index 0000000000..daf65a9522 --- /dev/null +++ b/packages/vitnode/src/api/modules/moderation/routes/account-status.route.ts @@ -0,0 +1,126 @@ +import { z } from "@hono/zod-openapi"; + +import { buildRoute } from "@/api/builders"; +import { findReportTarget } from "@/api/lib/moderation/registry"; +import { findActiveSuspension } from "@/api/lib/moderation/warnings/suspension"; +import { + isSanctionActiveAt, + WARNING_STATUSES, + warningStatusAt, +} from "@/lib/moderation/warnings"; + +import { json } from "../lib/schema"; +import { loadActivePoints } from "../lib/warnings-service"; +import { loadUserSanctions, loadUserWarnings } from "../lib/warnings-view"; + +const textSchema = z.array( + z.object({ languageCode: z.string(), value: z.string() }), +); + +/** + * What a member may read about their own warnings. Built field by field so + * staff notes, override explanations, revocation reasons and the issuing + * moderator never leave the server. + */ +const ownWarningSchema = z.object({ + id: z.number(), + status: z.enum(WARNING_STATUSES), + points: z.number(), + reason: textSchema.nullable(), + message: z.string(), + issuedAt: z.date(), + expiresAt: z.date().nullable(), + content: z + .object({ title: z.string().nullable(), url: z.string().nullable() }) + .nullable(), +}); + +const ownSanctionSchema = z.object({ + id: z.number(), + active: z.boolean(), + startsAt: z.date(), + endsAt: z.date().nullable(), + revoked: z.boolean(), + warningId: z.number().nullable(), +}); + +export const accountStatusRoute = buildRoute({ + route: { + method: "get", + path: "/account-status", + description: + "The signed-in member's own standing: active warning points, warning history and suspensions. Also answers for a suspended account, whose session is otherwise treated as a guest. Never includes staff notes", + responses: { + 200: json( + z.object({ + suspension: z.object({ endsAt: z.date().nullable() }).nullable(), + activePoints: z.number(), + warnings: z.array(ownWarningSchema), + sanctions: z.array(ownSanctionSchema), + }), + "The account's standing", + ), + 401: { description: "Sign in first" }, + }, + }, + handler: async c => { + const userId = c.get("user")?.id ?? c.get("suspension")?.userId; + if (!userId) return c.json({ error: "unauthenticated" }, 401); + + const now = new Date(); + const [warnings, sanctions, activePoints, suspension] = await Promise.all([ + loadUserWarnings(c, userId), + loadUserSanctions(c, userId), + loadActivePoints(c.get("db"), userId, now), + findActiveSuspension(c, userId, now), + ]); + const registry = c.get("core").moderation; + + const contentOf = async (row: (typeof warnings)[number]) => { + if (!row.pluginId || !row.targetType || !row.targetId) return null; + const target = findReportTarget(registry, { + pluginId: row.pluginId, + type: row.targetType, + }); + // Link only to content that is still there and not hidden. + const current = target + ? await target.definition + .current(c, { locale: null, targetId: row.targetId }) + .catch(() => null) + : null; + + return { + title: row.contentSnapshot?.title ?? null, + url: current && !current.hidden ? current.url : null, + }; + }; + + return c.json( + { + suspension: suspension ? { endsAt: suspension.endsAt } : null, + activePoints, + warnings: await Promise.all( + warnings.map(async row => ({ + id: row.id, + status: warningStatusAt(row, now), + points: row.points, + reason: row.reasonSnapshot?.title ?? null, + message: row.message, + issuedAt: row.issuedAt, + expiresAt: row.expiresAt, + content: await contentOf(row), + })), + ), + sanctions: sanctions.map(row => ({ + id: row.id, + active: isSanctionActiveAt(row, now), + startsAt: row.startsAt, + endsAt: row.endsAt, + revoked: row.revokedAt !== null, + warningId: row.warningId, + })), + }, + 200, + ); + }, +}); diff --git a/packages/vitnode/src/api/modules/moderation/routes/show.route.ts b/packages/vitnode/src/api/modules/moderation/routes/show.route.ts index db0c63f8fa..1ec4ecb0a3 100644 --- a/packages/vitnode/src/api/modules/moderation/routes/show.route.ts +++ b/packages/vitnode/src/api/modules/moderation/routes/show.route.ts @@ -4,6 +4,7 @@ import { asc, eq } from "drizzle-orm"; import { buildRoute } from "@/api/builders"; import { canOnReport, requireModerator } from "@/api/lib/moderation/access"; import { safeReportTargetUrl } from "@/api/lib/moderation/snapshot"; +import { canWarnForContent } from "@/api/lib/moderation/warnings/access"; import { core_moderation_report_events } from "@/database/moderation"; import { MODERATION_PERMISSIONS, @@ -77,6 +78,8 @@ const detailSchema = z.object({ }), permissions: z.object({ manage: z.boolean(), + /** Whether the moderator may warn the content's authors from this report. */ + warn: z.boolean(), actions: z.array( z.object({ action: z.enum(MODERATION_TARGET_ACTIONS), @@ -218,6 +221,9 @@ export const showReportRoute = buildRoute({ }, permissions: { manage, + warn: + target?.definition.resolveAuthors !== undefined && + canWarnForContent(c, moderator.set, ref), actions: MODERATION_TARGET_ACTIONS.flatMap(action => { const definition = target?.definition.actions?.[action]; if (!definition) return []; diff --git a/packages/vitnode/src/api/modules/moderation/routes/warnings.routes.ts b/packages/vitnode/src/api/modules/moderation/routes/warnings.routes.ts new file mode 100644 index 0000000000..a565b927b6 --- /dev/null +++ b/packages/vitnode/src/api/modules/moderation/routes/warnings.routes.ts @@ -0,0 +1,633 @@ +import type { Context } from "hono"; + +import { z } from "@hono/zod-openapi"; +import { + and, + count, + desc, + eq, + gt, + gte, + ilike, + isNotNull, + isNull, + lte, + or, +} from "drizzle-orm"; +import { HTTPException } from "hono/http-exception"; + +import type { ModeratorContext } from "@/api/lib/moderation/access"; + +import { buildRoute } from "@/api/builders"; +import { requireModerator } from "@/api/lib/moderation/access"; +import { + canWarnAnywhere, + hasWarningPermission, + viewableWarningsCondition, +} from "@/api/lib/moderation/warnings/access"; +import { + listWarningReasons, + warningReasonSchema, +} from "@/api/lib/moderation/warnings/reasons"; +import { findActiveSuspension } from "@/api/lib/moderation/warnings/suspension"; +import { core_users } from "@/database/users"; +import { core_moderation_warnings } from "@/database/warnings"; +import { containsLikePattern } from "@/lib/api/like-pattern"; +import { + WARNING_LIMITS, + WARNING_PERMISSIONS, + WARNING_THRESHOLD_ACTIONS, +} from "@/lib/moderation/warnings"; + +import type { WarningDraftInput } from "../lib/warnings-service"; + +import { loadReportPeople } from "../lib/people"; +import { errorSchema, json, reportPersonSchema } from "../lib/schema"; +import { + issueWarning, + loadActivePoints, + projectWarning, + resolveWarningDraft, + revokeSanction, + revokeWarning, +} from "../lib/warnings-service"; +import { + loadUserSanctions, + loadUserWarnings, + sanctionSchema, + serializeSanctions, + serializeStaffWarnings, + staffWarningSchema, +} from "../lib/warnings-view"; + +const body = (schema: T) => ({ + body: { content: { "application/json": { schema } }, required: true }, +}); + +const idParams = z.object({ + id: z.coerce.number().int().positive().openapi({ example: 1 }), +}); + +const expirationSchema = z.discriminatedUnion("mode", [ + z.object({ mode: z.literal("default") }), + z.object({ mode: z.literal("never") }), + z.object({ + mode: z.literal("hours"), + hours: z.number().int().min(1).max(WARNING_LIMITS.maxDurationHours), + }), + z.object({ mode: z.literal("at"), at: z.iso.datetime() }), +]); + +const draftSchema = z.object({ + userId: z.number().int().positive(), + reasonId: z.number().int().positive().nullable(), + points: z.number().int().min(0).max(WARNING_LIMITS.maxPoints).nullish(), + expiration: expirationSchema.default({ mode: "default" }), + content: z + .object({ + pluginId: z.string().min(1).max(100), + targetType: z.string().min(1).max(100), + targetId: z.string().min(1).max(64), + }) + .nullish(), + reportId: z.number().int().positive().nullish(), +}); + +const issueSchema = draftSchema.extend({ + message: z + .string() + .trim() + .min(WARNING_LIMITS.messageMinLength) + .max(WARNING_LIMITS.messageMaxLength), + staffNote: z.string().trim().max(WARNING_LIMITS.noteMaxLength).nullish(), + overrideNote: z.string().trim().max(WARNING_LIMITS.noteMaxLength).nullish(), + resolveReport: z.boolean().default(false), + idempotencyKey: z + .string() + .min(8) + .max(64) + .regex(/^[\w-]+$/), +}); + +const revokeSchema = z.object({ + reason: z + .string() + .trim() + .min(WARNING_LIMITS.explanationMinLength) + .max(WARNING_LIMITS.noteMaxLength), +}); + +const toDraft = (input: z.infer): WarningDraftInput => ({ + content: input.content ?? null, + expiration: + input.expiration.mode === "at" + ? { at: new Date(input.expiration.at), mode: "at" } + : input.expiration, + points: input.points ?? null, + reasonId: input.reasonId, + reportId: input.reportId ?? null, + userId: input.userId, +}); + +const commonResponses = { + 400: json(errorSchema, "Invalid input"), + 401: { description: "Sign in first" }, + 403: json(errorSchema, "Not allowed for this moderator or this member"), + 404: json(errorSchema, "No such member, reason, content or report"), + 409: json(errorSchema, "The state changed, or the reason is disabled"), +}; + +const requireWarner = async (c: Context): Promise => { + const moderator = await requireModerator(c); + if (!canWarnAnywhere(c, moderator.set)) { + throw new HTTPException(403, { message: "Forbidden" }); + } + + return moderator; +}; + +export const listWarningReasonsRoute = buildRoute({ + route: { + method: "get", + path: "/warnings/reasons", + description: + "Enabled warning reasons, in display order, for the warn form (moderators who can warn)", + responses: { + 200: json( + z.object({ reasons: z.array(warningReasonSchema) }), + "Warning reasons", + ), + 401: commonResponses[401], + 403: { description: "Cannot warn anyone" }, + }, + }, + handler: async c => { + await requireWarner(c); + + return c.json( + { reasons: await listWarningReasons(c, { enabledOnly: true }) }, + 200, + ); + }, +}); + +const projectedSanctionSchema = z.object({ + action: z.enum(WARNING_THRESHOLD_ACTIONS), + durationHours: z.number().nullable(), + endsAt: z.date().nullable(), + points: z.number(), + thresholdId: z.number(), +}); + +export const previewWarningRoute = buildRoute({ + route: { + method: "post", + path: "/warnings/preview", + description: + "Runs every check of a warning without issuing it and predicts its consequences: the member's active points before and after, and the suspension a threshold would apply. Advisory - issuing recalculates", + request: body(draftSchema), + responses: { + 200: json( + z.object({ + user: reportPersonSchema, + points: z.object({ + current: z.number(), + warning: z.number(), + projected: z.number(), + }), + defaults: z.object({ + points: z.number(), + expiresAfterHours: z.number().nullable(), + }), + expiresAt: z.date().nullable(), + overrides: z.object({ points: z.boolean(), expiration: z.boolean() }), + sanction: projectedSanctionSchema.nullable(), + suspendedUntil: z.object({ endsAt: z.date().nullable() }).nullable(), + permissions: z.object({ + overridePoints: z.boolean(), + overrideExpiration: z.boolean(), + resolveReport: z.boolean(), + viewWarnings: z.boolean(), + }), + related: z.array(staffWarningSchema), + }), + "What the warning would do", + ), + ...commonResponses, + }, + }, + handler: async c => { + const moderator = await requireWarner(c); + const draft = await resolveWarningDraft( + c, + moderator, + toDraft(c.req.valid("json")), + ); + const now = new Date(); + const expiresAt = draft.expiresAt(now); + const projection = await projectWarning(c, { + expiresAt, + points: draft.points, + userId: draft.user.id, + }); + const canViewSanctions = hasWarningPermission( + moderator.set, + WARNING_PERMISSIONS.viewSanctions, + ); + const [people, suspension, relatedRows] = await Promise.all([ + loadReportPeople(c, [draft.user.id]), + canViewSanctions ? findActiveSuspension(c, draft.user.id, now) : null, + draft.content + ? c + .get("db") + .select() + .from(core_moderation_warnings) + .where( + and( + eq(core_moderation_warnings.pluginId, draft.content.pluginId), + eq( + core_moderation_warnings.targetType, + draft.content.targetType, + ), + eq(core_moderation_warnings.targetId, draft.content.targetId), + ), + ) + .orderBy(desc(core_moderation_warnings.issuedAt)) + .limit(10) + : [], + ]); + const user = people.get(draft.user.id); + if (!user) throw new HTTPException(404, { message: "user_not_found" }); + + return c.json( + { + user, + points: { + current: projection.before, + warning: draft.points, + projected: projection.after, + }, + defaults: { + points: draft.defaultPoints, + expiresAfterHours: draft.defaultExpiresAfterHours, + }, + expiresAt, + overrides: draft.overrides, + sanction: projection.sanction, + suspendedUntil: suspension ? { endsAt: suspension.endsAt } : null, + permissions: { + overridePoints: hasWarningPermission( + moderator.set, + WARNING_PERMISSIONS.overridePoints, + ), + overrideExpiration: hasWarningPermission( + moderator.set, + WARNING_PERMISSIONS.overrideExpiration, + ), + resolveReport: + draft.report !== null && draft.report.status !== "done", + viewWarnings: hasWarningPermission( + moderator.set, + WARNING_PERMISSIONS.view, + ), + }, + related: await serializeStaffWarnings(c, moderator, relatedRows, now), + }, + 200, + ); + }, +}); + +export const issueWarningRoute = buildRoute({ + route: { + method: "post", + path: "/warnings", + description: + "Issue a warning. The server resolves the content's author and scope, recalculates points under the member's lock, applies at most one threshold suspension and notifies the member. Retrying with the same `idempotencyKey` returns the first result", + request: body(issueSchema), + responses: { + 200: json( + z.object({ + warningId: z.number(), + duplicate: z.boolean(), + points: z.object({ before: z.number(), after: z.number() }), + reportResolved: z.boolean(), + sanction: z + .object({ + id: z.number(), + endsAt: z.date().nullable(), + thresholdId: z.number().nullable(), + }) + .nullable(), + }), + "The warning as issued", + ), + ...commonResponses, + }, + }, + handler: async c => { + const moderator = await requireWarner(c); + const input = c.req.valid("json"); + const result = await issueWarning(c, moderator, { + ...toDraft(input), + idempotencyKey: input.idempotencyKey, + message: input.message, + overrideNote: input.overrideNote ?? null, + resolveReport: input.resolveReport, + staffNote: input.staffNote ?? null, + }); + + return c.json(result, 200); + }, +}); + +export const revokeWarningRoute = buildRoute({ + route: { + method: "post", + path: "/warnings/{id}/revoke", + description: + "Revoke a warning with a reason. Its points stop counting at once; the record and any suspension it led to stay", + request: { params: idParams, ...body(revokeSchema) }, + responses: { + 200: json( + z.object({ warningId: z.number(), userId: z.number() }), + "Warning revoked", + ), + ...commonResponses, + }, + }, + handler: async c => { + const moderator = await requireModerator(c); + const { id } = c.req.valid("param"); + const { reason } = c.req.valid("json"); + + return c.json(await revokeWarning(c, moderator, { id, reason }), 200); + }, +}); + +export const revokeSanctionRoute = buildRoute({ + route: { + method: "post", + path: "/sanctions/{id}/revoke", + description: + "Lift one sanction with a reason. Other active sanctions of the member keep applying", + request: { params: idParams, ...body(revokeSchema) }, + responses: { + 200: json( + z.object({ sanctionId: z.number(), userId: z.number() }), + "Sanction revoked", + ), + ...commonResponses, + }, + }, + handler: async c => { + const moderator = await requireModerator(c); + const { id } = c.req.valid("param"); + const { reason } = c.req.valid("json"); + + return c.json(await revokeSanction(c, moderator, { id, reason }), 200); + }, +}); + +const statusCondition = ( + status: "active" | "all" | "expired" | "revoked", + now: Date, +) => { + const table = core_moderation_warnings; + switch (status) { + case "active": + return and( + isNull(table.revokedAt), + or(isNull(table.expiresAt), gt(table.expiresAt, now)), + ); + case "all": + return undefined; + case "expired": + return and(isNull(table.revokedAt), lte(table.expiresAt, now)); + case "revoked": + return isNotNull(table.revokedAt); + } +}; + +export const listWarningsRoute = buildRoute({ + route: { + method: "get", + path: "/warnings", + description: + "Warnings the moderator may see, newest first, with filters. Moderators without `can_view` see the warnings they issued and those linked to content in their scope", + request: { + query: z.object({ + userId: z.coerce.number().int().positive().optional(), + reasonId: z.coerce.number().int().positive().optional(), + issuedById: z.coerce.number().int().positive().optional(), + status: z.enum(["all", "active", "expired", "revoked"]).default("all"), + pluginId: z.string().max(100).optional(), + targetType: z.string().max(100).optional(), + from: z.iso.datetime().optional(), + to: z.iso.datetime().optional(), + page: z.coerce.number().int().positive().max(10_000).default(1), + pageSize: z.coerce + .number() + .int() + .positive() + .max(100) + .default(WARNING_LIMITS.pageSize), + }), + }, + responses: { + 200: json( + z.object({ + items: z.array( + staffWarningSchema.extend({ user: reportPersonSchema.nullable() }), + ), + page: z.number(), + pageSize: z.number(), + total: z.number(), + }), + "One page of warnings", + ), + 401: commonResponses[401], + 403: { description: "Not a moderator" }, + }, + }, + handler: async c => { + const moderator = await requireModerator(c); + const query = c.req.valid("query"); + const table = core_moderation_warnings; + const now = new Date(); + const where = and( + viewableWarningsCondition(c, moderator), + statusCondition(query.status, now), + query.userId ? eq(table.userId, query.userId) : undefined, + query.reasonId ? eq(table.reasonId, query.reasonId) : undefined, + query.issuedById ? eq(table.issuedById, query.issuedById) : undefined, + query.pluginId ? eq(table.pluginId, query.pluginId) : undefined, + query.targetType ? eq(table.targetType, query.targetType) : undefined, + query.from ? gte(table.issuedAt, new Date(query.from)) : undefined, + query.to ? lte(table.issuedAt, new Date(query.to)) : undefined, + ); + + const [[totalRow], rows] = await Promise.all([ + c.get("db").select({ value: count() }).from(table).where(where), + c + .get("db") + .select() + .from(table) + .where(where) + .orderBy(desc(table.issuedAt), desc(table.id)) + .limit(query.pageSize) + .offset((query.page - 1) * query.pageSize), + ]); + const [items, people] = await Promise.all([ + serializeStaffWarnings(c, moderator, rows, now), + loadReportPeople( + c, + rows.map(row => row.userId), + ), + ]); + + return c.json( + { + items: items.map(item => ({ + ...item, + user: people.get(item.userId) ?? null, + })), + page: query.page, + pageSize: query.pageSize, + total: totalRow?.value ?? 0, + }, + 200, + ); + }, +}); + +export const searchMembersRoute = buildRoute({ + route: { + method: "get", + path: "/members", + description: + "Find members by name for the ModCP member lookup (moderators who can warn or view warnings)", + request: { + query: z.object({ search: z.string().trim().min(1).max(100) }), + }, + responses: { + 200: json( + z.object({ items: z.array(reportPersonSchema) }), + "Up to 10 members", + ), + 401: commonResponses[401], + 403: { description: "Cannot warn or view warnings" }, + }, + }, + handler: async c => { + const moderator = await requireModerator(c); + if ( + !hasWarningPermission(moderator.set, WARNING_PERMISSIONS.view) && + !hasWarningPermission(moderator.set, WARNING_PERMISSIONS.warn) + ) { + throw new HTTPException(403, { message: "Forbidden" }); + } + const { search } = c.req.valid("query"); + const pattern = containsLikePattern(search); + const rows = await c + .get("db") + .select({ id: core_users.id }) + .from(core_users) + .where( + or( + ilike(core_users.name, pattern), + ilike(core_users.nameCode, pattern), + ), + ) + .orderBy(core_users.name) + .limit(10); + const people = await loadReportPeople( + c, + rows.map(row => row.id), + ); + + return c.json( + { + items: rows.flatMap(row => { + const person = people.get(row.id); + + return person ? [person] : []; + }), + }, + 200, + ); + }, +}); + +export const memberStandingRoute = buildRoute({ + route: { + method: "get", + path: "/members/{id}", + description: + "A member's standing for the ModCP: global active points, the warnings this moderator may see and, with `can_view_sanctions`, their sanctions", + request: { params: idParams }, + responses: { + 200: json( + z.object({ + user: reportPersonSchema, + activePoints: z.number(), + hiddenWarnings: z.number(), + warnings: z.array(staffWarningSchema), + sanctions: z.array(sanctionSchema).nullable(), + permissions: z.object({ + warn: z.boolean(), + revoke: z.boolean(), + viewSanctions: z.boolean(), + manageSanctions: z.boolean(), + }), + }), + "The member's standing", + ), + 401: commonResponses[401], + 403: { description: "Not a moderator" }, + 404: json(errorSchema, "No such member"), + }, + }, + handler: async c => { + const moderator = await requireModerator(c); + const { id } = c.req.valid("param"); + const has = ( + permission: (typeof WARNING_PERMISSIONS)[keyof typeof WARNING_PERMISSIONS], + ) => hasWarningPermission(moderator.set, permission); + if (!has(WARNING_PERMISSIONS.view) && !canWarnAnywhere(c, moderator.set)) { + throw new HTTPException(403, { message: "Forbidden" }); + } + + const people = await loadReportPeople(c, [id]); + const user = people.get(id); + if (!user) return c.json({ error: "not_found" }, 404); + + const now = new Date(); + const viewSanctions = has(WARNING_PERMISSIONS.viewSanctions); + const [rows, sanctionRows, activePoints] = await Promise.all([ + loadUserWarnings(c, id), + viewSanctions ? loadUserSanctions(c, id) : null, + loadActivePoints(c.get("db"), id, now), + ]); + const warnings = await serializeStaffWarnings(c, moderator, rows, now); + + return c.json( + { + user, + activePoints, + hiddenWarnings: rows.length - warnings.length, + warnings, + sanctions: sanctionRows + ? await serializeSanctions(c, sanctionRows, now) + : null, + permissions: { + warn: has(WARNING_PERMISSIONS.warn) && id !== moderator.user.id, + revoke: has(WARNING_PERMISSIONS.revoke), + viewSanctions, + manageSanctions: has(WARNING_PERMISSIONS.manageSanctions), + }, + }, + 200, + ); + }, +}); diff --git a/packages/vitnode/src/api/modules/users/routes/session.route.ts b/packages/vitnode/src/api/modules/users/routes/session.route.ts index 7af52f1dac..0675de0413 100644 --- a/packages/vitnode/src/api/modules/users/routes/session.route.ts +++ b/packages/vitnode/src/api/modules/users/routes/session.route.ts @@ -41,6 +41,13 @@ export const sessionRoute = buildRoute({ notifications: zodNotificationState, }) .nullable(), + suspension: z + .object({ endsAt: z.date().nullable() }) + .nullable() + .optional() + .describe( + "Set while the session's account is suspended; `user` is then `null`", + ), }), }, }, @@ -50,7 +57,14 @@ export const sessionRoute = buildRoute({ }, handler: async c => { const user = c.get("user"); - if (!user) return c.json({ user: null }); + if (!user) { + const suspension = c.get("suspension"); + + return c.json({ + user: null, + suspension: suspension ? { endsAt: suspension.endsAt } : null, + }); + } const [{ isAdmin, isModerator }, notifications] = await Promise.all([ getStaffFlags(c, user), diff --git a/packages/vitnode/src/api/plugin.ts b/packages/vitnode/src/api/plugin.ts index c0809290d8..25b63dd698 100644 --- a/packages/vitnode/src/api/plugin.ts +++ b/packages/vitnode/src/api/plugin.ts @@ -54,6 +54,19 @@ export const newBuildPluginApiCore = buildApiPlugin({ moderator: { users: ["can_edit", "can_view_hidden_presence"], widgets: ["can_edit"], + warnings: [ + "can_view", + "can_warn", + "can_override_points", + "can_override_expiration", + { permission: "can_view_notes", dependsOn: ["can_view"] }, + { permission: "can_revoke", dependsOn: ["can_view"] }, + "can_view_sanctions", + { + permission: "can_manage_sanctions", + dependsOn: ["can_view_sanctions"], + }, + ], }, admin: { dashboard: [ @@ -117,6 +130,10 @@ export const newBuildPluginApiCore = buildApiPlugin({ { permission: "can_edit", dependsOn: ["can_view"] }, { permission: "can_delete", dependsOn: ["can_view"] }, ], + warning_settings: [ + "can_view", + { permission: "can_manage", dependsOn: ["can_view"] }, + ], staff_moderators: [ "can_view", { permission: "can_create", dependsOn: ["can_view"] }, diff --git a/packages/vitnode/src/content/server/public-permissions.ts b/packages/vitnode/src/content/server/public-permissions.ts index aba2d4efb0..71cf78c45d 100644 --- a/packages/vitnode/src/content/server/public-permissions.ts +++ b/packages/vitnode/src/content/server/public-permissions.ts @@ -9,6 +9,8 @@ import type { ContentId } from "../ids"; import type { AnyContentTypeDefinition } from "../types"; import type { ContentModel } from "./model"; +import { resolveStaffPermissions } from "../../api/lib/check-staff-permission"; +import { canWarnForContent } from "../../api/lib/moderation/warnings/access"; import { buildRoute } from "../../api/lib/route"; import { CONTENT_ACCESS_OPEN_FLAGS } from "../access/policy"; import { contentRecordAccessFlags } from "../access/server/decide"; @@ -57,6 +59,13 @@ export interface ContentViewerPermissions extends ContentAccessFlags { * to sign in. */ canReport: boolean; + /** + * A **Warn author** action makes sense: the type has `reports`, the + * visitor is a moderator with `can_warn` for the record's scope and did not + * write it. `POST /moderation/warnings` re-checks all of it, with the + * author resolved on the server. + */ + canWarn: boolean; } const parseIds = ( @@ -108,6 +117,25 @@ export const resolveContentViewerPermissions = async ( accessModel, rows.map(row => row.id as ContentId), ); + const moderatorSet = + user && definition.reports.enabled + ? await resolveStaffPermissions(c, { type: "moderator", user }) + : null; + const scopeColumn = model.moderationScope?.column; + const canWarnOn = (row: Record, ownerId: unknown) => { + if (!moderatorSet?.staff || typeof ownerId !== "number") return false; + if (ownerId === user?.id) return false; + const scope = scopeColumn ? row[scopeColumn] : null; + + return canWarnForContent(c, moderatorSet, { + pluginId, + scopeId: + typeof scope === "number" || typeof scope === "string" + ? String(scope) + : null, + targetType: definition.id, + }); + }; const decided = await Promise.all( rows.map(async row => { @@ -162,6 +190,7 @@ export const resolveContentViewerPermissions = async ( definition.reports.enabled && isPublic && (!isOwner || definition.reports.selfReport), + canWarn: definition.reports.enabled && canWarnOn(row, ownerId), id, }; }), @@ -200,7 +229,7 @@ export const contentPermissionsRoute = < route: { method: "get", path: "/permissions", - description: `What the current visitor may do with each listed ${name}: read, comment on, download from, edit, delete, hide or report it`, + description: `What the current visitor may do with each listed ${name}: read, comment on, download from, edit, delete, hide, report it or warn its author`, request: { query: z.object({ ids: z @@ -224,6 +253,7 @@ export const contentPermissionsRoute = < canRead: z.boolean(), canReport: z.boolean(), canView: z.boolean(), + canWarn: z.boolean(), id: schemas.params.shape.id, }), ), diff --git a/packages/vitnode/src/content/server/report-target.ts b/packages/vitnode/src/content/server/report-target.ts index 23064ad96c..d03cb0284f 100644 --- a/packages/vitnode/src/content/server/report-target.ts +++ b/packages/vitnode/src/content/server/report-target.ts @@ -7,6 +7,7 @@ import type { ModerationCaptureResult, ModerationReportTargetDefinition, ModerationTargetActionDefinition, + ModerationTargetAuthors, ModerationTargetState, } from "../../api/lib/moderation/targets"; import type { @@ -410,6 +411,35 @@ const currentContent = async ( }; }; +const contentAuthors = async ( + c: Context, + model: AnyContentModel, + targetId: string, +): Promise => { + const definition = contentDefinitionOf(model); + const itemId = parseContentId(definition.idStrategy, targetId); + if (itemId === null) return null; + + const db = c.get("db"); + const base = await readBaseRow(db, model, itemId); + if (!base) return null; + + const titleField = definition.reports.titleField; + let rawTitle: unknown = titleField ? base[titleField] : null; + if (titleField && rawTitle === undefined && definition.localization.enabled) { + const translation = await readTranslationRow(c, db, model, { + itemId, + locale: definition.localization.defaultLocale, + }); + rawTitle = translation?.[titleField] ?? null; + } + + return { + authorIds: await contentAuthorIds(c, model, itemId), + title: reportTitleOf(rawTitle), + }; +}; + export const visibilityAction = ( model: AnyContentModel, pluginId: string, @@ -485,6 +515,8 @@ export const contentReportTarget = ( return id === null ? null : contentIdKey(id); }, permissionModule: definition.permissionModule, + resolveAuthors: async (c, { targetId }) => + await contentAuthors(c, model, targetId), ...(scope && scopeColumn ? { resolveScope: async (c: Context, targetId: string) => { diff --git a/packages/vitnode/src/database/warnings.ts b/packages/vitnode/src/database/warnings.ts new file mode 100644 index 0000000000..6712101838 --- /dev/null +++ b/packages/vitnode/src/database/warnings.ts @@ -0,0 +1,218 @@ +import { sql } from "drizzle-orm"; +import { camelCase, index, unique, uniqueIndex } from "drizzle-orm/pg-core"; + +import type { + ModerationAuditType, + SanctionSource, + SanctionThresholdSnapshot, + SanctionType, + WarningContentSnapshot, + WarningReasonSnapshot, + WarningThresholdAction, +} from "@/lib/moderation/warnings"; + +import { + MODERATION_AUDIT_TYPES, + SANCTION_SOURCES, + SANCTION_TYPES, + WARNING_THRESHOLD_ACTIONS, +} from "@/lib/moderation/warnings"; + +import { core_moderation_reports } from "./moderation"; +import { core_users } from "./users"; + +/** Warning reasons. Their title, description and default message live in `core_languages_words`. */ +export const core_moderation_warning_reasons = camelCase.table.withRLS( + "core_moderation_warning_reasons", + t => ({ + id: t.serial().primaryKey(), + points: t.integer().notNull().default(0), + /** `null` means the points never expire. */ + expiresAfterHours: t.integer(), + enabled: t.boolean().notNull().default(true), + position: t.integer().notNull().default(0), + createdAt: t.timestamp().notNull().defaultNow(), + updatedAt: t + .timestamp() + .notNull() + .defaultNow() + .$onUpdate(() => new Date()), + }), + t => [index("core_moderation_warning_reasons_position_idx").on(t.position)], +); + +export const core_moderation_warning_thresholds = camelCase.table.withRLS( + "core_moderation_warning_thresholds", + t => ({ + id: t.serial().primaryKey(), + points: t.integer().notNull(), + action: t + .varchar({ enum: WARNING_THRESHOLD_ACTIONS, length: 32 }) + .$type() + .notNull(), + /** Length of a temporary suspension; `null` for an indefinite one. */ + durationHours: t.integer(), + enabled: t.boolean().notNull().default(true), + createdAt: t.timestamp().notNull().defaultNow(), + updatedAt: t + .timestamp() + .notNull() + .defaultNow() + .$onUpdate(() => new Date()), + }), + t => [ + // Two enabled rules at the same points would be ambiguous. + uniqueIndex("core_moderation_warning_thresholds_enabled_points_idx") + .on(t.points) + .where(sql`"enabled" = true`), + ], +); + +export const core_moderation_warnings = camelCase.table.withRLS( + "core_moderation_warnings", + t => ({ + id: t.serial().primaryKey(), + userId: t + .integer() + .notNull() + .references(() => core_users.id, { onDelete: "cascade" }), + issuedById: t.integer().references(() => core_users.id, { + onDelete: "set null", + }), + reasonId: t.integer().references(() => core_moderation_warning_reasons.id, { + onDelete: "set null", + }), + reasonSnapshot: t.jsonb().$type(), + points: t.integer().notNull(), + /** The reason's points when the warning was issued, for auditing overrides. */ + defaultPoints: t.integer(), + /** Staff-only explanation of a points or expiration override. */ + overrideNote: t.text(), + /** Shown to the warned member. */ + message: t.text().notNull(), + /** Shown to staff with `can_view_notes` only. */ + staffNote: t.text(), + issuedAt: t.timestamp().notNull().defaultNow(), + /** When the points stop counting; `null` never. */ + expiresAt: t.timestamp(), + /** Active points just before and after this warning, as the server computed them. */ + pointsBefore: t.integer().notNull().default(0), + pointsAfter: t.integer().notNull().default(0), + pluginId: t.varchar({ length: 100 }), + targetType: t.varchar({ length: 100 }), + targetId: t.varchar({ length: 64 }), + scopeFamily: t.varchar({ length: 100 }), + scopeId: t.varchar({ length: 64 }), + contentSnapshot: t.jsonb().$type(), + reportId: t.integer().references(() => core_moderation_reports.id, { + onDelete: "set null", + }), + idempotencyKey: t.varchar({ length: 64 }), + revokedAt: t.timestamp(), + revokedById: t.integer().references(() => core_users.id, { + onDelete: "set null", + }), + revokeReason: t.text(), + }), + t => [ + unique("core_moderation_warnings_idempotency_key").on( + t.issuedById, + t.idempotencyKey, + ), + index("core_moderation_warnings_user_idx").on(t.userId, t.issuedAt), + index("core_moderation_warnings_issued_at_id_idx").on(t.issuedAt, t.id), + index("core_moderation_warnings_issued_by_idx").on(t.issuedById), + index("core_moderation_warnings_reason_idx").on(t.reasonId), + index("core_moderation_warnings_target_idx").on( + t.pluginId, + t.targetType, + t.targetId, + ), + index("core_moderation_warnings_report_idx").on(t.reportId), + ], +); + +export const core_moderation_sanctions = camelCase.table.withRLS( + "core_moderation_sanctions", + t => ({ + id: t.serial().primaryKey(), + userId: t + .integer() + .notNull() + .references(() => core_users.id, { onDelete: "cascade" }), + type: t + .varchar({ enum: SANCTION_TYPES, length: 32 }) + .$type() + .notNull(), + source: t + .varchar({ enum: SANCTION_SOURCES, length: 32 }) + .$type() + .notNull(), + startsAt: t.timestamp().notNull().defaultNow(), + /** `null` for an indefinite sanction. */ + endsAt: t.timestamp(), + /** The warning whose threshold evaluation created it - at most one per warning. */ + warningId: t + .integer() + .unique() + .references(() => core_moderation_warnings.id, { onDelete: "set null" }), + thresholdId: t + .integer() + .references(() => core_moderation_warning_thresholds.id, { + onDelete: "set null", + }), + thresholdSnapshot: t.jsonb().$type(), + createdById: t.integer().references(() => core_users.id, { + onDelete: "set null", + }), + createdAt: t.timestamp().notNull().defaultNow(), + revokedAt: t.timestamp(), + revokedById: t.integer().references(() => core_users.id, { + onDelete: "set null", + }), + revokeReason: t.text(), + }), + t => [ + index("core_moderation_sanctions_user_idx").on( + t.userId, + t.revokedAt, + t.endsAt, + ), + index("core_moderation_sanctions_created_at_id_idx").on(t.createdAt, t.id), + ], +); + +/** + * The warning system's audit trail. Never holds staff notes or override + * explanations - those stay on the warning, behind `can_view_notes`. + */ +export const core_moderation_audit = camelCase.table.withRLS( + "core_moderation_audit", + t => ({ + id: t.serial().primaryKey(), + type: t + .varchar({ enum: MODERATION_AUDIT_TYPES, length: 40 }) + .$type() + .notNull(), + actorId: t.integer().references(() => core_users.id, { + onDelete: "set null", + }), + userId: t.integer().references(() => core_users.id, { + onDelete: "set null", + }), + warningId: t.integer().references(() => core_moderation_warnings.id, { + onDelete: "set null", + }), + sanctionId: t.integer().references(() => core_moderation_sanctions.id, { + onDelete: "set null", + }), + data: t.jsonb().$type>().notNull().default({}), + createdAt: t.timestamp().notNull().defaultNow(), + }), + t => [ + index("core_moderation_audit_user_idx").on(t.userId, t.createdAt), + index("core_moderation_audit_created_at_id_idx").on(t.createdAt, t.id), + index("core_moderation_audit_warning_idx").on(t.warningId), + index("core_moderation_audit_sanction_idx").on(t.sanctionId), + ], +); diff --git a/packages/vitnode/src/lib/moderation/const.ts b/packages/vitnode/src/lib/moderation/const.ts index 2dabe6a6ce..b8ed079406 100644 --- a/packages/vitnode/src/lib/moderation/const.ts +++ b/packages/vitnode/src/lib/moderation/const.ts @@ -5,6 +5,7 @@ export const MODERATION_PERMISSIONS = { deleteNotes: "can_delete_notes", viewHidden: "can_view_hidden", hide: "can_hide", + warn: "can_warn", } as const; export const MODERATION_DEFAULT_REPORT_REASONS = [ @@ -63,6 +64,7 @@ export const MODERATION_REPORT_EVENT_TYPES = [ "action", "resolved", "reopened", + "warning", ] as const; export type ModerationReportEventType = diff --git a/packages/vitnode/src/lib/moderation/warnings.ts b/packages/vitnode/src/lib/moderation/warnings.ts new file mode 100644 index 0000000000..83cafdf343 --- /dev/null +++ b/packages/vitnode/src/lib/moderation/warnings.ts @@ -0,0 +1,193 @@ +/** Moderator permissions of core's `warnings` module. */ +export const WARNING_PERMISSIONS = { + view: "can_view", + warn: "can_warn", + overridePoints: "can_override_points", + overrideExpiration: "can_override_expiration", + viewNotes: "can_view_notes", + revoke: "can_revoke", + viewSanctions: "can_view_sanctions", + manageSanctions: "can_manage_sanctions", +} as const; + +/** The core permission module holding {@link WARNING_PERMISSIONS}. */ +export const WARNING_PERMISSION_MODULE = "warnings"; + +export const WARNING_LIMITS = { + maxPoints: 100, + maxThresholdPoints: 1000, + maxReasons: 50, + maxThresholds: 20, + /** Five years, the longest expiration or suspension a form accepts. */ + maxDurationHours: 24 * 365 * 5, + messageMinLength: 3, + messageMaxLength: 2000, + noteMaxLength: 2000, + explanationMinLength: 3, + reasonTitleMaxLength: 100, + reasonDescriptionMaxLength: 255, + reasonMessageMaxLength: 2000, + pageSize: 25, +} as const; + +export const WARNING_STATUSES = ["active", "expired", "revoked"] as const; + +export type WarningStatus = (typeof WARNING_STATUSES)[number]; + +export const WARNING_THRESHOLD_ACTIONS = [ + "suspend_temporary", + "suspend_indefinite", +] as const; + +export type WarningThresholdAction = (typeof WARNING_THRESHOLD_ACTIONS)[number]; + +/** Kinds of account sanction. Only suspensions exist so far. */ +export const SANCTION_TYPES = ["suspension"] as const; + +export type SanctionType = (typeof SANCTION_TYPES)[number]; + +/** What created a sanction. */ +export const SANCTION_SOURCES = ["threshold", "manual"] as const; + +export type SanctionSource = (typeof SANCTION_SOURCES)[number]; + +export const MODERATION_AUDIT_TYPES = [ + "warning_issued", + "warning_overridden", + "warning_revoked", + "threshold_evaluated", + "sanction_applied", + "sanction_revoked", + "reason_created", + "reason_updated", + "threshold_created", + "threshold_updated", + "threshold_deleted", +] as const; + +export type ModerationAuditType = (typeof MODERATION_AUDIT_TYPES)[number]; + +/** A multi-language value, as stored in `core_languages_words`. */ +export type WarningReasonText = { languageCode: string; value: string }[]; + +/** What a warning remembers about its reason, so renaming the reason never rewrites history. */ +export interface WarningReasonSnapshot { + title: WarningReasonText; +} + +/** What a warning remembers about the content it was issued for. */ +export interface WarningContentSnapshot { + title: null | string; + url: null | string; +} + +/** What a threshold sanction remembers about the rule that created it. */ +export interface SanctionThresholdSnapshot { + action: WarningThresholdAction; + durationHours: null | number; + points: number; +} + +/** + * The status of a warning at `now`. Derived from timestamps on every read, so + * it is right whether or not a scheduled job has run. Pure. + */ +export const warningStatusAt = ( + warning: { expiresAt: Date | null; revokedAt: Date | null }, + now: Date, +): WarningStatus => { + if (warning.revokedAt) return "revoked"; + if (warning.expiresAt && warning.expiresAt.getTime() <= now.getTime()) { + return "expired"; + } + + return "active"; +}; + +/** + * The points a user holds at `now`: warnings that are not revoked and have + * not expired. Pure. + */ +export const activeWarningPoints = ( + warnings: readonly { + expiresAt: Date | null; + points: number; + revokedAt: Date | null; + }[], + now: Date, +): number => + warnings.reduce( + (total, warning) => + warningStatusAt(warning, now) === "active" + ? total + warning.points + : total, + 0, + ); + +export interface WarningThresholdRule { + action: WarningThresholdAction; + durationHours: null | number; + enabled: boolean; + id: number; + points: number; +} + +export interface ThresholdEvaluation { + /** The one rule to apply: the highest crossed one, or `null`. */ + applied: null | T; + /** Every enabled rule the change crossed, lowest first. */ + crossed: T[]; +} + +/** + * Which threshold rules a change of active points from `before` to `after` + * crosses - a rule at `points` is crossed when `before < points <= after` - + * and which one applies: only the highest, so one warning never creates two + * suspensions. A rule already at or below `before` was crossed earlier and is + * not applied again. Pure. + */ +export const evaluateWarningThresholds = ({ + after, + before, + rules, +}: { + after: number; + before: number; + rules: readonly T[]; +}): ThresholdEvaluation => { + const crossed = rules + .filter( + rule => rule.enabled && before < rule.points && rule.points <= after, + ) + .sort((a, b) => a.points - b.points || a.id - b.id); + + return { applied: crossed.at(-1) ?? null, crossed }; +}; + +/** When a suspension created by `rule` at `now` ends; `null` for indefinite. Pure. */ +export const thresholdSuspensionEnd = ( + rule: Pick, + now: Date, +): Date | null => + rule.action === "suspend_temporary" && rule.durationHours + ? new Date(now.getTime() + rule.durationHours * 60 * 60 * 1000) + : null; + +/** Whether a sanction applies at `now`: not revoked, started and not ended. Pure. */ +export const isSanctionActiveAt = ( + sanction: { endsAt: Date | null; revokedAt: Date | null; startsAt: Date }, + now: Date, +): boolean => + !sanction.revokedAt && + sanction.startsAt.getTime() <= now.getTime() && + (sanction.endsAt === null || sanction.endsAt.getTime() > now.getTime()); + +/** Picks the reason text in `locale`, else English, else the first one. Pure. */ +export const warningReasonText = ( + values: null | undefined | WarningReasonText, + locale: string, +): string => + values?.find(item => item.languageCode === locale)?.value ?? + values?.find(item => item.languageCode === "en")?.value ?? + values?.[0]?.value ?? + ""; diff --git a/packages/vitnode/src/locales/api/en.json b/packages/vitnode/src/locales/api/en.json index 24a6aeaaff..3709f8f4d7 100644 --- a/packages/vitnode/src/locales/api/en.json +++ b/packages/vitnode/src/locales/api/en.json @@ -55,6 +55,31 @@ "admin_message": { "label": "Messages from administrators", "description": "Announcements an administrator sends you directly." + }, + "warning_issued": { + "label": "Warnings from moderators", + "description": "When a moderator warns you. Always on, so you never miss one.", + "title": "You received a warning ({points, plural, one {# point} other {# points}})", + "title_reason": "You received a warning for {reason} ({points, plural, one {# point} other {# points}})" + }, + "warning_revoked": { + "label": "Revoked warnings", + "description": "When a moderator takes back a warning you received.", + "title": "A warning was revoked", + "body": "Its {points, plural, one {# point no longer counts} other {# points no longer count}} against your account." + }, + "account_suspended": { + "label": "Account suspensions", + "description": "When your account is suspended after reaching a warning limit.", + "title": "Your account is suspended", + "body_until": "You can use your account again after {endsAt} (UTC).", + "body_indefinite": "Your account is suspended until a moderator lifts it." + }, + "suspension_revoked": { + "label": "Lifted suspensions", + "description": "When a moderator lifts a suspension of your account.", + "title": "A suspension was lifted", + "body": "A moderator lifted a suspension of your account. Check your account status to see if anything else still applies." } }, "email": { diff --git a/packages/vitnode/src/locales/en.json b/packages/vitnode/src/locales/en.json index abd5d7aeed..c583e589f0 100644 --- a/packages/vitnode/src/locales/en.json +++ b/packages/vitnode/src/locales/en.json @@ -881,7 +881,9 @@ "removed": "Cover image removed." } }, - "presence": "Online status" + "presence": "Online status", + "warn": "Warn", + "view_warnings": "Warnings ({points, plural, one {# point} other {# points}})" }, "files": { "title": "My Files", @@ -1167,7 +1169,8 @@ "security": "Security", "sso": "Connected accounts", "notifications": "Notifications", - "ai": "AI usage" + "ai": "AI usage", + "standing": "Account status" }, "overview": { "desc": "Your avatar and the details on your account.", @@ -1746,6 +1749,7 @@ "by_author": "Comment deleted by user", "by_moderator": "Comment removed by a moderator" }, + "warn": "Warn author", "live": { "new_comments": "New comments were posted.", "show_new": "Show new comments", @@ -2901,12 +2905,223 @@ "resolved": "closed it as {resolution}", "reopened": "reopened it", "note_edited": "edited a note", - "note_deleted": "deleted a note" + "note_deleted": "deleted a note", + "warning": "warned ({points, plural, one {# point} other {# points}})" }, "note_required": "Write something before adding the note.", "before": "Before", "after": "After" } + }, + "warnings": { + "title": "Warnings" + } + }, + "warn": { + "button": "Warn author", + "button_member": "Warn member", + "title": "Warn {name}", + "title_loading": "Warn member", + "desc": "The member gets your message in a notification. Points count across the whole site.", + "no_reasons": "No warning reasons are set up yet - an administrator can add them in the AdminCP.", + "cancel": "Cancel", + "submit": "Issue warning", + "points": "{count, plural, one {# point} other {# points}}", + "custom_reason": "Custom warning", + "deleted_user": "Deleted account", + "global": { + "title": "Points count everywhere", + "desc": "This warning is about content in one place, but its points count toward account-wide limits that can suspend the whole account." + }, + "consequences": { + "title": "What happens", + "current": "Active points now", + "warning": "This warning", + "projected": "Active points after", + "action": "Automatic action:", + "no_action": "None.", + "already_suspended": "This account is already suspended.", + "related": "{count, plural, one {# warning was} other {# warnings were}} already issued for this content.", + "advisory": "A preview. The server checks again when you send it, in case someone else warned this member in the meantime." + }, + "sanction": { + "indefinite": "Account suspended indefinitely.", + "for": "Account suspended for", + "until": "(until {endsAt}).", + "suspended_until": "Account suspended until {endsAt}." + }, + "form": { + "reason": "Reason", + "no_reason": "No reason (custom warning)", + "points": "Points", + "points_desc": "Starts at the reason's points. Changing them needs an explanation.", + "expiration": "Points expire", + "expiration_modes": { + "default": "As the reason says", + "never": "Never", + "hours": "After a number of hours", + "at": "On a date" + }, + "expiration_hours": "Expire after", + "hours_unit": "hours", + "expiration_at": "Expire on", + "override_note": "Why the change?", + "override_note_desc": "Staff only. Required when you change the points or the expiry.", + "override_note_required": "Explain why you changed the points or the expiry.", + "expiration_at_required": "Pick when the points expire.", + "message": "Message to the member", + "message_desc": "The member reads this in the notification and on their account status page.", + "staff_note": "Staff note", + "staff_note_desc": "Only staff who can read notes see this. The member never does.", + "resolve_report": "Also close the report as action taken", + "resolve_report_desc": "Closes the report in the same step. If either one fails, neither happens." + }, + "done": { + "title": "Warning issued", + "points": "{name} now has {after, plural, one {# active point} other {# active points}}.", + "suspended_until": "Their account is suspended until {endsAt}.", + "suspended_indefinitely": "Their account is suspended indefinitely.", + "report_resolved": "The report is closed.", + "differs": "That's different from the preview - their points changed in the meantime." + }, + "errors": { + "title": "Could not issue the warning", + "unknown": "Something went wrong. Try again in a moment.", + "unauthenticated": "Your session has ended. Sign in again.", + "validation": "Check the fields and try again.", + "forbidden": "You can't warn this member here.", + "not_found": "That no longer exists.", + "already_revoked": "It was already revoked or lifted.", + "author_mismatch": "This member didn't write that content.", + "content_not_found": "That content is gone, so the warning can't point at it.", + "expiration_override_forbidden": "You can't change when points expire.", + "idempotency_conflict": "Reopen the form and try again.", + "invalid_expiration": "Pick an expiry in the future, at most five years away.", + "invalid_points": "Points must be a whole number from 0 to 100.", + "override_note_required": "Explain why you changed the points or the expiry.", + "points_override_forbidden": "You can't change the points.", + "protected": "Staff accounts can't be warned here.", + "reason_disabled": "That reason was just disabled. Pick another one.", + "reason_not_found": "That reason was removed. Pick another one.", + "report_closed": "The report is already closed. Warn without closing it.", + "report_mismatch": "The report is about different content.", + "self": "You can't warn yourself.", + "user_not_found": "That member no longer exists." + }, + "duration": { + "days": "{count, plural, one {# day} other {# days}}", + "hours": "{count, plural, one {# hour} other {# hours}}" + }, + "statuses": { + "active": "Active", + "expired": "Expired", + "revoked": "Revoked" + }, + "revoke": { + "button": "Revoke", + "title": "Revoke this warning?", + "desc": "Its points stop counting right away. The warning stays in the history, and a suspension it led to stays until you lift it separately.", + "reason": "Reason", + "reason_desc": "Kept with the warning for staff.", + "done": "Warning revoked", + "done_desc": "The member was notified." + }, + "list": { + "title": "Warnings", + "desc": "Every warning you can see, newest first.", + "status_label": "Filter by status", + "all": "All", + "reason_filter": "Reason", + "all_reasons": "All reasons", + "empty_title": "No warnings here", + "empty_desc": "Nothing matches these filters. Peaceful.", + "member": "Member", + "issued_by": "Issued by", + "expires": "Points expire", + "never": "Never", + "default_points": "Reason's points", + "content": "Content", + "untitled": "Untitled", + "report": "Report", + "report_link": "Report #{id}", + "message": "Message to the member", + "staff_only": "Staff only", + "staff_note": "Staff note", + "override_note": "Why the points or expiry changed", + "revoked": "Revoked by .", + "lifted": "Lifted by .", + "sanction_stays": "The suspension this warning led to was not lifted with it." + }, + "lookup": { + "title": "Find a member", + "label": "Member name", + "placeholder": "Search by name", + "results": "{count, plural, =0 {No members found} one {# member found} other {# members found}}", + "empty": "No members match that name.", + "forbidden": "You can't look up members." + }, + "member": { + "title": "{name}'s standing", + "breadcrumb": "Member", + "desc": "Active points, warnings and suspensions.", + "active_points": "Active points", + "status": "Status", + "suspended": "Suspended", + "in_good_standing": "In good standing", + "sanctions": "Suspensions", + "no_sanctions": "No suspensions.", + "warnings": "Warnings", + "no_warnings": "No warnings you can see.", + "hidden_warnings": "{count, plural, one {# more warning is} other {# more warnings are}} outside your moderation scope." + }, + "sanctions": { + "temporary": "Temporary suspension", + "indefinite": "Indefinite suspension", + "active": "Active", + "lifted": "Lifted", + "ended": "Ended", + "lift": "Lift", + "lift_title": "Lift this suspension?", + "lift_desc": "The account works again unless another suspension still applies.", + "lifted_done": "Suspension lifted", + "lifted_done_desc": "Any other active suspension still applies.", + "ends": "Ends", + "source": "Source", + "from_threshold": "Reached {points, plural, one {# point} other {# points}}", + "manual": "Applied by staff" + } + }, + "account_status": { + "title": "Account status", + "desc": "Your warnings and whether anything restricts your account.", + "loading": "Loading your account status", + "signed_out": "Sign in to see your account status.", + "sign_in": "Sign in", + "suspended_title": "Your account is suspended", + "suspended_until": "You can use your account again after {endsAt}.", + "suspended_indefinite": "Your account stays suspended until a moderator lifts the suspension.", + "suspended_restrictions": "Until then you can browse like a visitor, but you can't post, comment or change anything on your account.", + "contact": "Think this is a mistake? Contact the site's administrators.", + "good_title": "Your account is in good standing", + "good_desc": "Nothing restricts your account.", + "points_title": "Active points", + "points_desc": "Points from warnings that haven't expired or been revoked. Reaching a limit can suspend your account.", + "warnings_title": "Your warnings", + "no_warnings": "You haven't received any warnings. Keep it up!", + "expires": "Points expire:", + "never": "Never", + "content": "About:", + "untitled": "Untitled", + "sanctions_title": "Suspensions", + "sanction_temporary": "Temporary suspension", + "sanction_indefinite": "Indefinite suspension", + "sanction_active": "Active", + "sanction_lifted": "Lifted", + "sanction_ended": "Ended", + "notice": { + "until": "Your account is suspended until {endsAt}.", + "indefinite": "Your account is suspended.", + "details": "See details" } } }, @@ -3445,7 +3660,9 @@ }, "moderation": { "title": "Moderation", - "report_reasons": "Report reasons" + "report_reasons": "Report reasons", + "warning_reasons": "Warning reasons", + "warning_thresholds": "Warning thresholds" }, "user_bar": { "home_page": "Home Page", @@ -4201,6 +4418,116 @@ "last_reason": "Keep at least one reason - members need something to pick.", "unknown": "Something went wrong. Try again in a moment." } + }, + "warning_reasons": { + "title": "Warning reasons", + "desc": "The reasons moderators pick when they warn someone, with their default points and expiry. Drag to change the order.", + "create": "Add reason", + "limit": "You can have up to {max} reasons. Disable one you no longer use.", + "columns": { + "reason": "Reason", + "points": "Points", + "expires": "Points expire", + "status": "Status" + }, + "never": "Never", + "enabled": "Enabled", + "disabled": "Disabled", + "edit": "Edit", + "empty_title": "No warning reasons yet", + "empty_desc": "Add one so moderators can warn with consistent points.", + "reorder": { + "success": "Order saved", + "success_desc": "Moderators see the reasons in the new order." + }, + "form": { + "create_title": "Add warning reason", + "edit_title": "Edit warning reason", + "sheet_desc": "Changes apply to new warnings only. Warnings already issued keep their points, expiry and title.", + "title": "Title", + "title_desc": "Short and specific, like “Spam” or “Harassment”.", + "description": "Description", + "description_desc": "Helps moderators pick the right reason. Members don't see it.", + "points": "Points", + "points_desc": "How many points a warning adds. 0 makes it an official warning that adds none.", + "points_unit": "points", + "expires": "Points expire after", + "expires_desc": "After this, the points stop counting. The warning stays in the history.", + "or": "or", + "never_expires": "Never expire", + "days_unit": "days", + "message": "Default message", + "message_desc": "Prefilled in the warn form. Moderators can edit it before sending.", + "enabled": "Enabled", + "enabled_desc": "Disabled reasons can't be picked for new warnings. Old warnings stay as they are.", + "submit_create": "Add reason", + "submit_edit": "Save changes", + "created": "Reason added", + "created_desc": "Moderators can pick “{reason}” when they warn someone.", + "updated": "Reason saved", + "updated_desc": "New warnings use the new settings.", + "error": "Could not save the reason", + "error_desc": "Check the fields and try again.", + "limit_reached": "You can have up to {max} reasons." + } + }, + "warning_thresholds": { + "title": "Warning thresholds", + "desc": "What happens automatically when a warning brings a member's active points up to a limit.", + "create": "Add threshold", + "limit": "You can have up to {max} thresholds.", + "columns": { + "points": "Active points", + "action": "Action", + "status": "Status" + }, + "at_points": "At {points, plural, one {# point} other {# points}}", + "suspend_for": "Suspend for", + "suspend_indefinitely": "Suspend indefinitely", + "enabled": "Enabled", + "disabled": "Disabled", + "edit": "Edit", + "actions_label": "Actions for the threshold at {points} points", + "empty_title": "No thresholds yet", + "empty_desc": "Without one, warnings never suspend anyone automatically.", + "delete": { + "button": "Delete", + "title": "Delete the threshold at {points} points?", + "desc": "New warnings won't trigger it anymore. Suspensions it already created stay until they end or are lifted.", + "error": "Could not delete the threshold", + "success": "Threshold deleted", + "success_desc": "Reaching {points} points no longer suspends anyone." + }, + "form": { + "create_title": "Add threshold", + "edit_title": "Edit threshold", + "sheet_desc": "When a warning brings active points from below this number to it or above, the action runs once.", + "future_only": "Changes apply to warnings issued from now on. Nobody is suspended or unsuspended just because you saved this.", + "points": "Active points", + "points_desc": "Only one enabled threshold can use each number.", + "points_unit": "points", + "action": "Action", + "actions": { + "suspend_temporary": "Temporary suspension", + "suspend_temporary_desc": "Ends on its own after the duration, even if the points are still there.", + "suspend_indefinite": "Indefinite suspension", + "suspend_indefinite_desc": "Lasts until a moderator lifts it." + }, + "duration": "Duration", + "duration_desc": "How long the suspension lasts. 24 is a day, 168 a week.", + "hours_unit": "hours", + "enabled": "Enabled", + "enabled_desc": "Disabled thresholds never run.", + "submit_create": "Add threshold", + "submit_edit": "Save changes", + "created": "Threshold added", + "updated": "Threshold saved", + "saved_desc": "It applies to the next warnings.", + "error": "Could not save the threshold", + "error_desc": "Check the fields and try again.", + "duplicate_points": "Another enabled threshold already uses this number of points.", + "limit_reached": "You can have up to {max} thresholds." + } } }, "system": { @@ -5257,5 +5584,18 @@ "description": "Rewrites the selected text: shorter, corrected, simpler or in another tone." } } - } + }, + "@vitnode/core:warnings": "Warnings", + "@vitnode/core:warnings:can_view": "View every member's warnings", + "@vitnode/core:warnings:can_warn": "Warn members directly, without content", + "@vitnode/core:warnings:can_override_points": "Change a warning's points", + "@vitnode/core:warnings:can_override_expiration": "Change when a warning's points expire", + "@vitnode/core:warnings:can_view_notes": "Read staff notes on warnings", + "@vitnode/core:warnings:can_revoke": "Revoke warnings", + "@vitnode/core:warnings:can_view_sanctions": "View suspensions", + "@vitnode/core:warnings:can_manage_sanctions": "Lift suspensions", + "@vitnode/core:warning_settings": "Warning settings", + "@vitnode/core:warning_settings:can_view": "View warning reasons and thresholds", + "@vitnode/core:warning_settings:can_manage": "Manage warning reasons and thresholds", + "@vitnode/core:moderation:can_warn": "Warn authors of this content" } diff --git a/packages/vitnode/src/pages/account-status.tsx b/packages/vitnode/src/pages/account-status.tsx new file mode 100644 index 0000000000..530c8790b1 --- /dev/null +++ b/packages/vitnode/src/pages/account-status.tsx @@ -0,0 +1,22 @@ +import type { AccountStatusRouteData } from "../tanstack/account-status/route"; + +import { loadAccountStatusRoute } from "../tanstack/account-status/route"; +import { + AccountStatusBreadcrumb, + AccountStatusRouteContent, +} from "../tanstack/account-status/screen"; +import { defineRoute } from "../tanstack/plugin-routes"; + +const AccountStatusPage = () => ; + +// Open to every visitor on purpose: a suspended account is a guest to every +// other route, and this is where it learns why. +export const route = defineRoute({ + load: async ({ context, t }) => + await loadAccountStatusRoute({ ...context, t }), + head: ({ loaderData }) => ({ robots: "noindex, nofollow", ...loaderData }), + + breadcrumb: AccountStatusBreadcrumb, +}); + +export default AccountStatusPage; diff --git a/packages/vitnode/src/pages/admin/moderation/warning-reasons.tsx b/packages/vitnode/src/pages/admin/moderation/warning-reasons.tsx new file mode 100644 index 0000000000..c3727a6d96 --- /dev/null +++ b/packages/vitnode/src/pages/admin/moderation/warning-reasons.tsx @@ -0,0 +1,25 @@ +import type { PluginRoutePageProps } from "@/routing"; +import type { AdminWarningsRouteData } from "@/tanstack/admin/warnings/route"; + +import { adminBreadcrumb } from "@/tanstack/admin/breadcrumb"; +import { loadAdminWarningReasonsRoute } from "@/tanstack/admin/warnings/route"; +import { AdminWarningReasonsRouteContent } from "@/tanstack/admin/warnings/screen"; +import { defineAdminRoute } from "@/tanstack/plugin-routes"; + +const AdminWarningReasonsPage = ({ + loaderData, +}: PluginRoutePageProps) => ( + +); + +export const route = defineAdminRoute({ + load: async ({ context, t }) => + await loadAdminWarningReasonsRoute({ ...context, t }), + head: ({ loaderData }) => ({ ...loaderData }), + + breadcrumb: adminBreadcrumb({ + segments: ["core", "moderation", "warning-reasons"], + }), +}); + +export default AdminWarningReasonsPage; diff --git a/packages/vitnode/src/pages/admin/moderation/warning-thresholds.tsx b/packages/vitnode/src/pages/admin/moderation/warning-thresholds.tsx new file mode 100644 index 0000000000..eaeaa132a0 --- /dev/null +++ b/packages/vitnode/src/pages/admin/moderation/warning-thresholds.tsx @@ -0,0 +1,25 @@ +import type { PluginRoutePageProps } from "@/routing"; +import type { AdminWarningsRouteData } from "@/tanstack/admin/warnings/route"; + +import { adminBreadcrumb } from "@/tanstack/admin/breadcrumb"; +import { loadAdminWarningThresholdsRoute } from "@/tanstack/admin/warnings/route"; +import { AdminWarningThresholdsRouteContent } from "@/tanstack/admin/warnings/screen"; +import { defineAdminRoute } from "@/tanstack/plugin-routes"; + +const AdminWarningThresholdsPage = ({ + loaderData, +}: PluginRoutePageProps) => ( + +); + +export const route = defineAdminRoute({ + load: async ({ context, t }) => + await loadAdminWarningThresholdsRoute({ ...context, t }), + head: ({ loaderData }) => ({ ...loaderData }), + + breadcrumb: adminBreadcrumb({ + segments: ["core", "moderation", "warning-thresholds"], + }), +}); + +export default AdminWarningThresholdsPage; diff --git a/packages/vitnode/src/pages/modcp/member.tsx b/packages/vitnode/src/pages/modcp/member.tsx new file mode 100644 index 0000000000..5672c9d143 --- /dev/null +++ b/packages/vitnode/src/pages/modcp/member.tsx @@ -0,0 +1,24 @@ +import type { PluginRoutePageProps } from "@/routing"; + +import type { ModcpMemberRouteData } from "../../tanstack/moderation/warnings-route"; + +import { ModcpMemberBreadcrumb } from "../../tanstack/moderation/breadcrumb"; +import { loadModcpMemberRoute } from "../../tanstack/moderation/warnings-route"; +import { ModcpMemberRouteContent } from "../../tanstack/moderation/warnings-screen"; +import { defineAuthenticatedRoute } from "../../tanstack/plugin-routes"; + +const ModcpMemberPage = ({ + loaderData, +}: PluginRoutePageProps) => ( + +); + +export const route = defineAuthenticatedRoute({ + load: async ({ context, params, t }) => + await loadModcpMemberRoute({ ...context, id: params.id ?? "", t }), + head: ({ loaderData }) => ({ robots: "noindex, nofollow", ...loaderData }), + + breadcrumb: ModcpMemberBreadcrumb, +}); + +export default ModcpMemberPage; diff --git a/packages/vitnode/src/pages/modcp/warnings.tsx b/packages/vitnode/src/pages/modcp/warnings.tsx new file mode 100644 index 0000000000..ec13779a49 --- /dev/null +++ b/packages/vitnode/src/pages/modcp/warnings.tsx @@ -0,0 +1,34 @@ +import type { PluginRoutePageProps } from "@/routing"; + +import type { ModcpRouteData } from "../../tanstack/moderation/route"; +import type { ModcpWarningsRouteSearch } from "../../tanstack/moderation/warnings-route"; + +import { ModcpWarningsBreadcrumb } from "../../tanstack/moderation/breadcrumb"; +import { loadModcpWarningsRoute } from "../../tanstack/moderation/warnings-route"; +import { ModcpWarningsRouteContent } from "../../tanstack/moderation/warnings-screen"; +import { defineAuthenticatedRoute } from "../../tanstack/plugin-routes"; + +const ModcpWarningsPage = ({ + loaderData, + navigate, + search, +}: PluginRoutePageProps) => ( + +); + +export const route = defineAuthenticatedRoute< + ModcpRouteData, + ModcpWarningsRouteSearch +>({ + load: async ({ context, search, t }) => + await loadModcpWarningsRoute({ ...context, search, t }), + head: ({ loaderData }) => ({ robots: "noindex, nofollow", ...loaderData }), + + breadcrumb: ModcpWarningsBreadcrumb, +}); + +export default ModcpWarningsPage; diff --git a/packages/vitnode/src/routes.tsx b/packages/vitnode/src/routes.tsx index 9c56c1f056..f0f877036b 100644 --- a/packages/vitnode/src/routes.tsx +++ b/packages/vitnode/src/routes.tsx @@ -1,4 +1,5 @@ import { defineRoutes, index, layout, lazy, page } from "./routing"; +import { ACCOUNT_STATUS_NAMESPACES } from "./tanstack/account-status/route"; import { ADMIN_AI_NAMESPACES } from "./tanstack/admin/ai/route"; import { normalizeAiHistoryRouteSearch, @@ -41,6 +42,7 @@ import { normalizeStaffRouteSearch } from "./tanstack/admin/staff/route-search"; import { ADMIN_USER_NAMESPACES } from "./tanstack/admin/users/detail-route"; import { ADMIN_USERS_NAMESPACES } from "./tanstack/admin/users/route"; import { normalizeUsersRouteSearch } from "./tanstack/admin/users/route-search"; +import { ADMIN_WARNINGS_NAMESPACES } from "./tanstack/admin/warnings/route"; import { LOGIN_NAMESPACES } from "./tanstack/auth/login-route"; import { LoginPendingSkeleton, @@ -65,6 +67,7 @@ import { normalizeMyFilesRouteSearch } from "./tanstack/files/route-search"; import { normalizeModcpApprovalsSearch } from "./tanstack/moderation/approvals-route-search"; import { MODCP_NAMESPACES } from "./tanstack/moderation/namespaces"; import { normalizeModcpReportsSearch } from "./tanstack/moderation/route-search"; +import { normalizeModcpWarningsSearch } from "./tanstack/moderation/warnings-route"; import { NOTIFICATIONS_NAMESPACES } from "./tanstack/notifications/namespaces"; import { normalizeNotificationsRouteSearch } from "./tanstack/notifications/route-search"; import { @@ -237,9 +240,26 @@ export const routes = defineRoutes([ component: lazy(() => import("./pages/modcp/approval")), pendingComponent: () => , }), + + page("warnings", { + component: lazy(() => import("./pages/modcp/warnings")), + pendingComponent: () => , + search: normalizeModcpWarningsSearch, + }), + + page("members/:id", { + component: lazy(() => import("./pages/modcp/member")), + pendingComponent: () => , + }), ], }), + page("/account-status", { + component: lazy(() => import("./pages/account-status")), + messages: ACCOUNT_STATUS_NAMESPACES, + pendingComponent: () => , + }), + page("/notifications", { component: lazy(() => import("./pages/notifications")), messages: NOTIFICATIONS_NAMESPACES, @@ -371,6 +391,22 @@ export const routes = defineRoutes([ pendingComponent: TablePendingSkeleton, }), + page("/admin/core/moderation/warning-reasons", { + area: "admin", + component: lazy(() => import("./pages/admin/moderation/warning-reasons")), + messages: ADMIN_WARNINGS_NAMESPACES, + pendingComponent: TablePendingSkeleton, + }), + + page("/admin/core/moderation/warning-thresholds", { + area: "admin", + component: lazy( + () => import("./pages/admin/moderation/warning-thresholds"), + ), + messages: ADMIN_WARNINGS_NAMESPACES, + pendingComponent: TablePendingSkeleton, + }), + page("/admin/core/users", { area: "admin", component: lazy(() => import("./pages/admin/users/index")), diff --git a/packages/vitnode/src/tanstack/account-status/route.ts b/packages/vitnode/src/tanstack/account-status/route.ts new file mode 100644 index 0000000000..d964163bff --- /dev/null +++ b/packages/vitnode/src/tanstack/account-status/route.ts @@ -0,0 +1,33 @@ +import type { QueryClient } from "@tanstack/react-query"; + +import type { PluginRouteTranslator } from "@/routing"; + +import { accountStatusQueryOptions } from "@/views/moderation/warnings/warnings-api"; + +export const ACCOUNT_STATUS_NAMESPACES = [ + "core.global", + "core.moderation", +] as const; + +export interface AccountStatusRouteData { + description: string; + title: string; +} + +export const loadAccountStatusRoute = async ({ + queryClient, + t, +}: { + queryClient: QueryClient; + t: PluginRouteTranslator; +}): Promise => { + await queryClient.query({ + ...accountStatusQueryOptions(), + staleTime: "static", + }); + + return { + description: t("core.moderation.account_status.desc"), + title: t("core.moderation.account_status.title"), + }; +}; diff --git a/packages/vitnode/src/tanstack/account-status/screen.tsx b/packages/vitnode/src/tanstack/account-status/screen.tsx new file mode 100644 index 0000000000..2700b3e7ef --- /dev/null +++ b/packages/vitnode/src/tanstack/account-status/screen.tsx @@ -0,0 +1,18 @@ +import { useTranslations } from "use-intl"; + +import { AccountStatusContent } from "@/views/moderation/warnings/account-status-content"; + +import { RouteMessages } from "../i18n/route-messages"; +import { ACCOUNT_STATUS_NAMESPACES } from "./route"; + +export const AccountStatusBreadcrumb = () => { + const t = useTranslations("core.moderation.account_status"); + + return <>{t("title")}; +}; + +export const AccountStatusRouteContent = () => ( + + + +); diff --git a/packages/vitnode/src/tanstack/admin/warnings/route.ts b/packages/vitnode/src/tanstack/admin/warnings/route.ts new file mode 100644 index 0000000000..eb2db6188b --- /dev/null +++ b/packages/vitnode/src/tanstack/admin/warnings/route.ts @@ -0,0 +1,62 @@ +import type { PluginRouteTranslator } from "@/routing"; + +import { + adminWarningReasonsQueryOptions, + adminWarningThresholdsQueryOptions, +} from "@/views/admin/views/core/moderation/warnings/warnings-admin-api"; +import { ADMIN_WARNING_PERMISSIONS } from "@/views/admin/views/core/shared/admin-permissions"; + +import type { AdminScreenContext } from "../screen"; + +import { adminIdentityOf } from "../identity"; +import { requireAdminPermission } from "../screen"; + +export const ADMIN_WARNINGS_NAMESPACES = [ + "admin.moderation.warning_reasons", + "admin.moderation.warning_thresholds", + "core.global", + "core.moderation", +] as const; + +export interface AdminWarningsRouteData { + description: string; + title: string; +} + +export const loadAdminWarningReasonsRoute = async ({ + adminAccess, + queryClient, + t, +}: AdminScreenContext & { + t: PluginRouteTranslator; +}): Promise => { + requireAdminPermission(adminAccess, ADMIN_WARNING_PERMISSIONS.view); + await queryClient.query({ + ...adminWarningReasonsQueryOptions(adminIdentityOf(adminAccess)), + staleTime: "static", + }); + + return { + description: t("admin.moderation.warning_reasons.desc"), + title: t("admin.moderation.warning_reasons.title"), + }; +}; + +export const loadAdminWarningThresholdsRoute = async ({ + adminAccess, + queryClient, + t, +}: AdminScreenContext & { + t: PluginRouteTranslator; +}): Promise => { + requireAdminPermission(adminAccess, ADMIN_WARNING_PERMISSIONS.view); + await queryClient.query({ + ...adminWarningThresholdsQueryOptions(adminIdentityOf(adminAccess)), + staleTime: "static", + }); + + return { + description: t("admin.moderation.warning_thresholds.desc"), + title: t("admin.moderation.warning_thresholds.title"), + }; +}; diff --git a/packages/vitnode/src/tanstack/admin/warnings/screen.tsx b/packages/vitnode/src/tanstack/admin/warnings/screen.tsx new file mode 100644 index 0000000000..82907f0ac0 --- /dev/null +++ b/packages/vitnode/src/tanstack/admin/warnings/screen.tsx @@ -0,0 +1,188 @@ +import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; +import React from "react"; +import { toast } from "sonner"; +import { useTranslations } from "use-intl"; + +import type { WarningReasonSheetState } from "@/views/admin/views/core/moderation/warnings/warning-reasons-content"; +import type { WarningThresholdSheetState } from "@/views/admin/views/core/moderation/warnings/warning-thresholds-content"; + +import { + AdminStaffPermissionGate, + useAdminStaffPermission, +} from "@/components/staff-permission/provider"; +import { PageTitle } from "@/components/ui/page-title"; +import { + CreateWarningReasonButton, + WarningReasonsContent, +} from "@/views/admin/views/core/moderation/warnings/warning-reasons-content"; +import { + CreateWarningThresholdButton, + WarningThresholdsContent, +} from "@/views/admin/views/core/moderation/warnings/warning-thresholds-content"; +import { + adminWarningReasonsQueryOptions, + adminWarningReasonsQueryRoot, + adminWarningThresholdsQueryOptions, + adminWarningThresholdsQueryRoot, + deleteAdminWarningThreshold, + reorderAdminWarningReasons, + saveAdminWarningReason, + saveAdminWarningThreshold, +} from "@/views/admin/views/core/moderation/warnings/warnings-admin-api"; +import { isAdminMutationError } from "@/views/admin/views/core/shared/admin-mutation"; +import { ADMIN_WARNING_PERMISSIONS } from "@/views/admin/views/core/shared/admin-permissions"; +import { WARNINGS_QUERY_ROOT } from "@/views/moderation/warnings/warnings-api"; + +import type { AdminWarningsRouteData } from "./route"; + +import { RouteMessages } from "../../i18n/route-messages"; +import { useAdminIdentity } from "../identity"; +import { ADMIN_WARNINGS_NAMESPACES } from "./route"; + +const WarningReasonsScreen = ({ + description, + title, +}: AdminWarningsRouteData) => { + const t = useTranslations("admin.moderation.warning_reasons"); + const adminUserId = useAdminIdentity(); + const queryClient = useQueryClient(); + const { data } = useSuspenseQuery( + adminWarningReasonsQueryOptions(adminUserId), + ); + const canEdit = useAdminStaffPermission(ADMIN_WARNING_PERMISSIONS.manage); + const [sheet, setSheet] = React.useState({ + mode: "create", + }); + const [sheetOpen, setSheetOpen] = React.useState(false); + const refresh = async () => { + await Promise.all([ + queryClient.invalidateQueries({ + queryKey: adminWarningReasonsQueryRoot(adminUserId), + }), + queryClient.invalidateQueries({ queryKey: WARNINGS_QUERY_ROOT }), + ]); + }; + + return ( +
+ + + { + setSheet({ mode: "create" }); + setSheetOpen(true); + }} + /> + + + + { + setSheet(state); + setSheetOpen(true); + }} + onReorder={async ids => { + const result = await reorderAdminWarningReasons(ids); + if (isAdminMutationError(result)) { + throw new Error( + `Saving the order answered ${result.error.status}.`, + ); + } + await refresh(); + toast.success(t("reorder.success"), { + description: t("reorder.success_desc"), + }); + }} + onSave={async args => { + const result = await saveAdminWarningReason(args); + if (!isAdminMutationError(result)) await refresh(); + + return result; + }} + onSheetOpenChange={setSheetOpen} + reasons={data} + sheet={sheet} + sheetOpen={sheetOpen} + /> +
+ ); +}; + +export const AdminWarningReasonsRouteContent = ( + props: AdminWarningsRouteData, +) => ( + + + +); + +const WarningThresholdsScreen = ({ + description, + title, +}: AdminWarningsRouteData) => { + const adminUserId = useAdminIdentity(); + const queryClient = useQueryClient(); + const { data } = useSuspenseQuery( + adminWarningThresholdsQueryOptions(adminUserId), + ); + const canEdit = useAdminStaffPermission(ADMIN_WARNING_PERMISSIONS.manage); + const [sheet, setSheet] = React.useState({ + mode: "create", + }); + const [sheetOpen, setSheetOpen] = React.useState(false); + const refresh = async () => { + await queryClient.invalidateQueries({ + queryKey: adminWarningThresholdsQueryRoot(adminUserId), + }); + }; + + return ( +
+ + + { + setSheet({ mode: "create" }); + setSheetOpen(true); + }} + /> + + + + { + const result = await deleteAdminWarningThreshold(id); + if (!isAdminMutationError(result)) await refresh(); + + return result; + }} + onOpenSheet={state => { + setSheet(state); + setSheetOpen(true); + }} + onSave={async args => { + const result = await saveAdminWarningThreshold(args); + if (!isAdminMutationError(result)) await refresh(); + + return result; + }} + onSheetOpenChange={setSheetOpen} + sheet={sheet} + sheetOpen={sheetOpen} + thresholds={data} + /> +
+ ); +}; + +export const AdminWarningThresholdsRouteContent = ( + props: AdminWarningsRouteData, +) => ( + + + +); diff --git a/packages/vitnode/src/tanstack/comments/author.ts b/packages/vitnode/src/tanstack/comments/author.ts new file mode 100644 index 0000000000..0db6795df3 --- /dev/null +++ b/packages/vitnode/src/tanstack/comments/author.ts @@ -0,0 +1,21 @@ +import type { ContentPublicPerson } from "@/content/public-person"; + +import { zodContentPublicPerson } from "@/content/public-person"; + +const zodAuthor = zodContentPublicPerson.nullable().catch(null); + +/** + * The comment's author from its public record's `author` field, or `null` + * for a deleted account or a content type without one. Pure. + */ +export const contentCommentAuthor = ( + record: unknown, +): ContentPublicPerson | null => + typeof record === "object" && record !== null && "author" in record + ? zodAuthor.parse(record.author) + : null; + +/** The name to show for a person, preferring a real name they share. Pure. */ +export const contentCommentAuthorName = ( + person: ContentPublicPerson | null, +): null | string => (person ? (person.fullName ?? person.name) : null); diff --git a/packages/vitnode/src/tanstack/comments/item.tsx b/packages/vitnode/src/tanstack/comments/item.tsx index c29def8970..cf7b13b1e4 100644 --- a/packages/vitnode/src/tanstack/comments/item.tsx +++ b/packages/vitnode/src/tanstack/comments/item.tsx @@ -6,13 +6,12 @@ import { FlagIcon, PencilIcon, Trash2Icon, + TriangleAlertIcon, } from "lucide-react"; import React from "react"; import { toast } from "sonner"; import { useTranslations } from "use-intl"; -import type { ContentPublicPerson } from "@/content/public-person"; - import { Button } from "@/components/ui/button"; import { DropdownMenu, @@ -29,13 +28,13 @@ import { PopoverTitle, } from "@/components/ui/popover"; import { Skeleton } from "@/components/ui/skeleton"; -import { zodContentPublicPerson } from "@/content/public-person"; import type { ContentCommentComposerFormProps } from "./composer-form"; import type { ContentCommentHideFormProps } from "./hide-form"; import type { ContentCommentNode } from "./thread"; import type { ContentCommentEditable } from "./use-content-comments"; +import { contentCommentAuthor, contentCommentAuthorName } from "./author"; import { useContentCommentErrorToast, useContentCommentsContext } from "./root"; const loadComposer = async () => await import("./composer-form"); @@ -75,23 +74,7 @@ const LazyHideForm = (props: ContentCommentHideFormProps) => ( ); -const zodAuthor = zodContentPublicPerson.nullable().catch(null); - -/** - * The comment's author from its public record's `author` field, or `null` - * for a deleted account or a content type without one. Pure. - */ -export const contentCommentAuthor = ( - record: unknown, -): ContentPublicPerson | null => - typeof record === "object" && record !== null && "author" in record - ? zodAuthor.parse(record.author) - : null; - -/** The name to show for a person, preferring a real name they share. Pure. */ -export const contentCommentAuthorName = ( - person: ContentPublicPerson | null, -): null | string => (person ? (person.fullName ?? person.name) : null); +export { contentCommentAuthor, contentCommentAuthorName } from "./author"; /** * The state one comment needs: what the visitor may do with it, whether an @@ -142,6 +125,10 @@ export const useContentCommentItem = (node: ContentCommentNode) => { canEdit: comments.features.edit && permissions.canEdit, canHide: comments.features.hide && permissions.canHide, canReport: comments.features.report && permissions.canReport, + canWarn: + comments.features.warn && + permissions.canWarn && + contentCommentAuthor(node.record) !== null, }, setIsReplying, /** Loads the editable values, then opens the edit form. */ @@ -174,7 +161,7 @@ const HIDE_PROMPT_MORPH_CLASS = "[clip-path:inset(-2rem_round_var(--radius-lg))] transition-[opacity,scale,clip-path] duration-200 data-starting-style:scale-100 data-starting-style:[clip-path:inset(0_calc(100%-var(--menu-width,100%))_calc(100%-var(--menu-height,100%))_0_round_var(--radius-lg))] data-[side=top]:data-starting-style:[clip-path:inset(calc(100%-var(--menu-height,100%))_calc(100%-var(--menu-width,100%))_0_0_round_var(--radius-lg))]"; /** - * The **More actions** menu of one comment: Edit, Report, Hide or Unhide and + * The **More actions** menu of one comment: Edit, Report, Warn author, Hide or Unhide and * Delete, each shown only when the plugin wired it up and the server allows * it. Hiding asks for an optional reason in a popover; deleting asks for * confirmation. Renders nothing when no action is available. @@ -194,7 +181,7 @@ export const ContentCommentActions = ({ node: ContentCommentNode; }) => { const t = useTranslations("core.comments"); - const { comments, requestDelete, requestReport } = + const { comments, requestDelete, requestReport, requestWarn } = useContentCommentsContext(); const showError = useContentCommentErrorToast(); const menuRef = React.useRef(null); @@ -202,13 +189,16 @@ export const ContentCommentActions = ({ const [isHidePromptOpen, setIsHidePromptOpen] = React.useState(false); const [hidePromptOrigin, setHidePromptOrigin] = React.useState(null); - const { canDelete, canEdit, canHide, canReport } = item.permissions; + const { canDelete, canEdit, canHide, canReport, canWarn } = item.permissions; const isHidden = node.hidden; const name = contentCommentAuthorName(contentCommentAuthor(node.record)) ?? t("deleted_user"); - if (!node.record || (!canEdit && !canReport && !canHide && !canDelete)) { + if ( + !node.record || + (!canEdit && !canReport && !canWarn && !canHide && !canDelete) + ) { return children ? ( ) : null} - {(canHide || canDelete) && (canEdit || canReport || children) ? ( + {canWarn ? ( + { + requestWarn(node); + }} + > + + ) : null} + {(canHide || canDelete) && + (canEdit || canReport || canWarn || children) ? ( ) : null} {canHide ? ( diff --git a/packages/vitnode/src/tanstack/comments/root.tsx b/packages/vitnode/src/tanstack/comments/root.tsx index 33a5aa8739..9bb82af208 100644 --- a/packages/vitnode/src/tanstack/comments/root.tsx +++ b/packages/vitnode/src/tanstack/comments/root.tsx @@ -17,6 +17,8 @@ import { useLocale } from "../i18n/locale"; import { intlQueryOptions } from "../i18n/query"; import { getIntlRuntime } from "../i18n/runtime"; import { ReportDialog } from "../moderation/report-button"; +import { WarnDialog } from "../moderation/warn-button"; +import { contentCommentAuthor } from "./author"; /** The message namespaces the shared comments components render with. */ export const CONTENT_COMMENTS_NAMESPACES = [ @@ -30,6 +32,8 @@ interface ContentCommentsContextValue { requestDelete: (node: ContentCommentNode) => void; /** Opens the report dialog for one comment. */ requestReport: (node: ContentCommentNode) => void; + /** Opens the warn dialog for one comment's author. */ + requestWarn: (node: ContentCommentNode) => void; } /** @@ -108,17 +112,23 @@ const RootDialogs = ({ deleting, onDeleteClosed, onReportClosed, + onWarnClosed, reporting, + warning, }: { deleting: ContentCommentNode | null; onDeleteClosed: () => void; onReportClosed: () => void; + onWarnClosed: () => void; reporting: ContentCommentNode | null; + warning: ContentCommentNode | null; }) => { const t = useTranslations("core.comments"); const { comments } = useContentCommentsContext(); const showError = useContentCommentErrorToast(); const target = reporting ? comments.reportTarget(reporting.id) : null; + const warnTarget = warning ? comments.reportTarget(warning.id) : null; + const warnAuthor = warning ? contentCommentAuthor(warning.record) : null; return ( <> @@ -162,14 +172,25 @@ const RootDialogs = ({ typeLabel={t("report_type")} /> ) : null} + + {warnTarget && warnAuthor ? ( + { + if (!open) onWarnClosed(); + }} + open + userId={warnAuthor.id} + /> + ) : null} ); }; /** * Provides one comment thread to the shared comment components below it and - * renders the dialogs they open: the delete confirmation and the report - * dialog. Adds its own messages to the page's, so it works on any page and + * renders the dialogs they open: the delete confirmation, the report + * dialog and the warn dialog. Adds its own messages to the page's, so it works on any page and * your components inside keep their translations. Use it to lay out * comments your own way; `ContentComments` renders a complete section with * it. @@ -204,6 +225,8 @@ export const ContentCommentsRoot = ({ React.useState | null>(null); const [reporting, setReporting] = React.useState | null>(null); + const [warning, setWarning] = + React.useState | null>(null); const value = React.useMemo( () => ({ @@ -211,6 +234,7 @@ export const ContentCommentsRoot = ({ comments: comments as unknown as ContentCommentsController, requestDelete: setDeleting, requestReport: setReporting, + requestWarn: setWarning, }), [comments], ); @@ -228,7 +252,11 @@ export const ContentCommentsRoot = ({ onReportClosed={() => { setReporting(null); }} + onWarnClosed={() => { + setWarning(null); + }} reporting={reporting} + warning={warning} /> diff --git a/packages/vitnode/src/tanstack/comments/use-content-comments.ts b/packages/vitnode/src/tanstack/comments/use-content-comments.ts index dc55b0afa6..3f8000e1a3 100644 --- a/packages/vitnode/src/tanstack/comments/use-content-comments.ts +++ b/packages/vitnode/src/tanstack/comments/use-content-comments.ts @@ -355,6 +355,8 @@ export interface ContentCommentsController< hide: boolean; replies: boolean; report: boolean; + /** Warning authors reuses `report`'s target, so it is on whenever `report` is. */ + warn: boolean; }; /** The rich-text field that holds a comment's text. */ field: string; @@ -807,6 +809,7 @@ export const useContentComments = < hide: Boolean(visibility), replies: maxDepth !== undefined, report: Boolean(report), + warn: Boolean(report), }, field, hasMore: comments.hasNextPage, diff --git a/packages/vitnode/src/tanstack/content/permissions.ts b/packages/vitnode/src/tanstack/content/permissions.ts index 81304b7f4b..dbea1c7bef 100644 --- a/packages/vitnode/src/tanstack/content/permissions.ts +++ b/packages/vitnode/src/tanstack/content/permissions.ts @@ -26,11 +26,19 @@ export interface ContentPermissions { * the visitor's - unless `reports.selfReport` lets authors report their own. */ canReport: boolean; + /** + * A **Warn author** action makes sense: the visitor is a moderator with + * `can_warn` for the record's scope and did not write it. + */ + canWarn: boolean; } /** The permissions route's answer, as `fetch` returns it. */ export interface ContentPermissionsResponse { - items: (ContentPermissions & { id: TId })[]; + items: (Omit & { + canWarn?: boolean; + id: TId; + })[]; } const NO_PERMISSIONS: ContentPermissions = { @@ -38,6 +46,7 @@ const NO_PERMISSIONS: ContentPermissions = { canEdit: false, canHide: false, canReport: false, + canWarn: false, }; /** Batches in flight at once for one hook. */ @@ -194,6 +203,7 @@ export const useContentPermissions = ({ canEdit: item.canEdit, canHide: item.canHide, canReport: item.canReport, + canWarn: item.canWarn ?? false, } : NO_PERMISSIONS; }, diff --git a/packages/vitnode/src/tanstack/layout/main-header.tsx b/packages/vitnode/src/tanstack/layout/main-header.tsx index f9bed610ef..bcea09f740 100644 --- a/packages/vitnode/src/tanstack/layout/main-header.tsx +++ b/packages/vitnode/src/tanstack/layout/main-header.tsx @@ -1,7 +1,16 @@ +import { SuspensionNotice } from "../moderation/suspension-notice"; import { Header } from "./header"; import { UserHeader } from "./user-header"; -/** The site `Header` with the signed-in user's menu - what the main layout renders. */ +/** + * The site `Header` with the signed-in user's menu - what the main layout + * renders - followed by the suspension notice while the account is suspended. + */ export const MainHeader = ( props: Omit, "user">, -) =>
-

- {warning.message} -

+
{t("expires")}
diff --git a/packages/vitnode/src/views/moderation/warnings/modcp-member-content.tsx b/packages/vitnode/src/views/moderation/warnings/modcp-member-content.tsx index c0c52cad93..eab03e70f2 100644 --- a/packages/vitnode/src/views/moderation/warnings/modcp-member-content.tsx +++ b/packages/vitnode/src/views/moderation/warnings/modcp-member-content.tsx @@ -5,6 +5,7 @@ import { useTranslations } from "use-intl"; import { Button } from "@/components/ui/button"; import { PageTitle } from "@/components/ui/page-title"; +import { SlidingNumber } from "@/components/ui/sliding-number"; import { WarnDialog } from "@/tanstack/moderation/warn-button"; import { Person } from "../modcp/report-tone"; @@ -74,7 +75,7 @@ export const ModcpMemberContent = ({ userId }: { userId: number }) => { {t("member.active_points")}
- {data.activePoints} +
{data.sanctions ? ( diff --git a/packages/vitnode/src/views/moderation/warnings/warn-dialog-content.tsx b/packages/vitnode/src/views/moderation/warnings/warn-dialog-content.tsx index 4a2815909f..3ae9ec594d 100644 --- a/packages/vitnode/src/views/moderation/warnings/warn-dialog-content.tsx +++ b/packages/vitnode/src/views/moderation/warnings/warn-dialog-content.tsx @@ -7,16 +7,14 @@ import { import { ShieldAlertIcon, TriangleAlertIcon } from "lucide-react"; import React from "react"; import { toast } from "sonner"; -import { useFormatter, useLocale, useTranslations } from "use-intl"; +import { useFormatter, useTranslations } from "use-intl"; import { z } from "zod"; -import type { - AutoFormOnSubmit, - ItemAutoFormComponentProps, -} from "@/components/form/auto-form"; +import type { AutoFormOnSubmit } from "@/components/form/auto-form"; import { AutoForm, AutoFormSubmitButton } from "@/components/form/auto-form"; import { AutoFormDateTime } from "@/components/form/fields/date-time"; +import { AutoFormEditor } from "@/components/form/fields/editor"; import { AutoFormNumber } from "@/components/form/fields/number"; import { AutoFormSelect } from "@/components/form/fields/select"; import { AutoFormSwitch } from "@/components/form/fields/switch"; @@ -32,8 +30,14 @@ import { } from "@/components/ui/dialog"; import { useFormApi } from "@/components/ui/form"; import { Skeleton } from "@/components/ui/skeleton"; +import { + isRichTextDocument, + isRichTextEmpty, +} from "@/content/rich-text/document"; +import { richTextToPlainText } from "@/content/rich-text/plain-text"; import { WARNING_LIMITS } from "@/lib/moderation/warnings"; +import type { DocumentDraft, MessageDraft } from "./warn-dialog-parts"; import type { IssuedWarning, ModcpWarningReason, @@ -43,8 +47,16 @@ import type { } from "./warnings-api"; import { ModcpRequestError } from "../modcp/modcp-query"; -import { resolveReasonText } from "../report-reasons-query"; -import { WarningDuration } from "./warning-format"; +import { + filledMessage, + MemberPanel, + messageText, + NO_REASON, + OutcomeSummary, + reasonMessage, + ReasonPicker, + Reveal, +} from "./warn-dialog-parts"; import { ACCOUNT_STATUS_QUERY_KEY, issueWarning, @@ -69,24 +81,32 @@ export interface WarnDialogContentProps { userId: number; } -const NO_REASON = "none"; const EXPIRATION_MODES = ["default", "never", "hours", "at"] as const; interface FormValues { expirationAt?: null | string; expirationHours?: number; expirationMode?: (typeof EXPIRATION_MODES)[number]; - message?: string; + message?: MessageDraft; overrideNote?: string; points?: number; reasonId?: string; resolveReport?: boolean; - staffNote?: string; + staffNote?: DocumentDraft | null; } +type WarnBase = Pick; + +const documentSchema = z + .looseObject({ + content: z.array(z.unknown()).optional(), + type: z.literal("doc"), + }) + .nullable(); + const draftOf = ( values: FormValues, - base: Pick, + base: WarnBase, canOverridePoints: boolean, ): WarningDraft => { const mode = values.expirationMode ?? "default"; @@ -124,61 +144,15 @@ const useFailureMessage = () => { failure === "unknown" || failure === "conflict" ? t("unknown") : t(failure); }; -const PointsLine = ({ - label, - strong, - value, -}: { - label: string; - strong?: boolean; - value: React.ReactNode; -}) => ( -
-
{label}
-
- {value} -
-
-); - -/** The sanction a warning leads to, in words. */ -export const SanctionSummary = ({ - sanction, -}: { - sanction: { durationHours?: null | number; endsAt: Date | null | string }; -}) => { - const t = useTranslations("core.moderation.warn"); - const format = useFormatter(); - if (sanction.endsAt === null) return <>{t("sanction.indefinite")}; - const endsAt = format.dateTime(new Date(sanction.endsAt), { - dateStyle: "medium", - timeStyle: "short", - }); - - return sanction.durationHours ? ( - <> - {t("sanction.for")} {" "} - {t("sanction.until", { endsAt })} - - ) : ( - <>{t("sanction.suspended_until", { endsAt })} - ); -}; - -const Consequences = ({ +const LiveOutcome = ({ base, canOverridePoints, initial, }: { - base: Pick; + base: WarnBase; canOverridePoints: boolean; initial: WarningPreview; }) => { - const t = useTranslations("core.moderation.warn"); const { form } = useFormApi(); const values = useSelector(form.store, state => state.values as FormValues); const draft = React.useDeferredValue( @@ -196,101 +170,20 @@ const Consequences = ({ retry: false, staleTime: 5_000, }); - const data = preview.data ?? initial; const failure = useFailureMessage(); return ( -
-

- {t("consequences.title")} -

-
- - - -
-

- {t("consequences.action")} - {data.sanction ? ( - - ) : ( - t("consequences.no_action") - )} -

+
+ {preview.isError ? ( -

+

{failure(preview.error.message as WarningFailure)}

) : null} - {data.suspendedUntil ? ( -

- {t("consequences.already_suspended")} -

- ) : null} - {data.related.length > 0 ? ( -

- {t("consequences.related", { count: data.related.length })} -

- ) : null} -

- {t("consequences.advisory")} -

-
- ); -}; - -/** - * The reason dropdown. Picking a reason resets the points to its default and - * swaps in its message, unless the moderator already wrote their own. - */ -const ReasonSelect = ({ - labels, - messageOf, - reasonOf, - ...props -}: ItemAutoFormComponentProps & { - labels: { label: string; value: string }[]; - messageOf: (reason: ModcpWarningReason | undefined) => string; - reasonOf: (id: string | undefined) => ModcpWarningReason | undefined; -}) => { - const t = useTranslations("core.moderation.warn"); - const { form } = useFormApi(); - - return ( - { - 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)) { - form.setFieldValue("message", messageOf(next)); - } - }} - /> + ); }; @@ -301,24 +194,29 @@ const WarnForm = ({ onIssued, reasons, }: { - base: Pick; + base: WarnBase; initial: WarningPreview; onClose: () => void; onIssued?: (result: IssuedWarning) => void; reasons: ModcpWarningReason[]; }) => { const t = useTranslations("core.moderation.warn"); - const locale = useLocale(); const format = useFormatter(); const queryClient = useQueryClient(); const failure = useFailureMessage(); const [idempotencyKey] = React.useState(() => crypto.randomUUID()); + const [messageRevision, setMessageRevision] = React.useState(0); const { permissions } = initial; const reasonOf = (id: string | undefined) => reasons.find(reason => String(reason.id) === id); - const messageOf = (reason: ModcpWarningReason | undefined) => - reason ? resolveReasonText(reason.message, locale) : ""; const first = reasons[0]; + const templates = new Set( + reasons.map(reason => messageText(reasonMessage(reason))), + ); + const isOverridden = (values: FormValues) => + (permissions.overridePoints && + values.points !== (reasonOf(values.reasonId)?.points ?? 0)) || + (values.expirationMode ?? "default") !== "default"; const formSchema = z .object({ @@ -345,25 +243,53 @@ const WarnForm = ({ .max(WARNING_LIMITS.noteMaxLength) .default(""), message: z - .string() - .trim() - .min(WARNING_LIMITS.messageMinLength) - .max(WARNING_LIMITS.messageMaxLength) - .default(messageOf(first)), - staffNote: z - .string() - .trim() - .max(WARNING_LIMITS.noteMaxLength) - .default(""), + .array(z.object({ languageCode: z.string(), value: documentSchema })) + .default(reasonMessage(first)) + .superRefine((message, ctx) => { + const filled = filledMessage(message); + if (filled.length === 0) { + ctx.addIssue({ + code: "custom", + message: t("form.message_required"), + }); + + return; + } + const outOfRange = filled.some(item => { + const length = richTextToPlainText(item.value).length; + + return ( + length < WARNING_LIMITS.messageMinLength || + length > WARNING_LIMITS.messageMaxLength + ); + }); + if (outOfRange) { + ctx.addIssue({ + code: "custom", + message: t("form.message_length", { + max: WARNING_LIMITS.messageMaxLength, + min: WARNING_LIMITS.messageMinLength, + }), + }); + } + }), + staffNote: documentSchema + .default(null) + .refine( + note => + !isRichTextDocument(note) || + richTextToPlainText(note).length <= WARNING_LIMITS.noteMaxLength, + { + message: t("form.staff_note_length", { + max: WARNING_LIMITS.noteMaxLength, + }), + }, + ), resolveReport: z.boolean().default(false), }) .superRefine((values, ctx) => { - const defaultPoints = reasonOf(values.reasonId)?.points ?? 0; - const overridden = - (permissions.overridePoints && values.points !== defaultPoints) || - values.expirationMode !== "default"; if ( - overridden && + isOverridden(values) && values.overrideNote.length < WARNING_LIMITS.explanationMinLength ) { ctx.addIssue({ @@ -386,10 +312,14 @@ const WarnForm = ({ const result = await issueWarning({ ...draft, idempotencyKey, - message: values.message, + message: filledMessage(values.message), overrideNote: values.overrideNote || null, resolveReport: values.resolveReport, - staffNote: values.staffNote || null, + staffNote: + isRichTextDocument(values.staffNote) && + !isRichTextEmpty(values.staffNote) + ? values.staffNote + : null, }); if (!result.ok) { toast.error(t("errors.title"), { description: failure(result.failure) }); @@ -412,7 +342,6 @@ const WarnForm = ({ }) : t("done.suspended_indefinitely") : null; - // Someone else warned the member after the form opened. const differs = !data.duplicate && data.points.before !== initial.points.current; toast.success(t("done.title"), { @@ -436,14 +365,6 @@ const WarnForm = ({ onClose(); }; - const reasonLabels = [ - ...reasons.map(reason => ({ - label: `${resolveReasonText(reason.title, locale)} (${t("points", { count: reason.points })})`, - value: String(reason.id), - })), - { label: t("form.no_reason"), value: NO_REASON }, - ]; - return ( ( - { + setMessageRevision(revision => revision + 1); + }} + reasons={reasons} + templates={templates} /> ), }, @@ -465,8 +389,9 @@ const WarnForm = ({ component: props => ( ), }, @@ -486,30 +411,24 @@ const WarnForm = ({ }, { id: "expirationHours", - hidden: values => values.expirationMode !== "hours", component: props => ( ), }, { id: "expirationAt", - hidden: values => values.expirationMode !== "at", component: props => ( ), }, { id: "overrideNote", - hidden: values => - !( - (permissions.overridePoints && - values.points !== (reasonOf(values.reasonId)?.points ?? 0)) || - (values.expirationMode ?? "default") !== "default" - ), component: props => ( ( - ), }, { id: "staffNote", component: props => ( - ), }, @@ -553,38 +482,66 @@ const WarnForm = ({ formSchema={formSchema} layout={rendered => ( <> -
- {base.content || base.reportId ? ( - } variant="warning"> - {t("global.title")} - - {t("global.desc")} - - - ) : null} - {rendered.reasonId} - {rendered.points} - {rendered.expirationMode} - {rendered.expirationHours} - {rendered.expirationAt} - {rendered.overrideNote} - + +
+ + + {t("title", { name: initial.user.name })} + + + {t("desc")} + {first ? null : ` ${t("no_reasons")}`} + + + {base.content || base.reportId ? ( + } variant="warning"> + {t("global.title")} + + {t("global.desc")} + + + ) : null} + {rendered.reasonId} + {permissions.overridePoints || permissions.overrideExpiration ? ( +
+
+ {rendered.points} + {rendered.expirationMode} +
+ values.expirationMode === "hours"} + > + {rendered.expirationHours} + + values.expirationMode === "at"}> + {rendered.expirationAt} + + + {rendered.overrideNote} + +
+ ) : null} + {rendered.message} + {rendered.staffNote} + {rendered.resolveReport} +
+
+ + - {rendered.message} - {rendered.staffNote} - {rendered.resolveReport} - - - }> - {t("cancel")} - - - +
+ }> + {t("cancel")} + + + +
)} @@ -593,19 +550,79 @@ const WarnForm = ({ ); }; +const RevealWhen = ({ + children, + when, +}: { + children: React.ReactNode; + when: (values: FormValues) => boolean; +}) => { + const { form } = useFormApi(); + const open = useSelector(form.store, state => + when(state.values as FormValues), + ); + + return {children}; +}; + +const ReasonPickerField = ({ + onTemplateSwap, + templates, + ...props +}: Omit, "onReason"> & { + onTemplateSwap: () => void; + templates: Set; +}) => { + const { form } = useFormApi(); + + return ( + { + const { message } = form.state.values as FormValues; + const current = messageText(message); + form.setFieldValue("points", reason?.points ?? 0); + if (current === "" || templates.has(current)) { + form.setFieldValue("message", reasonMessage(reason)); + onTemplateSwap(); + } + }} + /> + ); +}; + const DialogSkeleton = () => ( -
- - - - +
+
+
+ +
+ + +
+
+ + +
+
+ + + + +
); /** - * The shared warn form: reason, points, expiry, the message the member reads, - * a private staff note and a live preview of the consequences. Loads lazily - * inside `WarnDialog`; every check runs again on the server when it submits. + * The shared warn form: who the member is and their recent warnings, the + * reason, points, expiry, the rich-text message the member reads (one per + * language), a private rich-text staff note and a live preview of the + * consequences. Loads lazily inside `WarnDialog`; every check runs again on + * the server when it submits. */ export const WarnDialogContent = ({ content = null, @@ -682,24 +699,13 @@ export const WarnDialogContent = ({ } return ( - <> - - - {t("title", { name: initial.data.user.name })} - - - {t("desc")} - {first ? null : ` ${t("no_reasons")}`} - - - - + ); }; diff --git a/packages/vitnode/src/views/moderation/warnings/warn-dialog-parts.tsx b/packages/vitnode/src/views/moderation/warnings/warn-dialog-parts.tsx new file mode 100644 index 0000000000..fe5840eb2c --- /dev/null +++ b/packages/vitnode/src/views/moderation/warnings/warn-dialog-parts.tsx @@ -0,0 +1,436 @@ +import { useQuery } from "@tanstack/react-query"; +import { cn } from "cn"; +import { ArrowRightIcon, PencilLineIcon } from "lucide-react"; +import React from "react"; +import { useFormatter, useLocale, useNow, useTranslations } from "use-intl"; + +import type { ItemAutoFormComponentProps } from "@/components/form/auto-form"; +import type { MultiLangValue } from "@/lib/helpers/multi-lang"; +import type { WarningMessage } from "@/lib/moderation/warnings"; + +import { Avatar } from "@/components/avatar"; +import { FEEDBACK_TONE_TEXT } from "@/components/ui/feedback-tone"; +import { FormControl, FormMessage } from "@/components/ui/form"; +import { + Select, + SelectContent, + SelectItem, + SelectSeparator, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Skeleton } from "@/components/ui/skeleton"; +import { SlidingNumber } from "@/components/ui/sliding-number"; +import { + isRichTextDocument, + isRichTextEmpty, +} from "@/content/rich-text/document"; +import { richTextToPlainText } from "@/content/rich-text/plain-text"; +import { + activeSuspensionOf, + standingFromModcp, +} from "@/views/profile/warnings/standing"; +import { + reasonTextOf, + WarningNode, +} from "@/views/profile/warnings/standing-parts"; + +import type { ModcpWarningReason, WarningPreview } from "./warnings-api"; + +import { resolveReasonText } from "../report-reasons-query"; +import { WarningDuration } from "./warning-format"; +import { SanctionSummary } from "./warning-sanction"; +import { memberStandingQueryOptions } from "./warnings-api"; + +export const NO_REASON = "none"; + +export interface DocumentDraft { + [key: string]: unknown; + content?: unknown[]; + type: "doc"; +} + +export type MessageDraft = MultiLangValue; + +const RECENT_WARNINGS = 3; + +const textToDocument =(text: string): DocumentDraft => ({ + content: text + .split(/\n{2,}/) + .map(block => block.trim()) + .filter(Boolean) + .map(block => ({ + content: block + .split("\n") + .flatMap((line, index) => [ + ...(index > 0 ? [{ type: "hardBreak" }] : []), + ...(line ? [{ text: line, type: "text" }] : []), + ]), + type: "paragraph", + })), + type: "doc", +}); + +export const reasonMessage = ( + reason: ModcpWarningReason | undefined, +): MessageDraft => + (reason?.message ?? []) + .filter(item => item.value.trim() !== "") + .map(item => ({ + languageCode: item.languageCode, + value: textToDocument(item.value), + })); + +export const filledMessage = ( + message: MessageDraft | undefined, +): WarningMessage => + (message ?? []).flatMap(item => + isRichTextDocument(item.value) && !isRichTextEmpty(item.value) + ? [{ languageCode: item.languageCode, value: item.value }] + : [], + ); + +export const messageText = (message: MessageDraft | undefined) => + filledMessage(message) + .map(item => `${item.languageCode}:${richTextToPlainText(item.value)}`) + .sort() + .join("\n"); + +const CustomReasonIcon = () => ( + +); + +const ReasonText = ({ + description, + meta, + title, +}: { + description?: string; + meta: React.ReactNode; + title: string; +}) => ( + + + {title} + + {description ? ( + + {description} + + ) : null} + {meta} + +); + +export const ReasonPicker = ({ + field, + label, + onReason, + reasons, +}: ItemAutoFormComponentProps & { + onReason: (reason: ModcpWarningReason | undefined) => void; + reasons: ModcpWarningReason[]; +}) => { + const t = useTranslations("core.moderation.warn"); + const locale = useLocale(); + const labelId = React.useId(); + const reasonOf = (value: unknown) => + reasons.find(reason => String(reason.id) === String(value)); + const expiryOf = ({ + expiresAfterHours, + }: ModcpWarningReason): React.ReactNode => + expiresAfterHours === null + ? t("form.never_expires") + : t.rich("form.expires_after", { + duration: () => , + }); + + const custom = ( + + + + + ); + + return ( + <> +

+ {label} +

+ + + + ); +}; + +export const MemberPanel = ({ preview }: { preview: WarningPreview }) => { + const t = useTranslations("core.moderation.warn.dossier"); + const tCard = useTranslations("core.moderation.profile_card"); + const tWarn = useTranslations("core.moderation.warn"); + const format = useFormatter(); + const locale = useLocale(); + const now = useNow({ updateInterval: 60_000 }); + const headingId = React.useId(); + const { user } = preview; + const query = useQuery({ + ...memberStandingQueryOptions(user.id), + enabled: preview.permissions.viewWarnings, + }); + const standing = query.data ? standingFromModcp(query.data) : null; + const suspension = standing ? activeSuspensionOf(standing) : null; + const recent = standing?.warnings.slice(0, RECENT_WARNINGS) ?? []; + const older = (standing?.warnings.length ?? 0) - recent.length; + const suspendedUntil = suspension?.endsAt ?? preview.suspendedUntil?.endsAt; + const suspended = !!suspension || !!preview.suspendedUntil; + + return ( + + ); +}; + +export const OutcomeSummary = ({ + isFetching, + preview, +}: { + isFetching: boolean; + preview: WarningPreview; +}) => { + const t = useTranslations("core.moderation.warn"); + + return ( +
+

+ {preview.points.current} +

+

+ {preview.sanction ? ( + + ) : ( + t("consequences.no_action") + )} +

+ {preview.suspendedUntil ? ( +

+ {t("consequences.already_suspended")} +

+ ) : null} + {preview.related.length > 0 ? ( +

+ {t("consequences.related", { count: preview.related.length })} +

+ ) : null} +
+ ); +}; + +export const Reveal = ({ + children, + open, +}: { + children: React.ReactNode; + open: boolean; +}) => ( +
+
+
{children}
+
+
+); diff --git a/packages/vitnode/src/views/moderation/warnings/warning-list.tsx b/packages/vitnode/src/views/moderation/warnings/warning-list.tsx index 00597f9317..a51f27e852 100644 --- a/packages/vitnode/src/views/moderation/warnings/warning-list.tsx +++ b/packages/vitnode/src/views/moderation/warnings/warning-list.tsx @@ -37,8 +37,9 @@ import type { import { Person } from "../modcp/report-tone"; import { resolveReasonText } from "../report-reasons-query"; -import { SanctionSummary } from "./warn-dialog-content"; import { WarningDuration, WarningStatusBadge } from "./warning-format"; +import { WarningMessageContent, WarningNoteContent } from "./warning-message"; +import { SanctionSummary } from "./warning-sanction"; import { ACCOUNT_STATUS_QUERY_KEY, revokeSanction, @@ -244,7 +245,7 @@ const StaffNotes = ({ warning }: { warning: ModcpStaffWarning }) => {

{warning.staffNote ? ( - {warning.staffNote} + ) : null} {warning.overrideNote ? ( @@ -316,7 +317,7 @@ export const StaffWarningCard = ({ - {warning.message} + diff --git a/packages/vitnode/src/views/moderation/warnings/warning-message.tsx b/packages/vitnode/src/views/moderation/warnings/warning-message.tsx new file mode 100644 index 0000000000..5f32c0ad97 --- /dev/null +++ b/packages/vitnode/src/views/moderation/warnings/warning-message.tsx @@ -0,0 +1,52 @@ +import { cn } from "cn"; +import { useLocale } from "use-intl"; + +import type { RichTextDocument } from "@/content/rich-text/document"; +import type { WarningMessage } from "@/lib/moderation/warnings"; + +import { RichTextContent } from "@/components/rich-text/rich-text-content"; +import { isRichTextEmpty } from "@/content/rich-text/document"; +import { findLangValue, pickLangCodeWhere } from "@/lib/helpers/multi-lang"; + +export const pickWarningMessage = ( + message: WarningMessage, + locale: string, +): null | RichTextDocument => + findLangValue( + message, + pickLangCodeWhere({ + isFilledIn: code => !isRichTextEmpty(findLangValue(message, code)), + languageCodes: message.map(item => item.languageCode), + locale, + }), + ) ?? null; + +export const WarningMessageContent = ({ + className, + message, +}: { + className?: string; + message: WarningMessage; +}) => { + const locale = useLocale(); + + return ( + + ); +}; + +export const WarningNoteContent = ({ + className, + note, +}: { + className?: string; + note: RichTextDocument; +}) => ( + +); diff --git a/packages/vitnode/src/views/moderation/warnings/warning-sanction.tsx b/packages/vitnode/src/views/moderation/warnings/warning-sanction.tsx new file mode 100644 index 0000000000..2c40cd0113 --- /dev/null +++ b/packages/vitnode/src/views/moderation/warnings/warning-sanction.tsx @@ -0,0 +1,27 @@ +import { useFormatter, useTranslations } from "use-intl"; + +import { WarningDuration } from "./warning-format"; + +/** The sanction a warning leads to, in words. */ +export const SanctionSummary = ({ + sanction, +}: { + sanction: { durationHours?: null | number; endsAt: Date | null | string }; +}) => { + const t = useTranslations("core.moderation.warn"); + const format = useFormatter(); + if (sanction.endsAt === null) return <>{t("sanction.indefinite")}; + const endsAt = format.dateTime(new Date(sanction.endsAt), { + dateStyle: "medium", + timeStyle: "short", + }); + + return sanction.durationHours ? ( + <> + {t("sanction.for")} {" "} + {t("sanction.until", { endsAt })} + + ) : ( + <>{t("sanction.suspended_until", { endsAt })} + ); +}; diff --git a/packages/vitnode/src/views/moderation/warnings/warnings-api.ts b/packages/vitnode/src/views/moderation/warnings/warnings-api.ts index cc715a7ede..d12952ef7e 100644 --- a/packages/vitnode/src/views/moderation/warnings/warnings-api.ts +++ b/packages/vitnode/src/views/moderation/warnings/warnings-api.ts @@ -4,6 +4,9 @@ import { queryOptions, } from "@tanstack/react-query"; +import type { RichTextDocument } from "@/content/rich-text/document"; +import type { WarningMessage } from "@/lib/moderation/warnings"; + import { fetcherClient } from "@/lib/fetcher-client"; import { manifest } from "@/manifest"; import { fetcher } from "@/tanstack/fetcher"; @@ -315,10 +318,10 @@ export const previewWarning = async (draft: WarningDraft) => export interface IssueWarningInput extends WarningDraft { idempotencyKey: string; - message: string; + message: WarningMessage; overrideNote?: null | string; resolveReport: boolean; - staffNote?: null | string; + staffNote: null | RichTextDocument; } export interface IssuedWarning { diff --git a/packages/vitnode/src/views/profile/warnings/profile-warnings.tsx b/packages/vitnode/src/views/profile/warnings/profile-warnings.tsx index d2c01639db..b36b52cf24 100644 --- a/packages/vitnode/src/views/profile/warnings/profile-warnings.tsx +++ b/packages/vitnode/src/views/profile/warnings/profile-warnings.tsx @@ -26,6 +26,7 @@ import { import { FEEDBACK_TONE_TEXT } from "@/components/ui/feedback-tone"; import { Sheet, SheetContent } from "@/components/ui/sheet"; import { Skeleton } from "@/components/ui/skeleton"; +import { SlidingNumber } from "@/components/ui/sliding-number"; import { TooltipWithContent } from "@/components/ui/tooltip"; import { WarnDialog } from "@/tanstack/moderation/warn-button"; import { ModcpRequestError } from "@/views/moderation/modcp/modcp-query"; @@ -287,6 +288,7 @@ const StandingCard = ({ const entries = standingEntries(standing); const visible = entries.slice(0, VISIBLE_ENTRIES); const showLift = standing.can.lift && suspension; + const showHistory = visible.length > 0 || standing.hiddenWarnings > 0; return ( @@ -296,7 +298,10 @@ const StandingCard = ({ className="text-foreground text-2xl font-semibold text-balance tabular-nums" id={titleId} > - {t("points", { count: standing.activePoints })} + {t.rich("points_animated", { + count: standing.activePoints, + n: () => , + })}
@@ -330,35 +335,33 @@ const StandingCard = ({ ) : null} - - {visible.length === 0 ? ( -

- {standing.own ? t("empty_own") : t("empty")} -

- ) : ( -
    - {visible.map((entry, index) => ( - - ))} -
- )} - {standing.hiddenWarnings > 0 ? ( -

- {t("hidden", { count: standing.hiddenWarnings })} -

- ) : null} -
+ {showHistory ? ( + + {visible.length > 0 ? ( +
    + {visible.map((entry, index) => ( + + ))} +
+ ) : null} + {standing.hiddenWarnings > 0 ? ( +

+ {t("hidden", { count: standing.hiddenWarnings })} +

+ ) : null} +
+ ) : null} {entries.length > 0 ? ( @@ -367,9 +370,10 @@ const StandingCard = ({ to={standing.seeAllHref} > {t("see_all")} - - {entries.length} - +
diff --git a/packages/vitnode/src/views/profile/warnings/standing.ts b/packages/vitnode/src/views/profile/warnings/standing.ts index 4e14f824c9..2d55c6d629 100644 --- a/packages/vitnode/src/views/profile/warnings/standing.ts +++ b/packages/vitnode/src/views/profile/warnings/standing.ts @@ -1,4 +1,5 @@ -import type { WarningStatus } from "@/lib/moderation/warnings"; +import type { RichTextDocument } from "@/content/rich-text/document"; +import type { WarningMessage, WarningStatus } from "@/lib/moderation/warnings"; import type { ReportPerson } from "@/views/moderation/modcp/report-tone"; import type { AccountStatus, @@ -11,7 +12,7 @@ export interface StandingWarning { id: number; issuedAt: Date; issuedBy: null | ReportPerson | undefined; - message: string; + message: WarningMessage; overrideNote: null | string; points: number; reason: AccountStatus["warnings"][number]["reason"]; @@ -19,7 +20,7 @@ export interface StandingWarning { revokedAt: Date | null; revokedBy: null | ReportPerson | undefined; revokeReason: null | string; - staffNote: null | string; + staffNote: null | RichTextDocument; status: WarningStatus; } From 357d4edaef56f2a73bf9abb7f76595ae13ed0172 Mon Sep 17 00:00:00 2001 From: aXenDeveloper Date: Fri, 6 Nov 2026 21:11:14 +0100 Subject: [PATCH 7/9] =?UTF-8?q?feat(moderation):=20=E2=9C=A8=20implement?= =?UTF-8?q?=20suspension=20handling=20and=20dialog=20for=20suspended=20mem?= =?UTF-8?q?bers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../content/docs/dev/moderation/warnings.mdx | 12 +- apps/web/content/docs/dev/search-external.mdx | 2 +- apps/web/src/routes/_docs.tsx | 4 +- apps/web/src/routes/_main.tsx | 3 +- .../root/src/routes/_main.tsx | 3 +- .../vitnode/src/components/form/auto-form.tsx | 4 + packages/vitnode/src/locales/en.json | 21 +- .../vitnode/src/tanstack/layout/header.tsx | 22 +- .../src/tanstack/layout/main-header.tsx | 7 +- .../tanstack/moderation/suspension-gate.tsx | 72 +++++ .../tanstack/moderation/suspension-notice.tsx | 61 ---- .../warnings/reason-form-footer.tsx | 30 ++ .../warnings/suspension-dialog-content.tsx | 283 ++++++++++++++++++ .../moderation/warnings/warning-list.tsx | 16 +- .../warnings/standing-confirm-content.tsx | 32 +- 15 files changed, 474 insertions(+), 98 deletions(-) create mode 100644 packages/vitnode/src/tanstack/moderation/suspension-gate.tsx delete mode 100644 packages/vitnode/src/tanstack/moderation/suspension-notice.tsx create mode 100644 packages/vitnode/src/views/moderation/warnings/reason-form-footer.tsx create mode 100644 packages/vitnode/src/views/moderation/warnings/suspension-dialog-content.tsx diff --git a/apps/web/content/docs/dev/moderation/warnings.mdx b/apps/web/content/docs/dev/moderation/warnings.mdx index 6482dfd644..e2d0de74a7 100644 --- a/apps/web/content/docs/dev/moderation/warnings.mdx +++ b/apps/web/content/docs/dev/moderation/warnings.mdx @@ -33,9 +33,17 @@ A temporary suspension ends at its own end time, even when the member still has ## What a suspended member sees -The server checks suspensions when it reads the session, for every request: core routes, plugin routes, server functions and WebSocket handshakes. The API treats a suspended account as a guest everywhere, so it can browse public pages but cannot post, comment, report, moderate or open the AdminCP. Applying a suspension also closes the member's open WebSocket connections. +The server checks suspensions when it reads the session, for every request: core routes, plugin routes, server functions and WebSocket handshakes. The API treats a suspended account as a guest everywhere, so it cannot post, comment, report, moderate or open the AdminCP. Applying a suspension also closes the member's open WebSocket connections. -The member keeps their session. A strip under the header says the account is suspended and links to **Account status** at `/account-status`, which shows the suspension end time, active points and the warning history with each message. Members reach the same page from **Settings → Account status** at any time. It never shows staff notes or which moderator issued a warning. +The member keeps their session and can still sign in, but the site keeps them on the home page: every other page sends them back there. On the home page a dialog they can't close explains the suspension: + +- A ring around a lock fills with how much of the suspension has passed, and a countdown ticks down to the end, second by second. +- **Why**: the warning's reason, their active points and the moderator's message. +- When the suspension started and when it ends, or that it has no end date. + +When the countdown reaches zero, the session refreshes and the member can use the whole site again without reloading. The dialog never shows staff notes or which moderator issued a warning. In development it also has a **Sign out** button, so you can switch accounts while testing. + +Main layouts opt in by passing the route's `location` to `loadMainShell` (see [Search external](/docs/dev/search-external) for the docs layout); `MainHeader` applies the same rule in the browser for prerendered pages. ## On a member's profile diff --git a/apps/web/content/docs/dev/search-external.mdx b/apps/web/content/docs/dev/search-external.mdx index 5d80f5a395..c6052f03e2 100644 --- a/apps/web/content/docs/dev/search-external.mdx +++ b/apps/web/content/docs/dev/search-external.mdx @@ -134,7 +134,7 @@ import { RootProvider } from "fumadocs-ui/provider/tanstack"; ; ``` -`variant="bar"` turns the floating header card into a flat, full-width bar that sits flush on the docs layout, and `--fd-banner-height` tells Fumadocs how tall it is, so its sidebar and page fit underneath. Need a button before the logo, like a sidebar toggle? Pass it as `leading`. Load the header's data in the docs route too, with `loadMainShell(context)` in its `loader`. +`variant="bar"` turns the floating header card into a flat, full-width bar that sits flush on the docs layout, and `--fd-banner-height` tells Fumadocs how tall it is, so its sidebar and page fit underneath. Need a button before the logo, like a sidebar toggle? Pass it as `leading`. Load the header's data in the docs route too, with `loadMainShell(context, { location })` in its `loader`, so a suspended member is sent back to the home page from the docs too. Got a layout without the VitNode header? Render `HeaderSearch` from `@vitnode/core/tanstack/search` on its own. It is the header's search button together with its dialog and ⌘ K shortcut. Pass `firstSourceId={docsSearchSource.id}` to put your source's group first. Core exports no dialog without its trigger. diff --git a/apps/web/src/routes/_docs.tsx b/apps/web/src/routes/_docs.tsx index f2778607aa..15760221e8 100644 --- a/apps/web/src/routes/_docs.tsx +++ b/apps/web/src/routes/_docs.tsx @@ -11,10 +11,10 @@ import { getDocsPageTree } from '@/docs/transport' import { withoutHmrTimestamp } from '@/lib/without-hmr-timestamp' export const Route = createFileRoute('/_docs')({ - loader: async ({ context }) => { + loader: async ({ context, location }) => { const [{ icons, pageTree }] = await Promise.all([ getDocsPageTree(), - loadMainShell(context), + loadMainShell(context, { location }), ]) return { icons, pageTree } diff --git a/apps/web/src/routes/_main.tsx b/apps/web/src/routes/_main.tsx index 201ccb9f33..742187ca02 100644 --- a/apps/web/src/routes/_main.tsx +++ b/apps/web/src/routes/_main.tsx @@ -20,7 +20,8 @@ const MainLayout = () => ( ) export const Route = createFileRoute('/_main')({ - loader: async ({ context }) => await loadMainShell(context), + loader: async ({ context, location }) => + await loadMainShell(context, { location }), // Marketing-only keyframes and layouts stay out of the global stylesheet, // so admin and docs never download them. head: () => ({ diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/routes/_main.tsx b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/routes/_main.tsx index a32b9cc5fb..52c903680a 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/routes/_main.tsx +++ b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/routes/_main.tsx @@ -9,7 +9,8 @@ import { import { MainFooter } from "@/components/main-footer"; export const Route = createFileRoute("/_main")({ - loader: async ({ context }) => await loadMainShell(context), + loader: async ({ context, location }) => + await loadMainShell(context, { location }), component: MainLayout, }); diff --git a/packages/vitnode/src/components/form/auto-form.tsx b/packages/vitnode/src/components/form/auto-form.tsx index 0166f76b53..6f3e407a87 100644 --- a/packages/vitnode/src/components/form/auto-form.tsx +++ b/packages/vitnode/src/components/form/auto-form.tsx @@ -243,11 +243,14 @@ type SubmitButtonProps = export const AutoFormSubmitButton = ({ children, className, + disabled, intent, variant, }: { children?: React.ReactNode; className?: string; + /** Blocks submitting, e.g. while a required field is still empty. */ + disabled?: boolean; intent?: string; variant?: React.ComponentProps["variant"]; }) => { @@ -258,6 +261,7 @@ export const AutoFormSubmitButton = ({ return ( } /> + {children} + + ); +}; diff --git a/packages/vitnode/src/views/moderation/warnings/suspension-dialog-content.tsx b/packages/vitnode/src/views/moderation/warnings/suspension-dialog-content.tsx new file mode 100644 index 0000000000..dec1b5866c --- /dev/null +++ b/packages/vitnode/src/views/moderation/warnings/suspension-dialog-content.tsx @@ -0,0 +1,283 @@ +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { cn } from "cn"; +import { LockIcon, LogOutIcon } from "lucide-react"; +import { useReducedMotion } from "motion/react"; +import * as m from "motion/react-m"; +import React from "react"; +import { toast } from "sonner"; +import { useFormatter, useLocale, useNow, useTranslations } from "use-intl"; + +import { MotionFeatures } from "@/components/motion-features"; +import { Button } from "@/components/ui/button"; +import { DialogDescription, DialogTitle } from "@/components/ui/dialog"; +import { FEEDBACK_TONE_TEXT } from "@/components/ui/feedback-tone"; +import { Skeleton } from "@/components/ui/skeleton"; +import { SlidingNumber } from "@/components/ui/sliding-number"; +import { useSignOutAction } from "@/tanstack/auth/actions"; +import { invalidateSession } from "@/tanstack/auth/session-query"; + +import { resolveReasonText } from "../report-reasons-query"; +import { WarningMessageContent } from "./warning-message"; +import { accountStatusQueryOptions } from "./warnings-api"; + +const RING = 44; +const STROKE = 4; +const RADIUS = RING - STROKE; +const SECOND = 1000; + +const servedShare = (startsAt: Date | null, endsAt: Date | null, now: Date) => { + if (!startsAt || !endsAt) return 1; + const total = endsAt.getTime() - startsAt.getTime(); + if (total <= 0) return 1; + + return Math.min(1, Math.max(0, (now.getTime() - startsAt.getTime()) / total)); +}; + +const remainingParts = (endsAt: Date, now: Date) => { + const seconds = Math.max( + 0, + Math.floor((endsAt.getTime() - now.getTime()) / SECOND), + ); + + return { + days: Math.floor(seconds / 86_400), + hours: Math.floor((seconds % 86_400) / 3600), + minutes: Math.floor((seconds % 3600) / 60), + seconds: seconds % 60, + total: seconds, + }; +}; + +const ProgressIcon = ({ served }: { served: number }) => { + const reduce = useReducedMotion(); + + return ( + + + + ); +}; + +const Unit = ({ label, value }: { label: string; value: number }) => ( +
+ + {label} +
+); + +const Countdown = ({ endsAt }: { endsAt: Date }) => { + const t = useTranslations("core.moderation.account_status.dialog"); + const now = useNow({ updateInterval: SECOND }); + const queryClient = useQueryClient(); + const left = remainingParts(endsAt, now); + const ended = left.total === 0; + + React.useEffect(() => { + if (ended) void invalidateSession(queryClient); + }, [ended, queryClient]); + + return ( +
+ {left.days > 0 ? ( + + ) : null} + + + +
+ ); +}; + +const DevSignOut = () => { + const t = useTranslations("core.moderation.account_status.dialog"); + const tErrors = useTranslations("core.global.errors"); + const signOut = useSignOutAction(); + const [pending, setPending] = React.useState(false); + + return ( + + ); +}; + +const Fact = ({ label, value }: { label: string; value: React.ReactNode }) => ( +
+
{label}
+
{value}
+
+); + +const DetailsSkeleton = () => ( + +); + +export const SuspensionDialogContent = ({ + endsAt: sessionEndsAt, +}: { + endsAt: Date | null; +}) => { + const t = useTranslations("core.moderation.account_status.dialog"); + const tStatus = useTranslations("core.moderation.account_status"); + const format = useFormatter(); + const locale = useLocale(); + const now = useNow({ updateInterval: 60 * SECOND }); + const status = useQuery(accountStatusQueryOptions()); + const sanction = status.data?.sanctions.find(item => item.active) ?? null; + const warning = + status.data?.warnings.find(item => item.id === sanction?.warningId) ?? null; + const startsAt = sanction ? new Date(sanction.startsAt) : null; + const endsAt = sanction?.endsAt + ? new Date(sanction.endsAt) + : sanction + ? null + : sessionEndsAt; + const reason = warning?.reason + ? resolveReasonText(warning.reason, locale) + : null; + const when = (date: Date) => + format.dateTime(date, { dateStyle: "medium", timeStyle: "short" }); + + return ( +
+ + +
+ {t("title")} + + {reason ? t("because", { reason }) : t("because_unknown")} + +
+ + {endsAt ? ( + + ) : ( +

{t("no_end")}

+ )} + + {status.isPending ? ( +
+ {t("loading")} + +
+ ) : ( +
+ {warning ? ( +
+

+ {t("why")} +

+
+

+ {reason ?? t("because_unknown")} + {status.data ? ( + + {" · "} + {t("points", { count: status.data.activePoints })} + + ) : null} +

+ +
+
+ ) : null} + +
+ + +
+
+ )} + +
+ {import.meta.env.DEV ? : null} +

+ {tStatus("contact")} +

+
+
+ ); +}; + +export default SuspensionDialogContent; diff --git a/packages/vitnode/src/views/moderation/warnings/warning-list.tsx b/packages/vitnode/src/views/moderation/warnings/warning-list.tsx index a51f27e852..4d748611bf 100644 --- a/packages/vitnode/src/views/moderation/warnings/warning-list.tsx +++ b/packages/vitnode/src/views/moderation/warnings/warning-list.tsx @@ -37,6 +37,7 @@ import type { import { Person } from "../modcp/report-tone"; import { resolveReasonText } from "../report-reasons-query"; +import { ReasonFormFooter } from "./reason-form-footer"; import { WarningDuration, WarningStatusBadge } from "./warning-format"; import { WarningMessageContent, WarningNoteContent } from "./warning-message"; import { SanctionSummary } from "./warning-sanction"; @@ -124,15 +125,16 @@ const RevokeDialog = ({ }, ]} formSchema={formSchema} - onSubmit={onSubmit} - submitButtonProps={{ - children: ( - <> + layout={fields => ( + <> + {fields.reason} + + + )} + onSubmit={onSubmit} /> diff --git a/packages/vitnode/src/views/profile/warnings/standing-confirm-content.tsx b/packages/vitnode/src/views/profile/warnings/standing-confirm-content.tsx index f415d476df..ee71ce62f4 100644 --- a/packages/vitnode/src/views/profile/warnings/standing-confirm-content.tsx +++ b/packages/vitnode/src/views/profile/warnings/standing-confirm-content.tsx @@ -14,6 +14,7 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { WARNING_LIMITS } from "@/lib/moderation/warnings"; +import { ReasonFormFooter } from "@/views/moderation/warnings/reason-form-footer"; import { ACCOUNT_STATUS_QUERY_KEY, revokeSanction, @@ -157,20 +158,25 @@ const StandingConfirmContent = ({ }, ]} formSchema={formSchema} + layout={fields => ( + <> + {fields.reason} + + {revoking ? ( + <> + + + )} onSubmit={onSubmit} - submitButtonProps={{ - children: revoking ? ( - <> -
+ + + + {t("load_error.title")} + {t("load_error.desc")} + + + + + + ); +}; + +const HistoryTable = ({ + onOpen, + params, +}: { + onOpen: (target: NonNullable) => void; + params: AccountStatusHistoryParams; +}) => { + const t = useTranslations("core.moderation.account_status.history"); + const format = useFormatter(); + const query = useQuery(accountStatusHistoryQueryOptions(params)); + + if (query.isPending) return ; + if (!query.data) { + return ( + void query.refetch()} + /> + ); + } + + const { page, pageSize, total } = query.data; + const edges = rowsOf(query.data.items); + const totalPages = Math.max(1, Math.ceil(total / pageSize)); + const columns: ColumnDef[] = [ + { + cell: ({ row }) => , + header: t("item"), + id: "item", + }, + { + accessorKey: "at", + cell: ({ row }) => ( + + {format.dateTime(row.at, { dateStyle: "medium" })} + + ), + className: "w-36", + header: t("date"), + }, + { + accessorKey: "points", + align: "right", + cell: ({ row }) => + row.kind === "warning" ? ( + {row.points} + ) : ( + + ), + className: "w-20", + header: t("points"), + }, + { + cell: ({ row }) => + row.kind === "warning" ? ( + + ) : ( + + ), + className: "w-28", + header: t("status"), + id: "status", + }, + ]; + + return ( +
-
-
-

- {warning.reason - ? resolveReasonText(warning.reason, locale) - : tWarn("custom_reason")} -

-

- -

-
-
- - {tWarn("points", { count: warning.points })} - - -
-
- + aria-label={t("label")} + columns={columns} + customNoResults={{ + description: t("no_results.desc"), + title: t("no_results.title"), + }} + edges={edges} + filters={[ + { + id: "type", + label: t("type"), + options: [ + { label: t("type_warnings"), value: "warning" }, + { label: t("type_suspensions"), value: "suspension" }, + ], + }, + { + id: "status", + label: t("status"), + options: [ + { label: t("status_active"), value: "active" }, + { label: t("status_expired"), value: "expired" }, + { label: t("status_revoked"), value: "revoked" }, + { label: t("status_lifted"), value: "lifted" }, + { label: t("status_ended"), value: "ended" }, + ], + }, + ]} + id="account-status-history" + order={{ + columns: ["at", "points"], + defaultOrder: { column: "at", order: "desc" }, + }} + pageInfo={{ + count: edges.length, + currentPage: page, + endCursor: null, + hasNextPage: page < totalPages, + hasPreviousPage: page > 1, + pageSize, + startCursor: null, + totalCount: total, + totalPages, + }} + rowOpens={row => { + onOpen( + row.kind === "warning" + ? { id: row.warning.id, kind: "warning" } + : { id: row.suspension.id, kind: "suspension" }, + ); + }} + search + searchPlaceholder={t("search")} /> -
-
-
{t("expires")}
-
- {warning.expiresAt ? ( - - ) : ( - t("never") - )} -
-
- {warning.content ? ( -
- ) : null} -
-
+
); }; -const Pending = ({ label }: { label: string }) => ( -
- - {label} - - - - -
-); +const DetailSheet = ({ + onTarget, + standing, + target, +}: { + onTarget: (target: StandingTarget) => void; + standing: ProfileStanding; + target: StandingTarget; +}) => { + const [shown, setShown] = React.useState(target); + if (target && target !== shown) setShown(target); -/** - * The member's own standing: an active suspension, if any, active points and - * warning history. Works for a suspended account too. Never shows staff notes - * or who issued a warning. - */ -export const AccountStatusContent = () => { + return ( + { + if (!open) onTarget(null); + }} + open={target !== null} + > + + {shown ? ( + + + + + + } + > + {}} + onSelect={onTarget} + standing={standing} + target={shown} + /> + + ) : null} + + + ); +}; + +const Heading = () => { const t = useTranslations("core.moderation.account_status"); - const { data, isPending } = useQuery(accountStatusQueryOptions()); + const tSettings = useTranslations("core.auth.settings"); return ( -
- - {isPending ? ( - - ) : !data ? ( -
-

{t("signed_out")}

- -
+ return ( + <> + + + {hasHistoryOf(status.data) ? ( + + ) : null} + + ); }; diff --git a/packages/vitnode/src/views/moderation/warnings/suspension-status-badge.tsx b/packages/vitnode/src/views/moderation/warnings/suspension-status-badge.tsx new file mode 100644 index 0000000000..d3f1ba62d3 --- /dev/null +++ b/packages/vitnode/src/views/moderation/warnings/suspension-status-badge.tsx @@ -0,0 +1,23 @@ +import { useTranslations } from "use-intl"; + +import type { StandingSuspension } from "@/views/profile/warnings/standing"; + +import { Badge } from "@/components/ui/badge"; + +export const SuspensionStatusBadge = ({ + suspension, +}: { + suspension: Pick; +}) => { + const t = useTranslations("core.moderation.account_status"); + + if (suspension.active) { + return {t("sanction_active")}; + } + + return suspension.lifted ? ( + {t("sanction_lifted")} + ) : ( + {t("sanction_ended")} + ); +}; diff --git a/packages/vitnode/src/views/moderation/warnings/warn-dialog-parts.tsx b/packages/vitnode/src/views/moderation/warnings/warn-dialog-parts.tsx index fe5840eb2c..986cba6576 100644 --- a/packages/vitnode/src/views/moderation/warnings/warn-dialog-parts.tsx +++ b/packages/vitnode/src/views/moderation/warnings/warn-dialog-parts.tsx @@ -54,7 +54,7 @@ export type MessageDraft = MultiLangValue; const RECENT_WARNINGS = 3; -const textToDocument =(text: string): DocumentDraft => ({ +const textToDocument = (text: string): DocumentDraft => ({ content: text .split(/\n{2,}/) .map(block => block.trim()) diff --git a/packages/vitnode/src/views/moderation/warnings/warnings-api.ts b/packages/vitnode/src/views/moderation/warnings/warnings-api.ts index d12952ef7e..f1ffc2fcce 100644 --- a/packages/vitnode/src/views/moderation/warnings/warnings-api.ts +++ b/packages/vitnode/src/views/moderation/warnings/warnings-api.ts @@ -203,6 +203,48 @@ export const accountStatusQueryOptions = () => staleTime: 10_000, }); +export interface AccountStatusHistoryParams { + kind?: string; + order: "asc" | "desc"; + orderBy: "issuedAt" | "points"; + page: number; + pageSize: number; + search?: string; + status?: string; +} + +const fetchAccountStatusHistory = async ( + params: AccountStatusHistoryParams, +) => { + const response = await fetcher({ + plugin: manifest.id, + args: { query: params }, + method: "get", + module: "moderation", + path: "/account-status/history", + }); + if (!response.ok) { + throw new ModcpRequestError(response.status, "the account history"); + } + + return await response.json(); +}; + +export type AccountStatusHistory = Awaited< + ReturnType +>; + +export const accountStatusHistoryQueryOptions = ( + params: AccountStatusHistoryParams, +) => + queryOptions({ + placeholderData: keepPreviousData, + queryFn: async () => await fetchAccountStatusHistory(params), + queryKey: [...ACCOUNT_STATUS_QUERY_KEY, "history", params], + retry: false, + staleTime: 10_000, + }); + const WARNING_FAILURES = [ "already_revoked", "author_mismatch", diff --git a/packages/vitnode/src/views/profile/warnings/standing.ts b/packages/vitnode/src/views/profile/warnings/standing.ts index 2d55c6d629..59256f619e 100644 --- a/packages/vitnode/src/views/profile/warnings/standing.ts +++ b/packages/vitnode/src/views/profile/warnings/standing.ts @@ -110,13 +110,13 @@ export const standingFromAccountStatus = ( can: { lift: false, revoke: false, warn: false }, hiddenWarnings: 0, own: true, - seeAllHref: "/account-status", + seeAllHref: "/settings/standing", suspensions: data.sanctions.map(sanction => ({ active: sanction.active, endsAt: toOptionalDate(sanction.endsAt), id: sanction.id, lifted: sanction.revoked, - liftedAt: null, + liftedAt: toOptionalDate(sanction.liftedAt), liftedBy: undefined, liftReason: null, source: null, From 2cb7364d7629d9e5b58149bafd97cf502c5a3dac Mon Sep 17 00:00:00 2001 From: aXenDeveloper Date: Sun, 8 Nov 2026 09:04:33 +0100 Subject: [PATCH 9/9] =?UTF-8?q?feat(warnings):=20=E2=9C=A8=20Add=20member?= =?UTF-8?q?=20warnings=20management=20and=20improve=20UI=20components?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Introduced `modcp-warnings-member` for managing member warnings. - Created `modcp-warnings-table` for displaying warnings in a structured table format. - Enhanced UI components for better user experience, including `WarningFacts`, `RevokeWarningButton`, and `StaffNotes`. - Improved member selection and warning display logic in various components. - Refactored existing code for better maintainability and readability. --- .../docs/dev/moderation/issue-warnings.mdx | 20 +- .../content/docs/dev/moderation/reference.mdx | 2 +- .../dev/moderation/warning-member-log.webp | Bin 49988 -> 0 bytes .../docs/dev/moderation/warning-member.webp | Bin 80526 -> 101556 bytes .../content/docs/dev/moderation/warnings.mdx | 2 +- apps/web/src/page-cache.gen.ts | 1 - .../moderation/routes/warnings.routes.ts | 11 + .../vitnode/src/components/table/filters.tsx | 2 +- packages/vitnode/src/components/ui/tabs.tsx | 2 +- packages/vitnode/src/locales/en.json | 54 +- packages/vitnode/src/pages/modcp/member.tsx | 24 - packages/vitnode/src/routes.tsx | 5 - .../src/tanstack/moderation/breadcrumb.tsx | 10 - .../src/tanstack/moderation/warnings-route.ts | 52 +- .../tanstack/moderation/warnings-screen.tsx | 7 - .../views/moderation/warnings/member-log.tsx | 215 ------- .../warnings/modcp-member-content.tsx | 142 ----- .../warnings/modcp-warnings-content.tsx | 244 ++------ .../warnings/modcp-warnings-member.tsx | 549 ++++++++++++++++++ .../warnings/modcp-warnings-table.tsx | 223 +++++++ .../warnings/suspension-dialog-content.tsx | 21 +- .../moderation/warnings/warning-list.tsx | 249 +++----- .../views/moderation/warnings/warnings-api.ts | 39 +- .../profile/warnings/profile-warnings.tsx | 22 +- .../warnings/standing-confirm-content.tsx | 8 +- .../warnings/standing-sheet-content.tsx | 7 +- .../src/views/profile/warnings/standing.ts | 2 +- 27 files changed, 983 insertions(+), 930 deletions(-) delete mode 100644 apps/web/content/docs/dev/moderation/warning-member-log.webp delete mode 100644 packages/vitnode/src/pages/modcp/member.tsx delete mode 100644 packages/vitnode/src/views/moderation/warnings/member-log.tsx delete mode 100644 packages/vitnode/src/views/moderation/warnings/modcp-member-content.tsx create mode 100644 packages/vitnode/src/views/moderation/warnings/modcp-warnings-member.tsx create mode 100644 packages/vitnode/src/views/moderation/warnings/modcp-warnings-table.tsx diff --git a/apps/web/content/docs/dev/moderation/issue-warnings.mdx b/apps/web/content/docs/dev/moderation/issue-warnings.mdx index b7d934b9f7..4b7a96e2ad 100644 --- a/apps/web/content/docs/dev/moderation/issue-warnings.mdx +++ b/apps/web/content/docs/dev/moderation/issue-warnings.mdx @@ -7,7 +7,6 @@ icon: TriangleAlert import { ImgDocs } from "@/components/fumadocs/img"; import form from "./warning-form.webp"; import member from "./warning-member.webp"; -import memberLog from "./warning-member-log.webp"; Moderators warn members with one shared form, opened from wherever the problem is. This guide warns a member for a blog comment, then revokes the warning and lifts the suspension it caused. @@ -20,7 +19,7 @@ Warnings need [warning reasons](/docs/dev/moderation/configure-warnings) and a m | **Warn authors of this content** (`can_warn`) | The content type's module, for example **Comment** in the blog. Can be [scoped](/docs/dev/moderation/scoped-permissions) | Warn the author of that content | | **Warn members directly, without content** | Core → **Warnings** | Warn from a profile or the ModCP without content | | **Change a warning's points** / **Change when a warning's points expire** | Core → **Warnings** | Override the reason's defaults, with an explanation | -| **View every member's warnings** | Core → **Warnings** | See all warnings, not only those in their scope, and a member's **Moderation log** | +| **View every member's warnings** | Core → **Warnings** | See all warnings, not only those in their scope | | **Read staff notes on warnings** | Core → **Warnings** | Read private notes and override explanations | | **Revoke warnings** | Core → **Warnings** | Revoke a warning | | **View suspensions** / **Lift suspensions** | Core → **Warnings** | See and lift suspensions | @@ -40,7 +39,7 @@ Pick the entry point that matches what you are looking at: | A comment | **More actions** (`…`) → **Warn author** | | A report | **Warn author** under the status button in the ModCP report | | A profile | **Warn member** on the warnings card in the profile's side column | -| The ModCP | **Moderator CP → Warnings → Find a member**, then **Warn member** | +| The ModCP | **Moderator CP → Warnings → Warn member**, then pick the member and choose **Continue** | Each button appears only when the server says you may use it. @@ -82,26 +81,19 @@ Choose **Issue warning**. The toast says how many active points the member has n ### Revoke a warning or lift a suspension -Open the member in **Moderator CP → Warnings**, or use the warnings card on their profile. On the card, the ↺ button next to a warning revokes it and the shield button next to an active suspension lifts it. Click a warning or suspension to open its details in a side sheet, with the same action at the bottom. +Open **Moderator CP → Warnings** and pick the member in the **Member** filter, or click their name in the list. The page filters to that member (`?user=` in the address, so you can share the link), and the top of the table shows their active points, active warnings and any active suspension. Expand a warning's row to read it and choose **Revoke**. Use **Lift** next to the active suspension. + +On their profile, the warnings card does the same: the ↺ button next to a warning revokes it and the shield button next to an active suspension lifts it. **Revoke** asks for a reason, which is kept for staff. The warning's points stop counting at once and the member is notified. The warning stays in the history, marked **Revoked**. Revoking a warning never lifts the suspension it caused. Lift it separately with **Lift** on the suspension. Other active suspensions keep applying. -The **Moderation log** at the bottom of the page lists who warned the member, revoked a warning or applied and lifted a suspension, newest first, with points before and after. It needs **View every member's warnings**, and suspension entries also need **View suspensions**. - - - diff --git a/apps/web/content/docs/dev/moderation/reference.mdx b/apps/web/content/docs/dev/moderation/reference.mdx index 03b54a89a0..46bd1f836b 100644 --- a/apps/web/content/docs/dev/moderation/reference.mdx +++ b/apps/web/content/docs/dev/moderation/reference.mdx @@ -257,7 +257,7 @@ Under `/api/@vitnode/core/moderation`, using the user session. | `POST /warnings/preview` | Anyone who can warn | `{ userId, reasonId, points?, expiration?, content?, reportId? }`. Runs every check and answers points before and after and the projected `sanction` | | `POST /warnings` | Anyone who can warn | The preview body plus `{ message, staffNote?, overrideNote?, resolveReport?, idempotencyKey }`. `message` is `{ languageCode, value }[]`, one non-empty rich-text document per language, each 3-2000 characters as plain text. `staffNote` is one rich-text document or `null` | | `POST /warnings/{id}/revoke` | `can_revoke` | `{ reason }` | -| `GET /warnings` | Moderators | Query `userId`, `reasonId`, `issuedById`, `status`, `pluginId`, `targetType`, `from`, `to`, `page`, `pageSize` | +| `GET /warnings` | Moderators | Query `userId`, `reasonId`, `issuedById`, `status`, `pluginId`, `targetType`, `from`, `to`, `page`, `pageSize`. Answers `items`, `page`, `pageSize`, `total` and `permissions: { revoke, warn }` for the moderator asking | | `GET /members?search=` | `can_view` or `can_warn` | Up to 10 members | | `GET /members/{id}` | `can_view` or anyone who can warn | Active points, visible warnings, `hiddenWarnings`, sanctions with `can_view_sanctions`, and `permissions.viewLog` | | `GET /members/{id}/log` | `can_view` | The member's moderation log from the Activity Logs, newest first, 20 per page with `cursor`. `sanction.*` entries need `can_view_sanctions` | diff --git a/apps/web/content/docs/dev/moderation/warning-member-log.webp b/apps/web/content/docs/dev/moderation/warning-member-log.webp deleted file mode 100644 index cf4b4b3786491bfcd77822976690fdae16734c35..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49988 zcmbrlV}NB{vL>9iZQHhOo0Yb0+gX`eY1_7K+jdskn!5PD>DzO=`@J*g*FI;*S`lmQ zz!T5fM@dpljAsAA)GOj8Xve$$;m#rXbd$|n%X!U<{#Z?-{?<1JAHpg8GoMX2?JN9~{$)` zhw0bc$Mw_w(aMqhZ@gXnqMddA3O|K!R9uRq|oF_Za?^fB=! z{@8P~v*Fj|xA`r6CBOGg^-2A%|KfL@v)Xg{-S{H;rTiZD1kcrN_ksWXecjXfb@Gk$ zmi&l+o^$j^v(@+B*UUctCI5Tot>WX@0_ob_nJG|Gr#qoM&E6}pzp_T z>C>LkKYNw-js9Nph5g;K3w}#~l(X~Q;`i&@;&bEs`7^GeXT&e{R`mVkUH;YY@Eg~+ z*Duje=iBJ>;Di3_`|i*3KfXwO556|uzhA%K@DKUFzYkv`Ud5l`U-0+&XTGn#S^vn0 zpVHsapYosbxA|p!SAG2v`s!(e|I}~A|NaX0-SC_G_FhLy&9XXneC=P?Z9njV>-v0?)&gRi%IzVoEPpzza5E zRmtUr7`H4F@I*{nQu2Qwru4KRd80RH}vgxsV$v1M5H%GewXrKVunNGhbF>Eb|0S|ODRjcCyS@{Z<* z+E&#!ynS+q61M__W>h^AaXz5EN#?FDtM#|K%ZmU{#gzY%smkk4SrYzIa>cdwPI6Ly zMtOE$sE%TTGeTY0+Yz??>*`4kLwHALzPm=VShZwg_$Ng4IBrdq5U1k6<`7Eb^Z~PN zW>@<Y1r4f5Ei9H#| zT#jL{CH~c)qmMjsEP7$9<^w)A$1)r8;528$XikMwoCznm5RS4S9H)ah&4h6N-)=OO zOd`7-f$6z4qyD=y{Li+6;sSb+{Iz*yufYvei1{&iebz0nd58ePI*AG#l}F;GJhR9w z9uLr9E{H6##w*E5rQ$_$aWHLguV)L8$9M`;`}b`WJtNc&D|UudNz~n1pE2aOYbl!< z##zNb_u2n2Q$|E$nKDmwu94#H9^6p#D$QvMb~4*>OJ?FXQ-XW?mjG$bHiI+jJqs-! zwLfkpzPkUu?>Pse5#I3~F-rl60|a__ICU!xXQA^1MXgV3r~&`xvSEk2gZC6`_;oKv z5Bhn`YSS1-1Hzy!NB>-GyJ+19@6~0wIk9k-g%X*?!j|lm^%UKAC3@txaFO@ZoG`Qs z_tKgoS>T8{G{lE&I(zGt4n{6K<2Y`|{>lpJ4CpPaCWyY*9pb_4UCq!!hT(6Jzl+6x zwEaN$X6?UFh=PJ?J%dXVt)Jh?_9}#iysMVK($BBGg6PvxbVvJG#_>7SaUn38ibb} z9NEmXsdBOi$R0qdDw62m1xDr}ldvJG-C${Mg|LHr!Vi9-hT%0#Df zHrTuz=o;W2%R)5#gyVo%cvRU@FLg+gf}c}OPN!+LJe>uu`3s@6>p+Vl+tk632xA-M zikQ#JKxc|VL6oZhy1ZbAnTe`(8uegSD2T(bp*voJSDWp|lCr02Mkylvl#fITam{z# z%pD|eGQ1BFjG1k??(&aoqkbT{bIASh>w*Q#ML&f@9R30u3Vw2Q*FE6)-3U)DJOBb! zf`Ph0qG=;ajSI)Iz+d3nf-G0`vuvrmsJ2ulz=e4I=ZJO0gNCt=Ak!{oc$Hu_mm0Ju zlAxL)--lo?24CrrKil8M#s60h|JFnDXD(`)+m6Xx0@tr8@`4#C;KN2OuHzSW2un(| zEbGlfEdD~C64~Yv`Th|9n`j5-JixJ2> zHv~!U04k$%J;`o*gi_q|)7iYFuI|r&q5A%|Ki4q_&L3$l<_R3yPG|Y)rypI= z$_2W+NXpC6I60Ns6TOUYrP+BmLJJ;Po^ zUqOL02qze#x|=*~A$#C?^y9t4W%H1H#N)bq(!9=WZd4L@Bgdcy4daF;LzPR%{>~#K zC&uwW95Bv#AcBSljMF)RkJf#Fk++WSo%C4&t(!2YAgw{|1)ojrF$~_80D4JsML69uIA9vl?j#4< z3>2(EycKFbu>?r#gvQW<*Fc;5VKB~8e#F!HnI*mv{l89F;~ye8D#T%@A)F#=u~zR| zRqCWc#m8ya28|uj^s(;5>vJm$_Pc;72#03v9naj{TDeq;DroBbaTQ$DFpG{&%JTFb zC$-23DjC@p08#N?K#jM?!PtX+#+zi|lWi079qv&s4qOKb*)58LHnr%n_dx{;epmsS zTg+yjSDZd%yM&=@rDUC^lB#NpNZ}#@rt4w^KOfd%fHf|vv1Q*@=(l1r11)>Bj`c?_ z5an)j(h2~A?X~{i5wVfqi33E&G#bbT2lhlbrD{>6TRU2=JCU|Gp>*?S(Y+NJF3mJ! z93^0q+hrF!8KZM72-D7hW3DxSYmI$ zI?Roo#kWUS5REi0rbBQhvir|!~O(0tAyOi4TQ7D!d*w#&P@4Vg8FAEoPP5P&DEsF+#QD?vqdVktm^1OY1YNr z)anxIf5@t8`D^*GTKxMsQ_GFI1wSLjroThQMAzVk1X8h!rt24#MJM&38!DJNvg^K5 z+u7}Vq1)3K91646wUA`_UBgo7aIF}W*qQ0KG1Id1#^-t}W_m`0wVE_dkAF8O@qab- z?>ZTSOX6_|Je<~G)EiELkg6r)IUk|Vhtl6|gs;D4)8LDbM6{J|FL>jVw$zf= zwD}M&gql5jQ`AS5oww80`;2h^b5Z}W0r@Wm1(4f{S54y>{f}|UUxyX{S(1{UBm>Jo z)mLA3Ocf=UO)3FdO>(u6G04Fi!Wht{%C2h-*cq*$1Zhc@D2-c62CGrprogt>q!EDz zF2@SF2SafYZ=?j;P(kH9tSEcOY|}ahwhW+|A{%Zo`4mORDvE9PR)C|$6sC2ys}PyU zp>ZR_nJvvu9=U`iV(ZVO{#8lKGRj*&!JiZ;-schvHMy>N58o4Yt*!~|(|Dg%3gLD~ zL3apxQ_yn{%o)BG2pIfav?sA-&8VY)=te+pq#LrcfUrOkqb130NI?atR!zKIL z2S!Cl)XUqRI|Gy7SveK1+k#{C%F3OX1@_J&kg(QL9@-3#!74bLTt*_<6DZr9FK%?_D>73^!KHSJmZnQ^y;8_$+_&U&}qgn#GF*|Z8D*(wBX@Y_<= zQOrF_i1*<<2l{|4LyYNZ7_5G%&%lrMG_R~mhYlT-fuT;&6-T7(@kd$k+aBWL{i&&0 zy`{pR&icPZ+TXdaS^|0z58fZ{M@EF~Or;8<>za*dD;_+_o#fvdbLBWLc<-g+3`)EZ zn031xz=x%O4z)(qrbk2bCS=Xv*b`>hX1&D8dW_niUCY#nu*pp_!E@76(DW@JDx$dm zt>JHbj{oPzh0qS+oQB~)lGkkHzqz{s5z-vd5XNfFZe$`skP~<7+sC^be_NZT4XBzY zB1KL+DgMgQH5NAq5n%nXnI5?}LB&B8Y#9*KcRkrA@MUD$)Fuor-2 zsjuxfeB+X!?fPI~nzO6koSBDLUAW}&Qw2(qw4mps>*mz%2bW;}w7MUlTbgrqGn^i< zK*s&D(5+h<@CJ$C##YJb6)&p{-(CSV>`#1Fz)d9uDn#Ljs^LO)I4#SXiyasTK|~P# zx5~itYb#H|yz8oz6mDm0~j*aB1kT%=a zyk11-(%UOh8s?4&S>jjJFKUrNxKr&NHYXC?zujaql^r)OhdDx+xHon!f2 zrY+2+1awPQ$RAe4Bjzu<{`jBAZts)JP9eJROZ^-+YqvoF~n2 z?RVWCmqy?^>)^`AUGAviJFVuy0Jd$MX%fkZj<~Hqevz}YN-wZ{#hwGZ^R{C@v|&m0 z5&WL_vWcwecPuY$5P!v)y(WLNwk3$1+ZgG*HmUSFjbz>&=Jt{>HU>+8O-o;O_Y)`S z*B%6d>>G%W+F^5-E70VhTpDpVa6l@@a3W)8(6R#|+jd7~w!#l*t3fWR=JYhh5WsazY4}#q670UQ_cHB}~(0ug~f3BP96J_WgUd^M5<(e@l*L-{1z- zMHG6VIVF35y$LCL0r+r2FJZ8uNw(f?wlOhV5*?WO@I@VST9gtsl8{QWv@KY{0pz1w zXow$mqyXxJag?8~rjs7{9^$` zlWQ6c0N|?@deiY%M=6iU-^t;H%9G(_Yk~nj*d8NT00p(9B6|_W2O9JwcWsAMvdeEI z2yB2}yaeC_sEyOk}z`@q7^Z_%$?{UmaZXb zN`eE1U}ETg4S-9kaU_T_vtrmSGhCA)i}Yz;qD6v)_WYC9@oqPXF0FP0Le))y8nX`h zq;J;dLtDcm(P5*iraMbzg-ATssbGtT@+$$EZ#5&5vTr{fb1ogbOsXAH^9$X9=mCiq zH&>qnOd{%#j)-miV!QK7BVX+=KKx{S`3DOVzSdNIX8~NnEa`?N0}Of73$kE!#&!w? zOyWLd-KE-UKXfwinqRzWJPY2oTCv$`4c$+bk1^ws7$ZuK`zfIuN>Rv-13DQ^Iwg&3 z?tUUX_EkA5V65SDRwH>4@QNqEbmNq{Bsp4hUc^~p0_7{+*Lql@_bwdn$eq5wO;fS| zwk%7d>&P)3E~qneQO=6<9^!r;UWK`;2a9Ek_yJFvVO$u@O^f;II=l?=+7n_`M}_rV_l25rrGye6H9x0w3@kW87F>1GpYJH`QI#NO^s(}Q zVys`l{^JY5DvG%`F+_zb%uSm_Py`0BowkW1CbXt9F_t<^V?>9t@_ktaBEDtI*1R7< zES#uz@u>O&)>lKEB$Cn5t<*~U{Ro*SUIzaSt0gM&)K>s`9t1mZO5+T3Fe((p5<&b) z#vv8`*7(vKZZ9YO_Nj;!Dr>xOA!h3N>>5z?bX})Kddn(p^+JwWnY@{QilSRhEjYR} zGlYm&L;c8tQ(af5ifRe1EW-h4X9-6uHs)IzSBxNbcEwC1i#K2bV{r?E z1YUg~ktaJxd3r9wJ)~WePtoI{HO1#u0oX;%9W#GuQ;%Q%X;tP99ZKumW9499()Rc^ z%>I-NPI|*1iyv_+KM8--v>UcC6l2%VnS$=h93RqZH?xP zk-v$3V1J|}s^)+9$h3CMg?M1Jc>YE{4I~$0E95T6{|c=?L!W2^KQ=YT2geXbDH9WK ztL-y4fTR9eg zD#5h;v}bHsQ~vA#fPb>(WrPoZnrpEKa0xMKdJWJCB@(j%iLyhqcP^3th*PEJ(bhMjS+HfBlURs@m zS*ZfX_0FSRQC`>6zp|DxpmYI{TH=zo2ytQXT3&4bj;0r{#$;IbFnC(7m3}L1T5O(NQVTTU)2*oj6L}oAFDjjx z(i}ebOOja$yZiuOgywS3c{Ti#bb*iI>SORu+N@8hnz8{+mZB&?lnuU zK+aPF199fpi04;{K^5|Sin{ajG^e3hLi?ic5ZzE6jb%j`*p_1V`nhE(XlOpS@Mcxx zZ4E<=8uh*b4Pkz9C_ZWQF%5Apf3?!GHr);v z`Jl;6LuO|bTZ?TWll|$X6((f#UW!tRWQ?sRD98qi#8XefQ0Tl2JAL`p^Aopd*2Q_O zJl}1PrLCMTvH^gQfZ5TeLRn!nc=g>H$jHC+~^V&AiE*Nc~L$N)pn4A4ut5ymoxG(G2% zi1|u^MoM{O-GyC7mGHas`)@5BY~R8~m+tQE+g6M0ye-R!jHkHd3K83*3N#)$MTRi5 zJll}*94GrFVyK|`XoDvV_lGY~H-C-}w5P8q99^hHb?{OW_;R0BjzMKVvP{Kz!PYTj zD&>PD)exLg&NWCfn8?+<_9dmMEodBIHWu74*$kOWm2K*&npTJuCbPpMU@0AVOi-_5+0={P_k*E>af`#=prw-b%rA*5DGQ zzC7u!{4)go0+f)s0<31PaAp!($r$K z`0iLQiLNsbprELsAc=&QhI9#`2FwNh(%FLR9|8Rvi)ao#?3wyE!E61GD?E|{^^Yn> z+EW?dok3HKGmK$pQkl|aS+Uj$NviE4%5BjdxzZf?f}0i!I<5uYJC_JhdzB(#!Qg`} z{ST3mPE*!>p2T%#MGozEt44i~7^t?hhnc&q;0z~~6)D;bae6E!EZsg-wj|q391~pn z1K(Y%U_^>SYgs~TVy!WP_JGagWCxZ&WC{{^5ln$!6Dg}OfhfU_o4;1PEZq)Q3ue5+ z$6j%59;93QS=gFo(}0Ynkd`hgv}V*BVpg>U(gplYVhcLVm5OL}@w&bh@JbsI$yloBOC2B*!$k zAJ?oA^l3$=z2F|Iw*hhmT|$V~iD>i196%@uDV(D_Ket*)U{CoF3+j&FUcA`%RhyWV zN8zInA>)zd#J;tmy1?9>rsUh-a2;5ipz8l_=UQ4$#?kW>~=5^h-NO zCV@SMv!ydL8I&F^KL8FtnD(@#JAintq!c;o&Iy}L)pvB8qVy|)mgET`x%|-W7qJJ4 z4b!a8@>FnX%>hK5YI>w>2p<$=*F-B@G;rcFn?Er>$33jnC4FY^xd(WqNOpvYoz~8t z1phe~sj=f&PsF`P6wWf#T@Komha_FJSPqDjYBEOwovlal%I@OHKYd!!{^Y`&;V%&u zQ-=d*HAzpX)8#O|N|e%ib?~JlV5%wbUePRD2Z)&^3% zSIiz2`zoO{Bp3R^-FXu&wXa$guXfG;)KMl<7IgLBd?-aDN1h7CBR0^N!;b zs|E?wL#9B`Q%s!c`=JEau3I#1!u{330S*pnDF?$6UEN6&2mdZuwk#zdz(ZmUTHmCu z6EFR6Vu-m!Ko*qQfH1AQs;7IH*I3i&qg~8Q~pn|(+#Cnk-Msg2m;HUDq8(7cxp|(C_4clUJ_+VHr&pM&oN4usBrUf68IJYcoueXWm}Iq3I4q!&KDAuA9+ZuU<=XRoAvG{;|$Yvyk=!`pO`w096w71Xriu`8plhMHp&n3rZV7>04bpc zvZj(h`s7 zDp~S72ljm)!}wdWNSc}iW^p+m`r(FY3(ebL{nFIx-3Ily#{C%d%bE&=pw>= zSL_U42z$0uJ6KdV>?iCnA*A^C7ZhGgnT36(U^LG2`bLUw+@eah12Lc4Q(DAIQwDKh zr5(~>QQ9t$qWEi=#-+ak`XSC^W%L>~3QtY9BNWD*Fd2*%P?jj{XX2uBPuS!}s$Hp@ zZ#Yo~ZIg_9Ee?ptJ#A>8Wh)Zq=w$-;D?y?Ptp2>Kq8AaI4R8y{ab%5G*hgld{{jZX zs0*?UrbS{OujEbj^_0ff%8TMD)AYHvU>W{{m}2k1W!d(qXDe=VqyPAx7y`53vP|h{ zX})NWkqq|Q-~Ry*rKv)&SS}Sw4ORtBv$o{34eE$cE*b0ag#qp%AyT4)9=MMKxP24H zZxm#StpzmzzPik!hSA|TGY1UNVu&8nu430h)OOUiWc<1skdZEr+mH6 zAx++BRKDH==to=)%@PhIgZ`EkiyrIVO|ba7!uydx5q-@55^%!hbN?ti@+}tRaWNQvZ5M7?KI|wd6n;m%En(R6}BCn z!CTiU<-iC?sEPr|lNL*eq3vBiosG|kmafq43tWXXg3TL&XL>1Zun9eip7s|%sUNN7 zY~D&-kj6854sLcTgN1pZu%s(zN2UBkljiyTu8&wN8PCnA07UBeW`nZQ1e1$Ugjp9& z4LArYIt+9wfFqergg&<3cV7q29Z}!lF^0q*9o#inc-i|thVIgqTRe&)(*L^=0fNf? zKsdK0*|eA5R`IMWuX`g$ zSs(Bx9E0gW*~A826+9o^^Oa)qVn)McI*kB3?*@BCjY61PMhMEOW6``MdLEaO(EQk= zanoGt&_ml)_~@OxzPiFXmUqV47R6{~X3;~(yfE9HAr%_MdMP)|Jk%KCY<5-9d_6{L znKo)Sl0f#DlGX%ERf^Sjm3+w!1X=fH*UkAF>Q#E1Wi=OSn zov#8MkCuRkvp^JtlIxW3)AKklI^45)1DtGYsu&7u*9@feO z&D)sc$Guo5rGP+s%H_PG^URXp=E9HZVrkwIzKa3DiHv!?#7g6`n?z^TKubo0oatwReeP@-Thrb#KaY50m zH(7&L^Z6KX)g$hr=b-swK?h19^+qJl7Tn<5bd5CiPPWo8DN!7*Dx(9x1e@`N#2*Kz zP9s~k&CbJw7YKFM0&0FYN7AJ~1B^Dj)I{3Icy$ky2^WQu$dcXNBe_ib>1jzX;w_Na zN0DAjD!3ZO2(e2&H8KZi_gHq6Ry4-vJp1-!kbdaiYcPl+eEOgWFj9xXshtMe*e&w! zV9?Tga%@FYm0M5L?nMR1f&r}K%<`qa0( z8hGj%yZoY*Vz>vOsRdVV4GjQqJ6#7ipa%$C0t&}jQ=Gx<*&4e22`UL@l1+V{xR?-E>54Yc8K{J+*=$09htD1q3yQTA#w=N=;I_D&&FAc zOk{Pb0X4pd7M9-HWqjV>Y3F9`SCC^nS16qwFW(meUV)M7c_9Zg8bc8lpZS^Qkz@m8 zOzJI6eI8(2@D*kkpcE=G(^tYpjm`ZDHd1qVaeKuhDPFm+yo;_tr2a6U$O>q~g3Ick@@!cgZZ|a$=AvO+-HM77hvM@795M%*rrtxdubxfQtA}bs8&MpO zAjh|=H+XJ7vFo#(2#L+VgkR4(!x{FTv$|B029&o#jvbMx zzAxX9y0ney%+sXD3iooDTZCqwsOF&2Y0{1ZMWlMV9$1tL008C>Ekbq2%ugmSE$+6y zeHCE-=rQ0x0^yxavS`gGs%r|C{k87BK^RVhNTl4&R9f78bmX#M4)0dby=F$={p#$} z=xYi%n8JL5{gQ`^bK>Y-VA62Mc@VCa=^;pcGR-wB3;okV_l}~7PkVifwuQ;0B{*sk zilsD;8b1#FH)F$ipUx)<%C(xF&6Su{9eKhh)&IG#Qj0kz=!yh_})ps&+Kc+{`Z_S|yx|Kq@UR>*UYT^{> zPxBSp>;Lctf8Huyz_gZnE<^NsPUYboc8?)LfN;z^L?O`vVEN>?gSX_sD2ipv1L70? z8R*$#BndAB@k5&A*Jx}vcIPUvkE}*dzN#oVd)zyT@LrAhbu3CqY+KS%e|A}0!b~`L z`y^@ENGUt?TO}s>5j?EHeC<{(A0{b`Yc2!b`aC6mt9QbO(iaEh0<0jn@fSD4NoEWv z*V?E1a!oBzBmVplw*Az8X5I`ZKv6C>nK)z|2#b6>Y_lG5uN4StSFp@IURL=T!Z7nP zH5l(Nt=+05N?|u32U?jqGPSX6>Uym&&tK`@xsozByouX&PnR97yBeQWWnKwM;!;aF z;pJRM?^J#-kShe6#V(L3$*3;bFuG%2*LTh-?)-)oB+9#68{F@^aA5@S!S9)dYH0c+ zkgE&P5AQNbrXDLJO-lwc=qit&Kgej34k7)}CVME6AX5_Hk!WT=PikxoIY9@#mXj^G zGt6#gD%y{obPFKJdoj@rhka9M3;L?$GGTd4sorY6D%q%K&AQRnm^L5o4`(lU5o4Xb5}*L#eljfloHL zjaEeKS39Ey6FqF`;v2C)Q^YWiq?x&UsRIOjfH3^EaA@*f8+lQ8}x>aj8f3g0g8K zlnJ*@Qw#_aSE!4*&q+TYAfA)o$T;a({ANxu|2PE^7O{`qg5e15^UO-b?AI}`E|&TU zZl6^+FF}sJi1rkdbbpFKUYublfO^8g!HSOI3!oAqARPpzMIU2hv1hz2ncKLc^Snw+ z@&I*DaULg5O*L>B(}uzOow8a{`SokZ?LgSKeVz4=l~2l)CV}Ee1lhq-JQ3{z`cirrV5Y+ilTW17#E3oE!sWc zr7Ihd^`Kjd2R8P7R@6EJYCQ`4M{;a#&kYVwJ+7yBXOME;$R`H2>|CbmYo8p^gX?h zB2GPC5CZ#8{u9MLc$ylhZOeg`2_?9-%+wNm0Oe3~RTDY4dC{9+Vz3LJdER`(h4DG# ziwtwQ4>i^N5Jw?5*lvfV<&L&}Usg&9@``h*iZ{`R3k4P#dd%$~2jz zzm%7m;xUi|Fa`iFCMtjw$EBNhQFZZ4eG`pHPgcB_`p@->o0V9}H)^~HLJ-y9z;0>3 zHgsr*&rMdny|SbqqR?_CK6`;9NFJCrx$GZWRMlx3gTT$%!6d@iE3nJPf8LZmUehVq z^vZbU_f>ykZV&#-UGC+>V^E%0dJc7e{Ve@h?bChKP6-radCM`y?k^!|$c9CnHovUUY4ZN=Hf^4820kX>G^sDa=_|@gzhxr=K;LBW z^GbHEzqq`g{YK!x5w9b`@ODXm)KLXmYy!8rK56yJ{a4%fcIxT`7vE80E}&WJ1Kfv&vck^OlBm^7m^sphzHx}c;hO;PVGenhIAZPaoKxJU?yXc zon6kPTpnk3{UdK(}<&c9OKlDwWjYE^%bDzR+*_a zk;I4h1NAH@p~a2Cuset(vtTE+mDrI>XiLpo~XJx&4IwH>oo{jn%*&Bri z77kH8_n=~9qcFHm`^d`2Dtc+U>ON8gOX@YFiYxQ?1`2bBdB;(BQ2?NAVZ%B{hVrPd z^QBs!6iYUxG?T_iev9#iX>yI|+w(N4KKQ;My3)9Hk{6?(1NgEk{c7s`Ym5NA+bd9- zt|0lF<6zU1x!eyB^A}+-EJw0ap%3o2P%<;*aer<_V($K>QRa8eIOb{c=Lg2hp-dQs zaMYbXRu+P`zU})-bE3%{ca&>dZ)@`4{hACIjBSgxc$)N149RZy! z*-d-DO-Wc2=mIjtB1qFYi>!vRx3Pu_ONFy4RMGABHm zX#~dIGbPdj0*!`_`kR_|6fN(q0trY>s3v;_W*lmqW?YGlK%XN7qylcU{9G7P!h8j; zQ=Ox?+adW(BU}}xC^@VE;NvnQD0!4j)cKgIUp_oTIJCb-AK5vHlg_vjP#Ax>Jd44h zZ_%kIi!)%0!|knP=dj`S8^9OTB@NJ8N>~o2VudS6Wg?a}YCzlT4-aOK*(%kA(5IjY z6s5E=9znFRT%P2~x+Kf2Gr~`a%Neqo20E{;1l2HlDOq^-+ZyM3q(5c8W$(`S>j`LS zUKaqm=z!=6iekWYZTMbnQ;evX3!feu@ zUo$oG2X3T7C}bYimdP#1J|`l1;!1us;%vxMdt>y|yDOe7oPN{%c!9AHEIwLQ^dt1I zz6$I&9d3pRm8*bEb}55d2RJJq#9$o;<_W079dVH2Pvk=B7w#T`Zq_Fyc`IyCexHoM^Y4JAliIIxuw8r=R`$ zd!FKD3|||SG&te|v=bSTgMWISpQxHQ>QHBur}SZjkT?@E>w-^NF>?0?KTc(~%Y9+q z69{uQ3w5yyNe~3>utZsuxQ011&_3r3r-Rg`4FN6DXaTlynwl|&TrIkI)KjSFRcWOX z3MYz`cNvlrnRZ?5kjDCG2$&>jK{*)3;qK(H63*Nfw-LtpiDi3vdGCi+r0O(IY=2w0 z%<;k{+O*8CJWLu4O0tid&=JK2Po;(@=@-cNscn<&%E>xUZ0%cdtrQE;-V12^9lf-b zzjtpp5{7i;?3!Dgb6qh$@|FwbI1E-Q!_w(hLPN_MU9c?jA_C8Wrc4hK6v-D3@~DB~ zs@Bp8+VZM8C6M5%M5NrQ&z$~ppCTNk10R6Uj%J483SD)VFFJc$od~w;Uzb3XipjDn zYxjE9zjRpr{k1sE5tPfN7ULQ)t)kB0%Q4(x>(QD{=FlZ4mK+;z z-TOlNa8jZrjv(uWA1+$IrJ&)NX_3O-G?ixm46~EALr``p zcyaDg3u2ij22bho4>esXRDqxLrXCFKHBC)T@#Bo94G#3gRlHr+2gvfK?R^cE5ZUET zV0+U^I$V(ri%7OG{Vg4qk+MSrh3`K^N6-Qt7Jc0GpsgnEGqLh2jqQ3yM-R=5dNs{) zYX=(#XeH=%f0{Qfp&Hg3-J<61jNVqvVcjAojvJMbFa3mpT`#KD$(qL^A1lgK^ghN{ z54J&1cH@C3o@`Ft$S8S8=o2@X!RW|#790&@8+YB^ehL0+i1Zr>ENoSnSJEnGmX|HZQZ^#$Wzu)ZUE(fmZyfz|^18#t1d$E*tXnBLZx zU?Uvaj~8tOm`D$B*-d*mYnp(&bw?glR!YsPMaR-N9n7-_B{g&2>v!Wsu`tR}_DAOY zd3sg5(PWchfy4ZOcEUF@OBLt&8JBnW3eNe5vkXPmrZ>M=D8E#ad+PNv3QlhUJMJ-6 zh~kMdeIjDYPspJas5)GZfh4Y4_NAjRg_1(CXPos}?sT#j^HB(N7v5=>&i3ks_*crf zEUzG#;Nao3d9ifVE>77-_;nMPQme*cIx{3QWQF;!Suws&L6n+gF;K9U?oaB*G2ijZ-|Yn~px~jCS8I za=H_OeNPGmnKy>0czm!)@b33*93-BU4H-@T3Sc`Gh?uZC)12PFLE^7Ma6hxF;7E6a z|6QpaRoG1O{vI5=lO*~Z>Q>GsP@g82LaU))`v(9(7Yno%3VCILNZrckzF_7BMF(#@ zLL|eME6Iv=a z(h_2#BOX4lC0!)MrlE52JKCI#ZmfgLiZ+K?>V7|4iOsD`=hlD(Oh@L-8@#TW4@8eF z7xqwQcbxepK=KfWqhZ%IGgpwG71ibomf3?v%*0sxxZX^f*?Gr>yG?Mx02Y3kbZ04O zHo>05&*F))V_LjrQy)x)=*)3LK}&%z59rHq217sm;j8NfkD|bkmJrpRA4YZJ`!1;DM0jF((kt@d-?X z4Agf;%Q=+}lU@OZ+FK}iuW$+=jKg`N*85yXDBu*2V}hmm~n<1_6C5 zHmiF-0C-n`Lc(VR&Oc&nDS$NyYP3YaIWu~W2*6QK5aUfi`{Z+3Bdx`t@g=?FQb}f@ zh=jak-g-U?f23Xrue2G~-7ahyH^2C<>Yu8mJj7$gSmftuEt@aMoOe+Gz!TG~ZF>$9qP%|LAKH={*@vPLDU!J7 z9!49W5#@n3<>HmFd$8rO6Rrgv#=rjXo6yywzCB>P**;6E? z7=-5?o{z|8?&+sc>-$MFB64S+bn(QYjAEu~vGiGpl5!7mHvNeHu-R0nov*biNflgC z_KHv1L7Ky+p-9KVmu6rAJ` zhA@Lj6P3SR?|<=iPD_FSTNW+bwr$(CZQHhOv&*(^+g6utyK4GIOx&l5$zRBf#L2bx zp?OQRV^KRd=|cfXt@%S)vz+_0BK&+A1PP8F+``-uZRfJ2bq|GQ zTaSE?2r`CDhKLf;o2vB~BoxNp*s90Z2~&dIn=-N*$xl2ZNuRJU!q5a!_nlz*)f!i= z_L`@ly@_tmraJQAhGf2;embZ5n86B&ormzF4QEr+?KOxkIS^*R>8Fi}>@LFHd&uY9 z^bg+BwVW7-uN@zAgttGDp5@mR2lo}ZxB(H@UPp6z`Pz<2jy!bHLXGgg6+T@R%o2#@ zFhOkyxL;{+3#$$o2PSJh6HMr;Nz8VjObjzsoW7xD6LxPugsP?@TE=lH*z|Hs4@B{# z>gDBavU*9mrjsA}6@Q9*3Z)J7ufO_o;IKI~^2!w!YC}C2bUKsc3j;6gkDS++%1|YE z#Mmr>eQ#Ydqc=(+WhA{r2oD(ff-UD51!B7tB+d&TVOs)N_l(S4DRaXtaRkh0a?=j5 zI<=>lsELsAGjJxd7cTAMa*N+@#yy-|F3Tg(NZzaW?L{^?J5FT%K2-lx!CJH{2B8NY z=5kgRAqv?+!jW_1pW2@!Je_5@r;L<~#Q&z5Hw#Vi{b03Xg5iogpPxf zGn`M6A`1AAr&9t7!8%cnJ=h4p6e&^|(+$OR9X2&bSlFdmni1`__|Sdw!i2SluFl$X zE0;<;o*HdKI3A~M>omB7su_6x;C7HJn_x3$*lE}4Gk33y8k$o~H}(S5;Mp%sf^iYY=!h+ZHf6N2WD}7Q zR?ayd`k<)NkOP+ga6l>^@}v{6zO_$2?hPHC=0`ke=@k`~PYbD;v7-dc?1zP3UfUPE z8E-N!yDA|T@cd%;Q@W*`&Fyhsepx*RUs#Y$IzgrBOM&Rxh4uehEHoJ?Jwudytp$?d zcc`o`DM|?1(*He?@c)fXUa*{zm);-#&C1IDB(?C`L+8j{h0&uASX3plLJ3LG?AzBu zARoqpi3!AoXPe}Hi(7Mr{=-qrqG@fX?dFsDfs7^y9-=$vWgxULFbq7u1;0Xp*2)cL1DB(GV$=F)z=W?C%`Q>sgl(H<|RDO zm}+R}EII*t?-~4pvX!ZI^T4^|eOhsH>E^UwwD?T1e|~dyv5-k<>*cocV5VYdhB4Q9 zBmhkIE++OeXHy4jkJ5q#QkPl#fw=lCL6c}nO9YZ^*C6lg`Kl$Vdj{B^fbVc?fyU*m zj<+`%h+XEq2%$*tQ=`yP;u!Td-eINS!Grc1REkamv|?$_1uWnKc&}b@I~<}EWD2yw zza&9Jm$T@DL7{ZWV;pyDW=oj1u&0a3=HT|YwB&Rb3&6MLu|D_wX$R5I`V;nyYVtPd zJDQ*1UG*dFuk8aAixCS;vOy!6Hg?a2vfYUpGf|FAF)Oi}=o6rK99mwd#ze>^BuIaJ z&Y2mv{C)*u=7s=fBtN#6oZTL}%J&3ZdZ-gcVDw>LMDVj*^M<|b}U z*|F}0b340ztB(U~!|nJN8@MSG5?X^vF~i`a*#9Jf7FAIhhNp7S9*qW$BVuMDh2nch z*=VRQYOtz7qGh_Uf!^68OB(qb$Zdb829c^^pLZUw$U`a_AlJ;cWCuKK63B-G# z<|?eFqUqD45bHw=Yv%f+p1^5zm9m(lxU^-ObH_s>Q8ZRer2ai|dNBZm{`#{ZM}}8{ zurd4&iIw4nNHzp&&EQ;({OswnTFLh4VCP}87$Y7vGEgZZv0Tz`w=Tze5z&u|SU~B* z$g(}h(W$Bk%E^R9)1$mo_;tVXHO|Y{H|cLy&8IlN&nZnOe{C=K`98HFByK_kT@#y6 zP;-wqPJG#?gU>HP{ILKiq2vOP?6z?#e}7&883C~B1nI}Xx19Q-tKAIX1C9Qm)8zFM zT3IfvVJPl6pWJuAu=>jkZboab5p;;NXfKFKTU)nKxT-%D1P>Sk1>(Er)9%PNPqu4I z>7EK=y62N_-Je#k&qNbPLzd(ivThXz5X+Bu+iqXEfI zVg!0BWRvj&!6`eTmhes&I5Wk9txpWjg2ivgU}mP`hxEu_r^TF~@c9ap$d5DOpfDZ& zG47|_eMWZV!d_Oalhw|5!^o9V2|+#K9p8`b@U)6bauiBgB#es3gYIT2%XgO=ueH6c zl^5IJJD3x%qqsWjG`OrJFOe`Lxl3`CJY2 zc{BO$Y|a&9due#bA7U-b5+;lfJ8c0$)sYt*Sc_4C%dhi@Wv?7+|Cirtwv{joN@fNH zQj`nC($Vqj;CD+LS2%3BbCodYan!S;WxAs2T|x$2AMVy4A12Zo-W|%tqbkCBVi%k2 zJ5&UxG0!`?NgH^6;4}Tclx6$01;@A5LxPdv|EBWN+m8<8$Y3%rmibFeul7mpdRd)mOlLhjL3lmzv=Lmk6@?v9K?se#o>6gM?7keyYOoRU3k51UWp#zF9~ z$wY1!5ucv;i3iNbSf27*)YJKxxO%5bg}`6f$x_g*iF@W&zi_yKu4+eeU`0IF{4WRC z+GoiMInf-+mW*wQG6$s^i7acBXJjaD5foRf+#mr2!Cd9MTuZ9LD~+{2sqkI znjj1;#|&VA#XtyXb6t=}kC;f|&zg3Pl0Gz4r;>!_-~4SD=aH8ZsnIH`lis)}!!#gj z*_IF9#~_6MS*#_cl{v>}+cVsBQV`)g1)NH*p368>(7u+|TGoi~vfD}C*-p7C2;V&^ ze4koOmk=XFKi+M@e$aj9(yEd9%kNA(dH|P-Zu0raAPf?rj4ps$CKYWZU8r2bpV}od zm@LrHyRevxiAG4Q4kW-dH8L$ZH-~vcN8oT4P{7Lal~W+KEvjj-NO6oy3JZK)CbuNn zIh8=JJexdSwPz&c=3*cXVLz2t2g&gKRhNZt#2Mf+Q(hyzhhGy=@bwt{@L|F^FHRAb z4(EYq3rP>-37#lhY@&dx^;Q;%uVw{2B(jmppbOp@#Rb}&?zdBF>gK@u?MCn2qh`$G zQWd=j3BBpQAv-^x6k(Cig)wjScBK}h;{w>gUiec^jFg*|pR@|1tJ@2~(C#Q|EHior zK~ZYjw^1JT$}?hWH<7e{cL8eda=iZ|9z;iZk73c!fi0Kmc0JJ`-hm7rt%`>Sb}*;` z_OlgKlUWfz`rul))X!%xyXe!4^Prdn0v+~r$U%3Z(8!w9N|hs63CSYq5a)Jp@AgEA zL?9iiY5;*Ck^easJPkl>2jh3bk~Vf^rltXA%r7|) z4bw!Hf)7vY!CfqWBZ<#Zj$<1Tos@A)iCJj65LtT`s}uG`<}hw->rB6n8O(`(tyWQn z)K&dD1lHsx%?#LT!_Ah9X$4EQAHh%!a!280gdWaD%HJzj3d%6LM6)no-6DN#d`r7; zFVwMrlY_P~T)l9kRU2A&R{|oUH>KRjfu%l^va%UdqEVOzf(nh=>*SkKcrY$)>ZzA7 z>~ObnH_Cx{vrIq>GPjRg+pAjIFHP*DSm5)#p#;?xd#jcgky!O%{Vmp_Y2{S-Pu9=0KX&v3K8nu&_t zmsM**)?X_y7ZW+vJo|1fbACZVJMuQ7)=Z_=$+$6SYw?2)(^@;lG@|->E$>z%!Sgf9 zOgs$+Zp&mF+U`aceb%)UdFgVmbU1=u@KbxT*!Nee+-D0;4;y1F%FJt~pb0bQik9)O z7|kk5NyD>qnWfoLh53(L&s!$e>?5?EB~>JPq&xU|c$Szzduo=Ak%*7m#^0`sR+v55 z1%mZXHj|Jf&rF-7LpHNu-AyQo*wiZBw6>-T0tobsvwu zZD&`@#zZzlvs1rQA+am0YV>5yZLL9Z1%R&vfXe{-FMyGDki^Y6>Vfq{)=VxNrQ0I^|;oNUw6InSOG@7uI0 zdYY#Y<>$8KEE!wVB~QwTV#`kraWi=PZa=iOI_&s-6uFn()AIqt(>vj>uL^h5@KU@+ z4BD(-{5ctN2sC$_#6B4goPY(pDAAUQx5|rVVZovZPJEskd9?j&f@fy}>flXbyT(P@ z7nUzLl5>x`?_}=43@66_Ec=drRbV!{s|ZQ zY)T&WO6#t-3qV=0RDpR2H7zJ{VFr$5dyUP|rPHeAZv-jwAuKN*T;xPXh5jY}t!X-> za9y8L8lyy30&r4j@0Acz=nH#k+@gH z-1D`-Swy+y1Jmq^&4Ua%msvtL6fHg8t{BT4Sns5DyMb*LH>Gw>!KEx3QyC7rbUz@s zQbK8a!$C|@WvNlk0^?vqsY?~}NLJSgV)jj<3U7#LG@5BtGzNSmKB16i)rny6pOY5Q z*^5kjkUisn51NU-xT>b11Nsq_azHnStb4I{I{LW%w74c4v@0v0+opej^z7e?$}taS z@PQ(gg#z6EVT_X4*~F`Cmv0+k9Zglodd}V1TwR~OleNwx55l$>>w{B)1B2!d!CXI` z(<5_izz1F11_TjCp}iLuIg^g?^U+JU`$rDtUY73>!%$e&HpCzkIgA7W5e{+we2eMe zXN2#VtD4Dd+jPstlj7DrHq@Ar3cQWo!mc?(!(t%$b5xHkz?`qFan(^>WWmeFD`GPo zKi0_MDzn0HViXmW2l^3yhdaX$WhfiV5n>LRG@82Q44lCzH&F4Gm%i&WqbVbGoP~;Y z|1)ktZ(73s#>aDyL;1YAnq9S@k_FfSZy@|k{YRi#)5Y>m4F-9_$FMd-D87_I_hH*| zSIJ*)ONLEpd_?X09$d*UR*m7K0be{eM4350l+I9ZLY1-^-WvqyVnp z)aI2;%dQ^*Fj?l6i*9P+Nj5>unvR1XQCSMPey>Z{O~GBF(F6J&1Z0$n<(KT?-1>Yz zTg35(^q%y?fD!yuLXPK!dt+oiYvwx z2KZp*{jZ2%AJO14r6`=y1M^H=;G=z5pwFS;QVW-OkhYdE#F$7R9HaWOeka`GQKUIw zM0xO3X#gB#=yzHuw?j?b84LhMk}JXvuxUc!$FjUtGOyh3X&#st*a>*ILjriUxmmp(KR%6S&bSUeI9WNI!4626|^|e(?&4)GI}OR|O5C2ulcNn~-nP=qf5&1~J#a0Bqzi zyu^Fz!pF3i+S&?~6sCq|EEv1kHP<0(Y z+koSsIxaHB)3Q`y4tVa{QsTgg=+Mzxr5=ysMXw)R$KOm3W0;zTh#SZsi7_4~Gb^58 z`2vLQJXztaCCZWA+{=%5B-RyXxpOW!&d8m*Dtb#LJPx+0*v^k5;N(7Dp@9CBlwukO zXi};5W4Py{v?JijtSH?EbexMmHx}fGe^$;WCPNpF>ihZh&%I9aVN{E$&7h^~ETw zt#a+&0}JjOPMw6tH3_xgG6PkhD+{VEQ=N3G+=!g*v~VimJH;W+Dvx*u%sXKU8!=7G zoX0fnFTs1P69i4t;)yrCpui=4_pKM^{<9Z5;nq!5BU@Y00-^sxr`**EbNk)apeu1w zgx>_`oWK$(6Uh`l7S+Vkl)iQ__as}gx-utz4`JsnFUs&Zr^cLaOx%0@i{Ow}&^e~< zbyV{nj}_#UTPd*iG^_IJ}t@$cNomr7m`c`2~4vNju{ z4<~X#l=Pzm^YqCh_>OJ!69Not_>A}gT};Q|7@CDKEz*8iNN>?Vrb6yG1+@r^;J3fr zA!mm!ScVghHf$Ch??0cAZwLLtEhSBmuV8@Pi4C}Fn-S#){H?|bC}-L`zZMjy1X+!u zLy#Mv=H;PRJLp`Sd@2I*^*e74x$r?Wy_J+F>$`tU%k`LMlrA@u?7`RHm+ar%z%U7&B*7myN)(}Hqe`QGm^ zi%R29ss-xi&kezCbmBBoouT8zCGUD1KaaEa(hKYmw(J>BS#YFDI4}`vy-+}tW%d-; z>vhp}i2(wx6-(PCxL3AEIxw_)R|7d}!d5PE@HNj^R%87Z70nFM-D5Eqb^1a)mn~~r1yW}-p?WCxMh)DrIrws&KIzlZ z*<359x7m%7tOZ7D%?;5&3H=ONk_`xd6_3KZAu{QWMk!x}`zSn_Mi|Zio&n_e3uMEv zin_%FEQgtxrh%xR4u*HNC8HWz~SRr48QGnjrHOI9L(P9cTB7SCqbBf1N zWvDAPgfXA-T!_+GZ$^Q!PKm^OdkxCy1z~6GfG4n_O?WE{!2W1p-jKsm8sBM@3K#CK zh~GTRG*tfyR9LCt$0-;nWSNfFI)h~ z%c{`Wk~o|LrZ*xA~z5NdAz_6h;vrL|45etiCXmHfJaaGiC+l)Z1ix-|5-5 zIq?7zi0|OrxiTxVgsJwIFK%!21hhHzS#xNSsDG=)Cz#yHN!^Bnvb^7&IN8{MJ&;kU8u{zdtI=9K zyYA}qXV2E*>%u|Jo9fu^lJGc6NKyrF zL9I5x%&^yrSaN&Vk-62?C>SvxEHbLTGQQJE8EZ}9qsj>{9SDtMnW5-@fxgtdCm(L_EHuHkIu|a&z zkJw4d*hT~1TzFBE$CGtPvmzDE4kD-%kxe$UUxNrh4Uq+J9CxVlP_9UxaCC#c-~}1P zWYRdv#uI2eQ7+xxY1Zx#h@G?C)wIoA0x4C=t@K^mC2SO?nuVjMv9{C^p`%UWuu<$D zwXbhKi`??(_|!I4jv`D7psukS(H5pt^4hrZ6M5RGM$!I$1WIMaF=~nGB?n-#)K+-j zpvdfj2q!Y-BBT2TR}e@h=w8vbi>W0JtB9W zq|9L7EF4fc+T3vR9s;C@O}fCm@I9 zY`ld@d{eHtJ@{&7ca2#u&^9E47SZl9xt-(L6lMCDIN`v0e8X9PH4=C{P)ew2EfTf( zV=~&9PIOaPbotqUMYLs1t4t*N2Yr?fNWxY21|tjU)jbS=f$;j{cXrU54PKt zZ@Boh5{#iLi3a=>Hvc+}U;+czn1=^N*J`UpU|LD$;CS)H78>picFlSV@p^}BAV-bwC| zw@LwxNip#8k_II(MVQpSH1S%OY`paae0(4KK=U=4^Ub5_Sg+mcKT<~QR7?gei;2iN zZ*JRRs}-+hdWbg)vkvNdRRmj@$hI}hgfv}%x! z%QO?p99hg-RKS+A^^A?9Sd)GcOtpBJ8I@hlThDI3DS)q>I|H|j9ik#vLj-jyKxBie z5Fub!n^whl33jd0gWPF=$fV)+vS@5pS+Ms=YJr~HFc_o@yyNniVK0>x?;yPTZ_%o@ z;}y7G2*$=tpdlKbd;hPYD-?aB8!8Pss+hOK8J;9yv2K?zREX7$ZrNm>AXl0WH-Eti z$r{q;7T2Sla4kdoJfu$WKG5Lf`|bbdT$4Ue|AAy(Sz5G6%Ld^Di6AS?OEbmF zZT89MB^8g25WIt)W@wZhocM2*V6Y!p{NdR{Ug1*)o*q@b7yTRJkI_{^Vc!v z6A~Q_Nfm7!z>#D64)}H7P5V{C))Kw*pq4(CL(X1$hbcNRx~P_(MK;4czfoQL9W+}?yD{3o^j7uC(Bcw^nRvH`%X_Z( z;6QY*$f*@3f5?$d9ruGc2 zPJI@%);r<9L5l2W$d76aWEl!n241?QR{(<0J8$Vt%39DFq`u_b1SWRo!%e6>!`ER% zWm~-ocs@t>0ce7DvU%{+B(?GJ4cSQzJyM>Un;Ikq+q5xvU0;(Ut?m3<5{~3G{GA$8 z-^cG<1rTQ4D-^-Jv0jWV#{Mh$rK09AgmbdfCG_EBqqq_d>Wr)S;hmre!C7xk^Y`Z> ze=axX$tNK-u3fN8tAxlV@2~Ng zdK@qK(&?#*EgoSy1B+&4ryS@2Zd5^_~5-g?$Muu-)R57UyXIpR6ha3``)F@%*DOsG=8_$kV5%gG)+1A-f zAi^H&^JKI0YIa)(2xzT6=-!{3-xtyReQe@KWa4xt_)1IV2T=wRio2G^5U-u-yC@vm zXB2vLZfjl6CfW<^;`)}N@d|D+6cPvm4*i|x^~vZ61u~hl84a|jXWdC7*kp})baIoT z*Sl}VK!cr}7rPGnMolKlaA&MFAe%qSpZ9oc@bK_zNnK9cTH${rJ%(trtbYrB5DB zsQZQ;hlw-$u8Y|pyPH=RZim68QVwkYz<9u}`j+f30e<3DOFBUQwFC8yn|b2uz_EqB z_v}4k&;OS1mCrg4S?JYe+qEoDYyggTrki{`w2*E-RZ3||t$I$tyfqRYP#80r{o%5N zs|_paRR%M?pe5?^o*mR?j6VzP(y@iZf$sj2z_QP6DX|;N68KBav=-_0(432aqhmKo z-D)SBWBLP~gxR8qDl428@hphSOBmwx#5zdQyafTeDxz9-SC(BHpKgTE&$BEXL&Nk= z93wjowYnDO*(y{FwW*IoQUtwk==rc+_xB21@wHry4u|ephWD_LI|-C$CnK@1vgwviiof|poP>u!&+mP_-3bMG zhYi=W1*`YDxW=vQD87wUXw@l4)M-*m&Kr*r{5SY+F#R%1u*_z|VAuW6%Jk}juljl3 zCD5Hc2S@o7nZW)uqI~%v5HXD;d^Eipx@5OHjvJEaYXvETS8Ls~c#O_)I(U&fYK>}D zaAi-6?NY+RT}hZSsr5iq#b|uyFyp?N`pa-vBy5^50|h>%QqeWVM2T18OW+oc@Sma^QQvMmt0^eBy09J%w6hoqKbaMy>SE6x;}tRwKql_ z^VrP7ypZpbtv%jKEd$w?;!EL#^Qh-|PRUP<6Y~vyRyjMl`MJTS-XvPhKyhdJIEUAC zDAjO={HyX)(qh)O0!m2lCEFtPio>g(PO55j6BM1v;WnwMSVj{fw{i8w&2p%iSRsk2XaYcSh{JPvVtVnQnx56r=f% zD-4s7f1|N;nfX%FVW}C@p>CRDs=^Pu^OxiaaqDFHDZ*O~sSL#P34rsI3jJY4jaupC zL&g(wMIl^C$*^HUY$h(l-KNB`d&oMKugqd;V-PkswTFpaA1e5}% z1<-A!A{sL|lMtcv`AaGIB>-j=Nm_3FkzWA;hsP4&`a7YFOX<-4bcrp^-Lim5IB$cZ zTyzl)_xF}0A|*n6P9q|16i{S_=UcI^{MS=Ctx+d6__{XjWqBD_%wCTXt6$b=5GU+% zEa?~F_#roauSqbX`_wF#r^YIFxk7)NtN4zjC5sP_#yp@Y=gb5z3P@C==2H$zV)jzq z#EGyrmZc99_ZX%b0@NEftvzEWMn8T9n&ZnEAJPybY6E;r70F!ZLKr>LCB|pJDzal! zRmj`qD?)>zo(K)miwlleJ!A@5eoU~>Lz;T|J?3+S(a4Zpyl9$FgUqP$V9&#WOz@V# zwO%}~(r5~-4%+xnSS6;4ao>#hbL~_kzH?i7!~^4enOdk^A;2NL=tsAB5#sj4OYUjb zsL{ktBS?;&KvTr@WLZy(mO{g{0?HH+z8S`+0S=<5h zDX2`BAl!ZA7&Y@0u?YZ7s2MvYj{iJuxo;}^0F_Kxh#&~7ONX}HTvvq0uCO+2ke zoSITEFd3CF(NuP$YZ=KzjeN0MeIRO&OC_W=GLcjTj#%&5fzCUqEHi*F4f49g}r~(Jzj{NB=$)${7pmJ9m176HEiaz+@rN(mg!-|Hc zaypo~s+na&j;(mgd3|H8$@QP;u!u;R)dbY?1rMX8UY0Ku9ottMP=3a{Ob^aek;kvZ z@gKNU(r#?zzb+3*h6xQH1|gdDwVWlcsp*wBP-oO{U~$a{2EnaH;+R7iM688=PJ!;M zg93z2A0~0pb4|UQO+{E^%WP;YmNHkZ@A6?^39l?2{wID)uJFcgx`p2wLjM$8F)t#Z zKJDbRc6`&8Hu9s$^{oktt4EdKf`>ku)7>{5Dn1KWqudgxrw7a~aO2G(j@t z&Edy)QTG=Rt4A@Fnz5zNz~ZJj<2d&_?=35CTnnwqL{M&%HvC$B&d?|UOmy7TlTP8K z5@uFTPE&oil%TH?!#145RKt+GC}uh9YQO>lm!AjMVayYuE1%1ngz?@ABmKiHF`q(b zt768N(pRSx41~h&mN}dhrCO?*7%W49V+NQ#U@%)1OC9T;j^vB~Pxni|M z8he32_>O2V^0z7jJD0IHwHTzwX8X@~MHe~Nj<>E)(^8>CN_1x5kL@Bsd(uNL`a91+ zOXXlsAj{hJMTP7K(Op`jT8#>tcDT(iDF3+NnQ1~$U!NA0a)tl)!n|@Wtba{c_%~fF zJVEt+U>C&QaUcPe>h?c7obZ`Dp{NDTIBS|U(5hk>d51#}yJH*4ENIB2N~*S3G81So zl(?~MoXFN9u@-@j-pWF4_e_?iz|B|M=EVRo)_rdMVcu~gDW-8>b_MUl^{=YtVT`7f zay96)?(dQLx`3-2GD$5zt~<30>pXsH27R2>h)E=4?OBXq7?-VQ_AOE<{gd~(yo*=~ z<>0t2y#T0&f+De-##Wp(5Bz|8%)(Ieg zsF?SI5gTKNksm}oi6O99%fT3vs{9N^-qbGV?}&VwOXo2T6l~274ap5%95wm1Kc94jc@3FcK?zjTXGQ zO9ZjJLxmJ>1$BIn7iK3&*uIqxo`8qG8UAzcZQdcjA^GiO<@UznSmK@#>WwJ!XbJh@ zmdkoyQj&g3IliytEc=3Z`P|t=#x27a7PZQ`GkEkp}t8b{K;VYBFn< zrTBs%ZeAI^$p-b>JVwpLfF@9EjBEq-QHHQDnnS|UqARKG3aPfm$qaK>XUX}uP?q}I ze}{bVdC?r|!_>zcv&qG(=iVrkA}c+GnBo41!*&2@UY*H{h>6Z9a;8NM7clHzljh}P z*1GHq)#Wz$AMbZg(LC9b_?5+fiUII4)#vI|BU3eRKr>?EsfF`P9pP-pm8rSxp&hnh z+%@^9h?bPFu-4>;&R!+Th~-3|Ext{fw$d22&o*B1IS4!R#-Hyg9M(54K&@7O(C+!% zWBO{?E%TcJx`J3{&id2CUNGjRYhJ12z1T0YKYrLPLZ|w}c>SBx`7GpGS$Nucpwz)n z)OQ&PJkp)2gB{>OI~CzH@!!!o9a9rB%Hk1XWa03DhxBte;grr}*ns0c$mv;spbBV$ z>ffr;*P{M$lzzSk_IrqY^9)H$$t_4Fl93{sS(kn516@qo9- z#H~&;LGbgM`*tx5fMICdz-0-zzv`gDbH(tB%zuhNsPMDY4s12p3VVeU+`i z*_MO@HO{D`o@|@<0_-_jk&nEY!3jtVp?TDjqC)%2;^#~>RpC^D@IrLpMr$+}vHF4p z{L00>j77^#WCZt`har)J^=YBnzXrHvxb}%{%T-{$GLuRt+)+pL)0)at{+b+ld@^Gn z_O1PkkZucaiCmh=819$0MVm8N6n)#n&2k&nfGW;%OX)|CR=pI#YT2Djx&{C}*E(e( z$^gJI3k?DPi1tKA$AMPAF87v!to85V=gc}IdKm^2k=|bDbf0wXTf=Ynf{_gV#j}`# z5Z3YE-oim}SNDvLo@a%%D<`IpEVeNydOA6+PjBE9Gmts?>g$g@BaVXy*s3z4Sd#Y! zbUctcF2G*BMsj&5drEE3R)gWF|%q zIY(}N);?qNG-xSe!&FWLSv%?h^{vvt{h`dxF3jSb&=4?E^^F49vib@=s>fm zF@}htUTN|yUuio+U}e|bRiHv$jb#PEpdYlzo^%16c0x#Bs27(2`lRhWc|~y()Dmr| zPY1e8INjk@1PzqaNv6|OtlQ#IoujQg>?&F=0MY=z9oIdBp&NpWNr-*Bq7l`KQt@_B zf=%LQFe&lyDV}s{bx3^o_K*jS6G}&mf@NOq88R^DNz;Auj4+-*r<#Lu_g2<28K7!%pvz>{EJ3) zOXB1-n>B17xNFs-jc+@^!WK%2FL;1zZdhOAlZ86V2c47W_YTUBZkpW1cKlYoB9^TpKu3(zb@Db1=jX{)s#MGiscBX#=YZY54B*M=@Faf~=u;28lItrs^p2ZuITKN*1* z(9+^O%UN`vP=f(^$D`7j0{45C_#^_S|6t35e7Gpxr(q0VjXUJ(`{G*8J%_Gz6<2uq zaJ-Ak1VHd7qTbcjm#+vwXQpW~&2+C9aS$KuZq|jDKoFRRG)gS&Ar_)0wC`fP!*R{5 z&Ek|koGN3d^WdHzbZh4CtZ^+VRuk&t>yaa!`f%o1@6V-W_j5Or81y(d1sdN15w|y7 zAAO0j>5zJZ4hT|gz&#BIgTeT`aF-+R2-s(zpCh`HjlD+|ypuSnrbZ@#NC3gI2YTK} z<49eINEUi+XJ=*}=*6E4>;fp(Sk$g$IKl`1(NrFQ4L|On-eV=jB6ac5wV@yLs3OrXiFK}Am1_BmvIw_@}jYc z7uzGkqD&h3%WUT+F3Xz9zH1;_5q-hh>FAIs=k{fj_Wa?5S7?t44Y59PV`aCNEo zHw+8zBJ*Td0lX}8>4f5@Gm3ay){gL9iMufA+^(n|<;>h0MGO|RXI6m$tu?)geDDa} zrQa!t_CaX#txJw)fp4k28jk^dKu2&CjEL*xGDmY`&Jra(A|!!W6M>Rsy@(>9l_W)k zdm~5$E`$qX^FhH?QfMP5y*-oecFFWFdIqjeZb>@A-piLD80oY7*&)E;Pn|HVUK4cD z)y~GQP%O_Gh+Sq=W1K=w@@!_&WZOa+=A3H1m?Q0t=7eWhy3FCj{Ke}@m=h^Ei<}w_ z73!UzBmh3#jAV0w81=kj zr=m(dVr=ZC*cXEC$!*ghS9ALTT_hcNBb~bv{`eP ze9gOi6&?y%bR%Hr9sUy#E2m+E4s8tGzVgd)Zo0h5XY#~#{IAgQfCiIXnLAKa6K?R= zn+zsK=EsdnqABU}@!w$a7*1@( zG=+q1yB?6>L$;MXj?#_nezbi^8*AmYDOD32RYCh6KvKNeUMCQ{u{4ORuo}fQ0xWkb zNmQo+B?=JoT4oC@6x*L`{{!&B^-H|V+@Cmm(jX?41q4Kw9GgxSG4a-fESp+mn$FF74+w1+47)W+s1H& z4UQx~UT51h{?~3b@h*3K)UJld^Q#2tT24Sw_vDAqKjB$E-!e?=msYj5k?$LrU0ajvHD6G_1)^Ri zya&(k=}^e&toOgX_4?Q^9gdD(BN8)#KB^X95!cr6R946@Ca7%|kJl*rMlO}y)3zcb z^>cpR*MDa7pru-Vm3^#}{$X{$2KzLbmN!QAuNDHeB@}0{Sc5VK;m-%Ku<65O*^{L;4YG2dd zDPWw*l_#lJU4O}p;ZF?SrD?Z%L1MaQ4v|9>-zT~SBCB$P{Ha*AgagkzTefJIiNDb! zn%7~51xBjylhgq+aO_R%u2xYBiU&Ybjo%;uvF-)r%fgNSVdTmrrLv}$W+I7R%seQN z-oPulADk`DLsZOgu#9vvPj_Ra9w_L@hVL3*@=wW!b=|fTB~e&jx6C2gVS zR0ub!{iDzaw1)tV0Pprr4H84ltiXnZx`Jr)FY1?ylppazS^#5 zH@D-bQ}X&uZ97M&^%5-8E^t~S?)Dg)_Q;VqeXLslDyf?0sYMp!Nt{YxnAz=iS-z(%<3_9~Wxlan_=#7Fz2y^=2f|;0rjx7+ z7{oA_swS4o{z!|(^e>7tObe(>9xe8{vzw~Ef zlW@`2>aXy~a##A2_9Ti9!?6gJPHred3vYy53V)Yc-z5hK=@-08rOw=w=v7&3UvT%4aCrQke zS3Ijgbm%zW7Lfhv7eT{z9Ab{nwa0JFMq{&RKYiFi+*946wLFA!dFps&!-CXGPr3>u zY!}cmw4NbVB##hHU9=w@t@AkR$n#ci1Lib!ze23s1elcs3XfNAD>TEmTbXZiCK*g4 ziNWXoy9tnr$AeUCJrYt+a*Bi4BFJ0ysm_eLQI+<ItS(gwDZDnNsXec#)<@aDPwOct7Ypp`vItpwWXA($_W2!=Zo zFbY?!@Bjnec$`Bi+w~Z}tAc)cLhQ}Dq3pRGlBOjASej(#@^8>eW^71Pw~ zRy5{Xu%SvVgs^O*;j2zRh0LQ0tjrQPlWpZEd4sxn8udU)!4KRB_Ooi@B z%28BYOFEq`jh80`5gr8S^zHo06>NztJ|(Ck>dDggn#nJFg_?2KOGmYyf41K&@cYYbUCQ_ef#r#tAS%cK`MfAS5 znaKIT$C2km7K2PHXSe)h=B<1p%t&y|gGzV4(iwPhxq%$gePS(8=k))E#NQv;?Sfe! zwg%B8Fr1LYjGCm;k{eGDe5QU#2M@E4Gfo!WSd3d3oou`4*m@Nf{&m|AeCBa_wd*h^ z@|rHM1Iytd9!#4~Fxc7vE!tQHG?6d0*w1%8h~tJDw` zZ}ji77SIhT%vX=DQW$G4Q{sMLbtjnsfyckKy!=pjpKQ)*H~UgmfNX8A&B1bI^*6US z_kb}Egfle3goijBy^@-FtY)KTj&~39v0lu5&U#tGXpOtltxWZwu_LQ#(?d8t zVU+%Y?3LMVdkxpxWy9hLgYKJf^SMHzpZPE;$*!L|H*Z_Uu00gaQdIP<8x+6B($PV@ zYCcbsZjmX;i=c}L3)2a9>#oUj(q;nekuGztl`(WNkVkM#~JXjHuc&%4=w&=P~+F#e~p za|jj$*3$5`ZQHhO+cx{!wr$(CZQHhO^u_d>S9HS4XjoCL*KHqJMI*&uxB=s!@9oBrRMGhX3kofO7dkGQ{hh9!>67Bde zKpDg{I{P?N$WE^;@QEH8^o8>)G*QD}9JyItS1Zm03U=ZQ22<>%OGI2Fdk|-S@OHk~ zr|Iw@e}t6+GRu-d&^pxJnk+<($g$070#K=ep~QC}59LxRE5^9=S5a$tG1L@Nd6^8E*! zu+!rc=>SG(UZG&XAr|Cmnf7HF*`%t096fhJCq46!piK0%Ot12}Is)Xwr$L{5L;I(5 z1<-Dr*qS$LDR=$sx>TQh*KZU-qCfJxE|;g|iOSPHhl)w=lQ+>ec>J#MeMHPHEY-P{ zsxT8IfNLsWJ?9}oyK3<#ECHPyh_sq0262Cd*y%&oxT(H@dZiwQ>bEA@gUrA^Ze4X$ zevnT53u8@5E`+2{7r?wAD!?X88~_KsBqxEyqVxd7eeg5l$dEa6GI$fbx9ntznF=r9 zMP`@0y@T0bd{gzp_8)iFk}~5gvwo4G(sr>+0GDlWBhJD+$y`l-xpJiHnVDB+&#xQj zOsd?<`>*EJT6v3UVg5RBg`W%G8J?4%F~cOL`of`y3w%$-wnc4p z4v32i03i$HJI1@n${FCRb&e@mXE~W06_Nf4e(x?QgwYWQYh#B+erKVP4q0Kq?I)PIoD(Yh!`dFiQ9V zpQ-1Mav4#Skfoy$yT9gzhAM8v>@3R|OJT_!C?IsI`i?p#uk)nnQ?@;XZG$ERDMK4< z^GAPsKSxR&eLgD(J+<%55YB+LcJ8&`N9Mbc6`Jjs@(SfPgclZw`DF+R6+kV95D}1) z5_jdZzRE2$#`j>r1<|N^Zoq5{=)&>Z_K&QkG+FvvClAb`^#EX_-(umL+&6NQd+AXY z`mIaeanGLj$2WYWqsYC%^4hG`7>H(Hgr(!QDM4guI9||zGC9NZbBw5X6(CbWI1lrJImEzitq=&-&xsyI7nO@d zxU8Ha>Q`@qFrvWsh6mE-FB#-_aGF|3*m{;1wRFG!MRq*J`@5S*-}e_)G3edXl0rCf zDY>s8OmK}{fLC;h@58s5_Du61&{nvA7)ulgi_+_(i{X?GE}pTY zt%l{+`=Bh{L2?G)3afflAsp*Kajmp*$hT{Xr!XEN*9^--sN14ZAha<9{1PQJ7d8kx zA{;71HhG*I=6fUBY7wTLsg&^h5wZNNI*6p}Qik;xqT&~J5$$+(+$i8Dvz)#hNyB_m zJh#9$ts4b5+FVa{ezt7Z$3M;Avt$L_T5=NlriH+R53osbA?A?2$n zD!CR@DN+kd3Z3eg$JM!plMnItxJ17|5#`+?=ZHeRQ_7@6e?_4QZywzSG8}tTeW)Aw z^+)yqkj`r%m<9jz8nvpqltiBAQ_y4Jwt4E%Ac0>=t;v#I`jM?$%pahbxxy|WGHk{8A#7+^W?eh%w{q|co)Py@yLQ1T$F`p?Kz zLFaAi#>^V~{UVlT-M`}#pQzz_YycYQp+vH)Q74DE8%Vg&=^&;@sJB~H4xHw`-8UQ0 z#gKq;;Vap?qouHb&rMRvPMX;2$)vx~lzV3?WOD+U8}Un<1C@IOfn(6$?)pr&Na@B^ zes{HA(rs)Tucd|?X?<3op9@0MoF=?kR(AWUFFhmAC@U;5_VBUSGlHfelDR;oFG72{ z)vb{jM((9BV7E3?JF(Bofbz5Y(~OudRJg8*<6PLcs0`#I5M%nvCGyQ>KIG4~9c}XT zZ++t!hUS(=j1}xJ@l9s+Wul|y(_~FmnNXHQ_z5keU=jZ$jg1}=@I2Ydkk9{-q7>Xx zdsF;*$r;MjYXRDr+?%}wUAT{8%IdjY5~uKy^DFS4ONXK$^=ats<@KHpo!y%ah%OF& zNeY0WR(W%%gAj_X0)a{LL1O9f*oJF>>t$+;7VBh#j)-dT6TF|eg?F6S2PqcQp5QnU z=pEy`Mk3FE+6W)+1kB-bE4}aTmGyDzWRw_*zFO#OF%pZLgh72a3aS&J-mqcq9z8zt z@hg==*y8fAzH&(FrKlJt8tWAeHZ{mp%iasMpM*_$CcJ|Zv+F9+?7LrrUO?$pCM1f5}s&8%}OS0u3=d z>o(lCf(8n)!LctsE1;!-4oa)@$Pg`18k%1f)2b zvwMrmJ5;)u=`F3FN{hB1ojQ_vFXWgK`>{a#EN@BJk=akbpQ`Y3@l+{mvWIs4h(i;)TwcN zS?8VHi?aReM2P7u@J5 z^@uU?$qTXno#>9)&naI&u&D}F3>fpkE7bxpXDt9M_IKUpo*sgS~Fe>P3eV zub5mc3|(LIXDbEFw@hWICk)>o1)42-J~v>pV!7*^;f?5XU8##d5lzT3 za{O@ ztoQTIon|#Vm)>kDjB%UHUnTPBeHPXysk43SIA+O(HUz^&1m@RP{D2a`6ZXFr9{5}5 zccR~Nv7p(CaCpBOWjnsU#h+N96-k$?Pd`x}SJ}~glaSKpQCEOZ-a&P1h5i7@2B`D? zdw-!YF<-A#DB$S7H)I-TQwFH>9Yp0qxmjE0nrXYFwGT-Y&PGO${BBF0Ypc6Fdj8m0 zPz~?LTW55OyxsGj#i1Nftzm)jbcM5&(`z_B=WOmb=ON*fyVtZ9Qk0>qmHc}I(~r6+ z?wM-Ob3jF{t6<9HeNn35kAbeHo=%;)h2c%(B9JFX{75*X8sa8I>~?5jh?f8;@E`@n zM%wTI08kR%_cC}SP- zGxIzy6J&Q(&57FzXGo*iRimJ#(I>g4>py=UQt9>*Rm`#M#8&UzcaAo6njrR^M6Te| zP}?a#8TMv>lRoBI5==>>05$;#eOP5nGx@w3+=q`9PBgJr-<+OhAoSQ7X;K)403(H5 z6X6X_pBwUOrhkiL^fij*{NS05E;IL27R%|!Oxn4vqG$B)#)KLN+O9HxO0hsgo@Mx_ z-Xthyg-}Z=8A*(h4CEI3KGb*X11wpMVhsB6@695Xg2TA?0EO>Ch6>Q8P!66Tc*+%) z4So{kv~T?Q6h75Zy&XyQ;?xfJOfD2j-{*x|W)Iy|H3&TqRU%%F+x^Q)fR(gIjuI2_ zvK+Q?ibE$z#Kd`_Hnq8}sNceGjXje*vMiX7nb(b48$rT-V1Ru#iyXb>=-WTvTVQFj zh-6k9(GezyDN{OoIxIsU6b=p9<@Wf+tiz9~_EE3Iu*00PQN>vK`l3#jnd9J?Vz(NpXwiAnhU6w=;0+{F(ykUXY5+dUIsM0A2!N82e|c zuk-IyF%f0I=NuYVWeT}TbLGtuA&WkE~Pp>c~Hy zMSOx)SKlEuhnb;Ef}{F(pz>=f4F;BVN9w!k5<93msI2eF3N&dmRa(#P(zQm?<3H<( zRmgdu?U_87=Hb;2)N1hfx=&?kh#<)t{O6En=ezHhPB9AaOTthP{FgqmEZ0*UnC%ZO zTPE>m%fYA>Fpga|<#2}ss@zlsi3@eOjP%$~kRRR$pw^K-Fq=ThF6n;7c3=4{=ceaH z7k8s+QwKj41`I;<*?fFEgW|C3FQmkqen>E5@cXiXjFfScF{h+O4T1C3UBgWePL>5U z2lk>?Lid=5X`?y`H&J>f7fq-$mV~g-ijg<`M1^TNGxw%fQm6aJK`BjZ!`MYs0_kX8 zq*bpJvU-ub`KxkRJ{6hU zF=j*t=}P3@OQVyrRxQ;Uvy3j`g$;wNQ=Ibv7u)quTPX2Zr}l$}<^9QCmCu}o=3lQO zVoII3TxbS<89lcATLPpjqR3HRS`6Ad1Wry3y;Xn&3(gcVv6d(*taXJ0pn#g1IN4Z$ zVn#7C@xVn8mc*DR>z-)iIFITfXBXd<_=iyOHx9)f>Tf|fKT4m~Q7VwnKKh&bfHYAf zj4(e$BWBeeuh?@&R!V2>Dgb#`Sjq1KgB!^gKm{VCTg2HJ1N&;wjLT#;b3=E-9p45v zH*T*EF3BOKEAZ2fnpH{S&J_CpMGyQZ)-G?>WUPg9u(2AZmugSW74}*A?z@fGU8+;q zB%fYGdgGY7v5T#Llxb$OOSx#zlT4u(j8kg6m~pZ6{%UFifhHuBysC9kfL~E#H2-~C z&wIzyVhyWM_{*nvCl~PCPMdjrxkiZ;nnzemQ%gK4RjHo>_JHJPy|UM4w+fO3tpE>= z3H9+6>-<1Y3MjR8?F>Dr7m0V}7#Sgt*gVH!45OLgJvr`CiqLdkx`n?zYIbMk&IpY^ z=~pZOw>igOt7lt1Ye51paRM;JV6lEO4!2CqdZR-TaIA9?{YLydZX||ZkSdC5MBP)T zEPRfdJ^@tAK@KG7I2l=z(5;t=0ZmU+aV8`{N-3HkJw zCiX!Reyyp?QJU5GJ=e_dPdEH_3e*&0dNFGRW+NqEBovrAR|?2_(nmS=Zdtga{j++)Wv_E`dN^pO7{_Yvs_ML zE$nH@nbf#xComF(stbD`)*-!`@GzsuMt)Tuxi_Us%6M-`+VGTpwMY2;T+rrMA{0S< zvBZiY_7;9=$Gc#C?=+*IB(>zTS}VM@Vdyb}sz>mP4?q}4Y$CCrMgK5t zZB_7QIl9kad;irRGO9n&&{~<}a5BaI8tcPUsJ(^p`|P3v(Y>Q4t~%yd ze_}LF=9h*+%C3+028Fbz`)F3bun^D}s^i+UZ-pR0-AWI)I%W4CeVj-T35_a3&ItWi zq|MNPxS9O$YSwbiuX>#42hj%I*K%nKRS#^!yvE!PoKqa7C{~MH?Pw!YCDNQS#9i!E zDsa;rD#|#bC>Kr6qa;($TZY-K0o)gh#Pk;!%Mb7mErqO1)5%3R|4HIqd zraFSjod0uj)H4YaOY5AGz%ke4fmQ@gx-usfq!-CicH}W8o5BJWL^d)Dz1*GKRidHO zYJ}sz-Fvj%#`2A^r4I+h=Lr9;>cBucTh`bbJchy~vds?H{ z2?}4|3Q~W4BBjV@t|TBrbb-HPOa-Cb;v$^ zoPUxNs}J?a5hWz!?h?6j(9@j_FOVUh#rkZ`V7HzbwcekrU(C`MDa8}aFqQmkB5Ate zY9n`uX;yM_+v}V;nFQoPu?E2`M7j}_(!;W{>>G${i>{8|6*AnY&K3w3&@(^LRwfdq z(HcCp2m60eDc8UKNQCYjtN}4RgnM#d9H~&@HG1&z6noQz3=I6Hw(8}vGLN+mj_ah7m z?kczUp$<(}zCi~nZs)1ooXqHTKp5!6IiJ?H1qfBa7DlKg#sA`dY}L>r(H+aTfpd(f zFWD%cxdffDMbj+M%22Xh*GbHbgls^q24oqfp&}+;hIwX*@MN3F7abQ{7u>haTz6H! z#GRk%Rd(IsI#HGx?7QfwV+Uh?-<>e*m&seh!0&6JCi#(k)(k5rW^A-c6b86Q3)|ld z2q2C!k=ys`BP|N@4E2H);-GjHEi>z~Ud{Rk=KZoJ1rc>(_lpcgA?m@WlN9{+eoaYo@2b0OuF_~#@8mJ_+ck}k$>)rruW zmmkFE`k?UHclv4*Z#;m05f;6UAmWg=XL{DN|Gocb4GU$FyG$xieGupu%Cw$xr$qF2 z$KdW1{eUIyTvul--uNUPvS_Bn z52WZWw~th^ej6X*HSqKXH|rI|THyvr?qwDo4tiye|6xK+^(h*pd?p~4%kwA<4jk*9 zpjM^W!*>dZdDUTNtN2x3B=zDu05@xkw06{Y>&$S74gv*)iX=)|eDztgT2mV8b zyjv2D#jhKwZg{a&>Qy(p2F5f#TcF=Gp3e!$%|}R*hKPd5s%^R_cW%Bj86VXIXfRO{ z4A4G-<|=?Gr@QUA$I}%oK=Ix}$&-OUze{>!6T;kmXBo}-QZx|E<7oq|to;J`CjzBH z0nxB{{p*>~3fM}DL%r!44M{;n4m{TMc)ThJvU}gLTk6~aoNrR)dp5`v4JsLjUT#1? zR^R3D{v|54`~&iy>sHsb-%TLvC9%>s<022sysUzE6|VPkXU_=5kcCJe54BqbSnL&p z^zmxcH*{%rbP{=syMDHEYtP;=^HzScm{}maKv)bX6}*}TC1ecU%RdEf&M8pDkmG&>TDiH1tbyxKN&scrqGYscE*ED}%ZWCrK4xEfD{%-R_>?0HMEkAK z4~w*Ot+E_kza@I%v&fvH(1vQ7hYVLS3Mg=ZpQqE@6&nsXj22VQwt=-70HB(PM;p)| zA#k479#O!!lsWj0gPKElSG`=NVW|In5#?^f&rW;yxfrHyQzQ*!dkDD`R>WnFNVdcn zmY`SemdZ@OO`|?wR~kZ;sJx7WTE5f${pR-|RveE$zd&1FV#=ul0SvOcy*LQe$b+FA$OX`ii z6+K2yDyi@5YFvd?tu{+Ik*c*bKbDS)=7QLz#*^1(o&>WmXz5Tpj=pN)x-KAuGpyGv z^7Je~#p!bzw>G~RDgh9?!g|>s#dBj)tjssQlc_JyKd_ID8fIzeX)e1#%y2sXQJMW< z8iih2qy1%06d&y^Z! zZKF*{M=5`n;z(IoCkpo` zGdEYY%0Mt}#eBe}|4_;0b(z06Jyen16Y5KDjnsRC;y_$1qX4&fq*&Y?>F@>IUdo`M zQ(~gzd!-40{3k!UG+_e*Ptv>Sh-?^BsZ=dcxAb7yy!;fiz@0G&q&?3!d$hQd;pDl~ z8%>9HINhQ^jmV}c3b7Z;B)C6CU>~#3?z1aGYjOXjx)9glh)_Wml(88wsY4rDb91?w)0uO(RW`~d10K%Vk>C2CT{ zSIEl+{SNcdQvvWTJYzkH+ZzkQivoX*=uMeHzvsX-_AO=Q^sRm;faxa7v2^@y=`h_O z2KbDx^2J#X@8=y&}-SMd-4^N z*VQ&tben!(o>?P>fu!}aeb~;Vc$K)qxlnwC5dfNKj#eUl54FXpJ+!>g7qaGq?S0Nk zwv%yjd%31`QZ{vy%Q4_tLjRBS^MQ|zU~$JqgQK{a^nud*JY*knx7vu-4R4I@cy zw732olRTeUv2FO8fzH#oP?Ycu1X#rxcTG&~3s2{bYQ7cOj+AsRr;Ni-m728^)XsFU z4N{;lr|9UT!^cfySA9~q$z1p-hFSqbXSG%Wny&WCbqr2y-=TpB9=CGF2q z5Ypdw+=Z-)H{zLBCXb2j{?bjgbjA6J{*e47byIN*G27P1!# zGQr~^CJ9PKH#h{nYi$)M5BRV#c4H3*@>^I_bDYm`c5Un zPpvge!GXdpk3?fcS(+PH%6&&_FXs5JfbB1|CY_Zx*!^ZA8Da5o0r~aVUpJ z9jVHtbL`*1;=kbnW{cpG7~Oo6NsXkHCaHdq;Cp=<(#Svc1J~eREo6BfNybQ82fb&* z)w;^xM+IxeG$o5v9yuRIQ6LYPYtXS_ zsrS^2LRTn=NzQe_UxzkEawZkes5VSs8GAGbK)aPrU*omnm6@FcnsRE(btF>P#2(LF zGelmOb&a43$J>n=8<1XBeTAFXz*<3Oih%4 z3X(i;YiOrb)Jto%ph3%=61S8etck$xE4KQNAPnfrF$=5_4uI8uw3GITVXHM^_3VDh zx38#==U-k@M{Ms!FbXTwz#U%cb=^+MF(ssVH3Wt{F@dB$` zjaJ^%Qk!hAsMUY!PU^}(qp|j-y}vXg@4$9&OX+pjp~-4@hwsk^7ldV8I$7XcWlo@2 z5Uw>(%LT1jq3oACTMi8)oEkMl@<`0{r64vu~-+Mq`MF8XVB6>($&XmcsI z4NC&dqzg4mX-ZGP^8zYS?cQ+6kcMG5fajsh4}O%2U4Q5vZ`w5R8k?TJkb@XX7SCo7 zsa?9^bpPCgWj{d6*U#cP@;7PYL3H@z4V~+hS6(P&L(WvLe&xml$zaT4wMNaLT$%X- z^nA)%DpgiNU@sBH04H$IP|m(db3PrF8fx6Gz+Q=w!-OjXG8J6s=mk?Abzo=q|6L(F zlvz=&$Ly3h_j6D;#zz1!K5jbMp~H?+pExuDyfc2}HWtO0#zS}wk_$66Rod)u+YS`! z#pmYUN`~sc#66Xv6zW4De^$Q%1IH%#pua_~lgV7PCJ5jDvmV0!;F&JBE&F7y z0XvPRJiT2ngabXg9&?gFgHuJ+2^m`pZ@wPC;6=@jt%vl4V06Z2i0Ne%bj*=hZ)B%q z3*`&?mPeQ;>0ci5)q*EbaoFQ(+0n|QpItzuc5`%Q2|kG2CfXLg-`Nqj83>(nK0@+n zf?9{*C7vhur!;}Q=5|(iGk@^LTx|@%`YI8zyb^PDY>FLy?~K{%=iHctG2(Wiw8mYU-U;}E za5@}%wJ{!q+6*AFM5=al9E&)D>{%p~1rseN2B6GqhzZ%?45U0V1WfTsmskmPL*p?& z^th^DVWa}he;R2DPlUEuq{AI~Jzh>f;(%p-Vi=buppFXmc^u*2cVWAm;~wr1&xzcv z$4wNy8ToU8~zi%L}Cj?;cspU}%n)+dWN(!Pgg z*m^$NtV6+z`a7h2{yf69Try~Y^0Vj8`lPojyKOyuW~pKGHPlDkSf3~>1J;c!iMVi! z@!wdQr?u+)UR(QMeQtd=R4Hw>_-f536 z^tKuh<-2{rNO}_7=grbZVE9{0%yVR=b1p50*$poY6{5sLs#CU$jHDIhfxsa7`THS$ z|AP2BZ|lYgSxqjLJ^&?blc&Qxq2EOc8kS3)Ccye@QPf5J)xC#i6)sH=19> z8g=W5U$`dtA_q83q<*GN8j2fh%yfebX0diFRxAll&@C>a^FZEWZV{b5T|@am9-~N; zOVFzSBlSLhp)RTQj@l%Q32M`r2HvkY2Dxf}?8U=OPQl>PcgT>k)5V1f(?zxOG6=va zL+=>8%&(nz7h_Bh;~M_jCHjMlxES6*0Z@w|6>2Ns(nmGBCgD$rJa=+ejS9X4b&!{p zVeVL3;eMXg#=6JA4$*pUqba(F&{`{RXKz`}cJHUpD%d1cxIG0}1j>gRsLptqiNJi5 z#SpL;IjNW9h4u{wui{%7U5a4*U`i)19MQ3FwG`0hIa-FojBcsP%8*fLSRZMmoXj`j zq(3K@Z;)j)butHS1k}N4O?xnW;2O?YGj04U2_5eZR)p~&>Q(n5`}pVRBf$;nXXnr( zg9PiGT)-fTjkGo-&2{z!b|fHg{R}iDTq{drb>%!KrhFKS(0|aH9vEyw-Zzm344a<( z<>)A!!6Ji#x@ehgLg4Anh6#w*$?2H9TFotERIjY|>YtT2@I5NGi&h2z`;b%&hG>nM zx!~08LR+J2EF(VXv?y*d=OGZO5hoYP|0j3y4mO$JPTqYBWP_Gw2QFi+`E;@g^Tm-A zl#^M^jv-;QLBBUNSNh zgh6S+isZ<$=zoG8Qm4LY8cy^Pf3DDP1iQnYb&gUTxFY_ff<=e0yhH&XjiyepY9Z~p zg5OeTmD`TywN0+N4r6Bz01Y~Hik=?8rl*3-RjKDY_l|W()VQQ^PC%8EGcR}g0gRYYiwJuci zxc3Wr_@!l)tS0B&jxdeu(2eI(08NvYT!;Gn$NzwCM{%1=yEZrsX?$Gi?A(4I{~>Y~ z%+duca^$N<{+8-;cn|pnQ(@)?6y3O64WD$d?6;ZDgVejU)%_q=wHKJ3t%76sZ9sg5 zipW!;Cg=%)$_?MlqFEY&Bt0tF-MPlitxmeV~TEUF@D> z-i}@kH_$w)W@;|~Qf)dkJF8jY?kkGt@mqf413_VPo0*sWB(1=^jXUp0i4ccgo8s$p zhDepsLWpwgrAsT7#wyQGqdbxwZ}{@S??Mgz(A-c=p3zwQkAR}TARM!#CG%l<@><~^ zhzu)bAoAa4dbztS=#h<%p`mo>G*Z1M-s$GkqK$<;?C z<>Efp>^=?rf1vdh43teKdR>AoER5{LF)+X;>vZ%6h_a?vWxp*GvR8l z#iC|EMzK8Fw}eYcMtbK+g-&Nz^;5iUMVVSzqQEo@`$uT$)@7oshHb0`Xum2SxWaKh zmfoo*w`I?dt|hbm9?>yZz;4dgA#F!{nzXY*yZ<t8`%q{`X3WDun`_rpir|G!Xp9veM+PbOBb>7J`V@TZ3OU@ zTAn*CjFjRldZo%xZp}^~3$#~3DN5gKH>7PFu1*Q+ly3IocG&1*)jhKE?MQd@|A^jI z(0}hNpX;hTNDfS2JFyn=hhm8l*6p;SwgzZn!t%H9mNzd1!|Qw$o7=lb71hNPh^pX( z`*tVZ&yyUCgP=XNG3|NNjdZ7#@g>2370uxm`ZGxXej-WaG3)+@){@aI9_C6ya zs{n5Y8?VTfj4z?E%*8ug|M&2CSOs-^TGx^{&m0%oj04!A3q=segZxbGK6M-D(@Bg6 zFhb~c|HDs$mNj*hX5^gD^Mr&ss>lFEj$rF!)guF|s+bBN*?LH>EuUUEu_5uwMWak` zM$0KHzQJI~o9JS(2i&c=Y*=zkM=>ycp*{5WgEb4`$=sDz{yx1Zs8J<{u87p6=ooqD zFkNr;OD@Af50)ygdh9kPsg?_G5mUfw&1jIvI}6L({rdt0X>LB{^&XRI3gWAKgkHzh zO{1+pc26>8d7##nS7JX^_Nz4n{)EFt?x>u=tt!3dHl|Lr^j>nV<~?)+J!!^@;Pf+0 z+b`^pZmg(!4n6^AIoVrAY;#aywIGaQccEDOCtSs*itd0hg^g(`Z*83BBW?bL;W^d< z92FU8_7}d<@q({0D@+Og+kso10eRE~0!Tt#Z~V8Lr@WWVM`0`rZ~n1}jxT<|*v&!Z z>SMAfBqf+CY+2fKF`4<6JEG_vcwVgO$(PQL*O%^G(uJ(rjb?x~LuuA@3;eI6j7J+3 zq$`^_2MUot0F21v^kC~l#`FZUGzL`vXK~n$L08>p4eAiEQ?)3+Ha$LYuNiRIwK@Mt8*qG@TP<*lD> zve)+;*C;M!RW|MSH8tJJAT3X=}9ld>^%je=IjX1GzGDTBe*G_YW26vv%`Ex z=tpP%^H`GMP9JO^HqgzS122IfFb8RrP|#f@L~RK0rv)wX{TDS)QRw1EDPYvtA_2E2 zOh0Y0q4A(-KzF_2B_j!}6kCXCZkefCxHbdRI`XtfI5rX42k2*4(bsc_6+IQR=`I1L zkC+#aw$xRm(cI@RvU75QMP+2V;W^^JtpponBfEy(FZIeI*>#1uY7x5lSZ(lY6~Z3= z{6Li>viA49J!LeETJhb>zj}}wh>YFdA<4Xjl#d=lS=ebC5KIVEbx#|}6K=&&au!lR zt=He6Lx?YpRUC{d34Zudpfht`eFMOY-LJR7_nrh(ODgO2uh0RUi9_!wao;{!Xq^X;WIu!Hq-upr!v)k&K+q?0G(#Kk;ma%U-7< zt+5bj&@HvkS@wO_osL{qEvYB})F{5$IQ}XqtFZR{OIOE6HsCjEtAN6KJ)0cMfsHAJ zxJ9y)54?O-T~BfEDW&057+7J2p)P_9?@Z{GUVooTft+X`>rYI6(z-k}R8 zRESqxl8N7#Tp77(6fT2DVP^1E$wYS~Hn}Jqala$ocpKI5(exocY8D;cUT10R6@MI; zTP_%`dX|i?l6Eb%%fIH_O!Ica?9k;_oBW3A@9J|`^pZ;O=X{NxYEpXmRrYzq%bzF} z8Isown66O<#h=vZN}_GjH-8HjU$trjl+uUScs6?rZdfm0#|t@yH`*Ev{VfD%TbpF| z^T0c2i9R`LWE^JL97^&P@WR4j**BFMsHqe?Bj-;|Z%Z47o!n>b>}Ta?fz?nkd3qY$ zsFh~?hb3VPHHs1(VC~}b0-}#SU4Pu%(xUd&rv9cgCk|I1>V=4{!cug>GgX6FA)0*~ z;44;zN#%JIsh@22_zI7SQ@xB)&M_=QUy4O})Er?IX4qP_C~2IbM7X{AA88by9O9p8 zgx@s;h-zz~MVlwU`~?oiI^!Gx7dY=jlpb?~!-FMk?kEEgx5%4E90q^=6}a4_gRVjB z%)S_J=Kg@@89tBy1BptP6~DC@f8?N33p|;KWgTd6iQ7~l_Yc3FxSE>@;EooR*vdo@ zRpaEj7y{0P=_KwHEd!pUi2Tc=paKWFTzF&}^<+x*GI?KDD6QjS!ahFV8z{FjFfde; zBv;|NE~Tp(A`Y##_?VsZB%O2p;28C>z&L}e#O|lJt2#hXbiYV>zFi=JCc$_zq_!Jv z2Ay)xGW`-!{hNCl=(?N?@R#6{fx%ZyS4v5I*t|z1CPov(wd~3GGTj#weJg(;s$+{8 z+fwRQM$nlFqrzxXx*YrmSUj3FLqt+bW+xMoBck*o$I<=JFTm0} zF~K^mTr{_)82dcFTLHh{K1nrQ`dP9>)Bz*XuTld;EC-@*S&=Kn8))wCU8%FHs{FE1hRrvAdu0HVuNJxxuUF%iP=%7AIF z<4oWRV8HNiDZsP&JC_?Ps}S6$Fv6DLeH8tw+$R3DD7Z9ukEa?sbA@1(nMEDows>?o j3xR+3q!Q#xiHizS2KNz3b(q5;MBD(i(#kOV|EB&6k%Un* diff --git a/apps/web/content/docs/dev/moderation/warning-member.webp b/apps/web/content/docs/dev/moderation/warning-member.webp index 8010adae12899c5b1b97899b4fbeabf447ea4df6..6c3e831cbb12c99581beb58b6ce6bbbe00ee19be 100644 GIT binary patch literal 101556 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 T)1EED$+SB@Bu5JR-~a#saimlL diff --git a/apps/web/content/docs/dev/moderation/warnings.mdx b/apps/web/content/docs/dev/moderation/warnings.mdx index 529b005338..8a13a0b0d1 100644 --- a/apps/web/content/docs/dev/moderation/warnings.mdx +++ b/apps/web/content/docs/dev/moderation/warnings.mdx @@ -67,7 +67,7 @@ Who sees it: ## Where the history lives -Every warning, revocation and suspension is written to the [Activity Logs](/docs/dev/audit-logs), in the same transaction as the change. Administrators see them in **AdminCP → Staff → Moderators → Logs**. Moderators with **View every member's warnings** see one member's entries as the **Moderation log** on that member's ModCP page. The entries hold points, threshold decisions and links to reports, never staff notes, override explanations or revocation reasons. Changes to warning reasons and thresholds land in the administrators' log. +Every warning, revocation and suspension is written to the [Activity Logs](/docs/dev/audit-logs), in the same transaction as the change. Administrators see them in **AdminCP → Staff → Moderators → Logs**. The entries hold points, threshold decisions and links to reports, never staff notes, override explanations or revocation reasons. Changes to warning reasons and thresholds land in the administrators' log. ## Who is protected diff --git a/apps/web/src/page-cache.gen.ts b/apps/web/src/page-cache.gen.ts index b46e6c9ff9..6562cc36ee 100644 --- a/apps/web/src/page-cache.gen.ts +++ b/apps/web/src/page-cache.gen.ts @@ -60,7 +60,6 @@ export const pageCacheRoutes: PageCacheRouteTable = [ { path: '/forum/topics/:slug', maxAge: 60, staleWhileRevalidate: 30 }, { path: '/login/sso/:providerId' }, { path: '/modcp/approvals/:id' }, - { path: '/modcp/members/:id' }, { path: '/modcp/reports/:id' }, { path: '/news/comments/:slug' }, { path: '/newsroom/guide/:topic' }, diff --git a/packages/vitnode/src/api/modules/moderation/routes/warnings.routes.ts b/packages/vitnode/src/api/modules/moderation/routes/warnings.routes.ts index 6c4d0bae32..7489b33a93 100644 --- a/packages/vitnode/src/api/modules/moderation/routes/warnings.routes.ts +++ b/packages/vitnode/src/api/modules/moderation/routes/warnings.routes.ts @@ -447,6 +447,10 @@ export const listWarningsRoute = buildRoute({ page: z.number(), pageSize: z.number(), total: z.number(), + permissions: z.object({ + revoke: z.boolean(), + warn: z.boolean(), + }), }), "One page of warnings", ), @@ -499,6 +503,13 @@ export const listWarningsRoute = buildRoute({ page: query.page, pageSize: query.pageSize, total: totalRow?.value ?? 0, + permissions: { + revoke: hasWarningPermission( + moderator.set, + WARNING_PERMISSIONS.revoke, + ), + warn: hasWarningPermission(moderator.set, WARNING_PERMISSIONS.warn), + }, }, 200, ); diff --git a/packages/vitnode/src/components/table/filters.tsx b/packages/vitnode/src/components/table/filters.tsx index a1df1f524e..2a78269640 100644 --- a/packages/vitnode/src/components/table/filters.tsx +++ b/packages/vitnode/src/components/table/filters.tsx @@ -149,7 +149,7 @@ function FilterItem({ filter }: { filter: FilterDataTable }) { )} - + .", "lifted": "Lifted by .", - "sanction_stays": "The suspension this warning led to was not lifted with it." + "sanction_stays": "The suspension this warning led to was not lifted with it.", + "show_member": "Show warnings for {name}" }, "lookup": { - "title": "Find a member", "label": "Member name", - "placeholder": "Search by name", - "results": "{count, plural, =0 {No members found} one {# member found} other {# members found}}", - "empty": "No members match that name.", - "forbidden": "You can't look up members." + "placeholder": "Search by name" }, "member": { - "title": "{name}'s standing", - "breadcrumb": "Member", - "desc": "Active points, warnings and suspensions.", "active_points": "Active points", - "status": "Status", - "suspended": "Suspended", - "in_good_standing": "In good standing", - "sanctions": "Suspensions", - "no_sanctions": "No suspensions.", - "warnings": "Warnings", - "no_warnings": "No warnings you can see.", - "hidden_warnings": "{count, plural, one {# more warning is} other {# more warnings are}} outside your moderation scope." + "hidden_warnings": "{count, plural, one {# more warning is} other {# more warnings are}} outside your moderation scope.", + "active_warnings": "Active warnings", + "clear": "Show every member", + "profile": "Profile", + "loading": "Loading the member…", + "load_error": { + "title": "This member didn't load", + "desc": "They may be gone, or outside what you can see. Try again, or clear the member filter." + } }, "sanctions": { "temporary": "Temporary suspension", @@ -3095,22 +3094,9 @@ "from_threshold": "Reached {points, plural, one {# point} other {# points}}", "manual": "Applied by staff" }, - "log": { - "title": "Moderation log", - "loading": "Loading the moderation log", - "error": "Could not load the moderation log.", - "empty": "Nothing has happened to this account yet.", - "load_more": "Show older", - "someone": "Someone", - "warning_issued": " warned them for {points, plural, one {# point} other {# points}} ({before} → {after} active)", - "warning_revoked": " revoked a warning of {points, plural, one {# point} other {# points}}", - "sanction_applied_until": "'s warning suspended the account until {endsAt} (the {threshold}-point threshold)", - "sanction_applied_indefinite": "'s warning suspended the account indefinitely (the {threshold}-point threshold)", - "sanction_revoked": " lifted a suspension", - "other": ": {action}", - "points_overridden": "Points changed from the reason's {points, plural, one {# point} other {# points}}", - "expiration_overridden": "Expiry changed", - "report": "From report #{id}" + "pick": { + "desc": "Pick the member, then choose a reason.", + "continue": "Continue" } }, "account_status": { @@ -3118,7 +3104,6 @@ "desc": "Your warnings and whether anything restricts your account.", "loading": "Loading your account status", "suspended_restrictions": "Until then you can browse like a visitor, but you can't post, comment or change anything on your account.", - "contact": "Think this is a mistake? Contact the site's administrators.", "good_title": "Your account is in good standing", "points_title": "Active points", "sanction_active": "Active", @@ -3126,7 +3111,6 @@ "sanction_ended": "Ended", "dialog": { "title": "Your account is suspended", - "because": "Because of a warning for {reason}.", "because_unknown": "A moderator suspended your account.", "loading": "Loading the details of your suspension", "time_left": "Time left", diff --git a/packages/vitnode/src/pages/modcp/member.tsx b/packages/vitnode/src/pages/modcp/member.tsx deleted file mode 100644 index 5672c9d143..0000000000 --- a/packages/vitnode/src/pages/modcp/member.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import type { PluginRoutePageProps } from "@/routing"; - -import type { ModcpMemberRouteData } from "../../tanstack/moderation/warnings-route"; - -import { ModcpMemberBreadcrumb } from "../../tanstack/moderation/breadcrumb"; -import { loadModcpMemberRoute } from "../../tanstack/moderation/warnings-route"; -import { ModcpMemberRouteContent } from "../../tanstack/moderation/warnings-screen"; -import { defineAuthenticatedRoute } from "../../tanstack/plugin-routes"; - -const ModcpMemberPage = ({ - loaderData, -}: PluginRoutePageProps) => ( - -); - -export const route = defineAuthenticatedRoute({ - load: async ({ context, params, t }) => - await loadModcpMemberRoute({ ...context, id: params.id ?? "", t }), - head: ({ loaderData }) => ({ robots: "noindex, nofollow", ...loaderData }), - - breadcrumb: ModcpMemberBreadcrumb, -}); - -export default ModcpMemberPage; diff --git a/packages/vitnode/src/routes.tsx b/packages/vitnode/src/routes.tsx index 287d164636..f2f7ffa808 100644 --- a/packages/vitnode/src/routes.tsx +++ b/packages/vitnode/src/routes.tsx @@ -256,11 +256,6 @@ export const routes = defineRoutes([ pendingComponent: () => , search: normalizeModcpWarningsSearch, }), - - page("members/:id", { - component: lazy(() => import("./pages/modcp/member")), - pendingComponent: () => , - }), ], }), diff --git a/packages/vitnode/src/tanstack/moderation/breadcrumb.tsx b/packages/vitnode/src/tanstack/moderation/breadcrumb.tsx index 5dc70417eb..33b74b64e9 100644 --- a/packages/vitnode/src/tanstack/moderation/breadcrumb.tsx +++ b/packages/vitnode/src/tanstack/moderation/breadcrumb.tsx @@ -43,13 +43,3 @@ export const ModcpWarningsBreadcrumb = () => { return <>{t("title")}; }; - -export const ModcpMemberBreadcrumb = ({ - loaderData, -}: { - loaderData?: { title: string }; -}) => { - const t = useTranslations("core.moderation.warn.member"); - - return <>{loaderData?.title ?? t("breadcrumb")}; -}; diff --git a/packages/vitnode/src/tanstack/moderation/warnings-route.ts b/packages/vitnode/src/tanstack/moderation/warnings-route.ts index 84c51e9974..20b3c46822 100644 --- a/packages/vitnode/src/tanstack/moderation/warnings-route.ts +++ b/packages/vitnode/src/tanstack/moderation/warnings-route.ts @@ -1,21 +1,15 @@ import type { QueryClient } from "@tanstack/react-query"; -import { notFound } from "@tanstack/react-router"; - import type { PluginRouteTranslator } from "@/routing"; import type { ModcpWarningsFilter } from "@/views/moderation/warnings/warnings-api"; import { asSearchValue, readPageSize } from "@/lib/table-params"; -import { ModcpRequestError } from "@/views/moderation/modcp/modcp-query"; -import { - memberStandingQueryOptions, - warningsListQueryOptions, -} from "@/views/moderation/warnings/warnings-api"; +import { warningsListQueryOptions } from "@/views/moderation/warnings/warnings-api"; import type { AuthenticatedState } from "../auth/state"; import type { ModcpRouteData } from "./route"; -import { requireModeratorRoute, routeIdOf, withNotFound } from "./route"; +import { requireModeratorRoute } from "./route"; export const MODCP_WARNINGS_PAGE_SIZE = 25; const MAX_PAGE = 10_000; @@ -110,45 +104,3 @@ export const loadModcpWarningsRoute = async ({ title: t("core.moderation.warn.list.title"), }; }; - -export interface ModcpMemberRouteData extends ModcpRouteData { - id: number; -} - -export const loadModcpMemberRoute = async ({ - auth, - id: raw, - queryClient, - t, -}: { - auth: AuthenticatedState; - id: string; - queryClient: QueryClient; - t: PluginRouteTranslator; -}): Promise => { - requireModeratorRoute(auth); - const id = routeIdOf(raw); - let name = ""; - try { - await withNotFound(async () => { - const standing = await queryClient.query({ - ...memberStandingQueryOptions(id), - staleTime: "static", - }); - name = standing.user.name; - }); - } catch (error) { - // A moderator who may neither warn nor view warnings gets the same 404. - if (error instanceof ModcpRequestError && error.status === 403) { - // oxlint-disable-next-line typescript/only-throw-error - throw notFound(); - } - throw error; - } - - return { - description: t("core.moderation.warn.member.desc"), - id, - title: t("core.moderation.warn.member.title", { name }), - }; -}; diff --git a/packages/vitnode/src/tanstack/moderation/warnings-screen.tsx b/packages/vitnode/src/tanstack/moderation/warnings-screen.tsx index f662c3a588..0d6c52098e 100644 --- a/packages/vitnode/src/tanstack/moderation/warnings-screen.tsx +++ b/packages/vitnode/src/tanstack/moderation/warnings-screen.tsx @@ -3,7 +3,6 @@ import React from "react"; import type { DataTableNavigation } from "@/components/table/navigation"; import { DataTableNavigationProvider } from "@/components/table/navigation"; -import { ModcpMemberContent } from "@/views/moderation/warnings/modcp-member-content"; import { ModcpWarningsContent } from "@/views/moderation/warnings/modcp-warnings-content"; import type { ModcpRouteData } from "./route"; @@ -54,9 +53,3 @@ export const ModcpWarningsRouteContent = ({ ); }; - -export const ModcpMemberRouteContent = ({ id }: { id: number }) => ( - - - -); diff --git a/packages/vitnode/src/views/moderation/warnings/member-log.tsx b/packages/vitnode/src/views/moderation/warnings/member-log.tsx deleted file mode 100644 index e76590a8ec..0000000000 --- a/packages/vitnode/src/views/moderation/warnings/member-log.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import type { LucideIcon } from "lucide-react"; - -import { useInfiniteQuery } from "@tanstack/react-query"; -import { Link } from "@tanstack/react-router"; -import { - ChevronDownIcon, - HistoryIcon, - ShieldAlertIcon, - ShieldOffIcon, - TriangleAlertIcon, - Undo2Icon, -} from "lucide-react"; -import React from "react"; -import { useFormatter, useTranslations } from "use-intl"; - -import { DateFormat } from "@/components/date-format"; -import { Button } from "@/components/ui/button"; -import { Skeleton } from "@/components/ui/skeleton"; - -import type { ModcpMemberLogEntry } from "./warnings-api"; - -import { PersonName } from "../modcp/report-tone"; -import { memberLogQueryOptions } from "./warnings-api"; - -const ICONS: Record = { - "sanction.applied": ShieldAlertIcon, - "sanction.revoked": ShieldOffIcon, - "warning.issued": TriangleAlertIcon, - "warning.revoked": Undo2Icon, -}; - -const numberOf = (value: unknown): null | number => - typeof value === "number" ? value : null; - -const EntryText = ({ - entry, -}: { - entry: ModcpMemberLogEntry; -}): React.ReactNode => { - const t = useTranslations("core.moderation.warn.log"); - const format = useFormatter(); - const { metadata } = entry; - const actor = () => - entry.actor ? ( - - ) : ( - (entry.actorName ?? t("someone")) - ); - const points = numberOf(metadata.points) ?? 0; - - switch (entry.action) { - case "sanction.applied": { - const endsAt = - typeof metadata.endsAt === "string" ? new Date(metadata.endsAt) : null; - const values = { - actor, - threshold: numberOf(metadata.thresholdPoints) ?? 0, - }; - - return endsAt - ? t.rich("sanction_applied_until", { - ...values, - endsAt: format.dateTime(endsAt, { - dateStyle: "medium", - timeStyle: "short", - }), - }) - : t.rich("sanction_applied_indefinite", values); - } - case "sanction.revoked": - return t.rich("sanction_revoked", { actor }); - case "warning.issued": - return t.rich("warning_issued", { - actor, - after: numberOf(metadata.pointsAfter) ?? points, - before: numberOf(metadata.pointsBefore) ?? 0, - points, - }); - case "warning.revoked": - return t.rich("warning_revoked", { actor, points }); - default: - return t.rich("other", { action: entry.action, actor }); - } -}; - -const EntryDetails = ({ entry }: { entry: ModcpMemberLogEntry }) => { - const t = useTranslations("core.moderation.warn.log"); - const { metadata } = entry; - const reportId = numberOf(metadata.reportId); - const details = [ - metadata.pointsOverridden === true - ? t("points_overridden", { - points: numberOf(metadata.defaultPoints) ?? 0, - }) - : null, - metadata.expirationOverridden === true ? t("expiration_overridden") : null, - ].filter((detail): detail is string => detail !== null); - - if (details.length === 0 && reportId === null) return null; - - return ( -

- {details.map(detail => ( - {detail} - ))} - {reportId === null ? null : ( - - {t("report", { id: reportId })} - - )} -

- ); -}; - -const PendingRows = () => ( - -); - -/** - * The member's moderation log: who warned them, revoked a warning, or applied - * and lifted a suspension, from the central activity log. Never shows staff - * notes. Only for moderators with `can_view`. - */ -export const MemberLog = ({ userId }: { userId: number }) => { - const t = useTranslations("core.moderation.warn.log"); - const titleId = React.useId(); - const query = useInfiniteQuery(memberLogQueryOptions(userId)); - const entries = query.data?.pages.flatMap(page => page.edges) ?? []; - - return ( -
-

- {t("title")} -

- -
- {query.isPending ? ( - <> - - {t("loading")} - - - - ) : query.isError ? ( -

- {t("error")} -

- ) : entries.length === 0 ? ( -

-

- ) : ( -
    - {entries.map(entry => { - const Icon = ICONS[entry.action] ?? HistoryIcon; - - return ( -
  1. - - -
    -

    - -

    - -

    - -

    -
    -
  2. - ); - })} -
- )} - - {query.hasNextPage ? ( - - ) : null} -
-
- ); -}; diff --git a/packages/vitnode/src/views/moderation/warnings/modcp-member-content.tsx b/packages/vitnode/src/views/moderation/warnings/modcp-member-content.tsx deleted file mode 100644 index eab03e70f2..0000000000 --- a/packages/vitnode/src/views/moderation/warnings/modcp-member-content.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { useSuspenseQuery } from "@tanstack/react-query"; -import { TriangleAlertIcon } from "lucide-react"; -import React from "react"; -import { useTranslations } from "use-intl"; - -import { Button } from "@/components/ui/button"; -import { PageTitle } from "@/components/ui/page-title"; -import { SlidingNumber } from "@/components/ui/sliding-number"; -import { WarnDialog } from "@/tanstack/moderation/warn-button"; - -import { Person } from "../modcp/report-tone"; -import { MemberLog } from "./member-log"; -import { SanctionCard, StaffWarningCard } from "./warning-list"; -import { memberStandingQueryOptions } from "./warnings-api"; - -const Section = ({ - children, - title, -}: { - children: React.ReactNode; - title: string; -}) => { - const id = React.useId(); - - return ( -
-

- {title} -

- {children} -
- ); -}; - -const Empty = ({ children }: { children: React.ReactNode }) => ( -

- {children} -

-); - -/** A member's standing in the ModCP: points, warnings and suspensions. */ -export const ModcpMemberContent = ({ userId }: { userId: number }) => { - const t = useTranslations("core.moderation.warn"); - const { data } = useSuspenseQuery(memberStandingQueryOptions(userId)); - const [warning, setWarning] = React.useState(false); - const { permissions } = data; - - return ( -
- - {permissions.warn ? ( - - ) : null} - - -
- -
-
-
- {t("member.active_points")} -
-
- -
-
- {data.sanctions ? ( -
-
- {t("member.status")} -
-
- {data.sanctions.some(sanction => sanction.active) - ? t("member.suspended") - : t("member.in_good_standing")} -
-
- ) : null} -
-
- - {data.sanctions ? ( -
- {data.sanctions.length === 0 ? ( - {t("member.no_sanctions")} - ) : ( -
    - {data.sanctions.map(sanction => ( -
  • - -
  • - ))} -
- )} -
- ) : null} - -
- {data.hiddenWarnings > 0 ? ( -

- {t("member.hidden_warnings", { count: data.hiddenWarnings })} -

- ) : null} - {data.warnings.length === 0 ? ( - {t("member.no_warnings")} - ) : ( -
    - {data.warnings.map(item => ( -
  • - -
  • - ))} -
- )} -
- - {permissions.viewLog ? : null} - - -
- ); -}; diff --git a/packages/vitnode/src/views/moderation/warnings/modcp-warnings-content.tsx b/packages/vitnode/src/views/moderation/warnings/modcp-warnings-content.tsx index 6630e71ecb..2b42f8b28f 100644 --- a/packages/vitnode/src/views/moderation/warnings/modcp-warnings-content.tsx +++ b/packages/vitnode/src/views/moderation/warnings/modcp-warnings-content.tsx @@ -1,31 +1,22 @@ import { useQuery, useSuspenseQuery } from "@tanstack/react-query"; -import { Link } from "@tanstack/react-router"; -import { cn } from "cn"; -import { ChevronRightIcon, SearchIcon, TriangleAlertIcon } from "lucide-react"; -import React from "react"; import { useLocale, useTranslations } from "use-intl"; import { FiltersDataTable } from "@/components/table/filters"; import { useDataTableUrl } from "@/components/table/navigation"; -import { PaginationDataTable } from "@/components/table/pagination"; import { withTableFilter } from "@/components/table/url-state"; -import { - Empty, - EmptyDescription, - EmptyHeader, - EmptyMedia, - EmptyTitle, -} from "@/components/ui/empty"; -import { Input } from "@/components/ui/input"; import { PageTitle } from "@/components/ui/page-title"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import type { ModcpWarningsFilter } from "./warnings-api"; -import { Person } from "../modcp/report-tone"; import { resolveReasonText } from "../report-reasons-query"; -import { StaffWarningCard } from "./warning-list"; import { - memberSearchQueryOptions, + MemberBand, + MemberFilter, + WarnMemberButton, +} from "./modcp-warnings-member"; +import { ModcpWarningsTable } from "./modcp-warnings-table"; +import { warningReasonsQueryOptions, warningsListQueryOptions, } from "./warnings-api"; @@ -42,41 +33,26 @@ const StatusTabs = ({ status }: { status: ModcpWarningsFilter["status"] }) => { const { navigate, searchParams } = useDataTableUrl(); return ( -
+ }), + ); + }} + value={status} + > + + {MODCP_WARNING_STATUSES.map(value => ( + + {value === "all" ? t("list.all") : t(`statuses.${value}`)} + + ))} + + ); }; @@ -105,136 +81,6 @@ const ReasonFilter = () => { ); }; -/** Finds a member by name and links to their standing. */ -export const MemberLookup = () => { - const t = useTranslations("core.moderation.warn.lookup"); - const [search, setSearch] = React.useState(""); - const deferred = React.useDeferredValue(search); - const results = useQuery(memberSearchQueryOptions(deferred)); - const inputId = React.useId(); - const statusId = React.useId(); - - return ( -
-

- {t("title")} -

-
-
-

- {results.data ? t("results", { count: results.data.length }) : ""} -

- {deferred.trim() && results.data ? ( - results.data.length === 0 ? ( -

{t("empty")}

- ) : ( -
    - {results.data.map(person => ( -
  • - - -
  • - ))} -
- ) - ) : null} - {results.isError ? ( -

{t("forbidden")}

- ) : null} -
- ); -}; - -const List = ({ filter }: { filter: ModcpWarningsFilter }) => { - const t = useTranslations("core.moderation.warn"); - const { data } = useSuspenseQuery(warningsListQueryOptions(filter)); - const { isPending } = useDataTableUrl(); - const totalPages = Math.max(1, Math.ceil(data.total / data.pageSize)); - - return ( - <> - {data.items.length === 0 ? ( - - - - - - {t("list.empty_title")} - {t("list.empty_desc")} - - - ) : ( -
    - {data.items.map(item => ( -
  • - -
  • - ))} -
- )} - - 1, - pageSize: data.pageSize, - startCursor: null, - totalCount: data.total, - totalPages, - }} - /> - - ); -}; - /** The ModCP warning log: every warning the moderator may see, filtered. */ export const ModcpWarningsContent = ({ description, @@ -244,14 +90,34 @@ export const ModcpWarningsContent = ({ description: string; filter: ModcpWarningsFilter; title: string; -}) => ( -
- - -
- - +}) => { + const { data } = useSuspenseQuery(warningsListQueryOptions(filter)); + const { userId } = filter; + + return ( +
+
+ + {data.permissions.warn && userId === undefined ? ( + + ) : null} +
+ + +
+ +
+ + +
+
+
+ } + label={title} + />
- -
-); + ); +}; diff --git a/packages/vitnode/src/views/moderation/warnings/modcp-warnings-member.tsx b/packages/vitnode/src/views/moderation/warnings/modcp-warnings-member.tsx new file mode 100644 index 0000000000..dc572a77b9 --- /dev/null +++ b/packages/vitnode/src/views/moderation/warnings/modcp-warnings-member.tsx @@ -0,0 +1,549 @@ +import { + keepPreviousData, + useQuery, + useQueryClient, +} from "@tanstack/react-query"; +import { Link } from "@tanstack/react-router"; +import { cn } from "cn"; +import { + ArrowRightIcon, + ExternalLinkIcon, + RotateCwIcon, + ShieldAlertIcon, + TriangleAlertIcon, + UserRoundIcon, + UserRoundXIcon, + XIcon, +} from "lucide-react"; +import { AnimatePresence, useReducedMotion } from "motion/react"; +import * as m from "motion/react-m"; +import React from "react"; +import { useTranslations } from "use-intl"; +import { z } from "zod"; + +import type { AutoFormOnSubmit } from "@/components/form/auto-form"; +import type { UserOption } from "@/components/form/fields/input-users"; + +import { AutoForm, AutoFormSubmitButton } from "@/components/form/auto-form"; +import { AsyncPicker } from "@/components/form/common/async-picker"; +import { + AutoFormUser, + UserOptionRow, +} from "@/components/form/fields/input-users"; +import { MotionFeatures } from "@/components/motion-features"; +import { useDataTableUrl } from "@/components/table/navigation"; +import { withTableFilter } from "@/components/table/url-state"; +import { Button, buttonVariants } from "@/components/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Skeleton } from "@/components/ui/skeleton"; +import { SlidingNumber } from "@/components/ui/sliding-number"; +import { userProfileHref } from "@/components/user-format"; +import { WarnDialog } from "@/tanstack/moderation/warn-button"; + +import type { ReportPerson } from "../modcp/report-tone"; +import type { ModcpMemberStanding } from "./warnings-api"; + +import { Person, PersonAvatar, PersonName } from "../modcp/report-tone"; +import { LiftSanctionButton } from "./warning-list"; +import { SanctionSummary } from "./warning-sanction"; +import { + memberSearchQueryOptions, + memberStandingQueryOptions, +} from "./warnings-api"; + +export const useMemberFilter = () => { + const { navigate, searchParams } = useDataTableUrl(); + const raw = Number(searchParams.get("user")); + const userId = Number.isInteger(raw) && raw > 0 ? raw : null; + const setUser = (id: null | number) => { + navigate( + withTableFilter(searchParams, { + id: "user", + values: id === null ? [] : [String(id)], + }), + ); + }; + + return { setUser, userId }; +}; + +const toUserOption = (person: ReportPerson): UserOption => ({ + avatarColor: person.avatarColor, + id: person.id, + name: person.name, + nameCode: person.nameCode, + role: { color: person.role.color, prefix: person.role.prefix }, +}); + +const useMemberSearch = () => { + const queryClient = useQueryClient(); + + return React.useCallback( + async (value: string): Promise => { + if (!value.trim()) return []; + const people = await queryClient.query(memberSearchQueryOptions(value)); + + return people.map(toUserOption); + }, + [queryClient], + ); +}; + +const useStanding = (userId: null | number) => + useQuery({ + ...memberStandingQueryOptions(userId ?? 0), + enabled: userId !== null, + placeholderData: keepPreviousData, + }); + +export const MemberFilter = () => { + const t = useTranslations("core.moderation.warn"); + const search = useMemberSearch(); + const { setUser, userId } = useMemberFilter(); + const standing = useStanding(userId); + const selected = userId ? (standing.data?.user ?? null) : null; + + return ( + + className="w-60" + onSelect={option => { + setUser(option.id); + }} + renderOption={option => } + search={search} + searchPlaceholder={t("lookup.placeholder")} + selectedIds={userId ? [userId] : []} + trigger={ + selected ? ( + + + + ) : ( + + + ) + } + /> + ); +}; + +const pickSchema = z.object({ userId: z.number() }); + +export const WarnMemberButton = () => { + const t = useTranslations("core.moderation.warn"); + const tGlobal = useTranslations("core.global"); + const search = useMemberSearch(); + const [picking, setPicking] = React.useState(false); + const [userId, setUserId] = React.useState(null); + + const onSubmit: AutoFormOnSubmit = values => { + setPicking(false); + window.setTimeout(() => { + setUserId(values.userId); + }, 200); + }; + + return ( + <> + + + + + {t("button_member")} + {t("pick.desc")} + + ( + + ), + }, + ]} + formSchema={pickSchema} + layout={fields => ( + <> + {fields.userId} + + {tGlobal("cancel")} + } + /> + + + + + )} + onSubmit={onSubmit} + /> + + + {userId === null ? null : ( + { + if (!open) setUserId(null); + }} + open + userId={userId} + /> + )} + + ); +}; + +const WarnThisMember = ({ standing }: { standing: ModcpMemberStanding }) => { + const t = useTranslations("core.moderation.warn"); + const [open, setOpen] = React.useState(false); + if (!standing.permissions.warn) return null; + + return ( + <> + + + + ); +}; + +const ClearMemberButton = () => { + const t = useTranslations("core.moderation.warn"); + const { setUser } = useMemberFilter(); + + return ( + + ); +}; + +const Fact = ({ + children, + label, +}: { + children: React.ReactNode; + label: string; +}) => ( +
+
{label}
+
+ {children} +
+
+); + +const EASE_OUT = [0.32, 0.72, 0, 1] as const; + +const useCollapse = () => { + const reduceMotion = useReducedMotion(); + const collapsed = reduceMotion ? { opacity: 0 } : { height: 0, opacity: 0 }; + + return { + animate: { height: "auto", opacity: 1 }, + exit: { + ...collapsed, + transition: { + height: { bounce: 0, duration: 0.2, type: "spring" as const }, + opacity: { duration: 0.15, ease: "easeIn" as const }, + }, + }, + initial: collapsed, + transition: { + height: { bounce: 0, duration: 0.3, type: "spring" as const }, + opacity: { duration: 0.2, ease: "easeOut" as const }, + }, + }; +}; + +const SuspensionNotice = ({ standing }: { standing: ModcpMemberStanding }) => { + const t = useTranslations("core.moderation.warn"); + const collapse = useCollapse(); + const sanction = + standing.sanctions?.find(candidate => candidate.active) ?? null; + + return ( + + {sanction ? ( + +
+

+

+ {standing.permissions.manageSanctions ? ( + + ) : null} +
+
+ ) : null} +
+ ); +}; + +const MemberBandBody = ({ userId }: { userId: number }) => { + const t = useTranslations("core.moderation.warn"); + const tGlobal = useTranslations("core.global"); + const reduceMotion = useReducedMotion(); + const standing = useStanding(userId); + const data = standing.data; + + if (!data) { + return standing.isError ? ( +
+

+

+
+ + +
+
+ ) : ( +
+ + {t("member.loading")} + + +
+ + +
+
+ ); + } + + const activeWarnings = data.warnings.filter( + warning => warning.status === "active", + ).length; + + return ( +
+ +
+ +
+

+ +

+
+ + + + + + + {" "} + / {data.warnings.length} + + +
+
+
+
+ +
+
+ + {data.hiddenWarnings > 0 ? ( +

+ {t("member.hidden_warnings", { count: data.hiddenWarnings })} +

+ ) : null} +
+ ); +}; + +const MEMBER_BAND_ID = "modcp-member-band"; + +export const revealMemberBand = () => { + const band = document.getElementById(MEMBER_BAND_ID); + if (!band) return; + const reduceMotion = window.matchMedia( + "(prefers-reduced-motion: reduce)", + ).matches; + band.scrollIntoView({ + behavior: reduceMotion ? "auto" : "smooth", + block: "start", + }); +}; + +const useRevealOnPick = (userId: null | number) => { + const reduceMotion = useReducedMotion(); + const bandRef = React.useRef(null); + const shownUserIdRef = React.useRef(userId); + const waitingForOpenRef = React.useRef(false); + + const reveal = React.useCallback(() => { + waitingForOpenRef.current = false; + const band = bandRef.current; + if (!band) return; + const { top } = band.getBoundingClientRect(); + if (window.scrollY === 0 && top < window.innerHeight / 2) return; + band.scrollIntoView({ + behavior: reduceMotion ? "auto" : "smooth", + block: "start", + }); + }, [reduceMotion]); + + React.useEffect(() => { + const previous = shownUserIdRef.current; + shownUserIdRef.current = userId; + if (userId === null || userId === previous) return; + if (previous !== null) { + reveal(); + + return; + } + waitingForOpenRef.current = true; + const fallback = window.setTimeout(() => { + if (waitingForOpenRef.current) reveal(); + }, 120); + + return () => { + window.clearTimeout(fallback); + }; + }, [reveal, userId]); + + const onAnimationStart = () => { + if (waitingForOpenRef.current) { + window.requestAnimationFrame(reveal); + } + }; + + return { bandRef, onAnimationStart }; +}; + +export const MemberBand = ({ userId }: { userId: null | number }) => { + const t = useTranslations("core.moderation.warn"); + const collapse = useCollapse(); + const { bandRef, onAnimationStart } = useRevealOnPick(userId); + + return ( + + + {userId === null ? null : ( + + + + )} + + + ); +}; diff --git a/packages/vitnode/src/views/moderation/warnings/modcp-warnings-table.tsx b/packages/vitnode/src/views/moderation/warnings/modcp-warnings-table.tsx new file mode 100644 index 0000000000..d7eca9ecfa --- /dev/null +++ b/packages/vitnode/src/views/moderation/warnings/modcp-warnings-table.tsx @@ -0,0 +1,223 @@ +import { cn } from "cn"; +import { TriangleAlertIcon } from "lucide-react"; +import React from "react"; +import { useLocale, useTranslations } from "use-intl"; + +import type { ColumnDef } from "@/components/table/data-table-content"; + +import { DateFormat } from "@/components/date-format"; +import { ContentDataTable } from "@/components/table/content"; +import { useDataTableUrl } from "@/components/table/navigation"; +import { richTextToPlainText } from "@/content/rich-text/plain-text"; + +import type { ModcpWarningsPage } from "./warnings-api"; + +import { Person } from "../modcp/report-tone"; +import { resolveReasonText } from "../report-reasons-query"; +import { revealMemberBand, useMemberFilter } from "./modcp-warnings-member"; +import { WarningStatusBadge } from "./warning-format"; +import { + RevokeWarningButton, + StaffNotes, + WarningFacts, + WarningRevokedNote, +} from "./warning-list"; +import { pickWarningMessage, WarningMessageContent } from "./warning-message"; + +type WarningRow = ModcpWarningsPage["items"][number]; + +const MemberCell = ({ row }: { row: WarningRow }) => { + const t = useTranslations("core.moderation.warn"); + const { setUser, userId } = useMemberFilter(); + const { user } = row; + + return ( +
+ {user ? ( + + ) : ( + {t("deleted_user")} + )} +
+ ); +}; + +const ReasonCell = ({ row }: { row: WarningRow }) => { + const t = useTranslations("core.moderation.warn"); + const locale = useLocale(); + const preview = richTextToPlainText( + pickWarningMessage(row.message, locale), + ).replace(/\s+/g, " "); + + return ( +
+ + {row.reason + ? resolveReasonText(row.reason, locale) + : t("custom_reason")} + + {preview ? ( + + {preview} + + ) : null} +
+ ); +}; + +const PointsCell = ({ row }: { row: WarningRow }) => ( + + {row.points} + +); + +const ExpandedWarning = ({ + canRevoke, + warning, +}: { + canRevoke: boolean; + warning: WarningRow; +}) => { + const t = useTranslations("core.moderation.warn"); + + return ( +
+
+
+

+ {t("list.message")} +

+ +
+ + + {canRevoke ? ( +
+ +
+ ) : null} +
+ +
+ ); +}; + +export const ModcpWarningsTable = ({ + data, + header, + label, +}: { + data: ModcpWarningsPage; + header: React.ReactNode; + label: string; +}) => { + const t = useTranslations("core.moderation.warn"); + const { isPending } = useDataTableUrl(); + const totalPages = Math.max(1, Math.ceil(data.total / data.pageSize)); + const columns: ColumnDef[] = [ + { + cell: ({ row }) => , + className: "whitespace-normal", + header: t("list.member"), + id: "member", + }, + { + cell: ({ row }) => , + className: "w-full max-w-0 min-w-48", + header: t("list.reason"), + id: "reason", + }, + { + accessorKey: "points", + align: "right", + cell: ({ row }) => , + className: "w-20", + header: t("list.points"), + }, + { + accessorKey: "status", + cell: ({ row }) => , + className: "w-28", + header: t("list.status"), + }, + { + accessorKey: "issuedAt", + cell: ({ row }) => ( + + + + ), + className: "w-36", + header: t("list.issued"), + }, + ]; + + return ( +
+ + aria-label={label} + columns={columns} + customNoResults={{ + description: t("list.empty_desc"), + icon:
+ ); +}; diff --git a/packages/vitnode/src/views/moderation/warnings/suspension-dialog-content.tsx b/packages/vitnode/src/views/moderation/warnings/suspension-dialog-content.tsx index dec1b5866c..fdaa21fbb6 100644 --- a/packages/vitnode/src/views/moderation/warnings/suspension-dialog-content.tsx +++ b/packages/vitnode/src/views/moderation/warnings/suspension-dialog-content.tsx @@ -9,7 +9,7 @@ import { useFormatter, useLocale, useNow, useTranslations } from "use-intl"; import { MotionFeatures } from "@/components/motion-features"; import { Button } from "@/components/ui/button"; -import { DialogDescription, DialogTitle } from "@/components/ui/dialog"; +import { DialogTitle } from "@/components/ui/dialog"; import { FEEDBACK_TONE_TEXT } from "@/components/ui/feedback-tone"; import { Skeleton } from "@/components/ui/skeleton"; import { SlidingNumber } from "@/components/ui/sliding-number"; @@ -184,7 +184,6 @@ export const SuspensionDialogContent = ({ endsAt: Date | null; }) => { const t = useTranslations("core.moderation.account_status.dialog"); - const tStatus = useTranslations("core.moderation.account_status"); const format = useFormatter(); const locale = useLocale(); const now = useNow({ updateInterval: 60 * SECOND }); @@ -208,12 +207,7 @@ export const SuspensionDialogContent = ({
-
- {t("title")} - - {reason ? t("because", { reason }) : t("because_unknown")} - -
+ {t("title")} {endsAt ? ( @@ -270,12 +264,11 @@ export const SuspensionDialogContent = ({
)} -
- {import.meta.env.DEV ? : null} -

- {tStatus("contact")} -

-
+ {import.meta.env.DEV ? ( +
+ +
+ ) : null}
); }; diff --git a/packages/vitnode/src/views/moderation/warnings/warning-list.tsx b/packages/vitnode/src/views/moderation/warnings/warning-list.tsx index 4d748611bf..3beef471c4 100644 --- a/packages/vitnode/src/views/moderation/warnings/warning-list.tsx +++ b/packages/vitnode/src/views/moderation/warnings/warning-list.tsx @@ -1,5 +1,6 @@ import { useQueryClient } from "@tanstack/react-query"; import { Link } from "@tanstack/react-router"; +import { cn } from "cn"; import { ExternalLinkIcon, FlagIcon, @@ -9,7 +10,7 @@ import { } from "lucide-react"; import React from "react"; import { toast } from "sonner"; -import { useLocale, useTranslations } from "use-intl"; +import { useTranslations } from "use-intl"; import { z } from "zod"; import type { AutoFormOnSubmit } from "@/components/form/auto-form"; @@ -17,7 +18,6 @@ import type { AutoFormOnSubmit } from "@/components/form/auto-form"; import { DateFormat } from "@/components/date-format"; import { AutoForm } from "@/components/form/auto-form"; import { AutoFormTextarea } from "@/components/form/fields/textarea"; -import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -35,12 +35,9 @@ import type { WarningResult, } from "./warnings-api"; -import { Person } from "../modcp/report-tone"; -import { resolveReasonText } from "../report-reasons-query"; +import { Person, PersonAvatar, PersonName } from "../modcp/report-tone"; import { ReasonFormFooter } from "./reason-form-footer"; -import { WarningDuration, WarningStatusBadge } from "./warning-format"; -import { WarningMessageContent, WarningNoteContent } from "./warning-message"; -import { SanctionSummary } from "./warning-sanction"; +import { WarningNoteContent } from "./warning-message"; import { ACCOUNT_STATUS_QUERY_KEY, revokeSanction, @@ -162,32 +159,34 @@ const PersonOrDeleted = ({ person }: { person: null | ReportPerson }) => { return person ? : <>{t("deleted_user")}; }; +const InlinePersonOrDeleted = ({ person }: { person: null | ReportPerson }) => { + const t = useTranslations("core.moderation.warn"); + if (!person) return <>{t("deleted_user")}; + + return ( + + + + + ); +}; + /** Who, when, what for: the facts of one warning. */ -const WarningFacts = ({ - member, +export const WarningFacts = ({ + className, warning, }: { - member?: null | ReportPerson; + className?: string; warning: ModcpStaffWarning; }) => { const t = useTranslations("core.moderation.warn"); return ( -
- {member !== undefined ? ( - - {member ? ( - - - - ) : ( - - )} - - ) : null} +
@@ -235,7 +234,7 @@ const WarningFacts = ({ }; /** Staff notes and override explanations - `null` from the API unless the moderator may read them. */ -const StaffNotes = ({ warning }: { warning: ModcpStaffWarning }) => { +export const StaffNotes = ({ warning }: { warning: ModcpStaffWarning }) => { const t = useTranslations("core.moderation.warn"); if (!warning.staffNote && !warning.overrideNote) return null; @@ -259,87 +258,54 @@ const StaffNotes = ({ warning }: { warning: ModcpStaffWarning }) => { ); }; -/** One warning as staff see it, with Revoke when allowed. */ -export const StaffWarningCard = ({ - canRevoke, - member, +export const WarningRevokedNote = ({ warning, }: { - canRevoke: boolean; - /** Shown in lists that mix members. */ - member?: null | ReportPerson; warning: ModcpStaffWarning; }) => { const t = useTranslations("core.moderation.warn"); - const locale = useLocale(); - const [revoking, setRevoking] = React.useState(false); - const reason = warning.reason - ? resolveReasonText(warning.reason, locale) - : t("custom_reason"); - const titleId = React.useId(); const { revokedAt } = warning; + if (!revokedAt) return null; return ( -
-
-
-

- {reason} -

-

- -

-
-
- - {t("points", { count: warning.points })} - - - {canRevoke && warning.status !== "revoked" ? ( - - ) : null} -
-
- - - - - - - - - - {revokedAt ? ( -

- {t.rich("list.revoked", { - by: () => , - date: () => , - })} - {warning.revokeReason ? ` ${warning.revokeReason}` : null} -

- ) : null} - - {warning.sanctionId && revokedAt ? ( +
+

+ {t.rich("list.revoked", { + by: () => , + date: () => , + })} + {warning.revokeReason ? ` ${warning.revokeReason}` : null} +

+ {warning.sanctionId ? (

{t("list.sanction_stays")}

) : null} +
+ ); +}; + +export const RevokeWarningButton = ({ + warning, +}: { + warning: ModcpStaffWarning; +}) => { + const t = useTranslations("core.moderation.warn"); + const [revoking, setRevoking] = React.useState(false); + if (warning.status === "revoked") return null; + return ( + <> + { @@ -354,97 +320,30 @@ export const StaffWarningCard = ({ successDesc={t("revoke.done_desc")} title={t("revoke.title")} /> -
+ ); }; -/** One suspension, with Lift when allowed. */ -export const SanctionCard = ({ - canManage, +export const LiftSanctionButton = ({ sanction, }: { - canManage: boolean; sanction: ModcpSanction; }) => { const t = useTranslations("core.moderation.warn"); const [lifting, setLifting] = React.useState(false); - const { revokedAt } = sanction; return ( -
-
-
-

- {sanction.endsAt - ? t("sanctions.temporary") - : t("sanctions.indefinite")} -

-

- -

-
-
- - {sanction.active - ? t("sanctions.active") - : sanction.revokedAt - ? t("sanctions.lifted") - : t("sanctions.ended")} - - {canManage && sanction.active ? ( - - ) : null} -
-
-
- - - - - {sanction.threshold - ? t("sanctions.from_threshold", { - points: sanction.threshold.points, - }) - : t("sanctions.manual")} - {sanction.threshold?.durationHours ? ( - <> - {" "} - () - - ) : null} - -
- {revokedAt ? ( -

- {t.rich("list.lifted", { - by: () => , - date: () => , - })} - {sanction.revokeReason ? ` ${sanction.revokeReason}` : null} -

- ) : null} + <> + { @@ -459,6 +358,6 @@ export const SanctionCard = ({ successDesc={t("sanctions.lifted_done_desc")} title={t("sanctions.lift_title")} /> -
+ ); }; diff --git a/packages/vitnode/src/views/moderation/warnings/warnings-api.ts b/packages/vitnode/src/views/moderation/warnings/warnings-api.ts index f1ffc2fcce..ae0d6e90c1 100644 --- a/packages/vitnode/src/views/moderation/warnings/warnings-api.ts +++ b/packages/vitnode/src/views/moderation/warnings/warnings-api.ts @@ -1,8 +1,4 @@ -import { - infiniteQueryOptions, - keepPreviousData, - queryOptions, -} from "@tanstack/react-query"; +import { keepPreviousData, queryOptions } from "@tanstack/react-query"; import type { RichTextDocument } from "@/content/rich-text/document"; import type { WarningMessage } from "@/lib/moderation/warnings"; @@ -85,39 +81,6 @@ export const memberStandingQueryOptions = (userId: number) => staleTime: 10_000, }); -const fetchMemberLog = async (userId: number, cursor: null | string) => { - const response = await fetcher({ - plugin: manifest.id, - args: { - params: { id: userId }, - query: cursor ? { cursor } : {}, - }, - method: "get", - module: "moderation", - path: "/members/{id}/log", - }); - if (!response.ok) { - throw new ModcpRequestError(response.status, "the member's log"); - } - - return await response.json(); -}; - -export type ModcpMemberLogEntry = Awaited< - ReturnType ->["edges"][number]; - -export const memberLogQueryOptions = (userId: number) => - infiniteQueryOptions({ - getNextPageParam: (last: Awaited>) => - last.pageInfo.hasNextPage ? last.pageInfo.endCursor : undefined, - initialPageParam: null as null | string, - queryFn: async ({ pageParam }) => await fetchMemberLog(userId, pageParam), - queryKey: [...WARNINGS_QUERY_ROOT, "member-log", userId], - retry: false, - staleTime: 10_000, - }); - export interface ModcpWarningsFilter { issuedById?: number; page: number; diff --git a/packages/vitnode/src/views/profile/warnings/profile-warnings.tsx b/packages/vitnode/src/views/profile/warnings/profile-warnings.tsx index b36b52cf24..41cc93a106 100644 --- a/packages/vitnode/src/views/profile/warnings/profile-warnings.tsx +++ b/packages/vitnode/src/views/profile/warnings/profile-warnings.tsx @@ -30,6 +30,7 @@ import { SlidingNumber } from "@/components/ui/sliding-number"; import { TooltipWithContent } from "@/components/ui/tooltip"; import { WarnDialog } from "@/tanstack/moderation/warn-button"; import { ModcpRequestError } from "@/views/moderation/modcp/modcp-query"; +import { PersonName } from "@/views/moderation/modcp/report-tone"; import { accountStatusQueryOptions, memberStandingQueryOptions, @@ -202,10 +203,10 @@ const EntryRow = ({ ) : null} - + {entry.kind === "warning" ? ( entry.warning.issuedBy ? ( - entry.warning.issuedBy.name + ) : ( tWarn("points", { count: entry.warning.points }) ) @@ -295,12 +296,25 @@ const StandingCard = ({

{t.rich("points_animated", { count: standing.activePoints, - n: () => , + n: () => ( + 0 + ? ["bg-warn/15", FEEDBACK_TONE_TEXT.warning] + : "bg-muted text-muted-foreground", + )} + > + + + ), })}

diff --git a/packages/vitnode/src/views/profile/warnings/standing-confirm-content.tsx b/packages/vitnode/src/views/profile/warnings/standing-confirm-content.tsx index ee71ce62f4..2378b7c0e9 100644 --- a/packages/vitnode/src/views/profile/warnings/standing-confirm-content.tsx +++ b/packages/vitnode/src/views/profile/warnings/standing-confirm-content.tsx @@ -14,6 +14,7 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { WARNING_LIMITS } from "@/lib/moderation/warnings"; +import { PersonName } from "@/views/moderation/modcp/report-tone"; import { ReasonFormFooter } from "@/views/moderation/warnings/reason-form-footer"; import { ACCOUNT_STATUS_QUERY_KEY, @@ -50,7 +51,12 @@ const Subject = ({ confirm }: { confirm: NonNullable }) => {

{tWarn("points", { count: warning.points })} - {warning.issuedBy ? ` · ${warning.issuedBy.name}` : null} + {warning.issuedBy ? ( + <> + {" · "} + + + ) : null} {` · ${format.relativeTime(warning.issuedAt, now)}`}

diff --git a/packages/vitnode/src/views/profile/warnings/standing-sheet-content.tsx b/packages/vitnode/src/views/profile/warnings/standing-sheet-content.tsx index bf9bff1260..cba61e0773 100644 --- a/packages/vitnode/src/views/profile/warnings/standing-sheet-content.tsx +++ b/packages/vitnode/src/views/profile/warnings/standing-sheet-content.tsx @@ -427,7 +427,12 @@ const SuspensionBody = ({ icon={} meta={ <> - {cause.issuedBy ? `${cause.issuedBy.name} · ` : null} + {cause.issuedBy ? ( + <> + + {" · "} + + ) : null} {format.relativeTime(cause.issuedAt, now)} } diff --git a/packages/vitnode/src/views/profile/warnings/standing.ts b/packages/vitnode/src/views/profile/warnings/standing.ts index 59256f619e..00c5dbd3e2 100644 --- a/packages/vitnode/src/views/profile/warnings/standing.ts +++ b/packages/vitnode/src/views/profile/warnings/standing.ts @@ -68,7 +68,7 @@ export const standingFromModcp = ( }, hiddenWarnings: data.hiddenWarnings, own: false, - seeAllHref: `/modcp/members/${data.user.id}`, + seeAllHref: `/modcp/warnings?user=${data.user.id}`, suspensions: (data.sanctions ?? []).map(sanction => ({ active: sanction.active, endsAt: toOptionalDate(sanction.endsAt),
} />; +) => ( + <> +
} /> + + +); diff --git a/packages/vitnode/src/tanstack/moderation/breadcrumb.tsx b/packages/vitnode/src/tanstack/moderation/breadcrumb.tsx index 2d28b347f8..5dc70417eb 100644 --- a/packages/vitnode/src/tanstack/moderation/breadcrumb.tsx +++ b/packages/vitnode/src/tanstack/moderation/breadcrumb.tsx @@ -37,3 +37,19 @@ export const ModcpApprovalBreadcrumb = ({ return <>{t("title", { id: loaderData?.id ?? 0 })}; }; + +export const ModcpWarningsBreadcrumb = () => { + const t = useTranslations("core.moderation.warn.list"); + + return <>{t("title")}; +}; + +export const ModcpMemberBreadcrumb = ({ + loaderData, +}: { + loaderData?: { title: string }; +}) => { + const t = useTranslations("core.moderation.warn.member"); + + return <>{loaderData?.title ?? t("breadcrumb")}; +}; diff --git a/packages/vitnode/src/tanstack/moderation/index.ts b/packages/vitnode/src/tanstack/moderation/index.ts index 5be0fd2790..e8e1734b9b 100644 --- a/packages/vitnode/src/tanstack/moderation/index.ts +++ b/packages/vitnode/src/tanstack/moderation/index.ts @@ -6,4 +6,10 @@ export { ReportDialog, type ReportDialogProps, } from "./report-button"; +export { + WarnAuthorButton, + type WarnAuthorButtonProps, + WarnDialog, + type WarnDialogProps, +} from "./warn-button"; export type { ReportPreview } from "@/views/moderation/report/report-dialog-content"; diff --git a/packages/vitnode/src/tanstack/moderation/route.ts b/packages/vitnode/src/tanstack/moderation/route.ts index 5c386391de..f5d00a7a0b 100644 --- a/packages/vitnode/src/tanstack/moderation/route.ts +++ b/packages/vitnode/src/tanstack/moderation/route.ts @@ -39,7 +39,7 @@ export const requireModeratorRoute = (auth: AuthenticatedState): void => { }; /** A serial id from the URL, or the route's 404. */ -const routeIdOf = (raw: string): number => { +export const routeIdOf = (raw: string): number => { const id = /^[1-9]\d{0,9}$/.test(raw) ? Number(raw) : Number.NaN; if (!Number.isSafeInteger(id) || id > 2_147_483_647) { // oxlint-disable-next-line typescript/only-throw-error @@ -50,7 +50,9 @@ const routeIdOf = (raw: string): number => { }; /** Turns the API's 404 - which also covers "not in your scope" - into the route's. */ -const withNotFound = async (load: () => Promise): Promise => { +export const withNotFound = async ( + load: () => Promise, +): Promise => { try { await load(); } catch (error) { diff --git a/packages/vitnode/src/tanstack/moderation/suspension-notice.tsx b/packages/vitnode/src/tanstack/moderation/suspension-notice.tsx new file mode 100644 index 0000000000..9a0fac9090 --- /dev/null +++ b/packages/vitnode/src/tanstack/moderation/suspension-notice.tsx @@ -0,0 +1,61 @@ +import { Link } from "@tanstack/react-router"; +import { ShieldAlertIcon } from "lucide-react"; +import { useFormatter, useTranslations } from "use-intl"; + +import { useSessionQuery } from "../auth/session-query"; +import { RouteMessages } from "../i18n/route-messages"; +import { REPORT_NAMESPACES } from "./report-button"; + +export const ACCOUNT_STATUS_PATH = "/account-status"; + +const Notice = ({ endsAt }: { endsAt: Date | null | string }) => { + const t = useTranslations("core.moderation.account_status.notice"); + const format = useFormatter(); + + return ( +
+
+
+
+ ); +}; + +/** + * A strip under the header while the session's account is suspended. The API + * treats such a session as a guest everywhere; this tells the member why and + * links to their account status. Renders nothing otherwise. + */ +export const SuspensionNotice = () => { + const { data } = useSessionQuery(); + const suspension = data?.suspension; + if (!suspension) return null; + + return ( + + + + ); +}; diff --git a/packages/vitnode/src/tanstack/moderation/warn-button.tsx b/packages/vitnode/src/tanstack/moderation/warn-button.tsx new file mode 100644 index 0000000000..e98c0b2953 --- /dev/null +++ b/packages/vitnode/src/tanstack/moderation/warn-button.tsx @@ -0,0 +1,155 @@ +import { TriangleAlertIcon } from "lucide-react"; +import React from "react"; +import { useTranslations } from "use-intl"; + +import type { ContentTarget } from "@/content/target"; +import type { IssuedWarning } from "@/views/moderation/warnings/warnings-api"; + +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent } from "@/components/ui/dialog"; +import { Skeleton } from "@/components/ui/skeleton"; + +import { RouteMessages } from "../i18n/route-messages"; +import { REPORT_NAMESPACES } from "./report-button"; + +const WarnDialogContent = React.lazy( + async () => await import("@/views/moderation/warnings/warn-dialog-content"), +); + +export interface WarnDialogProps { + /** + * The content the warning is for. The server resolves its authors and + * moderation scope itself and refuses the warning unless `userId` is one of + * the authors and the moderator has `can_warn` there. Leave it out for a + * direct warning, which needs the core `warnings` `can_warn` permission. + */ + content?: ContentTarget | null; + /** Called with the server's result once the warning is issued. */ + onIssued?: (result: IssuedWarning) => void; + onOpenChange: (open: boolean) => void; + open: boolean; + /** The report the warning answers, for the ModCP report screen. */ + reportId?: null | number; + /** The member to warn. */ + userId: number; +} + +const WarnDialogInner = ({ onOpenChange, open, ...props }: WarnDialogProps) => ( + + + {open ? ( + + + + + + } + > + { + onOpenChange(false); + }} + /> + + ) : null} + + +); + +/** + * The warn dialog on its own, for a menu item or any trigger you already + * have. Shows the reason, points, expiry, the member's message, a staff note + * and a live preview of the consequences; the form loads lazily. + * + * @example + * ```tsx + * import { WarnDialog } from "@vitnode/core/tanstack/moderation"; + * + * ; + * ``` + */ +export const WarnDialog = (props: WarnDialogProps) => ( + + + +); + +export interface WarnAuthorButtonProps extends Omit< + WarnDialogProps, + "onOpenChange" | "open" +> { + className?: string; + /** The id of the element naming the author, read after the button's label. */ + describedBy?: string; +} + +/** + * A **Warn author** button that opens {@link WarnDialog}. Render it only when + * the server allowed it - for a content type, when the permissions route + * answers `canWarn` - and the API checks everything again anyway. + * + * @example + * ```tsx + * import { contentTarget } from "@vitnode/core/content"; + * import { WarnAuthorButton } from "@vitnode/core/tanstack/moderation"; + * + * {permissions.canWarn ? ( + * + * ) : null} + * ``` + */ +export const WarnAuthorButton = ({ + className, + describedBy, + ...props +}: WarnAuthorButtonProps) => { + const [open, setOpen] = React.useState(false); + + return ( + + { + setOpen(true); + }} + /> + + + ); +}; + +const WarnAuthorButtonLabel = ({ + className, + describedBy, + onClick, +}: { + className?: string; + describedBy?: string; + onClick: () => void; +}) => { + const t = useTranslations("core.moderation.warn"); + + return ( + + ); +}; diff --git a/packages/vitnode/src/tanstack/moderation/warnings-route.ts b/packages/vitnode/src/tanstack/moderation/warnings-route.ts new file mode 100644 index 0000000000..84c51e9974 --- /dev/null +++ b/packages/vitnode/src/tanstack/moderation/warnings-route.ts @@ -0,0 +1,154 @@ +import type { QueryClient } from "@tanstack/react-query"; + +import { notFound } from "@tanstack/react-router"; + +import type { PluginRouteTranslator } from "@/routing"; +import type { ModcpWarningsFilter } from "@/views/moderation/warnings/warnings-api"; + +import { asSearchValue, readPageSize } from "@/lib/table-params"; +import { ModcpRequestError } from "@/views/moderation/modcp/modcp-query"; +import { + memberStandingQueryOptions, + warningsListQueryOptions, +} from "@/views/moderation/warnings/warnings-api"; + +import type { AuthenticatedState } from "../auth/state"; +import type { ModcpRouteData } from "./route"; + +import { requireModeratorRoute, routeIdOf, withNotFound } from "./route"; + +export const MODCP_WARNINGS_PAGE_SIZE = 25; +const MAX_PAGE = 10_000; +const MAX_ID = 2_147_483_647; +const STATUSES = ["active", "expired", "revoked"] as const; + +export interface ModcpWarningsRouteSearch { + first?: number; + page?: number; + reason?: number; + /** `all` is what the URL says by saying nothing. */ + status?: (typeof STATUSES)[number]; + user?: number; +} + +const positiveInt = (raw: string | undefined, max: number) => { + if (raw === undefined || !/^\d+$/.test(raw)) return undefined; + const value = Number(raw); + + return Number.isSafeInteger(value) && value > 0 && value <= max + ? value + : undefined; +}; + +export const normalizeModcpWarningsSearch = ( + input: Record, +): ModcpWarningsRouteSearch => { + const status = asSearchValue(input.status); + const reason = positiveInt(asSearchValue(input.reason), MAX_ID); + const user = positiveInt(asSearchValue(input.user), MAX_ID); + const page = positiveInt(asSearchValue(input.page), MAX_PAGE); + const rawFirst = asSearchValue(input.first); + const first = + rawFirst === undefined ? undefined : readPageSize(rawFirst, 100); + + return { + ...(STATUSES.find(value => value === status) + ? { status: status as (typeof STATUSES)[number] } + : {}), + ...(reason === undefined ? {} : { reason }), + ...(user === undefined ? {} : { user }), + ...(page === undefined || page === 1 ? {} : { page }), + ...(first === undefined || Number(first) === MODCP_WARNINGS_PAGE_SIZE + ? {} + : { first: Number(first) }), + }; +}; + +export const modcpWarningsFilter = ( + search: ModcpWarningsRouteSearch, +): ModcpWarningsFilter => ({ + page: search.page ?? 1, + pageSize: search.first ?? MODCP_WARNINGS_PAGE_SIZE, + status: search.status ?? "all", + ...(search.reason === undefined ? {} : { reasonId: search.reason }), + ...(search.user === undefined ? {} : { userId: search.user }), +}); + +export const modcpWarningsSearchParams = ( + search: ModcpWarningsRouteSearch, +): URLSearchParams => + new URLSearchParams( + Object.entries(search) + .filter(([, value]) => value !== undefined) + .map(([key, value]) => [key, String(value)]), + ); + +export const modcpWarningsSearchFrom = ( + query: string, +): ModcpWarningsRouteSearch => + normalizeModcpWarningsSearch(Object.fromEntries(new URLSearchParams(query))); + +export const loadModcpWarningsRoute = async ({ + auth, + queryClient, + search, + t, +}: { + auth: AuthenticatedState; + queryClient: QueryClient; + search: ModcpWarningsRouteSearch; + t: PluginRouteTranslator; +}): Promise => { + requireModeratorRoute(auth); + await queryClient.query({ + ...warningsListQueryOptions(modcpWarningsFilter(search)), + staleTime: "static", + }); + + return { + description: t("core.moderation.warn.list.desc"), + title: t("core.moderation.warn.list.title"), + }; +}; + +export interface ModcpMemberRouteData extends ModcpRouteData { + id: number; +} + +export const loadModcpMemberRoute = async ({ + auth, + id: raw, + queryClient, + t, +}: { + auth: AuthenticatedState; + id: string; + queryClient: QueryClient; + t: PluginRouteTranslator; +}): Promise => { + requireModeratorRoute(auth); + const id = routeIdOf(raw); + let name = ""; + try { + await withNotFound(async () => { + const standing = await queryClient.query({ + ...memberStandingQueryOptions(id), + staleTime: "static", + }); + name = standing.user.name; + }); + } catch (error) { + // A moderator who may neither warn nor view warnings gets the same 404. + if (error instanceof ModcpRequestError && error.status === 403) { + // oxlint-disable-next-line typescript/only-throw-error + throw notFound(); + } + throw error; + } + + return { + description: t("core.moderation.warn.member.desc"), + id, + title: t("core.moderation.warn.member.title", { name }), + }; +}; diff --git a/packages/vitnode/src/tanstack/moderation/warnings-screen.tsx b/packages/vitnode/src/tanstack/moderation/warnings-screen.tsx new file mode 100644 index 0000000000..f662c3a588 --- /dev/null +++ b/packages/vitnode/src/tanstack/moderation/warnings-screen.tsx @@ -0,0 +1,62 @@ +import React from "react"; + +import type { DataTableNavigation } from "@/components/table/navigation"; + +import { DataTableNavigationProvider } from "@/components/table/navigation"; +import { ModcpMemberContent } from "@/views/moderation/warnings/modcp-member-content"; +import { ModcpWarningsContent } from "@/views/moderation/warnings/modcp-warnings-content"; + +import type { ModcpRouteData } from "./route"; +import type { ModcpWarningsRouteSearch } from "./warnings-route"; + +import { RouteMessages } from "../i18n/route-messages"; +import { MODCP_NAMESPACES } from "./namespaces"; +import { + modcpWarningsFilter, + modcpWarningsSearchFrom, + modcpWarningsSearchParams, +} from "./warnings-route"; + +export const ModcpWarningsRouteContent = ({ + description, + navigate, + search, + title, +}: ModcpRouteData & { + navigate: (options: { + resetScroll?: boolean; + search: ModcpWarningsRouteSearch; + }) => Promise; + search: ModcpWarningsRouteSearch; +}) => { + const navigation = React.useMemo( + () => ({ + navigate: async nextSearch => { + await navigate({ + resetScroll: false, + search: modcpWarningsSearchFrom(nextSearch), + }); + }, + searchParams: modcpWarningsSearchParams(search), + }), + [navigate, search], + ); + + return ( + + + + + + ); +}; + +export const ModcpMemberRouteContent = ({ id }: { id: number }) => ( + + + +); diff --git a/packages/vitnode/src/tanstack/profile/screen.tsx b/packages/vitnode/src/tanstack/profile/screen.tsx index 641990f12e..f49c0aebc2 100644 --- a/packages/vitnode/src/tanstack/profile/screen.tsx +++ b/packages/vitnode/src/tanstack/profile/screen.tsx @@ -6,13 +6,14 @@ import { } from "@tanstack/react-query"; import { Link } from "@tanstack/react-router"; import { cn } from "cn"; -import { UserRoundPenIcon } from "lucide-react"; +import { ShieldIcon, TriangleAlertIcon, UserRoundPenIcon } from "lucide-react"; import React from "react"; import { useTranslations } from "use-intl"; -import { buttonVariants } from "@/components/ui/button"; +import { Button, buttonVariants } from "@/components/ui/button"; import { UserPresenceStatus } from "@/components/user-presence-status"; import { USER_HEADER_HREF } from "@/views/layouts/theme/header/user/user-header-model"; +import { memberStandingQueryOptions } from "@/views/moderation/warnings/warnings-api"; import { userPresenceQueryOptions } from "@/views/presence/presence-query"; import { ProfileContent } from "@/views/profile/profile-content"; import { userProfileQueryKey } from "@/views/profile/profile-query"; @@ -22,6 +23,7 @@ import { SearchFeedList } from "@/views/search/search-feed-content"; import { useSessionQuery } from "../auth/session-query"; import { useLocale } from "../i18n/locale"; import { RouteMessages } from "../i18n/route-messages"; +import { WarnDialog } from "../moderation/warn-button"; import { useOwnUserImageEditor } from "./own-image-editor"; import { userProfileQuery } from "./query"; import { PROFILE_NAMESPACES } from "./route"; @@ -72,6 +74,43 @@ const ProfilePresence = ({ ); }; +/** + * Warn and View warnings for moderators. Shown only when the server's + * standing route answers for this member - it decides who may warn directly. + */ +const ModeratorActions = ({ userId }: { userId: number }) => { + const t = useTranslations("core.profile"); + const [open, setOpen] = React.useState(false); + const { data } = useQuery(memberStandingQueryOptions(userId)); + if (!data) return null; + + return ( +
+ +
+ ); +}; + const ProfileTimeline = ({ userId, viewerId, @@ -116,7 +155,13 @@ const ProfileScreen = ({ children, nameCode }: ProfileRouteProps) => { return ( : null} + action={ + isOwner ? ( + + ) : session?.user?.isModerator ? ( + + ) : null + } editor={editor} presence={ ({ href: "/admin/core/moderation/report-reasons", permission: core("report_reasons"), }, + { + title: key("admin.global.nav.moderation.warning_reasons"), + href: "/admin/core/moderation/warning-reasons", + permission: core("warning_settings"), + }, + { + title: key("admin.global.nav.moderation.warning_thresholds"), + href: "/admin/core/moderation/warning-thresholds", + permission: core("warning_settings"), + }, ], }, { diff --git a/packages/vitnode/src/views/admin/views/core/moderation/warnings/warning-reason-form.tsx b/packages/vitnode/src/views/admin/views/core/moderation/warnings/warning-reason-form.tsx new file mode 100644 index 0000000000..63d64f6bf4 --- /dev/null +++ b/packages/vitnode/src/views/admin/views/core/moderation/warnings/warning-reason-form.tsx @@ -0,0 +1,205 @@ +import { toast } from "sonner"; +import { useLocale, useTranslations } from "use-intl"; +import { z } from "zod"; + +import type { AutoFormOnSubmit } from "@/components/form/auto-form"; +import type { AdminMutationResult } from "@/views/admin/views/core/shared/admin-mutation"; + +import { AutoForm } from "@/components/form/auto-form"; +import { AutoFormSheetFooter } from "@/components/form/auto-form-sheet-footer"; +import { AutoFormInput } from "@/components/form/fields/input"; +import { AutoFormNullableNumber } from "@/components/form/fields/nullable-number"; +import { AutoFormNumber } from "@/components/form/fields/number"; +import { AutoFormSwitch } from "@/components/form/fields/switch"; +import { AutoFormTextarea } from "@/components/form/fields/textarea"; +import { multiLangValueSchema } from "@/lib/helpers/multi-lang"; +import { WARNING_LIMITS } from "@/lib/moderation/warnings"; +import { resolveReasonText } from "@/views/moderation/report-reasons-query"; + +import type { + AdminWarningReason, + AdminWarningReasonInput, +} from "./warnings-admin-api"; + +export interface WarningReasonFormProps { + onSave: (args: { + id?: number; + values: AdminWarningReasonInput; + }) => Promise>; + onSaved: () => void; + reason?: AdminWarningReason; +} + +export const WarningReasonForm = ({ + onSave, + onSaved, + reason, +}: WarningReasonFormProps) => { + const t = useTranslations("admin.moderation.warning_reasons.form"); + const locale = useLocale(); + + const formSchema = z.object({ + title: multiLangValueSchema({ + maxLength: WARNING_LIMITS.reasonTitleMaxLength, + minLength: 1, + }) + .min(1) + .default(reason?.title ?? []), + description: multiLangValueSchema({ + maxLength: WARNING_LIMITS.reasonDescriptionMaxLength, + }).default(reason?.description ?? []), + points: z + .number() + .int() + .min(0) + .max(WARNING_LIMITS.maxPoints) + .default(reason?.points ?? 1), + expiresAfterDays: z + .number() + .int() + .min(1) + .max(WARNING_LIMITS.maxDurationHours / 24) + .nullable() + .default( + reason + ? reason.expiresAfterHours === null + ? null + : Math.max(1, Math.round(reason.expiresAfterHours / 24)) + : 30, + ), + message: multiLangValueSchema({ + maxLength: WARNING_LIMITS.reasonMessageMaxLength, + }).default(reason?.message ?? []), + enabled: z.boolean().default(reason?.enabled ?? true), + }); + + const onSubmit: AutoFormOnSubmit = async ({ + expiresAfterDays, + ...values + }) => { + const result = await onSave({ + id: reason?.id, + values: { + ...values, + expiresAfterHours: + expiresAfterDays === null ? null : expiresAfterDays * 24, + }, + }); + + if ("error" in result) { + toast.error(t("error"), { + description: + result.error.status === 409 + ? t("limit_reached", { max: WARNING_LIMITS.maxReasons }) + : t("error_desc"), + }); + + return; + } + + toast.success(reason ? t("updated") : t("created"), { + description: reason + ? t("updated_desc") + : t("created_desc", { + reason: resolveReasonText(values.title, locale), + }), + }); + onSaved(); + }; + + return ( + ( + + ), + }, + { + id: "description", + component: props => ( + + ), + }, + { + id: "points", + component: props => ( + + ), + }, + { + id: "expiresAfterDays", + component: props => ( + + ), + }, + { + id: "message", + component: props => ( + + ), + }, + { + id: "enabled", + component: props => ( + + ), + }, + ]} + formSchema={formSchema} + id={reason ? `warning-reason-${reason.id}` : "warning-reason-new"} + layout={rendered => ( + <> +
+ {rendered.title} + {rendered.description} + {rendered.points} + {rendered.expiresAfterDays} + {rendered.message} + {rendered.enabled} +
+ + + )} + onSubmit={onSubmit} + /> + ); +}; + +export default WarningReasonForm; diff --git a/packages/vitnode/src/views/admin/views/core/moderation/warnings/warning-reasons-content.tsx b/packages/vitnode/src/views/admin/views/core/moderation/warnings/warning-reasons-content.tsx new file mode 100644 index 0000000000..9d4dcad238 --- /dev/null +++ b/packages/vitnode/src/views/admin/views/core/moderation/warnings/warning-reasons-content.tsx @@ -0,0 +1,216 @@ +import { PencilIcon, PlusIcon, TriangleAlertIcon } from "lucide-react"; +import React from "react"; +import { useLocale, useTranslations } from "use-intl"; + +import type { DataTableNavigation } from "@/components/table/navigation"; + +import { ContentDataTable } from "@/components/table/content"; +import { DataTableNavigationProvider } from "@/components/table/navigation"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet"; +import { Spinner } from "@/components/ui/spinner"; +import { TooltipWithContent } from "@/components/ui/tooltip"; +import { WARNING_LIMITS } from "@/lib/moderation/warnings"; +import { resolveReasonText } from "@/views/moderation/report-reasons-query"; +import { WarningDuration } from "@/views/moderation/warnings/warning-format"; + +import type { WarningReasonFormProps } from "./warning-reason-form"; +import type { AdminWarningReason } from "./warnings-admin-api"; + +const WarningReasonForm = React.lazy( + async () => await import("./warning-reason-form"), +); + +export type WarningReasonSheetState = + | { mode: "create" } + | { mode: "edit"; reason: AdminWarningReason }; + +const STATIC_NAVIGATION: DataTableNavigation = { + navigate: () => undefined, + searchParams: new URLSearchParams(), +}; + +export const CreateWarningReasonButton = ({ + count, + onCreate, +}: { + count: number; + onCreate: () => void; +}) => { + const t = useTranslations("admin.moderation.warning_reasons"); + const atLimit = count >= WARNING_LIMITS.maxReasons; + + return ( + + ); +}; + +export const WarningReasonsContent = ({ + canEdit, + onOpenSheet, + onReorder, + onSave, + onSheetOpenChange, + reasons, + sheet, + sheetOpen, +}: { + canEdit: boolean; + onOpenSheet: (state: WarningReasonSheetState) => void; + onReorder: (ids: number[]) => Promise; + onSave: WarningReasonFormProps["onSave"]; + onSheetOpenChange: (open: boolean) => void; + reasons: AdminWarningReason[]; + sheet: WarningReasonSheetState; + sheetOpen: boolean; +}) => { + const t = useTranslations("admin.moderation.warning_reasons"); + const locale = useLocale(); + + return ( + <> + + + columns={[ + { + accessorKey: "title", + cell: ({ row }) => { + const description = resolveReasonText(row.description, locale); + + return ( + + + {resolveReasonText(row.title, locale)} + + {description ? ( + + {description} + + ) : null} + + ); + }, + header: t("columns.reason"), + }, + { + accessorKey: "points", + cell: ({ row }) => ( + {row.points} + ), + className: "w-24", + header: t("columns.points"), + }, + { + accessorKey: "expiresAfterHours", + cell: ({ row }) => + row.expiresAfterHours === null ? ( + + {t("never")} + + ) : ( + + ), + className: "w-32", + header: t("columns.expires"), + }, + { + accessorKey: "enabled", + cell: ({ row }) => + row.enabled ? ( + {t("enabled")} + ) : ( + {t("disabled")} + ), + className: "w-28", + header: t("columns.status"), + }, + { + align: "right", + cell: ({ row }) => { + const title = resolveReasonText(row.title, locale); + + return canEdit ? ( + + + + ) : null; + }, + className: "w-16", + header: "", + id: "actions", + }, + ]} + customNoResults={{ + description: t("empty_desc"), + icon: , + title: t("empty_title"), + }} + edges={reasons} + id="warning-reasons-table" + order={{ defaultOrder: { column: "position", order: "asc" } }} + reorderable={{ + disabled: !canEdit, + getRowLabel: row => resolveReasonText(row.title, locale), + onReorder, + }} + /> + + + + + + + {sheet.mode === "edit" + ? t("form.edit_title") + : t("form.create_title")} + + + {t("form.sheet_desc")} + + + + + + } + > + { + onSheetOpenChange(false); + }} + reason={sheet.mode === "edit" ? sheet.reason : undefined} + /> + + + + + ); +}; diff --git a/packages/vitnode/src/views/admin/views/core/moderation/warnings/warning-threshold-form.tsx b/packages/vitnode/src/views/admin/views/core/moderation/warnings/warning-threshold-form.tsx new file mode 100644 index 0000000000..86b7315685 --- /dev/null +++ b/packages/vitnode/src/views/admin/views/core/moderation/warnings/warning-threshold-form.tsx @@ -0,0 +1,166 @@ +import { toast } from "sonner"; +import { useTranslations } from "use-intl"; +import { z } from "zod"; + +import type { AutoFormOnSubmit } from "@/components/form/auto-form"; +import type { AdminMutationResult } from "@/views/admin/views/core/shared/admin-mutation"; + +import { AutoForm } from "@/components/form/auto-form"; +import { AutoFormSheetFooter } from "@/components/form/auto-form-sheet-footer"; +import { AutoFormNumber } from "@/components/form/fields/number"; +import { AutoFormRadioGroup } from "@/components/form/fields/radio-group"; +import { AutoFormSwitch } from "@/components/form/fields/switch"; +import { + WARNING_LIMITS, + WARNING_THRESHOLD_ACTIONS, +} from "@/lib/moderation/warnings"; + +import type { + AdminWarningThreshold, + AdminWarningThresholdInput, +} from "./warnings-admin-api"; + +export interface WarningThresholdFormProps { + onSave: (args: { + id?: number; + values: AdminWarningThresholdInput; + }) => Promise>; + onSaved: () => void; + threshold?: AdminWarningThreshold; +} + +export const WarningThresholdForm = ({ + onSave, + onSaved, + threshold, +}: WarningThresholdFormProps) => { + const t = useTranslations("admin.moderation.warning_thresholds.form"); + + const formSchema = z.object({ + points: z + .number() + .int() + .min(1) + .max(WARNING_LIMITS.maxThresholdPoints) + .default(threshold?.points ?? 10), + action: z + .enum(WARNING_THRESHOLD_ACTIONS) + .default(threshold?.action ?? "suspend_temporary"), + durationHours: z + .number() + .int() + .min(1) + .max(WARNING_LIMITS.maxDurationHours) + .default(threshold?.durationHours ?? 24), + enabled: z.boolean().default(threshold?.enabled ?? true), + }); + + const onSubmit: AutoFormOnSubmit = async values => { + const result = await onSave({ + id: threshold?.id, + values: { + ...values, + durationHours: + values.action === "suspend_temporary" ? values.durationHours : null, + }, + }); + + if ("error" in result) { + toast.error(t("error"), { + description: + result.error.status === 409 + ? result.error.message?.includes("limit_reached") + ? t("limit_reached", { max: WARNING_LIMITS.maxThresholds }) + : t("duplicate_points") + : t("error_desc"), + }); + + return; + } + + toast.success(threshold ? t("updated") : t("created"), { + description: t("saved_desc"), + }); + onSaved(); + }; + + return ( + ( + + ), + }, + { + id: "action", + component: props => ( + ({ + description: t(`actions.${action}_desc`), + label: t(`actions.${action}`), + value: action, + }))} + /> + ), + }, + { + id: "durationHours", + hidden: values => values.action !== "suspend_temporary", + component: props => ( + + ), + }, + { + id: "enabled", + component: props => ( + + ), + }, + ]} + formSchema={formSchema} + id={ + threshold + ? `warning-threshold-${threshold.id}` + : "warning-threshold-new" + } + layout={rendered => ( + <> +
+

+ {t("future_only")} +

+ {rendered.points} + {rendered.action} + {rendered.durationHours} + {rendered.enabled} +
+ + + )} + onSubmit={onSubmit} + /> + ); +}; + +export default WarningThresholdForm; diff --git a/packages/vitnode/src/views/admin/views/core/moderation/warnings/warning-thresholds-content.tsx b/packages/vitnode/src/views/admin/views/core/moderation/warnings/warning-thresholds-content.tsx new file mode 100644 index 0000000000..33fb9e1fea --- /dev/null +++ b/packages/vitnode/src/views/admin/views/core/moderation/warnings/warning-thresholds-content.tsx @@ -0,0 +1,241 @@ +import { GaugeIcon, PencilIcon, PlusIcon, Trash2Icon } from "lucide-react"; +import React from "react"; +import { toast } from "sonner"; +import { useTranslations } from "use-intl"; + +import type { DataTableNavigation } from "@/components/table/navigation"; +import type { AdminMutationResult } from "@/views/admin/views/core/shared/admin-mutation"; + +import { ConfirmActionAlertDialog } from "@/components/confirm-action/confirm-action-alert-dialog"; +import { ContentDataTable } from "@/components/table/content"; +import { DataTableNavigationProvider } from "@/components/table/navigation"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet"; +import { Spinner } from "@/components/ui/spinner"; +import { TooltipWithContent } from "@/components/ui/tooltip"; +import { WARNING_LIMITS } from "@/lib/moderation/warnings"; +import { WarningDuration } from "@/views/moderation/warnings/warning-format"; + +import type { WarningThresholdFormProps } from "./warning-threshold-form"; +import type { AdminWarningThreshold } from "./warnings-admin-api"; + +const WarningThresholdForm = React.lazy( + async () => await import("./warning-threshold-form"), +); + +export type WarningThresholdSheetState = + | { mode: "create" } + | { mode: "edit"; threshold: AdminWarningThreshold }; + +const STATIC_NAVIGATION: DataTableNavigation = { + navigate: () => undefined, + searchParams: new URLSearchParams(), +}; + +export const CreateWarningThresholdButton = ({ + count, + onCreate, +}: { + count: number; + onCreate: () => void; +}) => { + const t = useTranslations("admin.moderation.warning_thresholds"); + const atLimit = count >= WARNING_LIMITS.maxThresholds; + + return ( + + ); +}; + +export const WarningThresholdsContent = ({ + canEdit, + onDelete, + onOpenSheet, + onSave, + onSheetOpenChange, + sheet, + sheetOpen, + thresholds, +}: { + canEdit: boolean; + onDelete: (id: number) => Promise>; + onOpenSheet: (state: WarningThresholdSheetState) => void; + onSave: WarningThresholdFormProps["onSave"]; + onSheetOpenChange: (open: boolean) => void; + sheet: WarningThresholdSheetState; + sheetOpen: boolean; + thresholds: AdminWarningThreshold[]; +}) => { + const t = useTranslations("admin.moderation.warning_thresholds"); + const [deleting, setDeleting] = React.useState( + null, + ); + + return ( + <> + + + columns={[ + { + accessorKey: "points", + cell: ({ row }) => ( + + {t("at_points", { points: row.points })} + + ), + header: t("columns.points"), + }, + { + accessorKey: "action", + cell: ({ row }) => + row.action === "suspend_temporary" && row.durationHours ? ( + + {t("suspend_for")}{" "} + + + ) : ( + {t("suspend_indefinitely")} + ), + header: t("columns.action"), + }, + { + accessorKey: "enabled", + cell: ({ row }) => + row.enabled ? ( + {t("enabled")} + ) : ( + {t("disabled")} + ), + className: "w-28", + header: t("columns.status"), + }, + { + align: "right", + cell: ({ row }) => + canEdit ? ( +
+ + + + + + +
+ ) : null, + className: "w-24", + header: "", + id: "actions", + }, + ]} + customNoResults={{ + description: t("empty_desc"), + icon: , + title: t("empty_title"), + }} + edges={thresholds} + id="warning-thresholds-table" + order={{ defaultOrder: { column: "points", order: "asc" } }} + /> +
+ + + + + + {sheet.mode === "edit" + ? t("form.edit_title") + : t("form.create_title")} + + + {t("form.sheet_desc")} + + + + + + } + > + { + onSheetOpenChange(false); + }} + threshold={sheet.mode === "edit" ? sheet.threshold : undefined} + /> + + + + + } + onOpenChange={open => { + if (!open) setDeleting(null); + }} + onSubmit={async ({ onClose }) => { + if (!deleting) return; + const result = await onDelete(deleting.id); + if ("error" in result) { + toast.error(t("delete.error")); + + return; + } + toast.success(t("delete.success"), { + description: t("delete.success_desc", { points: deleting.points }), + }); + onClose(); + }} + open={deleting !== null} + submitVariant="destructive" + textSubmit={ + <> +
; +}) => { + const t = useTranslations("core.moderation.account_status"); + const format = useFormatter(); + + return ( + } variant="destructive"> + {t("suspended_title")} + +

+ {suspension.endsAt + ? t("suspended_until", { + endsAt: format.dateTime(new Date(suspension.endsAt), { + dateStyle: "full", + timeStyle: "short", + }), + }) + : t("suspended_indefinite")} +

+

{t("suspended_restrictions")}

+

{t("contact")}

+
+
+ ); +}; + +const OwnWarning = ({ + warning, +}: { + warning: AccountStatus["warnings"][number]; +}) => { + const t = useTranslations("core.moderation.account_status"); + const tWarn = useTranslations("core.moderation.warn"); + const locale = useLocale(); + const titleId = React.useId(); + + return ( +
+ ); +}; + +const Pending = ({ label }: { label: string }) => ( +
+ + {label} + + + + +
+); + +/** + * The member's own standing: an active suspension, if any, active points and + * warning history. Works for a suspended account too. Never shows staff notes + * or who issued a warning. + */ +export const AccountStatusContent = () => { + const t = useTranslations("core.moderation.account_status"); + const { data, isPending } = useQuery(accountStatusQueryOptions()); + + return ( +
+ + {isPending ? ( + + ) : !data ? ( +
+

{t("signed_out")}

+ +
+ ) : ( + <> + {data.suspension ? ( + + ) : ( + + {t("good_title")} + + {t("good_desc")} + + + )} + +
+
+

+ {t("points_title")} +

+

+ {t("points_desc")} +

+
+

+ {data.activePoints} +

+
+ +
+

+ {t("warnings_title")} +

+ {data.warnings.length === 0 ? ( +

+ {t("no_warnings")} +

+ ) : ( +
    + {data.warnings.map(warning => ( +
  • + +
  • + ))} +
+ )} +
+ + {data.sanctions.length > 0 ? ( +
+

+ {t("sanctions_title")} +

+
    + {data.sanctions.map(sanction => ( +
  • + + + {sanction.endsAt + ? t("sanction_temporary") + : t("sanction_indefinite")} + + + + {sanction.endsAt ? ( + <> + {" – "} + + + ) : null} + + + + {sanction.active + ? t("sanction_active") + : sanction.revoked + ? t("sanction_lifted") + : t("sanction_ended")} + +
  • + ))} +
+
+ ) : null} + + )} +
+ ); +}; diff --git a/packages/vitnode/src/views/moderation/warnings/modcp-member-content.tsx b/packages/vitnode/src/views/moderation/warnings/modcp-member-content.tsx new file mode 100644 index 0000000000..7c767c2b57 --- /dev/null +++ b/packages/vitnode/src/views/moderation/warnings/modcp-member-content.tsx @@ -0,0 +1,138 @@ +import { useSuspenseQuery } from "@tanstack/react-query"; +import { TriangleAlertIcon } from "lucide-react"; +import React from "react"; +import { useTranslations } from "use-intl"; + +import { Button } from "@/components/ui/button"; +import { PageTitle } from "@/components/ui/page-title"; +import { WarnDialog } from "@/tanstack/moderation/warn-button"; + +import { Person } from "../modcp/report-tone"; +import { SanctionCard, StaffWarningCard } from "./warning-list"; +import { memberStandingQueryOptions } from "./warnings-api"; + +const Section = ({ + children, + title, +}: { + children: React.ReactNode; + title: string; +}) => { + const id = React.useId(); + + return ( +
+

+ {title} +

+ {children} +
+ ); +}; + +const Empty = ({ children }: { children: React.ReactNode }) => ( +

+ {children} +

+); + +/** A member's standing in the ModCP: points, warnings and suspensions. */ +export const ModcpMemberContent = ({ userId }: { userId: number }) => { + const t = useTranslations("core.moderation.warn"); + const { data } = useSuspenseQuery(memberStandingQueryOptions(userId)); + const [warning, setWarning] = React.useState(false); + const { permissions } = data; + + return ( +
+ + {permissions.warn ? ( + + ) : null} + + +
+ +
+
+
+ {t("member.active_points")} +
+
+ {data.activePoints} +
+
+ {data.sanctions ? ( +
+
+ {t("member.status")} +
+
+ {data.sanctions.some(sanction => sanction.active) + ? t("member.suspended") + : t("member.in_good_standing")} +
+
+ ) : null} +
+
+ + {data.sanctions ? ( +
+ {data.sanctions.length === 0 ? ( + {t("member.no_sanctions")} + ) : ( +
    + {data.sanctions.map(sanction => ( +
  • + +
  • + ))} +
+ )} +
+ ) : null} + +
+ {data.hiddenWarnings > 0 ? ( +

+ {t("member.hidden_warnings", { count: data.hiddenWarnings })} +

+ ) : null} + {data.warnings.length === 0 ? ( + {t("member.no_warnings")} + ) : ( +
    + {data.warnings.map(item => ( +
  • + +
  • + ))} +
+ )} +
+ + +
+ ); +}; diff --git a/packages/vitnode/src/views/moderation/warnings/modcp-warnings-content.tsx b/packages/vitnode/src/views/moderation/warnings/modcp-warnings-content.tsx new file mode 100644 index 0000000000..6630e71ecb --- /dev/null +++ b/packages/vitnode/src/views/moderation/warnings/modcp-warnings-content.tsx @@ -0,0 +1,257 @@ +import { useQuery, useSuspenseQuery } from "@tanstack/react-query"; +import { Link } from "@tanstack/react-router"; +import { cn } from "cn"; +import { ChevronRightIcon, SearchIcon, TriangleAlertIcon } from "lucide-react"; +import React from "react"; +import { useLocale, useTranslations } from "use-intl"; + +import { FiltersDataTable } from "@/components/table/filters"; +import { useDataTableUrl } from "@/components/table/navigation"; +import { PaginationDataTable } from "@/components/table/pagination"; +import { withTableFilter } from "@/components/table/url-state"; +import { + Empty, + EmptyDescription, + EmptyHeader, + EmptyMedia, + EmptyTitle, +} from "@/components/ui/empty"; +import { Input } from "@/components/ui/input"; +import { PageTitle } from "@/components/ui/page-title"; + +import type { ModcpWarningsFilter } from "./warnings-api"; + +import { Person } from "../modcp/report-tone"; +import { resolveReasonText } from "../report-reasons-query"; +import { StaffWarningCard } from "./warning-list"; +import { + memberSearchQueryOptions, + warningReasonsQueryOptions, + warningsListQueryOptions, +} from "./warnings-api"; + +export const MODCP_WARNING_STATUSES = [ + "all", + "active", + "expired", + "revoked", +] as const; + +const StatusTabs = ({ status }: { status: ModcpWarningsFilter["status"] }) => { + const t = useTranslations("core.moderation.warn"); + const { navigate, searchParams } = useDataTableUrl(); + + return ( + + ); +}; + +const ReasonFilter = () => { + const t = useTranslations("core.moderation.warn"); + const locale = useLocale(); + // Moderators who can only view warnings cannot list reasons; no filter then. + const reasons = useQuery(warningReasonsQueryOptions()); + if (!reasons.data || reasons.data.length === 0) return null; + + return ( + { + const label = resolveReasonText(reason.title, locale); + + return { keywords: [label], label, value: String(reason.id) }; + }), + single: { allLabel: t("list.all_reasons") }, + }, + ]} + /> + ); +}; + +/** Finds a member by name and links to their standing. */ +export const MemberLookup = () => { + const t = useTranslations("core.moderation.warn.lookup"); + const [search, setSearch] = React.useState(""); + const deferred = React.useDeferredValue(search); + const results = useQuery(memberSearchQueryOptions(deferred)); + const inputId = React.useId(); + const statusId = React.useId(); + + return ( +
+

+ {t("title")} +

+
+
+

+ {results.data ? t("results", { count: results.data.length }) : ""} +

+ {deferred.trim() && results.data ? ( + results.data.length === 0 ? ( +

{t("empty")}

+ ) : ( +
    + {results.data.map(person => ( +
  • + + +
  • + ))} +
+ ) + ) : null} + {results.isError ? ( +

{t("forbidden")}

+ ) : null} +
+ ); +}; + +const List = ({ filter }: { filter: ModcpWarningsFilter }) => { + const t = useTranslations("core.moderation.warn"); + const { data } = useSuspenseQuery(warningsListQueryOptions(filter)); + const { isPending } = useDataTableUrl(); + const totalPages = Math.max(1, Math.ceil(data.total / data.pageSize)); + + return ( + <> + {data.items.length === 0 ? ( + + + + + + {t("list.empty_title")} + {t("list.empty_desc")} + + + ) : ( +
    + {data.items.map(item => ( +
  • + +
  • + ))} +
+ )} + + 1, + pageSize: data.pageSize, + startCursor: null, + totalCount: data.total, + totalPages, + }} + /> + + ); +}; + +/** The ModCP warning log: every warning the moderator may see, filtered. */ +export const ModcpWarningsContent = ({ + description, + filter, + title, +}: { + description: string; + filter: ModcpWarningsFilter; + title: string; +}) => ( +
+ + +
+ + +
+ +
+); diff --git a/packages/vitnode/src/views/moderation/warnings/warn-dialog-content.tsx b/packages/vitnode/src/views/moderation/warnings/warn-dialog-content.tsx new file mode 100644 index 0000000000..4a2815909f --- /dev/null +++ b/packages/vitnode/src/views/moderation/warnings/warn-dialog-content.tsx @@ -0,0 +1,706 @@ +import { useSelector } from "@tanstack/react-form"; +import { + keepPreviousData, + useQuery, + useQueryClient, +} from "@tanstack/react-query"; +import { ShieldAlertIcon, TriangleAlertIcon } from "lucide-react"; +import React from "react"; +import { toast } from "sonner"; +import { useFormatter, useLocale, useTranslations } from "use-intl"; +import { z } from "zod"; + +import type { + AutoFormOnSubmit, + ItemAutoFormComponentProps, +} from "@/components/form/auto-form"; + +import { AutoForm, AutoFormSubmitButton } from "@/components/form/auto-form"; +import { AutoFormDateTime } from "@/components/form/fields/date-time"; +import { AutoFormNumber } from "@/components/form/fields/number"; +import { AutoFormSelect } from "@/components/form/fields/select"; +import { AutoFormSwitch } from "@/components/form/fields/switch"; +import { AutoFormTextarea } from "@/components/form/fields/textarea"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { + DialogClose, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { useFormApi } from "@/components/ui/form"; +import { Skeleton } from "@/components/ui/skeleton"; +import { WARNING_LIMITS } from "@/lib/moderation/warnings"; + +import type { + IssuedWarning, + ModcpWarningReason, + WarningDraft, + WarningFailure, + WarningPreview, +} from "./warnings-api"; + +import { ModcpRequestError } from "../modcp/modcp-query"; +import { resolveReasonText } from "../report-reasons-query"; +import { WarningDuration } from "./warning-format"; +import { + ACCOUNT_STATUS_QUERY_KEY, + issueWarning, + previewWarning, + warningReasonsQueryOptions, + WARNINGS_QUERY_ROOT, +} from "./warnings-api"; + +/** The content a warning is about, as a `ContentTarget`. */ +export interface WarnContentRef { + id: string; + pluginId: string; + type: string; +} + +export interface WarnDialogContentProps { + content?: null | WarnContentRef; + onClose: () => void; + /** Called with the server's result once the warning is issued. */ + onIssued?: (result: IssuedWarning) => void; + reportId?: null | number; + userId: number; +} + +const NO_REASON = "none"; +const EXPIRATION_MODES = ["default", "never", "hours", "at"] as const; + +interface FormValues { + expirationAt?: null | string; + expirationHours?: number; + expirationMode?: (typeof EXPIRATION_MODES)[number]; + message?: string; + overrideNote?: string; + points?: number; + reasonId?: string; + resolveReport?: boolean; + staffNote?: string; +} + +const draftOf = ( + values: FormValues, + base: Pick, + canOverridePoints: boolean, +): WarningDraft => { + const mode = values.expirationMode ?? "default"; + + return { + ...base, + expiration: + mode === "hours" + ? { hours: values.expirationHours ?? 24, mode } + : mode === "at" && values.expirationAt + ? { at: values.expirationAt, mode } + : mode === "never" + ? { mode } + : { mode: "default" }, + points: canOverridePoints ? (values.points ?? null) : null, + reasonId: + !values.reasonId || values.reasonId === NO_REASON + ? null + : Number(values.reasonId), + }; +}; + +const failureOfError = (error: Error | null): WarningFailure => { + if (error instanceof ModcpRequestError) { + return error.status === 403 ? "forbidden" : "unknown"; + } + + return (error?.message as undefined | WarningFailure) ?? "unknown"; +}; + +const useFailureMessage = () => { + const t = useTranslations("core.moderation.warn.errors"); + + return (failure: WarningFailure) => + failure === "unknown" || failure === "conflict" ? t("unknown") : t(failure); +}; + +const PointsLine = ({ + label, + strong, + value, +}: { + label: string; + strong?: boolean; + value: React.ReactNode; +}) => ( +
+
{label}
+
+ {value} +
+
+); + +/** The sanction a warning leads to, in words. */ +export const SanctionSummary = ({ + sanction, +}: { + sanction: { durationHours?: null | number; endsAt: Date | null | string }; +}) => { + const t = useTranslations("core.moderation.warn"); + const format = useFormatter(); + if (sanction.endsAt === null) return <>{t("sanction.indefinite")}; + const endsAt = format.dateTime(new Date(sanction.endsAt), { + dateStyle: "medium", + timeStyle: "short", + }); + + return sanction.durationHours ? ( + <> + {t("sanction.for")} {" "} + {t("sanction.until", { endsAt })} + + ) : ( + <>{t("sanction.suspended_until", { endsAt })} + ); +}; + +const Consequences = ({ + base, + canOverridePoints, + initial, +}: { + base: Pick; + canOverridePoints: boolean; + initial: WarningPreview; +}) => { + const t = useTranslations("core.moderation.warn"); + const { form } = useFormApi(); + const values = useSelector(form.store, state => state.values as FormValues); + const draft = React.useDeferredValue( + draftOf(values, base, canOverridePoints), + ); + const preview = useQuery({ + placeholderData: keepPreviousData, + queryFn: async () => { + const result = await previewWarning(draft); + if (!result.ok) throw new Error(result.failure); + + return result.data; + }, + queryKey: [...WARNINGS_QUERY_ROOT, "preview", draft], + retry: false, + staleTime: 5_000, + }); + const data = preview.data ?? initial; + const failure = useFailureMessage(); + + return ( +
+

+ {t("consequences.title")} +

+
+ + + +
+

+ {t("consequences.action")} + {data.sanction ? ( + + ) : ( + t("consequences.no_action") + )} +

+ {preview.isError ? ( +

+ {failure(preview.error.message as WarningFailure)} +

+ ) : null} + {data.suspendedUntil ? ( +

+ {t("consequences.already_suspended")} +

+ ) : null} + {data.related.length > 0 ? ( +

+ {t("consequences.related", { count: data.related.length })} +

+ ) : null} +

+ {t("consequences.advisory")} +

+
+ ); +}; + +/** + * The reason dropdown. Picking a reason resets the points to its default and + * swaps in its message, unless the moderator already wrote their own. + */ +const ReasonSelect = ({ + labels, + messageOf, + reasonOf, + ...props +}: ItemAutoFormComponentProps & { + labels: { label: string; value: string }[]; + messageOf: (reason: ModcpWarningReason | undefined) => string; + reasonOf: (id: string | undefined) => ModcpWarningReason | undefined; +}) => { + const t = useTranslations("core.moderation.warn"); + const { form } = useFormApi(); + + return ( + { + 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)) { + form.setFieldValue("message", messageOf(next)); + } + }} + /> + ); +}; + +const WarnForm = ({ + base, + initial, + onClose, + onIssued, + reasons, +}: { + base: Pick; + initial: WarningPreview; + onClose: () => void; + onIssued?: (result: IssuedWarning) => void; + reasons: ModcpWarningReason[]; +}) => { + const t = useTranslations("core.moderation.warn"); + const locale = useLocale(); + const format = useFormatter(); + const queryClient = useQueryClient(); + const failure = useFailureMessage(); + const [idempotencyKey] = React.useState(() => crypto.randomUUID()); + const { permissions } = initial; + const reasonOf = (id: string | undefined) => + reasons.find(reason => String(reason.id) === id); + const messageOf = (reason: ModcpWarningReason | undefined) => + reason ? resolveReasonText(reason.message, locale) : ""; + const first = reasons[0]; + + const formSchema = z + .object({ + reasonId: z + .enum([NO_REASON, ...reasons.map(reason => String(reason.id))]) + .default(first ? String(first.id) : NO_REASON), + points: z + .number() + .int() + .min(0) + .max(WARNING_LIMITS.maxPoints) + .default(first?.points ?? 0), + expirationMode: z.enum(EXPIRATION_MODES).default("default"), + expirationHours: z + .number() + .int() + .min(1) + .max(WARNING_LIMITS.maxDurationHours) + .default(24), + expirationAt: z.string().nullable().default(null), + overrideNote: z + .string() + .trim() + .max(WARNING_LIMITS.noteMaxLength) + .default(""), + message: z + .string() + .trim() + .min(WARNING_LIMITS.messageMinLength) + .max(WARNING_LIMITS.messageMaxLength) + .default(messageOf(first)), + staffNote: z + .string() + .trim() + .max(WARNING_LIMITS.noteMaxLength) + .default(""), + resolveReport: z.boolean().default(false), + }) + .superRefine((values, ctx) => { + const defaultPoints = reasonOf(values.reasonId)?.points ?? 0; + const overridden = + (permissions.overridePoints && values.points !== defaultPoints) || + values.expirationMode !== "default"; + if ( + overridden && + values.overrideNote.length < WARNING_LIMITS.explanationMinLength + ) { + ctx.addIssue({ + code: "custom", + message: t("form.override_note_required"), + path: ["overrideNote"], + }); + } + if (values.expirationMode === "at" && !values.expirationAt) { + ctx.addIssue({ + code: "custom", + message: t("form.expiration_at_required"), + path: ["expirationAt"], + }); + } + }); + + const onSubmit: AutoFormOnSubmit = async values => { + const draft = draftOf(values, base, permissions.overridePoints); + const result = await issueWarning({ + ...draft, + idempotencyKey, + message: values.message, + overrideNote: values.overrideNote || null, + resolveReport: values.resolveReport, + staffNote: values.staffNote || null, + }); + if (!result.ok) { + toast.error(t("errors.title"), { description: failure(result.failure) }); + + return; + } + + const { data } = result; + const summary = t("done.points", { + after: data.points.after, + name: initial.user.name, + }); + const sanction = data.sanction + ? data.sanction.endsAt + ? t("done.suspended_until", { + endsAt: format.dateTime(new Date(data.sanction.endsAt), { + dateStyle: "medium", + timeStyle: "short", + }), + }) + : t("done.suspended_indefinitely") + : null; + // Someone else warned the member after the form opened. + const differs = + !data.duplicate && data.points.before !== initial.points.current; + toast.success(t("done.title"), { + description: [ + summary, + sanction, + data.reportResolved ? t("done.report_resolved") : null, + differs ? t("done.differs") : null, + ] + .filter(Boolean) + .join(" "), + }); + await Promise.all([ + queryClient.invalidateQueries({ queryKey: WARNINGS_QUERY_ROOT }), + queryClient.invalidateQueries({ queryKey: ACCOUNT_STATUS_QUERY_KEY }), + queryClient.invalidateQueries({ + queryKey: ["vitnode", "moderation"], + }), + ]); + onIssued?.(data); + onClose(); + }; + + const reasonLabels = [ + ...reasons.map(reason => ({ + label: `${resolveReasonText(reason.title, locale)} (${t("points", { count: reason.points })})`, + value: String(reason.id), + })), + { label: t("form.no_reason"), value: NO_REASON }, + ]; + + return ( + ( + + ), + }, + { + id: "points", + hidden: () => !permissions.overridePoints, + component: props => ( + + ), + }, + { + id: "expirationMode", + hidden: () => !permissions.overrideExpiration, + component: props => ( + ({ + label: t(`form.expiration_modes.${mode}`), + value: mode, + }))} + /> + ), + }, + { + id: "expirationHours", + hidden: values => values.expirationMode !== "hours", + component: props => ( + + ), + }, + { + id: "expirationAt", + hidden: values => values.expirationMode !== "at", + component: props => ( + + ), + }, + { + id: "overrideNote", + hidden: values => + !( + (permissions.overridePoints && + values.points !== (reasonOf(values.reasonId)?.points ?? 0)) || + (values.expirationMode ?? "default") !== "default" + ), + component: props => ( + + ), + }, + { + id: "message", + component: props => ( + + ), + }, + { + id: "staffNote", + component: props => ( + + ), + }, + { + id: "resolveReport", + hidden: () => !base.reportId || !permissions.resolveReport, + component: props => ( + + ), + }, + ]} + formSchema={formSchema} + layout={rendered => ( + <> +
+ {base.content || base.reportId ? ( + } variant="warning"> + {t("global.title")} + + {t("global.desc")} + + + ) : null} + {rendered.reasonId} + {rendered.points} + {rendered.expirationMode} + {rendered.expirationHours} + {rendered.expirationAt} + {rendered.overrideNote} + + {rendered.message} + {rendered.staffNote} + {rendered.resolveReport} +
+ + }> + {t("cancel")} + + + + + + )} + onSubmit={onSubmit} + /> + ); +}; + +const DialogSkeleton = () => ( +
+ + + + +
+); + +/** + * The shared warn form: reason, points, expiry, the message the member reads, + * a private staff note and a live preview of the consequences. Loads lazily + * inside `WarnDialog`; every check runs again on the server when it submits. + */ +export const WarnDialogContent = ({ + content = null, + onClose, + onIssued, + reportId = null, + userId, +}: WarnDialogContentProps) => { + const t = useTranslations("core.moderation.warn"); + const failure = useFailureMessage(); + const reasons = useQuery(warningReasonsQueryOptions()); + const base = React.useMemo( + () => ({ + content: content + ? { + pluginId: content.pluginId, + targetId: content.id, + targetType: content.type, + } + : null, + reportId, + userId, + }), + [content, reportId, userId], + ); + const first = reasons.data?.[0]; + const initial = useQuery({ + enabled: reasons.isSuccess, + queryFn: async () => { + const result = await previewWarning({ + ...base, + expiration: { mode: "default" }, + reasonId: first?.id ?? null, + }); + if (!result.ok) throw new Error(result.failure); + + return result.data; + }, + queryKey: [...WARNINGS_QUERY_ROOT, "initial", base, first?.id ?? null], + retry: false, + staleTime: 0, + }); + + if (reasons.isPending || (reasons.isSuccess && initial.isPending)) { + return ( + <> + + {t("title_loading")} + + + + ); + } + + const error = reasons.error ?? initial.error; + if (error || !initial.data || !reasons.data) { + return ( +
+ + + {t("title_loading")} + + + {failure(failureOfError(error))} + + + + }> + {t("cancel")} + + +
+ ); + } + + return ( + <> + + + {t("title", { name: initial.data.user.name })} + + + {t("desc")} + {first ? null : ` ${t("no_reasons")}`} + + + + + ); +}; + +export default WarnDialogContent; diff --git a/packages/vitnode/src/views/moderation/warnings/warning-format.tsx b/packages/vitnode/src/views/moderation/warnings/warning-format.tsx new file mode 100644 index 0000000000..dae7560b5f --- /dev/null +++ b/packages/vitnode/src/views/moderation/warnings/warning-format.tsx @@ -0,0 +1,30 @@ +import { useTranslations } from "use-intl"; + +import type { WarningStatus } from "@/lib/moderation/warnings"; + +import { Badge } from "@/components/ui/badge"; + +/** A number of hours, as whole days when it divides evenly. */ +export const WarningDuration = ({ hours }: { hours: number }) => { + const t = useTranslations("core.moderation.warn.duration"); + + return ( + <> + {hours % 24 === 0 + ? t("days", { count: hours / 24 }) + : t("hours", { count: hours })} + + ); +}; + +const STATUS_VARIANT = { + active: "warning", + expired: "secondary", + revoked: "outline", +} as const; + +export const WarningStatusBadge = ({ status }: { status: WarningStatus }) => { + const t = useTranslations("core.moderation.warn.statuses"); + + return {t(status)}; +}; diff --git a/packages/vitnode/src/views/moderation/warnings/warning-list.tsx b/packages/vitnode/src/views/moderation/warnings/warning-list.tsx new file mode 100644 index 0000000000..07e2134f20 --- /dev/null +++ b/packages/vitnode/src/views/moderation/warnings/warning-list.tsx @@ -0,0 +1,438 @@ +import { useQueryClient } from "@tanstack/react-query"; +import { Link } from "@tanstack/react-router"; +import { + ExternalLinkIcon, + FlagIcon, + LockIcon, + ShieldOffIcon, + Undo2Icon, +} from "lucide-react"; +import React from "react"; +import { toast } from "sonner"; +import { useLocale, useTranslations } from "use-intl"; +import { z } from "zod"; + +import type { AutoFormOnSubmit } from "@/components/form/auto-form"; + +import { DateFormat } from "@/components/date-format"; +import { AutoForm } from "@/components/form/auto-form"; +import { AutoFormTextarea } from "@/components/form/fields/textarea"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { WARNING_LIMITS } from "@/lib/moderation/warnings"; + +import type { ReportPerson } from "../modcp/report-tone"; +import type { + ModcpSanction, + ModcpStaffWarning, + WarningResult, +} from "./warnings-api"; + +import { Person } from "../modcp/report-tone"; +import { resolveReasonText } from "../report-reasons-query"; +import { SanctionSummary } from "./warn-dialog-content"; +import { WarningDuration, WarningStatusBadge } from "./warning-format"; +import { + ACCOUNT_STATUS_QUERY_KEY, + revokeSanction, + revokeWarning, + WARNINGS_QUERY_ROOT, +} from "./warnings-api"; + +const RevokeDialog = ({ + description, + onClose, + onRevoke, + open, + submitLabel, + success, + successDesc, + title, +}: { + description: string; + onClose: () => void; + onRevoke: (reason: string) => Promise>; + open: boolean; + submitLabel: string; + success: string; + successDesc: string; + title: string; +}) => { + const t = useTranslations("core.moderation.warn"); + const queryClient = useQueryClient(); + const formSchema = z.object({ + reason: z + .string() + .trim() + .min(WARNING_LIMITS.explanationMinLength) + .max(WARNING_LIMITS.noteMaxLength), + }); + + const onSubmit: AutoFormOnSubmit = async values => { + const result = await onRevoke(values.reason); + if (!result.ok) { + toast.error(t("errors.title"), { + description: t( + `errors.${result.failure === "conflict" ? "unknown" : result.failure}`, + ), + }); + + return; + } + toast.success(success, { description: successDesc }); + await Promise.all([ + queryClient.invalidateQueries({ queryKey: WARNINGS_QUERY_ROOT }), + queryClient.invalidateQueries({ queryKey: ACCOUNT_STATUS_QUERY_KEY }), + ]); + onClose(); + }; + + return ( + { + if (!next) onClose(); + }} + open={open} + > + + + {title} + + {description} + + + ( + + ), + }, + ]} + formSchema={formSchema} + onSubmit={onSubmit} + submitButtonProps={{ + children: ( + <> + + + ); +}; + +const Detail = ({ + children, + label, +}: { + children: React.ReactNode; + label: string; +}) => ( +
+
{label}
+
+ {children} +
+
+); + +const PersonOrDeleted = ({ person }: { person: null | ReportPerson }) => { + const t = useTranslations("core.moderation.warn"); + + return person ? : <>{t("deleted_user")}; +}; + +/** One warning as staff see it, with Revoke when allowed. */ +export const StaffWarningCard = ({ + canRevoke, + member, + warning, +}: { + canRevoke: boolean; + /** Shown in lists that mix members. */ + member?: null | ReportPerson; + warning: ModcpStaffWarning; +}) => { + const t = useTranslations("core.moderation.warn"); + const locale = useLocale(); + const [revoking, setRevoking] = React.useState(false); + const reason = warning.reason + ? resolveReasonText(warning.reason, locale) + : t("custom_reason"); + const titleId = React.useId(); + + return ( +
+
+
+

+ {reason} +

+

+ +

+
+
+ + {t("points", { count: warning.points })} + + + {canRevoke && warning.status !== "revoked" ? ( + + ) : null} +
+
+ +
+ {member !== undefined ? ( + + {member ? ( + + + + ) : ( + + )} + + ) : null} + + + + + {warning.expiresAt ? ( + + ) : ( + t("list.never") + )} + + {warning.defaultPoints !== null && + warning.defaultPoints !== warning.points ? ( + + {t("points", { count: warning.defaultPoints })} + + ) : null} + {warning.content ? ( + + {warning.content.url ? ( + + {warning.content.title ?? t("list.untitled")} + + ) : ( + (warning.content.title ?? t("list.untitled")) + )} + + ) : null} + {warning.reportId ? ( + + + + ) : null} +
+ + + {warning.message} + + + {warning.staffNote || warning.overrideNote ? ( +
+

+

+ {warning.staffNote ? ( + + {warning.staffNote} + + ) : null} + {warning.overrideNote ? ( + + + {warning.overrideNote} + + + ) : null} +
+ ) : null} + + {warning.revokedAt ? ( +

+ {t.rich("list.revoked", { + by: () => , + date: () => , + })} + {warning.revokeReason ? ` ${warning.revokeReason}` : null} +

+ ) : null} + + {warning.sanctionId && warning.revokedAt ? ( +

+ {t("list.sanction_stays")} +

+ ) : null} + + { + setRevoking(false); + }} + onRevoke={async reasonText => + await revokeWarning({ id: warning.id, reason: reasonText }) + } + open={revoking} + submitLabel={t("revoke.button")} + success={t("revoke.done")} + successDesc={t("revoke.done_desc")} + title={t("revoke.title")} + /> +
+ ); +}; + +/** One suspension, with Lift when allowed. */ +export const SanctionCard = ({ + canManage, + sanction, +}: { + canManage: boolean; + sanction: ModcpSanction; +}) => { + const t = useTranslations("core.moderation.warn"); + const [lifting, setLifting] = React.useState(false); + + return ( +
+
+
+

+ {sanction.endsAt + ? t("sanctions.temporary") + : t("sanctions.indefinite")} +

+

+ +

+
+
+ + {sanction.active + ? t("sanctions.active") + : sanction.revokedAt + ? t("sanctions.lifted") + : t("sanctions.ended")} + + {canManage && sanction.active ? ( + + ) : null} +
+
+
+ + + + + {sanction.threshold + ? t("sanctions.from_threshold", { + points: sanction.threshold.points, + }) + : t("sanctions.manual")} + {sanction.threshold?.durationHours ? ( + <> + {" "} + () + + ) : null} + +
+ {sanction.revokedAt ? ( +

+ {t.rich("list.lifted", { + by: () => , + date: () => , + })} + {sanction.revokeReason ? ` ${sanction.revokeReason}` : null} +

+ ) : null} + { + setLifting(false); + }} + onRevoke={async reason => + await revokeSanction({ id: sanction.id, reason }) + } + open={lifting} + submitLabel={t("sanctions.lift")} + success={t("sanctions.lifted_done")} + successDesc={t("sanctions.lifted_done_desc")} + title={t("sanctions.lift_title")} + /> +
+ ); +}; diff --git a/packages/vitnode/src/views/moderation/warnings/warnings-api.ts b/packages/vitnode/src/views/moderation/warnings/warnings-api.ts new file mode 100644 index 0000000000..9ea7595444 --- /dev/null +++ b/packages/vitnode/src/views/moderation/warnings/warnings-api.ts @@ -0,0 +1,348 @@ +import { keepPreviousData, queryOptions } from "@tanstack/react-query"; + +import { fetcherClient } from "@/lib/fetcher-client"; +import { manifest } from "@/manifest"; +import { fetcher } from "@/tanstack/fetcher"; + +import { ModcpRequestError } from "../modcp/modcp-query"; + +export const WARNINGS_QUERY_ROOT = [ + "vitnode", + "moderation", + "warnings", +] as const; + +/** Every query that shows points, warnings or sanctions - invalidate after a write. */ +export const ACCOUNT_STATUS_QUERY_KEY = [ + "vitnode", + "moderation", + "account-status", +] as const; + +const fetchWarningReasons = async () => { + const response = await fetcher({ + plugin: manifest.id, + method: "get", + module: "moderation", + path: "/warnings/reasons", + }); + if (!response.ok) { + throw new ModcpRequestError(response.status, "warning reasons"); + } + + return (await response.json()).reasons; +}; + +export type ModcpWarningReason = Awaited< + ReturnType +>[number]; + +export const warningReasonsQueryOptions = () => + queryOptions({ + queryFn: fetchWarningReasons, + queryKey: [...WARNINGS_QUERY_ROOT, "reasons"], + retry: false, + staleTime: 60_000, + }); + +const fetchMemberStanding = async (userId: number) => { + const response = await fetcher({ + plugin: manifest.id, + args: { params: { id: userId } }, + method: "get", + module: "moderation", + path: "/members/{id}", + }); + if (!response.ok) { + throw new ModcpRequestError(response.status, "the member's standing"); + } + + return await response.json(); +}; + +export type ModcpMemberStanding = Awaited< + ReturnType +>; + +export type ModcpStaffWarning = ModcpMemberStanding["warnings"][number]; + +export type ModcpSanction = NonNullable< + ModcpMemberStanding["sanctions"] +>[number]; + +export const memberStandingQueryOptions = (userId: number) => + queryOptions({ + queryFn: async () => await fetchMemberStanding(userId), + queryKey: [...WARNINGS_QUERY_ROOT, "member", userId], + retry: false, + staleTime: 10_000, + }); + +export interface ModcpWarningsFilter { + issuedById?: number; + page: number; + pageSize: number; + pluginId?: string; + reasonId?: number; + status: "active" | "all" | "expired" | "revoked"; + userId?: number; +} + +const fetchWarnings = async (filter: ModcpWarningsFilter) => { + const response = await fetcher({ + plugin: manifest.id, + args: { query: filter }, + method: "get", + module: "moderation", + path: "/warnings", + }); + if (!response.ok) throw new ModcpRequestError(response.status, "warnings"); + + return await response.json(); +}; + +export type ModcpWarningsPage = Awaited>; + +export const warningsListQueryOptions = (filter: ModcpWarningsFilter) => + queryOptions({ + placeholderData: keepPreviousData, + queryFn: async () => await fetchWarnings(filter), + queryKey: [...WARNINGS_QUERY_ROOT, "list", filter], + retry: false, + staleTime: 10_000, + }); + +const fetchMembers = async (search: string) => { + const response = await fetcher({ + plugin: manifest.id, + args: { query: { search } }, + method: "get", + module: "moderation", + path: "/members", + }); + if (!response.ok) throw new ModcpRequestError(response.status, "members"); + + return (await response.json()).items; +}; + +export const memberSearchQueryOptions = (search: string) => + queryOptions({ + enabled: search.trim().length > 0, + placeholderData: keepPreviousData, + queryFn: async () => await fetchMembers(search.trim()), + queryKey: [...WARNINGS_QUERY_ROOT, "members", search.trim()], + retry: false, + staleTime: 30_000, + }); + +const fetchAccountStatus = async () => { + const response = await fetcher({ + plugin: manifest.id, + method: "get", + module: "moderation", + path: "/account-status", + }); + const { status } = response; + if (!response.ok) { + if (status === 401) return null; + throw new ModcpRequestError(status, "the account status"); + } + + return await response.json(); +}; + +export type AccountStatus = NonNullable< + Awaited> +>; + +export const accountStatusQueryOptions = () => + queryOptions({ + queryFn: fetchAccountStatus, + queryKey: ACCOUNT_STATUS_QUERY_KEY, + retry: false, + staleTime: 10_000, + }); + +const WARNING_FAILURES = [ + "already_revoked", + "author_mismatch", + "content_not_found", + "expiration_override_forbidden", + "forbidden", + "idempotency_conflict", + "invalid_expiration", + "invalid_points", + "not_found", + "override_note_required", + "points_override_forbidden", + "protected", + "reason_disabled", + "reason_not_found", + "report_closed", + "report_mismatch", + "self", + "user_not_found", + "conflict", +] as const; + +export type WarningFailure = + | "unauthenticated" + | "unknown" + | "validation" + | (typeof WARNING_FAILURES)[number]; + +export type WarningResult = + | { data: T; ok: true } + | { failure: WarningFailure; ok: false }; + +const failureOf = async (response: Response): Promise => { + const body = (await response.json().catch(() => null)) as null | { + error?: unknown; + }; + const reported = WARNING_FAILURES.find(failure => failure === body?.error); + if (reported) return reported; + if (response.status === 400) return "validation"; + if (response.status === 401) return "unauthenticated"; + if (response.status === 403) return "forbidden"; + if (response.status === 404) return "not_found"; + + return "unknown"; +}; + +const settle = async ( + request: () => Promise, +): Promise> => { + try { + const response = await request(); + if (!response.ok) return { failure: await failureOf(response), ok: false }; + + return { data: (await response.json()) as T, ok: true }; + } catch { + return { failure: "unknown", ok: false }; + } +}; + +const options = { credentials: "include" } as const; + +export type WarningExpirationInput = + | { at: string; mode: "at" } + | { hours: number; mode: "hours" } + | { mode: "default" } + | { mode: "never" }; + +export interface WarningDraft { + content?: null | { pluginId: string; targetId: string; targetType: string }; + expiration: WarningExpirationInput; + points?: null | number; + reasonId: null | number; + reportId?: null | number; + userId: number; +} + +/** What `POST /moderation/warnings/preview` answers. */ +export interface WarningPreview { + defaults: { expiresAfterHours: null | number; points: number }; + expiresAt: null | string; + overrides: { expiration: boolean; points: boolean }; + permissions: { + overrideExpiration: boolean; + overridePoints: boolean; + resolveReport: boolean; + viewWarnings: boolean; + }; + points: { current: number; projected: number; warning: number }; + related: ModcpStaffWarning[]; + sanction: null | { + action: "suspend_indefinite" | "suspend_temporary"; + durationHours: null | number; + endsAt: null | string; + points: number; + thresholdId: number; + }; + suspendedUntil: null | { endsAt: null | string }; + user: ModcpMemberStanding["user"]; +} + +export const previewWarning = async (draft: WarningDraft) => + await settle( + async () => + await fetcherClient({ + plugin: manifest.id, + args: { body: draft }, + method: "post", + module: "moderation", + options, + path: "/warnings/preview", + }), + ); + +export interface IssueWarningInput extends WarningDraft { + idempotencyKey: string; + message: string; + overrideNote?: null | string; + resolveReport: boolean; + staffNote?: null | string; +} + +export interface IssuedWarning { + duplicate: boolean; + points: { after: number; before: number }; + reportResolved: boolean; + sanction: null | { + endsAt: null | string; + id: number; + thresholdId: null | number; + }; + warningId: number; +} + +export const issueWarning = async (input: IssueWarningInput) => + await settle( + async () => + await fetcherClient({ + plugin: manifest.id, + args: { body: input }, + method: "post", + module: "moderation", + options, + path: "/warnings", + }), + ); + +export const revokeWarning = async ({ + id, + reason, +}: { + id: number; + reason: string; +}) => + await settle<{ userId: number; warningId: number }>( + async () => + await fetcherClient({ + plugin: manifest.id, + args: { body: { reason }, params: { id } }, + method: "post", + module: "moderation", + options, + path: "/warnings/{id}/revoke", + }), + ); + +export const revokeSanction = async ({ + id, + reason, +}: { + id: number; + reason: string; +}) => + await settle<{ sanctionId: number; userId: number }>( + async () => + await fetcherClient({ + plugin: manifest.id, + args: { body: { reason }, params: { id } }, + method: "post", + module: "moderation", + options, + path: "/sanctions/{id}/revoke", + }), + ); From 7f513e98f6f8942dcb2a5afbe06906c4fe056744 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 22:20:16 +0000 Subject: [PATCH 2/9] =?UTF-8?q?test(moderation):=20=E2=9C=85=20cover=20war?= =?UTF-8?q?nings,=20thresholds=20and=20suspensions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 Claude-Session: https://claude.ai/code/session_01CnAiFc4WPMdVa99L7jsTXY --- .../moderation/lib/warnings-service.ts | 322 +++--- .../moderation/warnings.postgres.test.ts | 199 ++++ .../api/modules/moderation/warnings.test.ts | 947 ++++++++++++++++++ .../users/routes/session.route.test.ts | 5 +- .../src/lib/moderation/warnings.test.ts | 100 ++ .../comments/use-content-comments.test.tsx | 1 + .../src/tanstack/content/permissions.test.tsx | 35 +- .../views/auth/settings/settings-nav.test.ts | 14 +- .../warnings/account-status-content.tsx | 6 +- packages/vitnode/vitest.config.ts | 1 + 10 files changed, 1458 insertions(+), 172 deletions(-) create mode 100644 packages/vitnode/src/api/modules/moderation/warnings.postgres.test.ts create mode 100644 packages/vitnode/src/api/modules/moderation/warnings.test.ts create mode 100644 packages/vitnode/src/lib/moderation/warnings.test.ts diff --git a/packages/vitnode/src/api/modules/moderation/lib/warnings-service.ts b/packages/vitnode/src/api/modules/moderation/lib/warnings-service.ts index b25b1697ed..0dbf51326b 100644 --- a/packages/vitnode/src/api/modules/moderation/lib/warnings-service.ts +++ b/packages/vitnode/src/api/modules/moderation/lib/warnings-service.ts @@ -11,6 +11,7 @@ import type { WarningThresholdAction, } from "@/lib/moderation/warnings"; +import { isUniqueViolation } from "@/api/lib/db-errors"; import { canOnReport } from "@/api/lib/moderation/access"; import { findReportTarget } from "@/api/lib/moderation/registry"; import { @@ -535,201 +536,212 @@ export const issueWarning = async ( if (report.status === "done") throw reportConflict(report, "report_closed"); } - const result = await c.get("db").transaction(async (tx: Db) => { - await lockUserWarnings(tx, draft.user.id); + const issue = async () => + await c.get("db").transaction(async (tx: Db) => { + await lockUserWarnings(tx, draft.user.id); - const existing = await findIssuedWarning(tx, moderator, input); - if (existing) return existing; + const existing = await findIssuedWarning(tx, moderator, input); + if (existing) return existing; - const now = new Date(); - const expiresAt = draft.expiresAt(now); - const before = await loadActivePoints(tx, draft.user.id, now); - const counts = expiresAt === null || expiresAt.getTime() > now.getTime(); - const after = before + (counts ? draft.points : 0); - - const [warning] = await tx - .insert(core_moderation_warnings) - .values({ - contentSnapshot: draft.content?.snapshot ?? null, - defaultPoints: draft.defaultPoints, - expiresAt, - idempotencyKey: input.idempotencyKey, - issuedAt: now, - issuedById: moderator.user.id, - message: input.message.trim(), - overrideNote: overridden ? overrideNote : null, - pluginId: draft.content?.pluginId ?? null, - points: draft.points, - pointsAfter: after, - pointsBefore: before, - reasonId: draft.reason?.id ?? null, - reasonSnapshot: draft.reason ? { title: draft.reason.title } : null, - reportId: report?.id ?? null, - scopeFamily: draft.content?.scopeFamily ?? null, - scopeId: draft.content?.scopeId ?? null, - staffNote: staffNote === "" ? null : staffNote, - targetId: draft.content?.targetId ?? null, - targetType: draft.content?.targetType ?? null, - userId: draft.user.id, - }) - .returning({ id: core_moderation_warnings.id }); + const now = new Date(); + const expiresAt = draft.expiresAt(now); + const before = await loadActivePoints(tx, draft.user.id, now); + const counts = expiresAt === null || expiresAt.getTime() > now.getTime(); + const after = before + (counts ? draft.points : 0); + + const [warning] = await tx + .insert(core_moderation_warnings) + .values({ + contentSnapshot: draft.content?.snapshot ?? null, + defaultPoints: draft.defaultPoints, + expiresAt, + idempotencyKey: input.idempotencyKey, + issuedAt: now, + issuedById: moderator.user.id, + message: input.message.trim(), + overrideNote: overridden ? overrideNote : null, + pluginId: draft.content?.pluginId ?? null, + points: draft.points, + pointsAfter: after, + pointsBefore: before, + reasonId: draft.reason?.id ?? null, + reasonSnapshot: draft.reason ? { title: draft.reason.title } : null, + reportId: report?.id ?? null, + scopeFamily: draft.content?.scopeFamily ?? null, + scopeId: draft.content?.scopeId ?? null, + staffNote: staffNote === "" ? null : staffNote, + targetId: draft.content?.targetId ?? null, + targetType: draft.content?.targetType ?? null, + userId: draft.user.id, + }) + .returning({ id: core_moderation_warnings.id }); - await appendModerationAudit(tx, { - actorId: moderator.user.id, - data: { - content: draft.content - ? { - pluginId: draft.content.pluginId, - targetId: draft.content.targetId, - targetType: draft.content.targetType, - } - : null, - expiresAt: expiresAt?.toISOString() ?? null, - points: draft.points, - reasonId: draft.reason?.id ?? null, - reportId: report?.id ?? null, - }, - type: "warning_issued", - userId: draft.user.id, - warningId: warning.id, - }); - if (overridden) { await appendModerationAudit(tx, { actorId: moderator.user.id, data: { - defaultExpiresAfterHours: draft.defaultExpiresAfterHours, - defaultPoints: draft.defaultPoints, + content: draft.content + ? { + pluginId: draft.content.pluginId, + targetId: draft.content.targetId, + targetType: draft.content.targetType, + } + : null, expiresAt: expiresAt?.toISOString() ?? null, - expiration: draft.overrides.expiration, points: draft.points, - pointsOverridden: draft.overrides.points, + reasonId: draft.reason?.id ?? null, + reportId: report?.id ?? null, }, - type: "warning_overridden", + type: "warning_issued", userId: draft.user.id, warningId: warning.id, }); - } - - const rules = await loadEnabledThresholds(tx); - const { applied, crossed } = evaluateWarningThresholds({ - after, - before, - rules, - }); - await appendModerationAudit(tx, { - actorId: moderator.user.id, - data: { - after, - appliedThresholdId: applied?.id ?? null, - before, - crossedThresholdIds: crossed.map(rule => rule.id), - }, - type: "threshold_evaluated", - userId: draft.user.id, - warningId: warning.id, - }); - - let sanction: IssuedWarning["sanction"] = null; - if (applied) { - const endsAt = thresholdSuspensionEnd(applied, now); - const [row] = await tx - .insert(core_moderation_sanctions) - .values({ - createdById: moderator.user.id, - endsAt, - source: "threshold", - startsAt: now, - thresholdId: applied.id, - thresholdSnapshot: { - action: applied.action, - durationHours: applied.durationHours, - points: applied.points, + if (overridden) { + await appendModerationAudit(tx, { + actorId: moderator.user.id, + data: { + defaultExpiresAfterHours: draft.defaultExpiresAfterHours, + defaultPoints: draft.defaultPoints, + expiresAt: expiresAt?.toISOString() ?? null, + expiration: draft.overrides.expiration, + points: draft.points, + pointsOverridden: draft.overrides.points, }, - type: "suspension", + type: "warning_overridden", userId: draft.user.id, warningId: warning.id, - }) - .returning({ id: core_moderation_sanctions.id }); - sanction = { endsAt, id: row.id, thresholdId: applied.id }; + }); + } + + const rules = await loadEnabledThresholds(tx); + const { applied, crossed } = evaluateWarningThresholds({ + after, + before, + rules, + }); await appendModerationAudit(tx, { actorId: moderator.user.id, data: { - action: applied.action, - endsAt: endsAt?.toISOString() ?? null, - source: "threshold", - thresholdId: applied.id, - thresholdPoints: applied.points, + after, + appliedThresholdId: applied?.id ?? null, + before, + crossedThresholdIds: crossed.map(rule => rule.id), }, - sanctionId: row.id, - type: "sanction_applied", + type: "threshold_evaluated", userId: draft.user.id, warningId: warning.id, }); - } - if (report) { - await appendReportEvent(tx, { - actorId: moderator.user.id, - data: { - points: draft.points, + let sanction: IssuedWarning["sanction"] = null; + if (applied) { + const endsAt = thresholdSuspensionEnd(applied, now); + const [row] = await tx + .insert(core_moderation_sanctions) + .values({ + createdById: moderator.user.id, + endsAt, + source: "threshold", + startsAt: now, + thresholdId: applied.id, + thresholdSnapshot: { + action: applied.action, + durationHours: applied.durationHours, + points: applied.points, + }, + type: "suspension", + userId: draft.user.id, + warningId: warning.id, + }) + .returning({ id: core_moderation_sanctions.id }); + sanction = { endsAt, id: row.id, thresholdId: applied.id }; + await appendModerationAudit(tx, { + actorId: moderator.user.id, + data: { + action: applied.action, + endsAt: endsAt?.toISOString() ?? null, + source: "threshold", + thresholdId: applied.id, + thresholdPoints: applied.points, + }, + sanctionId: row.id, + type: "sanction_applied", userId: draft.user.id, warningId: warning.id, - }, - reportId: report.id, - type: "warning", - }); - if (resolveReport) { - await updateReport(tx, report, { - openKey: null, - resolution: "action_taken", - resolvedAt: now, - resolvedById: moderator.user.id, - status: "done", }); + } + + if (report) { await appendReportEvent(tx, { actorId: moderator.user.id, - data: { resolution: "action_taken", warningId: warning.id }, + data: { + points: draft.points, + userId: draft.user.id, + warningId: warning.id, + }, reportId: report.id, - type: "resolved", + type: "warning", }); + if (resolveReport) { + await updateReport(tx, report, { + openKey: null, + resolution: "action_taken", + resolvedAt: now, + resolvedById: moderator.user.id, + status: "done", + }); + await appendReportEvent(tx, { + actorId: moderator.user.id, + data: { resolution: "action_taken", warningId: warning.id }, + reportId: report.id, + type: "resolved", + }); + } } - } - const notifications = c.get("notifications"); - await notifications.publish({ - actorId: null, - data: { - message: input.message.trim(), - points: draft.points, - reason: draft.reason?.title ?? null, - warningId: warning.id, - }, - idempotencyKey: `warning:${warning.id}`, - recipients: [draft.user.id], - tx, - type: warningIssuedNotification, - }); - if (sanction) { + const notifications = c.get("notifications"); await notifications.publish({ actorId: null, data: { - endsAt: sanction.endsAt?.toISOString() ?? null, - sanctionId: sanction.id, + message: input.message.trim(), + points: draft.points, + reason: draft.reason?.title ?? null, + warningId: warning.id, }, - idempotencyKey: `sanction:${sanction.id}`, + idempotencyKey: `warning:${warning.id}`, recipients: [draft.user.id], tx, - type: accountSuspendedNotification, + type: warningIssuedNotification, }); - } + if (sanction) { + await notifications.publish({ + actorId: null, + data: { + endsAt: sanction.endsAt?.toISOString() ?? null, + sanctionId: sanction.id, + }, + idempotencyKey: `sanction:${sanction.id}`, + recipients: [draft.user.id], + tx, + type: accountSuspendedNotification, + }); + } + + return { + duplicate: false, + points: { after, before }, + reportResolved: resolveReport, + sanction, + warningId: warning.id, + } satisfies IssuedWarning; + }); + + // The lock is per warned member, so the same key sent at once for two + // members only meets at the unique (issuedById, idempotencyKey) index. + const result = await issue().catch(async (error: unknown) => { + if (!isUniqueViolation(error)) throw error; + const existing = await findIssuedWarning(c.get("db"), moderator, input); + if (!existing) throw error; - return { - duplicate: false, - points: { after, before }, - reportResolved: resolveReport, - sanction, - warningId: warning.id, - } satisfies IssuedWarning; + return existing; }); if (result.duplicate) return result; diff --git a/packages/vitnode/src/api/modules/moderation/warnings.postgres.test.ts b/packages/vitnode/src/api/modules/moderation/warnings.postgres.test.ts new file mode 100644 index 0000000000..20a80c2a6e --- /dev/null +++ b/packages/vitnode/src/api/modules/moderation/warnings.postgres.test.ts @@ -0,0 +1,199 @@ +import type { Context } from "hono"; + +import { OpenAPIHono } from "@hono/zod-openapi"; +import { asc, eq } from "drizzle-orm"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; + +import type { PostgresTestSchema } from "@/tests/postgres"; + +import { core_languages, core_languages_words } from "@/database/languages"; +import { core_roles } from "@/database/roles"; +import { core_users } from "@/database/users"; +import { + core_moderation_sanctions, + core_moderation_warning_reasons, + core_moderation_warning_thresholds, + core_moderation_warnings, +} from "@/database/warnings"; +import { + WARNING_PERMISSION_MODULE, + WARNING_PERMISSIONS, +} from "@/lib/moderation/warnings"; +import { resolvePersonalInformationFields } from "@/lib/user-personal-information"; +import { manifest } from "@/manifest"; +import { createTestCache } from "@/tests/cache"; +import { createPostgresTestSchema, POSTGRES_TEST_URL } from "@/tests/postgres"; +import { grantStaffPermissions } from "@/tests/staff-permissions"; + +import { moderationModule } from "./moderation.module"; + +const MODERATOR = 1; +const CONCURRENT_MEMBER = 2; +const RETRIED_MEMBER = 3; +const USERS = [MODERATOR, CONCURRENT_MEMBER, RETRIED_MEMBER]; + +interface Issued { + duplicate: boolean; + sanction: null | { id: number }; + warningId: number; +} + +describe.skipIf(!POSTGRES_TEST_URL)("issuing warnings on PostgreSQL", () => { + let schema: PostgresTestSchema; + let reasonId: number; + const thresholds: Record<"eight" | "ten", number> = { eight: 0, ten: 0 }; + const cache = createTestCache(); + const publish = vi.fn(async (_args: unknown) => Promise.resolve()); + + const app = new OpenAPIHono(); + app.use("*", async (c, next) => { + c.set("user", { + id: Number(c.req.header("x-user")), + } as unknown as Context["var"]["user"]); + c.set("suspension", null); + c.set("cache", cache); + c.set("db", schema.db as unknown as Context["var"]["db"]); + c.set("events", { + emit: async () => Promise.resolve(), + } as unknown as Context["var"]["events"]); + c.set("notifications", { + publish, + } as unknown as Context["var"]["notifications"]); + c.set("core", { + moderation: { scopes: [], targets: [] }, + permissionStaff: [], + personalInformationFields: resolvePersonalInformationFields(), + } as unknown as Context["var"]["core"]); + await next(); + }); + app.route("/", moderationModule.hono); + + const issue = async (userId: number, idempotencyKey: string) => { + const res = await app.request("/warnings", { + body: JSON.stringify({ + idempotencyKey, + message: "Please keep to the topic.", + reasonId, + userId, + }), + headers: { + "content-type": "application/json", + "x-user": String(MODERATOR), + }, + method: "POST", + }); + expect(res.status).toBe(200); + + return (await res.json()) as Issued; + }; + + beforeAll(async () => { + schema = await createPostgresTestSchema({ prefix: "vitnode_warnings" }); + await schema.db + .insert(core_languages) + .values({ code: "en", name: "English" }); + const [role] = await schema.db + .insert(core_roles) + .values({ updatedAt: new Date() }) + .returning({ id: core_roles.id }); + await schema.db.insert(core_users).values( + USERS.map(id => ({ + avatarColor: "000000", + email: `user${id}@example.com`, + id, + ipAddress: "127.0.0.1", + name: `user${id}`, + nameCode: `user${id}`, + roleId: role.id, + })), + ); + const [reason] = await schema.db + .insert(core_moderation_warning_reasons) + .values({ expiresAfterHours: null, points: 2 }) + .returning({ id: core_moderation_warning_reasons.id }); + reasonId = reason.id; + await schema.db.insert(core_languages_words).values({ + itemId: reasonId, + languageCode: "en", + pluginCode: "core", + tableName: "core_moderation_warning_reasons", + value: "Off-topic posting", + variable: "title", + }); + const rows = await schema.db + .insert(core_moderation_warning_thresholds) + .values([ + { action: "suspend_temporary", durationHours: 24, points: 8 }, + { action: "suspend_indefinite", durationHours: null, points: 10 }, + ]) + .returning({ id: core_moderation_warning_thresholds.id }); + thresholds.eight = rows[0]?.id ?? 0; + thresholds.ten = rows[1]?.id ?? 0; + await grantStaffPermissions(cache, { + permissions: Object.values(WARNING_PERMISSIONS).map(permission => ({ + module: WARNING_PERMISSION_MODULE, + permission, + plugin: manifest.id, + })), + type: "moderator", + userId: MODERATOR, + }); + }, 120_000); + + afterAll(async () => { + await schema?.drop(); + }); + + const warningsOf = async (userId: number) => + await schema.db + .select({ + pointsAfter: core_moderation_warnings.pointsAfter, + pointsBefore: core_moderation_warnings.pointsBefore, + }) + .from(core_moderation_warnings) + .where(eq(core_moderation_warnings.userId, userId)) + .orderBy(asc(core_moderation_warnings.pointsBefore)); + + const sanctionsOf = async (userId: number) => + await schema.db + .select({ thresholdId: core_moderation_sanctions.thresholdId }) + .from(core_moderation_sanctions) + .where(eq(core_moderation_sanctions.userId, userId)) + .orderBy(asc(core_moderation_sanctions.thresholdId)); + + it("chains points across concurrent warnings and applies each crossed threshold once", async () => { + await Promise.all( + [1, 2, 3, 4, 5].map( + async n => await issue(CONCURRENT_MEMBER, `concurrent-${n}`), + ), + ); + + expect(await warningsOf(CONCURRENT_MEMBER)).toEqual([ + { pointsAfter: 2, pointsBefore: 0 }, + { pointsAfter: 4, pointsBefore: 2 }, + { pointsAfter: 6, pointsBefore: 4 }, + { pointsAfter: 8, pointsBefore: 6 }, + { pointsAfter: 10, pointsBefore: 8 }, + ]); + expect(await sanctionsOf(CONCURRENT_MEMBER)).toEqual([ + { thresholdId: thresholds.eight }, + { thresholdId: thresholds.ten }, + ]); + }); + + it("turns concurrent retries of one request into a single warning", async () => { + publish.mockClear(); + + const results = await Promise.all( + Array.from( + { length: 4 }, + async () => await issue(RETRIED_MEMBER, "retried-request"), + ), + ); + + expect(results.filter(result => !result.duplicate)).toHaveLength(1); + expect(new Set(results.map(result => result.warningId)).size).toBe(1); + expect(await warningsOf(RETRIED_MEMBER)).toHaveLength(1); + expect(publish).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/vitnode/src/api/modules/moderation/warnings.test.ts b/packages/vitnode/src/api/modules/moderation/warnings.test.ts new file mode 100644 index 0000000000..866870beb8 --- /dev/null +++ b/packages/vitnode/src/api/modules/moderation/warnings.test.ts @@ -0,0 +1,947 @@ +import type { Context } from "hono"; + +import { OpenAPIHono } from "@hono/zod-openapi"; +import { eq } from "drizzle-orm"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import type { + PermissionsStaffArgs, + StaffPermissionSet, +} from "@/api/lib/permission-staff"; + +import { findActiveSuspension } from "@/api/lib/moderation/warnings/suspension"; +import { + accountSuspendedNotification, + warningIssuedNotification, +} from "@/api/lib/notifications/core-types"; +import { defineModerationScope, defineReportTarget } from "@/api/moderation"; +import { core_languages_words } from "@/database/languages"; +import { + core_moderation_report_events, + core_moderation_report_reasons, + core_moderation_reports, +} from "@/database/moderation"; +import { core_roles } from "@/database/roles"; +import { core_users } from "@/database/users"; +import { + core_moderation_audit, + core_moderation_sanctions, + core_moderation_warning_reasons, + core_moderation_warning_thresholds, + core_moderation_warnings, +} from "@/database/warnings"; +import { + WARNING_PERMISSION_MODULE, + WARNING_PERMISSIONS, +} from "@/lib/moderation/warnings"; +import { resolvePersonalInformationFields } from "@/lib/user-personal-information"; +import { manifest } from "@/manifest"; +import { createTestCache } from "@/tests/cache"; +import { createMemoryDb } from "@/tests/memory-db"; +import { + grantStaffPermissions, + ROOT_STAFF_PERMISSIONS, +} from "@/tests/staff-permissions"; + +import { moderationModule } from "./moderation.module"; + +const FORUM = "@vitnode/forum"; +const FAMILY = "forum.section"; +const TYPE = "forum.topic"; +const SECTION_A = "10"; +const SECTION_B = "20"; + +const MEMBER_ROLE = 3; +const ROOT_ROLE = 4; + +const MEMBER = 100; +const OTHER_MEMBER = 101; +const ROOT_ROLE_MEMBER = 102; +const SENIOR = 200; +const BASIC = 201; +const VIEWER = 202; +const SCOPED = 203; +const ROOT_MOD = 300; + +const REASON_TWO = 1; +const REASON_FIVE = 2; +const REASON_ZERO = 3; +const REASON_OFF = 4; +const TITLE_OF_TWO = "Off-topic posting"; + +const THRESHOLD_EIGHT = 1; +const THRESHOLD_TEN = 2; + +const HOUR = 60 * 60 * 1000; + +const STAFF_NOTE = "staff-only-note"; +const OVERRIDE_NOTE = "override-explanation"; + +const warnings = (...permissions: string[]): PermissionsStaffArgs[] => + permissions.map(permission => ({ + module: WARNING_PERMISSION_MODULE, + permission, + plugin: manifest.id, + })); + +const sectionA = (permission: string): PermissionsStaffArgs => ({ + module: "topics", + permission, + plugin: FORUM, + scope: { family: FAMILY, ids: [SECTION_A], mode: "selected" }, +}); + +let keys = 0; +const nextKey = () => `warn-key-${++keys}`; + +const harness = async () => { + const sections = new Map([ + ["1", SECTION_A], + ["3", SECTION_B], + ]); + const authorOf = (targetId: string) => (targetId === "4" ? null : MEMBER); + + const target = defineReportTarget({ + capture: async (_c, { targetId }) => + Promise.resolve({ + authors: [{ id: MEMBER, name: "Member" }], + fields: [{ kind: "text", label: null, name: "body", value: "Body" }], + locale: { fallback: false, requested: null, served: "en" }, + scope: null, + source: { publishedAt: null, updatedAt: null, version: null }, + title: `Topic ${targetId}`, + url: `/topics/${targetId}`, + }), + current: async (_c, { targetId }) => + Promise.resolve({ hidden: false, url: `/topics/${targetId}` }), + label: "Topic", + parseId: value => (/^\d+$/.test(value) ? value : null), + permissionModule: "topics", + resolveAuthors: async (_c, { targetId }) => { + const author = authorOf(targetId); + + return Promise.resolve( + author === null + ? null + : { authorIds: [author], title: `Topic ${targetId}` }, + ); + }, + resolveScope: async (_c, targetId) => + Promise.resolve(sections.get(targetId)), + scope: FAMILY, + type: TYPE, + }); + const scope = defineModerationScope({ + find: async () => Promise.resolve([]), + id: FAMILY, + label: "Section", + list: async () => Promise.resolve({ items: [], nextCursor: null }), + }); + + const cache = createTestCache(); + const grants: [number, PermissionsStaffArgs[] | StaffPermissionSet][] = [ + [SENIOR, warnings(...Object.values(WARNING_PERMISSIONS))], + [BASIC, warnings(WARNING_PERMISSIONS.warn)], + [VIEWER, warnings(WARNING_PERMISSIONS.view)], + [ + SCOPED, + [ + sectionA("can_warn"), + sectionA("can_view_reports"), + sectionA("can_manage_reports"), + ], + ], + [ROOT_MOD, ROOT_STAFF_PERMISSIONS], + ]; + for (const [userId, permissions] of grants) { + await grantStaffPermissions(cache, { + permissions, + type: "moderator", + userId, + }); + } + + const users = [ + MEMBER, + OTHER_MEMBER, + ROOT_ROLE_MEMBER, + SENIOR, + BASIC, + VIEWER, + SCOPED, + ROOT_MOD, + ]; + const reasons = [ + { enabled: true, expiresAfterHours: 720, id: REASON_TWO, points: 2 }, + { enabled: true, expiresAfterHours: null, id: REASON_FIVE, points: 5 }, + { enabled: true, expiresAfterHours: null, id: REASON_ZERO, points: 0 }, + { enabled: false, expiresAfterHours: null, id: REASON_OFF, points: 3 }, + ]; + const memory = createMemoryDb([ + [ + core_users, + users.map(id => ({ + email: `${id}@example.com`, + id, + name: `User ${id}`, + nameCode: `user-${id}`, + roleId: id === ROOT_ROLE_MEMBER ? ROOT_ROLE : MEMBER_ROLE, + })), + ], + [ + core_roles, + [ + { color: null, id: MEMBER_ROLE, prefix: null, root: false }, + { color: null, id: ROOT_ROLE, prefix: null, root: true }, + ], + ], + [ + core_moderation_report_reasons, + [{ code: "spam", id: 1, position: 0, requiresDetails: false }], + ], + [ + core_moderation_warning_reasons, + reasons.map(reason => ({ ...reason, position: reason.id })), + ], + [ + core_languages_words, + reasons.map(reason => ({ + id: reason.id, + itemId: reason.id, + languageCode: "en", + pluginCode: "core", + tableName: "core_moderation_warning_reasons", + value: reason.id === REASON_TWO ? TITLE_OF_TWO : `Reason ${reason.id}`, + variable: "title", + })), + ], + [ + core_moderation_warning_thresholds, + [ + { + action: "suspend_temporary", + durationHours: 24, + enabled: true, + id: THRESHOLD_EIGHT, + points: 8, + }, + { + action: "suspend_indefinite", + durationHours: null, + enabled: true, + id: THRESHOLD_TEN, + points: 10, + }, + ], + ], + ]); + const emit = vi.fn(async (_name: string, _payload: unknown) => + Promise.resolve(), + ); + const publish = vi.fn(async (_args: Record) => + Promise.resolve(), + ); + const closeUserConnections = vi.fn((_userId: number) => undefined); + + const app = new OpenAPIHono(); + app.use("*", async (c, next) => { + const userId = Number(c.req.header("x-user")); + const suspendedId = Number(c.req.header("x-suspended")); + c.set( + "user", + (users.includes(userId) + ? { id: userId, roleId: MEMBER_ROLE } + : null) as unknown as Context["var"]["user"], + ); + c.set( + "suspension", + suspendedId ? { endsAt: null, userId: suspendedId } : null, + ); + c.set("cache", cache); + c.set("db", memory.db as unknown as Context["var"]["db"]); + c.set("events", { emit } as unknown as Context["var"]["events"]); + c.set("notifications", { + publish, + } as unknown as Context["var"]["notifications"]); + c.set("realtime", { + closeUserConnections, + } as unknown as Context["var"]["realtime"]); + c.set("core", { + moderation: { + scopes: [{ definition: scope, pluginId: FORUM }], + targets: [{ definition: target, pluginId: FORUM }], + }, + permissionStaff: [], + personalInformationFields: resolvePersonalInformationFields(), + } as unknown as Context["var"]["core"]); + await next(); + }); + app.route("/", moderationModule.hono); + + const call = async ( + userId: number, + method: "GET" | "POST", + path: string, + body?: Record, + headers: Record = {}, + ) => + await app.request(path, { + method, + headers: { + "content-type": "application/json", + "x-user": String(userId), + ...headers, + }, + ...(body ? { body: JSON.stringify(body) } : {}), + }); + + const issue = async (userId: number, body: Record = {}) => + await call(userId, "POST", "/warnings", { + idempotencyKey: nextKey(), + message: "Please keep to the topic.", + reasonId: REASON_TWO, + userId: MEMBER, + ...body, + }); + + const issued = async (userId: number, body: Record = {}) => { + const res = await issue(userId, body); + expect(res.status).toBe(200); + + return (await res.json()) as Issued; + }; + + const revoke = async ( + userId: number, + kind: "sanctions" | "warnings", + id: number, + ) => + await call(userId, "POST", `/${kind}/${id}/revoke`, { + reason: "Issued by mistake", + }); + + /** Active points the member already holds, as one warning outside the API. */ + const seedPoints = async (userId: number, points: number) => { + await memory.db.insert(core_moderation_warnings).values({ + issuedById: SENIOR, + message: "Earlier warning", + points, + userId, + }); + }; + + const standing = async (moderatorId: number, userId = MEMBER) => { + const res = await call(moderatorId, "GET", `/members/${userId}`); + expect(res.status).toBe(200); + + return (await res.json()) as Standing; + }; + + const suspensionOf = async (userId: number) => + await findActiveSuspension( + { + get: (key: string) => + key === "cache" ? cache : key === "db" ? memory.db : undefined, + } as unknown as Context, + userId, + ); + + const warningRows = () => memory.rows(core_moderation_warnings); + const sanctionRows = () => memory.rows(core_moderation_sanctions); + const reportEvents = (id: number) => + memory + .rows(core_moderation_report_events) + .filter(entry => entry.reportId === id) + .map(entry => entry.type); + + const fileReport = async (targetId: string) => { + const res = await call(OTHER_MEMBER, "POST", "/reports", { + pluginId: FORUM, + reason: "spam", + targetId, + targetType: TYPE, + }); + expect(res.status).toBe(201); + + return ((await res.json()) as { id: number }).id; + }; + const reportRow = (id: number) => + memory.rows(core_moderation_reports).find(entry => entry.id === id); + + return { + call, + closeUserConnections, + emit, + fileReport, + issue, + issued, + memory, + publish, + reportEvents, + reportRow, + revoke, + sanctionRows, + seedPoints, + standing, + suspensionOf, + warningRows, + }; +}; + +interface Issued { + duplicate: boolean; + points: { after: number; before: number }; + reportResolved: boolean; + sanction: null | { endsAt: null | string; id: number; thresholdId: number }; + warningId: number; +} + +interface StaffWarning { + id: number; + overrideNote: null | string; + reason: null | { languageCode: string; value: string }[]; + revokeReason: null | string; + staffNote: null | string; + status: string; +} + +interface Standing { + activePoints: number; + hiddenWarnings: number; + warnings: StaffWarning[]; +} + +interface AccountStatus { + activePoints: number; + sanctions: { active: boolean; id: number }[]; + suspension: null | { endsAt: null | string }; + warnings: Record[]; +} + +const accountStatus = async ( + h: Awaited>, + userId = MEMBER, +) => { + const res = await h.call(userId, "GET", "/account-status"); + expect(res.status).toBe(200); + + return (await res.json()) as AccountStatus; +}; + +const inHours = (hours: number) => + new Date(Date.now() + hours * HOUR).toISOString(); + +afterEach(() => { + vi.useRealTimers(); +}); + +describe("issuing a warning", () => { + it("takes points and expiry from the reason and keeps its title after the reason changes", async () => { + const h = await harness(); + + const { points, warningId } = await h.issued(SENIOR); + + expect(points).toEqual({ after: 2, before: 0 }); + const [row] = h.warningRows(); + expect(row).toMatchObject({ + defaultPoints: 2, + id: warningId, + overrideNote: null, + points: 2, + }); + expect( + (row.expiresAt as Date).getTime() - (row.issuedAt as Date).getTime(), + ).toBe(720 * HOUR); + + await h.memory.db + .update(core_languages_words) + .set({ value: "Renamed" }) + .where(eq(core_languages_words.itemId, REASON_TWO)); + await h.memory.db + .update(core_moderation_warning_reasons) + .set({ enabled: false }) + .where(eq(core_moderation_warning_reasons.id, REASON_TWO)); + + const original = [{ languageCode: "en", value: TITLE_OF_TWO }]; + expect((await h.standing(SENIOR)).warnings[0]?.reason).toEqual(original); + expect((await accountStatus(h)).warnings[0]?.reason).toEqual(original); + }); + + it("issues a zero-point warning that notifies the member without moving their points", async () => { + const h = await harness(); + await h.seedPoints(MEMBER, 7); + + const result = await h.issued(SENIOR, { reasonId: REASON_ZERO }); + + expect(result.points).toEqual({ after: 7, before: 7 }); + expect(result.sanction).toBeNull(); + expect(h.publish).toHaveBeenCalledTimes(1); + expect(h.publish.mock.calls[0]?.[0]).toMatchObject({ + recipients: [MEMBER], + type: warningIssuedNotification, + }); + }); + + it("accepts a points and expiration override with its explanation", async () => { + const h = await harness(); + + await h.issued(SENIOR, { + expiration: { hours: 48, mode: "hours" }, + overrideNote: OVERRIDE_NOTE, + points: 4, + }); + + const [row] = h.warningRows(); + expect(row).toMatchObject({ + defaultPoints: 2, + overrideNote: OVERRIDE_NOTE, + points: 4, + }); + expect( + (row.expiresAt as Date).getTime() - (row.issuedAt as Date).getTime(), + ).toBe(48 * HOUR); + expect(h.memory.rows(core_moderation_audit).map(e => e.type)).toContain( + "warning_overridden", + ); + }); + + it("lets a root moderator warn other staff", async () => { + const h = await harness(); + + expect((await h.issue(ROOT_MOD, { userId: SCOPED })).status).toBe(200); + }); + + it.each([ + [ + "a points override without permission", + BASIC, + { overrideNote: OVERRIDE_NOTE, points: 4 }, + 403, + "points_override_forbidden", + ], + [ + "a points override without explanation", + SENIOR, + { points: 4 }, + 400, + "override_note_required", + ], + [ + "an expiration override without permission", + BASIC, + { expiration: { mode: "never" }, overrideNote: OVERRIDE_NOTE }, + 403, + "expiration_override_forbidden", + ], + [ + "an expiration in the past", + SENIOR, + { expiration: { at: inHours(-1), mode: "at" }, overrideNote: "Past" }, + 400, + "invalid_expiration", + ], + [ + "an expiration beyond the longest allowed", + SENIOR, + { + expiration: { at: inHours(24 * 365 * 6), mode: "at" }, + overrideNote: "Far", + }, + 400, + "invalid_expiration", + ], + [ + "a disabled reason", + SENIOR, + { reasonId: REASON_OFF }, + 409, + "reason_disabled", + ], + ["a moderator's own account", SENIOR, { userId: SENIOR }, 403, "self"], + ["another staff member", BASIC, { userId: SCOPED }, 403, "protected"], + [ + "a root-role member, even by a root moderator", + ROOT_MOD, + { userId: ROOT_ROLE_MEMBER }, + 403, + "protected", + ], + ["a direct warning without core can_warn", SCOPED, {}, 403, "forbidden"], + [ + "content outside the moderator's scope", + SCOPED, + { content: { pluginId: FORUM, targetId: "3", targetType: TYPE } }, + 403, + "forbidden", + ], + [ + "a member who did not write the content", + SCOPED, + { + content: { pluginId: FORUM, targetId: "1", targetType: TYPE }, + userId: OTHER_MEMBER, + }, + 400, + "author_mismatch", + ], + ])("refuses %s", async (_name, actor, body, status, error) => { + const h = await harness(); + + const res = await h.issue(actor, body); + + expect(res.status).toBe(status); + expect(await res.json()).toMatchObject({ error }); + expect(h.warningRows()).toEqual([]); + expect(h.publish).not.toHaveBeenCalled(); + }); + + it("lets a scoped moderator warn an author, with the scope resolved on the server", async () => { + const h = await harness(); + + await h.issued(SCOPED, { + content: { + pluginId: FORUM, + scopeId: SECTION_B, + targetId: "1", + targetType: TYPE, + }, + reasonId: REASON_ZERO, + }); + + expect(h.warningRows()[0]).toMatchObject({ + contentSnapshot: { title: "Topic 1", url: "/topics/1" }, + scopeFamily: FAMILY, + scopeId: SECTION_A, + targetId: "1", + }); + }); +}); + +describe("thresholds", () => { + it.each([ + [REASON_TWO, 8, THRESHOLD_EIGHT, 24], + [REASON_FIVE, 11, THRESHOLD_TEN, null], + ])( + "reason %i takes 6 points to %i and applies only threshold %i", + async (reasonId, after, thresholdId, hours) => { + const h = await harness(); + await h.seedPoints(MEMBER, 6); + expect((await accountStatus(h)).suspension).toBeNull(); + + const result = await h.issued(SENIOR, { reasonId }); + + expect(result.points).toEqual({ after, before: 6 }); + expect(result.sanction?.thresholdId).toBe(thresholdId); + const sanctions = h.sanctionRows(); + expect(sanctions).toHaveLength(1); + const [sanction] = sanctions; + expect(sanction).toMatchObject({ + source: "threshold", + thresholdId, + userId: MEMBER, + warningId: result.warningId, + }); + expect( + hours === null + ? sanction.endsAt + : (sanction.endsAt as Date).getTime() - + (sanction.startsAt as Date).getTime(), + ).toBe(hours === null ? null : hours * HOUR); + + const status = await accountStatus(h); + expect(status.suspension).toEqual({ + endsAt: (sanction.endsAt as Date | null)?.toISOString() ?? null, + }); + expect(h.closeUserConnections).toHaveBeenCalledWith(MEMBER); + expect(h.publish.mock.calls.map(([args]) => args.type)).toEqual([ + warningIssuedNotification, + accountSuspendedNotification, + ]); + }, + ); + + it("answers a retry with the first result and writes nothing again", async () => { + const h = await harness(); + await h.seedPoints(MEMBER, 6); + const idempotencyKey = nextKey(); + + const first = await h.issued(SENIOR, { idempotencyKey }); + const writes = { + emits: h.emit.mock.calls.length, + publishes: h.publish.mock.calls.length, + }; + const retry = await h.issued(SENIOR, { idempotencyKey }); + + expect(retry).toEqual({ + ...first, + duplicate: true, + sanction: { ...first.sanction }, + }); + expect(h.warningRows()).toHaveLength(2); + expect(h.sanctionRows()).toHaveLength(1); + expect(h.emit).toHaveBeenCalledTimes(writes.emits); + expect(h.publish).toHaveBeenCalledTimes(writes.publishes); + + const other = await h.issue(SENIOR, { + idempotencyKey, + userId: OTHER_MEMBER, + }); + expect(other.status).toBe(409); + expect(await other.json()).toEqual({ error: "idempotency_conflict" }); + }); + + it("serializes concurrent warnings for one member", async () => { + const h = await harness(); + const repeated = nextKey(); + + const results = await Promise.all( + [repeated, nextKey(), nextKey(), nextKey(), nextKey(), repeated].map( + async idempotencyKey => await h.issued(SENIOR, { idempotencyKey }), + ), + ); + + expect(results.filter(result => result.duplicate)).toHaveLength(1); + expect( + h + .warningRows() + .map(row => [row.pointsBefore, row.pointsAfter]) + .sort(([a], [b]) => Number(a) - Number(b)), + ).toEqual([ + [0, 2], + [2, 4], + [4, 6], + [6, 8], + [8, 10], + ]); + expect( + h + .sanctionRows() + .map(row => row.thresholdId) + .sort((a, b) => Number(a) - Number(b)), + ).toEqual([THRESHOLD_EIGHT, THRESHOLD_TEN]); + }); + + it("does not suspend a member already above a threshold that is lowered", async () => { + const h = await harness(); + await h.seedPoints(MEMBER, 9); + await h.memory.db + .update(core_moderation_warning_thresholds) + .set({ points: 9 }) + .where(eq(core_moderation_warning_thresholds.id, THRESHOLD_TEN)); + + await h.issued(SENIOR, { reasonId: REASON_ZERO }); + await h.issued(SENIOR); + + expect(h.sanctionRows()).toEqual([]); + }); + + it("applies a threshold again once a revoked warning takes the member back under it", async () => { + const h = await harness(); + await h.seedPoints(MEMBER, 6); + + const first = await h.issued(SENIOR); + expect((await h.revoke(SENIOR, "warnings", first.warningId)).status).toBe( + 200, + ); + const second = await h.issued(SENIOR); + + expect(second.points).toEqual({ after: 8, before: 6 }); + expect(h.sanctionRows().map(row => row.thresholdId)).toEqual([ + THRESHOLD_EIGHT, + THRESHOLD_EIGHT, + ]); + }); + + it("stops a temporary suspension once it ends, with no job involved", async () => { + vi.useFakeTimers({ toFake: ["Date"] }); + vi.setSystemTime(new Date("2026-10-28T12:00:00.000Z")); + const h = await harness(); + await h.seedPoints(MEMBER, 6); + await h.issued(SENIOR); + expect((await accountStatus(h)).suspension).toEqual({ + endsAt: "2026-10-29T12:00:00.000Z", + }); + + vi.setSystemTime(new Date("2026-10-29T12:00:00.001Z")); + + const status = await accountStatus(h); + expect(status.suspension).toBeNull(); + expect(status.sanctions.map(sanction => sanction.active)).toEqual([false]); + }); +}); + +describe("revoking", () => { + it("removes a warning's points but keeps its record and the suspension it caused", async () => { + const h = await harness(); + await h.seedPoints(MEMBER, 6); + const { warningId } = await h.issued(BASIC); + + expect((await h.revoke(BASIC, "warnings", warningId)).status).toBe(403); + expect((await h.revoke(SENIOR, "warnings", warningId)).status).toBe(200); + const again = await h.revoke(SENIOR, "warnings", warningId); + expect(again.status).toBe(409); + + const { activePoints, warnings: list } = await h.standing(SENIOR); + expect(activePoints).toBe(6); + expect(list.find(warning => warning.id === warningId)).toMatchObject({ + revokeReason: "Issued by mistake", + status: "revoked", + }); + expect(await h.suspensionOf(MEMBER)).not.toBeNull(); + }); + + it("lifts one suspension and leaves the others in force", async () => { + const h = await harness(); + await h.seedPoints(MEMBER, 6); + const temporary = await h.issued(SENIOR); + const indefinite = await h.issued(SENIOR); + expect((await h.suspensionOf(MEMBER))?.endsAt).toBeNull(); + const indefiniteId = indefinite.sanction?.id ?? 0; + + expect((await h.revoke(BASIC, "sanctions", indefiniteId)).status).toBe(403); + expect((await h.revoke(SENIOR, "sanctions", indefiniteId)).status).toBe( + 200, + ); + + expect(await h.suspensionOf(MEMBER)).toEqual({ + endsAt: new Date(temporary.sanction?.endsAt ?? 0), + sanctionIds: [temporary.sanction?.id], + }); + }); +}); + +describe("reports", () => { + it("links a warning to a report and resolves it only when asked", async () => { + const h = await harness(); + const reportId = await h.fileReport("1"); + + const wrongUser = await h.issue(SCOPED, { reportId, userId: OTHER_MEMBER }); + expect(wrongUser.status).toBe(400); + expect(await wrongUser.json()).toEqual({ error: "author_mismatch" }); + + const linked = await h.issued(SCOPED, { + reasonId: REASON_ZERO, + reportId, + }); + expect(linked.reportResolved).toBe(false); + expect(h.reportRow(reportId)?.status).not.toBe("done"); + expect(h.reportEvents(reportId)).toEqual(["created", "warning"]); + + const resolving = await h.issued(SCOPED, { + reasonId: REASON_ZERO, + reportId, + resolveReport: true, + }); + expect(resolving.reportResolved).toBe(true); + expect(h.reportRow(reportId)).toMatchObject({ + resolution: "action_taken", + status: "done", + }); + expect(h.reportEvents(reportId)).toEqual([ + "created", + "warning", + "warning", + "resolved", + ]); + expect(h.warningRows().map(row => row.reportId)).toEqual([ + reportId, + reportId, + ]); + }); + + it("refuses to resolve a closed report and writes nothing", async () => { + const h = await harness(); + const reportId = await h.fileReport("1"); + const closed = await h.call( + SCOPED, + "POST", + `/reports/${reportId}/resolve`, + { + expectedVersion: h.reportRow(reportId)?.version, + resolution: "no_violation", + }, + ); + expect(closed.status).toBe(200); + const events = h.reportEvents(reportId); + + const res = await h.issue(SCOPED, { + reasonId: REASON_ZERO, + reportId, + resolveReport: true, + }); + + expect(res.status).toBe(409); + expect(await res.json()).toMatchObject({ error: "report_closed" }); + expect(h.warningRows()).toEqual([]); + expect(h.reportEvents(reportId)).toEqual(events); + expect(h.publish).not.toHaveBeenCalled(); + }); +}); + +describe("staff notes", () => { + it("shows them only with can_view_notes or to the moderator who wrote them", async () => { + const h = await harness(); + await h.issued(SENIOR, { + overrideNote: OVERRIDE_NOTE, + points: 1, + staffNote: STAFF_NOTE, + }); + await h.issued(BASIC, { staffNote: "basic-note" }); + + const notesOf = (list: StaffWarning[]) => + list.map(warning => [warning.staffNote, warning.overrideNote]); + + expect(notesOf((await h.standing(SENIOR)).warnings)).toEqual([ + ["basic-note", null], + [STAFF_NOTE, OVERRIDE_NOTE], + ]); + expect(notesOf((await h.standing(VIEWER)).warnings)).toEqual([ + [null, null], + [null, null], + ]); + const listed = await h.call(VIEWER, "GET", `/warnings?userId=${MEMBER}`); + expect( + notesOf(((await listed.json()) as { items: StaffWarning[] }).items), + ).toEqual([ + [null, null], + [null, null], + ]); + const basic = await h.standing(BASIC); + expect(notesOf(basic.warnings)).toEqual([["basic-note", null]]); + expect(basic.hiddenWarnings).toBe(1); + }); + + it("never reach the member's own status, notifications, events or audit trail", async () => { + const h = await harness(); + await h.seedPoints(MEMBER, 6); + const { sanction, warningId } = await h.issued(SENIOR, { + overrideNote: OVERRIDE_NOTE, + points: 3, + staffNote: STAFF_NOTE, + }); + await h.revoke(SENIOR, "warnings", warningId); + await h.revoke(SENIOR, "sanctions", sanction?.id ?? 0); + + const suspended = await h.call(0, "GET", "/account-status", undefined, { + "x-suspended": String(MEMBER), + }); + expect(suspended.status).toBe(200); + const status = await suspended.text(); + expect((await h.call(0, "GET", "/account-status")).status).toBe(401); + + expect(h.emit.mock.calls.map(([name]) => name)).toEqual([ + "moderation.warning.issued", + "moderation.sanction.applied", + "moderation.warning.revoked", + "moderation.sanction.revoked", + ]); + const leaked = JSON.stringify([ + status, + h.emit.mock.calls, + h.publish.mock.calls.map(([args]) => args.data), + h.memory.rows(core_moderation_audit), + ]); + expect(leaked).not.toContain(STAFF_NOTE); + expect(leaked).not.toContain(OVERRIDE_NOTE); + }); +}); diff --git a/packages/vitnode/src/api/modules/users/routes/session.route.test.ts b/packages/vitnode/src/api/modules/users/routes/session.route.test.ts index 452297d818..bc382479bc 100644 --- a/packages/vitnode/src/api/modules/users/routes/session.route.test.ts +++ b/packages/vitnode/src/api/modules/users/routes/session.route.test.ts @@ -86,6 +86,9 @@ describe("GET /users/session", () => { }); it("answers with no user when signed out", async () => { - expect(await readSession(null)).toEqual({ user: null }); + expect(await readSession(null)).toEqual({ + suspension: null, + user: null, + }); }); }); diff --git a/packages/vitnode/src/lib/moderation/warnings.test.ts b/packages/vitnode/src/lib/moderation/warnings.test.ts new file mode 100644 index 0000000000..cae903da9a --- /dev/null +++ b/packages/vitnode/src/lib/moderation/warnings.test.ts @@ -0,0 +1,100 @@ +import { describe, expect, it } from "vitest"; + +import type { WarningThresholdRule } from "./warnings"; + +import { + activeWarningPoints, + evaluateWarningThresholds, + isSanctionActiveAt, + thresholdSuspensionEnd, +} from "./warnings"; + +const NOW = new Date("2026-10-28T12:00:00.000Z"); +const HOUR = 60 * 60 * 1000; +const at = (hours: number) => new Date(NOW.getTime() + hours * HOUR); + +const rule = ( + id: number, + points: number, + extra: Partial = {}, +): WarningThresholdRule => ({ + action: "suspend_temporary", + durationHours: 24, + enabled: true, + id, + points, + ...extra, +}); + +describe("activeWarningPoints", () => { + it("counts warnings that are neither expired nor revoked", () => { + expect( + activeWarningPoints( + [ + { expiresAt: null, points: 3, revokedAt: null }, + { expiresAt: at(1), points: 2, revokedAt: null }, + { expiresAt: null, points: 0, revokedAt: null }, + { expiresAt: NOW, points: 5, revokedAt: null }, + { expiresAt: at(-1), points: 7, revokedAt: null }, + { expiresAt: null, points: 11, revokedAt: at(-2) }, + { expiresAt: at(1), points: 13, revokedAt: at(-2) }, + ], + NOW, + ), + ).toBe(5); + }); +}); + +describe("evaluateWarningThresholds", () => { + const eight = rule(1, 8); + const ten = rule(2, 10, { + action: "suspend_indefinite", + durationHours: null, + }); + const twelveOff = rule(3, 12, { enabled: false }); + const rules = [ten, twelveOff, eight]; + + it.each([ + { after: 8, applied: eight, before: 6, crossed: [eight] }, + { after: 11, applied: ten, before: 6, crossed: [eight, ten] }, + { after: 9, applied: null, before: 8, crossed: [] }, + { after: 12, applied: null, before: 10, crossed: [] }, + { after: 7, applied: null, before: 6, crossed: [] }, + { after: 20, applied: null, before: 11, crossed: [] }, + ])( + "applies only the highest enabled rule crossed from $before to $after", + ({ after, applied, before, crossed }) => { + expect(evaluateWarningThresholds({ after, before, rules })).toEqual({ + applied, + crossed, + }); + }, + ); +}); + +describe("thresholdSuspensionEnd", () => { + it.each([ + ["after its duration", rule(1, 8, { durationHours: 24 }), at(24)], + [ + "never for an indefinite one", + rule(2, 10, { action: "suspend_indefinite", durationHours: null }), + null, + ], + ])("ends a suspension %s", (_name, threshold, end) => { + expect(thresholdSuspensionEnd(threshold, NOW)).toEqual(end); + }); +}); + +describe("isSanctionActiveAt", () => { + it.each([ + ["active and temporary", { endsAt: at(1), revokedAt: null }, true], + ["indefinite", { endsAt: null, revokedAt: null }, true], + ["revoked", { endsAt: null, revokedAt: at(-1) }, false], + ["ended", { endsAt: NOW, revokedAt: null }, false], + ["not started", { endsAt: null, revokedAt: null, startsAt: at(1) }, false], + ])("treats a %s sanction as active: %s", (_name, sanction, active) => { + expect(isSanctionActiveAt({ startsAt: at(-2), ...sanction }, NOW)).toBe( + active, + ); + }); +}); diff --git a/packages/vitnode/src/tanstack/comments/use-content-comments.test.tsx b/packages/vitnode/src/tanstack/comments/use-content-comments.test.tsx index 78ca94350f..71f2134c53 100644 --- a/packages/vitnode/src/tanstack/comments/use-content-comments.test.tsx +++ b/packages/vitnode/src/tanstack/comments/use-content-comments.test.tsx @@ -154,6 +154,7 @@ const NONE = { canEdit: false, canHide: false, canReport: false, + canWarn: false, }; type Comments = ReturnType< diff --git a/packages/vitnode/src/tanstack/content/permissions.test.tsx b/packages/vitnode/src/tanstack/content/permissions.test.tsx index 53a7db1c81..4cccee504a 100644 --- a/packages/vitnode/src/tanstack/content/permissions.test.tsx +++ b/packages/vitnode/src/tanstack/content/permissions.test.tsx @@ -84,6 +84,7 @@ describe("useContentPermissions", () => { canEdit: false, canHide: false, canReport: false, + canWarn: false, }); for (let settled = 0; settled < 5; settled++) { @@ -106,10 +107,34 @@ describe("useContentPermissions", () => { ).toStrictEqual(ids); expect(result.current.isError).toBe(false); expect([2, 3, 228, 229].map(id => result.current.of(id))).toStrictEqual([ - { canDelete: false, canEdit: true, canHide: false, canReport: true }, - { canDelete: false, canEdit: false, canHide: false, canReport: true }, - { canDelete: false, canEdit: true, canHide: false, canReport: true }, - { canDelete: false, canEdit: false, canHide: false, canReport: true }, + { + canDelete: false, + canEdit: true, + canHide: false, + canReport: true, + canWarn: false, + }, + { + canDelete: false, + canEdit: false, + canHide: false, + canReport: true, + canWarn: false, + }, + { + canDelete: false, + canEdit: true, + canHide: false, + canReport: true, + canWarn: false, + }, + { + canDelete: false, + canEdit: false, + canHide: false, + canReport: true, + canWarn: false, + }, ]); }); @@ -137,12 +162,14 @@ describe("useContentPermissions", () => { canEdit: false, canHide: false, canReport: false, + canWarn: false, }); expect(result.current.of(answered)).toStrictEqual({ canDelete: false, canEdit: true, canHide: false, canReport: true, + canWarn: false, }); }); }); diff --git a/packages/vitnode/src/views/auth/settings/settings-nav.test.ts b/packages/vitnode/src/views/auth/settings/settings-nav.test.ts index de283924b2..c6588dfdf2 100644 --- a/packages/vitnode/src/views/auth/settings/settings-nav.test.ts +++ b/packages/vitnode/src/views/auth/settings/settings-nav.test.ts @@ -6,13 +6,13 @@ describe("visibleSettingsNavItems", () => { it("shows Security while passkeys are on", () => { expect( visibleSettingsNavItems({ passkeys: true }).map(item => item.key), - ).toEqual(["overview", "notifications", "devices", "security"]); + ).toEqual(["overview", "notifications", "devices", "security", "standing"]); }); it("hides Security when passkeys are switched off", () => { expect( visibleSettingsNavItems({ passkeys: false }).map(item => item.key), - ).toEqual(["overview", "notifications", "devices"]); + ).toEqual(["overview", "notifications", "devices", "standing"]); }); it("shows Connected accounts only while an SSO provider is configured", () => { @@ -21,12 +21,12 @@ describe("visibleSettingsNavItems", () => { passkeys: false, sso: [{ id: "google", name: "Google" }], }).map(item => item.key), - ).toEqual(["overview", "notifications", "devices", "sso"]); + ).toEqual(["overview", "notifications", "devices", "sso", "standing"]); expect( visibleSettingsNavItems({ passkeys: false, sso: [] }).map( item => item.key, ), - ).toEqual(["overview", "notifications", "devices"]); + ).toEqual(["overview", "notifications", "devices", "standing"]); }); it("shows AI usage only while the site has an AI model configured", () => { @@ -35,12 +35,12 @@ describe("visibleSettingsNavItems", () => { ai: { models: [{ id: "fast" }] }, passkeys: false, }).map(item => item.key), - ).toEqual(["overview", "notifications", "devices", "ai"]); + ).toEqual(["overview", "notifications", "devices", "ai", "standing"]); expect( visibleSettingsNavItems({ ai: { models: [] }, passkeys: false }).map( item => item.key, ), - ).toEqual(["overview", "notifications", "devices"]); + ).toEqual(["overview", "notifications", "devices", "standing"]); }); it("hides AI usage from a member without any AI feature", () => { @@ -50,6 +50,6 @@ describe("visibleSettingsNavItems", () => { aiFeatures: false, passkeys: false, }).map(item => item.key), - ).toEqual(["overview", "notifications", "devices"]); + ).toEqual(["overview", "notifications", "devices", "standing"]); }); }); diff --git a/packages/vitnode/src/views/moderation/warnings/account-status-content.tsx b/packages/vitnode/src/views/moderation/warnings/account-status-content.tsx index c4a73dcf5b..464d30c928 100644 --- a/packages/vitnode/src/views/moderation/warnings/account-status-content.tsx +++ b/packages/vitnode/src/views/moderation/warnings/account-status-content.tsx @@ -1,10 +1,6 @@ import { useQuery } from "@tanstack/react-query"; import { Link } from "@tanstack/react-router"; -import { - ExternalLinkIcon, - LogInIcon, - ShieldAlertIcon, -} from "lucide-react"; +import { ExternalLinkIcon, LogInIcon, ShieldAlertIcon } from "lucide-react"; import React from "react"; import { useFormatter, useLocale, useTranslations } from "use-intl"; diff --git a/packages/vitnode/vitest.config.ts b/packages/vitnode/vitest.config.ts index bdf4a8a79c..f6fdc75e1b 100644 --- a/packages/vitnode/vitest.config.ts +++ b/packages/vitnode/vitest.config.ts @@ -44,6 +44,7 @@ const INTEGRATION = [ "scripts/cli/dev/package-watch.test.ts", "scripts/watch-plugin-manifest.test.ts", "src/api/lib/updates/lock.postgres.test.ts", + "src/api/modules/moderation/warnings.postgres.test.ts", "src/content/approvals/server/approval-notifications.postgres.test.ts", "src/content/server/notifications/content-notifications.postgres.test.ts", ]; From 70c673b2faea30bd4e743030a40010e58e70642a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 19:41:44 +0000 Subject: [PATCH 3/9] =?UTF-8?q?feat(moderation):=20=E2=9C=A8=20warnings=20?= =?UTF-8?q?in=20the=20Activity=20Logs=20and=20a=20member=20moderation=20lo?= =?UTF-8?q?g?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 Claude-Session: https://claude.ai/code/session_01CnAiFc4WPMdVa99L7jsTXY --- .../migration.sql | 90 + .../snapshot.json | 24959 ++++++++++++++++ .../web/content/docs/dev/audit-logs/index.mdx | 4 +- .../dev/moderation/configure-warnings.mdx | 2 + .../docs/dev/moderation/issue-warnings.mdx | 12 +- .../content/docs/dev/moderation/reference.mdx | 20 +- .../dev/moderation/warning-member-log.webp | Bin 0 -> 49988 bytes .../content/docs/dev/moderation/warnings.mdx | 7 +- .../src/api/lib/moderation/warnings/audit.ts | 47 + .../api/lib/moderation/warnings/reasons.ts | 33 +- .../api/lib/moderation/warnings/suspension.ts | 2 +- .../src/api/lib/notifications/core-types.ts | 13 +- .../warnings/routes/warning-reasons.routes.ts | 80 +- .../routes/warning-thresholds.routes.ts | 70 +- .../moderation/lib/warnings-service.ts | 125 +- .../modules/moderation/moderation.module.ts | 2 + .../moderation/routes/account-status.route.ts | 3 +- .../moderation/routes/warnings.routes.ts | 87 + .../api/modules/moderation/warnings.test.ts | 110 +- packages/vitnode/src/database/warnings.ts | 37 - packages/vitnode/src/lib/audit/actions.ts | 10 + .../vitnode/src/lib/moderation/warnings.ts | 34 +- packages/vitnode/src/locales/en.json | 48 +- .../moderation/warnings/warnings-admin-api.ts | 4 +- .../views/moderation/warnings/member-log.tsx | 215 + .../warnings/modcp-member-content.tsx | 3 + .../moderation/warnings/warning-list.tsx | 12 +- .../views/moderation/warnings/warnings-api.ts | 39 +- 28 files changed, 25819 insertions(+), 249 deletions(-) create mode 100644 apps/api/migrations/20261103135143_add_moderation_warnings/migration.sql create mode 100644 apps/api/migrations/20261103135143_add_moderation_warnings/snapshot.json create mode 100644 apps/web/content/docs/dev/moderation/warning-member-log.webp create mode 100644 packages/vitnode/src/api/lib/moderation/warnings/audit.ts create mode 100644 packages/vitnode/src/views/moderation/warnings/member-log.tsx diff --git a/apps/api/migrations/20261103135143_add_moderation_warnings/migration.sql b/apps/api/migrations/20261103135143_add_moderation_warnings/migration.sql new file mode 100644 index 0000000000..cf29b46171 --- /dev/null +++ b/apps/api/migrations/20261103135143_add_moderation_warnings/migration.sql @@ -0,0 +1,90 @@ +CREATE TABLE "core_moderation_sanctions" ( + "id" serial PRIMARY KEY, + "userId" integer NOT NULL, + "type" varchar(32) NOT NULL, + "source" varchar(32) NOT NULL, + "startsAt" timestamp DEFAULT now() NOT NULL, + "endsAt" timestamp, + "warningId" integer UNIQUE, + "thresholdId" integer, + "thresholdSnapshot" jsonb, + "createdById" integer, + "createdAt" timestamp DEFAULT now() NOT NULL, + "revokedAt" timestamp, + "revokedById" integer, + "revokeReason" text +); +--> statement-breakpoint +ALTER TABLE "core_moderation_sanctions" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint +CREATE TABLE "core_moderation_warning_reasons" ( + "id" serial PRIMARY KEY, + "points" integer DEFAULT 0 NOT NULL, + "expiresAfterHours" integer, + "enabled" boolean DEFAULT true NOT NULL, + "position" integer DEFAULT 0 NOT NULL, + "createdAt" timestamp DEFAULT now() NOT NULL, + "updatedAt" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "core_moderation_warning_reasons" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint +CREATE TABLE "core_moderation_warning_thresholds" ( + "id" serial PRIMARY KEY, + "points" integer NOT NULL, + "action" varchar(32) NOT NULL, + "durationHours" integer, + "enabled" boolean DEFAULT true NOT NULL, + "createdAt" timestamp DEFAULT now() NOT NULL, + "updatedAt" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "core_moderation_warning_thresholds" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint +CREATE TABLE "core_moderation_warnings" ( + "id" serial PRIMARY KEY, + "userId" integer NOT NULL, + "issuedById" integer, + "reasonId" integer, + "reasonSnapshot" jsonb, + "points" integer NOT NULL, + "defaultPoints" integer, + "overrideNote" text, + "message" text NOT NULL, + "staffNote" text, + "issuedAt" timestamp DEFAULT now() NOT NULL, + "expiresAt" timestamp, + "pointsBefore" integer DEFAULT 0 NOT NULL, + "pointsAfter" integer DEFAULT 0 NOT NULL, + "pluginId" varchar(100), + "targetType" varchar(100), + "targetId" varchar(64), + "scopeFamily" varchar(100), + "scopeId" varchar(64), + "contentSnapshot" jsonb, + "reportId" integer, + "idempotencyKey" varchar(64), + "revokedAt" timestamp, + "revokedById" integer, + "revokeReason" text, + CONSTRAINT "core_moderation_warnings_idempotency_key" UNIQUE("issuedById","idempotencyKey") +); +--> statement-breakpoint +ALTER TABLE "core_moderation_warnings" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint +CREATE INDEX "core_moderation_sanctions_user_idx" ON "core_moderation_sanctions" ("userId","revokedAt","endsAt");--> statement-breakpoint +CREATE INDEX "core_moderation_sanctions_created_at_id_idx" ON "core_moderation_sanctions" ("createdAt","id");--> statement-breakpoint +CREATE INDEX "core_moderation_warning_reasons_position_idx" ON "core_moderation_warning_reasons" ("position");--> statement-breakpoint +CREATE UNIQUE INDEX "core_moderation_warning_thresholds_enabled_points_idx" ON "core_moderation_warning_thresholds" ("points") WHERE "enabled" = true;--> statement-breakpoint +CREATE INDEX "core_moderation_warnings_user_idx" ON "core_moderation_warnings" ("userId","issuedAt");--> statement-breakpoint +CREATE INDEX "core_moderation_warnings_issued_at_id_idx" ON "core_moderation_warnings" ("issuedAt","id");--> statement-breakpoint +CREATE INDEX "core_moderation_warnings_issued_by_idx" ON "core_moderation_warnings" ("issuedById");--> statement-breakpoint +CREATE INDEX "core_moderation_warnings_reason_idx" ON "core_moderation_warnings" ("reasonId");--> statement-breakpoint +CREATE INDEX "core_moderation_warnings_target_idx" ON "core_moderation_warnings" ("pluginId","targetType","targetId");--> statement-breakpoint +CREATE INDEX "core_moderation_warnings_report_idx" ON "core_moderation_warnings" ("reportId");--> statement-breakpoint +ALTER TABLE "core_moderation_sanctions" ADD CONSTRAINT "core_moderation_sanctions_userId_core_users_id_fkey" FOREIGN KEY ("userId") REFERENCES "core_users"("id") ON DELETE CASCADE;--> statement-breakpoint +ALTER TABLE "core_moderation_sanctions" ADD CONSTRAINT "core_moderation_sanctions_rlFetAHphSkc_fkey" FOREIGN KEY ("warningId") REFERENCES "core_moderation_warnings"("id") ON DELETE SET NULL;--> statement-breakpoint +ALTER TABLE "core_moderation_sanctions" ADD CONSTRAINT "core_moderation_sanctions_ejX3lH3GN7oz_fkey" FOREIGN KEY ("thresholdId") REFERENCES "core_moderation_warning_thresholds"("id") ON DELETE SET NULL;--> statement-breakpoint +ALTER TABLE "core_moderation_sanctions" ADD CONSTRAINT "core_moderation_sanctions_createdById_core_users_id_fkey" FOREIGN KEY ("createdById") REFERENCES "core_users"("id") ON DELETE SET NULL;--> statement-breakpoint +ALTER TABLE "core_moderation_sanctions" ADD CONSTRAINT "core_moderation_sanctions_revokedById_core_users_id_fkey" FOREIGN KEY ("revokedById") REFERENCES "core_users"("id") ON DELETE SET NULL;--> statement-breakpoint +ALTER TABLE "core_moderation_warnings" ADD CONSTRAINT "core_moderation_warnings_userId_core_users_id_fkey" FOREIGN KEY ("userId") REFERENCES "core_users"("id") ON DELETE CASCADE;--> statement-breakpoint +ALTER TABLE "core_moderation_warnings" ADD CONSTRAINT "core_moderation_warnings_issuedById_core_users_id_fkey" FOREIGN KEY ("issuedById") REFERENCES "core_users"("id") ON DELETE SET NULL;--> statement-breakpoint +ALTER TABLE "core_moderation_warnings" ADD CONSTRAINT "core_moderation_warnings_J1JvecLI20Z2_fkey" FOREIGN KEY ("reasonId") REFERENCES "core_moderation_warning_reasons"("id") ON DELETE SET NULL;--> statement-breakpoint +ALTER TABLE "core_moderation_warnings" ADD CONSTRAINT "core_moderation_warnings_0vlCswFpT46n_fkey" FOREIGN KEY ("reportId") REFERENCES "core_moderation_reports"("id") ON DELETE SET NULL;--> statement-breakpoint +ALTER TABLE "core_moderation_warnings" ADD CONSTRAINT "core_moderation_warnings_revokedById_core_users_id_fkey" FOREIGN KEY ("revokedById") REFERENCES "core_users"("id") ON DELETE SET NULL; \ No newline at end of file diff --git a/apps/api/migrations/20261103135143_add_moderation_warnings/snapshot.json b/apps/api/migrations/20261103135143_add_moderation_warnings/snapshot.json new file mode 100644 index 0000000000..d7626bf241 --- /dev/null +++ b/apps/api/migrations/20261103135143_add_moderation_warnings/snapshot.json @@ -0,0 +1,24959 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "2cc93fc1-e03a-4dd4-876e-89f6875a47f8", + "prevIds": [ + "2d32a353-3dc9-4b47-8d67-0bca5da5a89e" + ], + "ddl": [ + { + "isRlsEnabled": true, + "name": "core_admin_permissions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_admin_sessions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_action_settings", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_budget_periods", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_calls", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_cost_adjustments", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_reservations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_role_permissions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_role_policies", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_runs", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_settings", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_translation_sources", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_user_overrides", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_audit_logs", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_audit_settings", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_access_rule_roles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_access_rules", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_containers", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_documents", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_drafts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_field_locks", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_submissions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_file_refs", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_revisions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_schedules", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_slug_history", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_cron", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_admin_dashboard", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_files", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_files_alt", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_files_alt_analysis", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_files_alt_state", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_languages", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_languages_words", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_logs", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_moderation_report_events", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_moderation_report_reasons", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_moderation_reports", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_moderators_permissions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_notification_deliveries", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_notification_events", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_notification_receipts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_notification_settings", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_notification_user_state", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_notifications", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_page_layouts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_passkey_challenges", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_passkeys", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_payments", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_payments_webhook_events", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_queue", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_roles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_search_index", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_search_popularity", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_secrets", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_sessions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_sessions_known_devices", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_update_lock", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_update_results", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_update_sources", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_sso_attempts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_sso_pending", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_confirm_emails", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_forgot_password", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_secondary_roles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_sso", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_sso_operations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_sso_profile_sources", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_moderation_sanctions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_moderation_warning_reasons", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_moderation_warning_thresholds", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_moderation_warnings", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_categories", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_categories_translations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_comments", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_posts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_posts_author_id", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_posts_category_id", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_posts_translations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "forum_comments", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "forum_topics", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_articles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_bulletins", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_bulletins_translations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_categories", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_comments", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_events", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_events_gallery", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_events_related_events", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_events_sessions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_events_tags", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_events_translations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_explainers", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_explainers_categories", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_explainers_faq", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_explainers_related_explainers", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_explainers_tags", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_explainers_translations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_letters", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_pages", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_stories", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_stories_gallery", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_tags", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_tags_related_tags", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_tags_translations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "payments_demo_purchases", + "entityType": "tables", + "schema": "public" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "roleId", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "protected", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "unrestricted", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "permissions", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "lastSeen", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deviceId", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actionKey", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "enabled", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "modelId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fallbackModelId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxInputCharacters", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxOutputTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "timeoutMs", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxRetries", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxSteps", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "dailyLimit", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "instructions", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scopeKey", + "entityType": "columns", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "type": "varchar(10)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "unit", + "entityType": "columns", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "periodStart", + "entityType": "columns", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "periodEnd", + "entityType": "columns", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "limitAmount", + "entityType": "columns", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'0'", + "generated": null, + "identity": null, + "name": "spentAmount", + "entityType": "columns", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'0'", + "generated": null, + "identity": null, + "name": "reservedAmount", + "entityType": "columns", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "unknownCount", + "entityType": "columns", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "runId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "attempt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "modelId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerModelId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerRequestId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "errorCode", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inputTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "outputTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cacheReadTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cacheWriteTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reasoningTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "images", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "costUsd", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "costSource", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "costReason", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pricingVersion", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pricingSnapshot", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "startedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "finishedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reconciledAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "callId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "runId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "previousCostUsd", + "entityType": "columns", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "previousSource", + "entityType": "columns", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "newCostUsd", + "entityType": "columns", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "newSource", + "entityType": "columns", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deltaUsd", + "entityType": "columns", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reason", + "entityType": "columns", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "runId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "budgetPeriodId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "amount", + "entityType": "columns", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "type": "varchar(10)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'active'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "settledAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "roleId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_role_permissions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "permission", + "entityType": "columns", + "schema": "public", + "table": "core_ai_role_permissions" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "granted", + "entityType": "columns", + "schema": "public", + "table": "core_ai_role_permissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "dailyLimit", + "entityType": "columns", + "schema": "public", + "table": "core_ai_role_permissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "roleId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_role_policies" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "monthlyPoints", + "entityType": "columns", + "schema": "public", + "table": "core_ai_role_policies" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "unlimited", + "entityType": "columns", + "schema": "public", + "table": "core_ai_role_policies" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_role_policies" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actionKey", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(10)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actorType", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "idempotencyKey", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "resourceType", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "resourceId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "errorCode", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "promptVersion", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pointsConversionVersion", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "modelId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerModelId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inputTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "outputTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cacheReadTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cacheWriteTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reasoningTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "costUsd", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "costSource", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chargedUsd", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chargedPoints", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'0'", + "generated": null, + "identity": null, + "name": "reservedUsd", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'0'", + "generated": null, + "identity": null, + "name": "reservedPoints", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "settlement", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "leaseExpiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accepted", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "acceptedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceFingerprint", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "startedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "finishedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "settledAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "enabled", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "monthlyBudgetUsd", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "systemMonthlyBudgetUsd", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'0'", + "generated": null, + "identity": null, + "name": "defaultMonthlyPoints", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "pointsConversionVersion", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "10", + "generated": null, + "identity": null, + "name": "userRequestsPerMinute", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "2", + "generated": null, + "identity": null, + "name": "userConcurrency", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "2", + "generated": null, + "identity": null, + "name": "systemConcurrency", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "365", + "generated": null, + "identity": null, + "name": "historyRetentionDays", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "altEnabled", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "10", + "generated": null, + "identity": null, + "name": "altBatchSize", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "altScanCursor", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "altScanStartedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "locale", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "field", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceLocale", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceFingerprint", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "targetFingerprint", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "varchar(10)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "origin", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updatedById", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_user_overrides" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "monthlyPoints", + "entityType": "columns", + "schema": "public", + "table": "core_ai_user_overrides" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "unlimited", + "entityType": "columns", + "schema": "public", + "table": "core_ai_user_overrides" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "blocked", + "entityType": "columns", + "schema": "public", + "table": "core_ai_user_overrides" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_user_overrides" + }, + { + "type": "bigserial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "area", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "varchar(150)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "action", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actorType", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actorId", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actorName", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "targetType", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "targetId", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "targetLabel", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "changes", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "revisionId", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scopeFamily", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scopeId", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "varchar(45)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "correlationId", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_audit_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "365", + "generated": null, + "identity": null, + "name": "retentionDays", + "entityType": "columns", + "schema": "public", + "table": "core_audit_settings" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_audit_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ruleId", + "entityType": "columns", + "schema": "public", + "table": "core_content_access_rule_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "roleId", + "entityType": "columns", + "schema": "public", + "table": "core_content_access_rule_roles" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_access_rules" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "family", + "entityType": "columns", + "schema": "public", + "table": "core_content_access_rules" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scopeId", + "entityType": "columns", + "schema": "public", + "table": "core_content_access_rules" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "action", + "entityType": "columns", + "schema": "public", + "table": "core_content_access_rules" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "mode", + "entityType": "columns", + "schema": "public", + "table": "core_content_access_rules" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_access_rules" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_access_rules" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "definitionId", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "parentId", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "policies", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_documents" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_content_documents" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_content_documents" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "field", + "entityType": "columns", + "schema": "public", + "table": "core_content_documents" + }, + { + "type": "varchar(35)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "core_content_documents" + }, + { + "type": "bytea", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "state", + "entityType": "columns", + "schema": "public", + "table": "core_content_documents" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "baseVersion", + "entityType": "columns", + "schema": "public", + "table": "core_content_documents" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_documents" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updatedBy", + "entityType": "columns", + "schema": "public", + "table": "core_content_documents" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_drafts" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_content_drafts" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_content_drafts" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "core_content_drafts" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "values", + "entityType": "columns", + "schema": "public", + "table": "core_content_drafts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "baseVersion", + "entityType": "columns", + "schema": "public", + "table": "core_content_drafts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_drafts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updatedBy", + "entityType": "columns", + "schema": "public", + "table": "core_content_drafts" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "field", + "entityType": "columns", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "acquiredAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "containerDefinitionId", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "containerId", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "values", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "baseValues", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "baseVersion", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "baseUpdatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "authorId", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reviewerId", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reason", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "revisionId", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "decidedAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "revisionId", + "entityType": "columns", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileId", + "entityType": "columns", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "operation", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "snapshot", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "changedFields", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'system'", + "generated": null, + "identity": null, + "name": "actorType", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actorUserId", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "restoredFromRevisionId", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "action", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scheduledFor", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "generation", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "createdBy", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completedAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "effectsError", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "varchar(512)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "path", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "retiredAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastRun", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "module", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nextRun", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "schedule", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "widgets", + "entityType": "columns", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(512)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folder", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "mimeType", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "size", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fingerprint", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'automatic'", + "generated": null, + "identity": null, + "name": "altPolicy", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'public'", + "generated": null, + "identity": null, + "name": "visibility", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileId", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageCode", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "text", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt" + }, + { + "type": "varchar(10)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "origin", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileFingerprint", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "runId", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updatedById", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt_analysis" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileId", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt_analysis" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileFingerprint", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt_analysis" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt_analysis" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "runId", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt_analysis" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt_analysis" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileId", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt_state" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt_state" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt_state" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt_state" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "code", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'UTC'", + "generated": null, + "identity": null, + "name": "timezone", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "protected", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "default", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "time24", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageCode", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginCode", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tableName", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "variable", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "varchar(10)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "varchar(45)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "varchar(10)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'GET'", + "generated": null, + "identity": null, + "name": "method", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'localhost'", + "generated": null, + "identity": null, + "name": "path", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userAgent", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "500", + "generated": null, + "identity": null, + "name": "statusCode", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_report_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reportId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_report_events" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_report_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actorId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_report_events" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "data", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_report_events" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_report_events" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_report_reasons" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "code", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_report_reasons" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "requiresDetails", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_report_reasons" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_report_reasons" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_report_reasons" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_report_reasons" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "targetType", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "targetId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reporterId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reason", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "snapshot", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "varchar(500)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "targetTitle", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'new'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "assigneeId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "resolution", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "resolutionNote", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "resolvedAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "resolvedById", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scopeFamily", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scopeId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "openKey", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "roleId", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "protected", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "unrestricted", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "permissions", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "bigserial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "mode", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "idempotencyKey", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "attempts", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "5", + "generated": null, + "identity": null, + "name": "maxAttempts", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "availableAt", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "periodStart", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "periodEnd", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "itemCount", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerMessageId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "varchar(500)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "skipReason", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sentAt", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "bigserial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "schemaVersion", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "idempotencyKey", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actorId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subjectType", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subjectId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "groupKey", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "data", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "recipientIds", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "allowSelf", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "recipientCursor", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "deliveredCount", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completedAt", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "notificationId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "seq", + "entityType": "columns", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "emailPending", + "entityType": "columns", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "emailDeliveryId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "core_notification_settings" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "core_notification_settings" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_notification_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_user_state" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "unreadCount", + "entityType": "columns", + "schema": "public", + "table": "core_notification_user_state" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "revision", + "entityType": "columns", + "schema": "public", + "table": "core_notification_user_state" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "preferences", + "entityType": "columns", + "schema": "public", + "table": "core_notification_user_state" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_notification_user_state" + }, + { + "type": "bigserial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "category", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "groupKey", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "groupBucket", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subjectType", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subjectId", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "latestEventId", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "eventCount", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "activitySeq", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "readSeq", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "readAt", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "archivedAt", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "lastActivityAt", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "varchar(120)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pageId", + "entityType": "columns", + "schema": "public", + "table": "core_page_layouts" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "zones", + "entityType": "columns", + "schema": "public", + "table": "core_page_layouts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_page_layouts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_page_layouts" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenHash", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ceremony", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "challenge", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "webauthnUserId", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "varchar(1024)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "credentialId", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publicKey", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "counter", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "webauthnUserId", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "transports", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deviceType", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "backedUp", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "varchar(36)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "aaguid", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastUsedAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reference", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerId", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerType", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "amount", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "varchar(3)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "currency", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerStatus", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "checkoutId", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "checkoutUrl", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "livemode", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "errorCode", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "syncedAt", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completedAt", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerId", + "entityType": "columns", + "schema": "public", + "table": "core_payments_webhook_events" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventId", + "entityType": "columns", + "schema": "public", + "table": "core_payments_webhook_events" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "core_payments_webhook_events" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "paymentId", + "entityType": "columns", + "schema": "public", + "table": "core_payments_webhook_events" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "receivedAt", + "entityType": "columns", + "schema": "public", + "table": "core_payments_webhook_events" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'default'", + "generated": null, + "identity": null, + "name": "queue", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "payload", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "priority", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "attempts", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "3", + "generated": null, + "identity": null, + "name": "maxAttempts", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "availableAt", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reservedAt", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completedAt", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "dedupeKey", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "protected", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "default", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "root", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "guest", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "color", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prefix", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "allowUploadFiles", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "totalMaxStorage", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxStorageForSubmit", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "allowUploadAvatar", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "2048", + "generated": null, + "identity": null, + "name": "maxAvatarSize", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "allowUploadCover", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "5120", + "generated": null, + "identity": null, + "name": "maxCoverSize", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "allowEditPersonalInfo", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "allowBrowseAnonymously", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "forceBrowseAnonymously", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "allowEditOwnContent", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "editOwnContentMinutes", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "allowDeleteOwnContent", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "allowHideOwnContent", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemType", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "languageCode", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "authorIds", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "tsvector", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": { + "as": "setweight(to_tsvector(CASE lower(split_part(\"core_search_index\".\"languageCode\", '-', 1)) WHEN 'da' THEN 'danish'::regconfig WHEN 'de' THEN 'german'::regconfig WHEN 'en' THEN 'english'::regconfig WHEN 'es' THEN 'spanish'::regconfig WHEN 'fi' THEN 'finnish'::regconfig WHEN 'fr' THEN 'french'::regconfig WHEN 'hu' THEN 'hungarian'::regconfig WHEN 'it' THEN 'italian'::regconfig WHEN 'nl' THEN 'dutch'::regconfig WHEN 'no' THEN 'norwegian'::regconfig WHEN 'pl' THEN 'polish'::regconfig WHEN 'pt' THEN 'portuguese'::regconfig WHEN 'ro' THEN 'romanian'::regconfig WHEN 'ru' THEN 'russian'::regconfig WHEN 'sv' THEN 'swedish'::regconfig WHEN 'tr' THEN 'turkish'::regconfig ELSE 'simple'::regconfig END, coalesce(\"core_search_index\".\"title\", '')), 'A') || setweight(to_tsvector(CASE lower(split_part(\"core_search_index\".\"languageCode\", '-', 1)) WHEN 'da' THEN 'danish'::regconfig WHEN 'de' THEN 'german'::regconfig WHEN 'en' THEN 'english'::regconfig WHEN 'es' THEN 'spanish'::regconfig WHEN 'fi' THEN 'finnish'::regconfig WHEN 'fr' THEN 'french'::regconfig WHEN 'hu' THEN 'hungarian'::regconfig WHEN 'it' THEN 'italian'::regconfig WHEN 'nl' THEN 'dutch'::regconfig WHEN 'no' THEN 'norwegian'::regconfig WHEN 'pl' THEN 'polish'::regconfig WHEN 'pt' THEN 'portuguese'::regconfig WHEN 'ro' THEN 'romanian'::regconfig WHEN 'ru' THEN 'russian'::regconfig WHEN 'sv' THEN 'swedish'::regconfig WHEN 'tr' THEN 'turkish'::regconfig ELSE 'simple'::regconfig END, coalesce(\"core_search_index\".\"content\", '')), 'B')", + "type": "stored" + }, + "identity": null, + "name": "search_vector", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "containerType", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "containerId", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "accessScopes", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "isPublic", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isHidden", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "imageId", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "indexedAt", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemType", + "entityType": "columns", + "schema": "public", + "table": "core_search_popularity" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_search_popularity" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "logScore", + "entityType": "columns", + "schema": "public", + "table": "core_search_popularity" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "views", + "entityType": "columns", + "schema": "public", + "table": "core_search_popularity" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "clicks", + "entityType": "columns", + "schema": "public", + "table": "core_search_popularity" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "comments", + "entityType": "columns", + "schema": "public", + "table": "core_search_popularity" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_search_popularity" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_secrets" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "core_secrets" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_secrets" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_sessions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "core_sessions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_sessions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deviceId", + "entityType": "columns", + "schema": "public", + "table": "core_sessions" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_sessions_known_devices" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publicId", + "entityType": "columns", + "schema": "public", + "table": "core_sessions_known_devices" + }, + { + "type": "varchar(40)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "core_sessions_known_devices" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userAgent", + "entityType": "columns", + "schema": "public", + "table": "core_sessions_known_devices" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "lastSeen", + "entityType": "columns", + "schema": "public", + "table": "core_sessions_known_devices" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_update_lock" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "owner", + "entityType": "columns", + "schema": "public", + "table": "core_update_lock" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_update_lock" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastManualAt", + "entityType": "columns", + "schema": "public", + "table": "core_update_lock" + }, + { + "type": "varchar(214)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "componentId", + "entityType": "columns", + "schema": "public", + "table": "core_update_results" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceUrl", + "entityType": "columns", + "schema": "public", + "table": "core_update_results" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "core_update_results" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "versions", + "entityType": "columns", + "schema": "public", + "table": "core_update_results" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hasUnknownVersion", + "entityType": "columns", + "schema": "public", + "table": "core_update_results" + }, + { + "type": "varchar(256)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "coreVersion", + "entityType": "columns", + "schema": "public", + "table": "core_update_results" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "result", + "entityType": "columns", + "schema": "public", + "table": "core_update_results" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastAttemptAt", + "entityType": "columns", + "schema": "public", + "table": "core_update_results" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastSuccessAt", + "entityType": "columns", + "schema": "public", + "table": "core_update_results" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "core_update_results" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "public", + "table": "core_update_sources" + }, + { + "type": "varchar(512)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "etag", + "entityType": "columns", + "schema": "public", + "table": "core_update_sources" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastModified", + "entityType": "columns", + "schema": "public", + "table": "core_update_sources" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "document", + "entityType": "columns", + "schema": "public", + "table": "core_update_sources" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastAttemptAt", + "entityType": "columns", + "schema": "public", + "table": "core_update_sources" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastSuccessAt", + "entityType": "columns", + "schema": "public", + "table": "core_update_sources" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "core_update_sources" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_sso_attempts" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "stateHash", + "entityType": "columns", + "schema": "public", + "table": "core_sso_attempts" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "bindingHash", + "entityType": "columns", + "schema": "public", + "table": "core_sso_attempts" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerId", + "entityType": "columns", + "schema": "public", + "table": "core_sso_attempts" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "codeVerifier", + "entityType": "columns", + "schema": "public", + "table": "core_sso_attempts" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nonce", + "entityType": "columns", + "schema": "public", + "table": "core_sso_attempts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_sso_attempts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_sso_attempts" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_sso_pending" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenHash", + "entityType": "columns", + "schema": "public", + "table": "core_sso_pending" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerId", + "entityType": "columns", + "schema": "public", + "table": "core_sso_pending" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerAccountId", + "entityType": "columns", + "schema": "public", + "table": "core_sso_pending" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerUsername", + "entityType": "columns", + "schema": "public", + "table": "core_sso_pending" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "profile", + "entityType": "columns", + "schema": "public", + "table": "core_sso_pending" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "core_sso_pending" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "confirmHash", + "entityType": "columns", + "schema": "public", + "table": "core_sso_pending" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "confirmExpiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_sso_pending" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "confirmSends", + "entityType": "columns", + "schema": "public", + "table": "core_sso_pending" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "confirmSentAt", + "entityType": "columns", + "schema": "public", + "table": "core_sso_pending" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_sso_pending" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_sso_pending" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nameCode", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "firstName", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastName", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "phone", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "headline", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "showRealName", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "password", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "newsletter", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(6)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avatarColor", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "emailVerified", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "roleId", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "birthday", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "timeZone", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(40)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'en'", + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avatarId", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "coverId", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastSeenAt", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hideOnlineStatus", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "type": "varchar(40)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "type": "varchar(40)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "roleId", + "entityType": "columns", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerAccountId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerEmail", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerUsername", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "syncOnSignIn", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avatarSourceUrl", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avatarSha256", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avatarFileId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenHash", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "intent", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "fields", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerAccountId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "preview", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "codeVerifier", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nonce", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "field", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "startsAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "endsAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "warningId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "thresholdId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "thresholdSnapshot", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "createdById", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "revokedAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "revokedById", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "revokeReason", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warning_reasons" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "points", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warning_reasons" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAfterHours", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warning_reasons" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "enabled", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warning_reasons" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warning_reasons" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warning_reasons" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warning_reasons" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warning_thresholds" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "points", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warning_thresholds" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "action", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warning_thresholds" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "durationHours", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warning_thresholds" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "enabled", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warning_thresholds" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warning_thresholds" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warning_thresholds" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "issuedById", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reasonId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reasonSnapshot", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "points", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "defaultPoints", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "overrideNote", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "message", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "staffNote", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "issuedAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "pointsBefore", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "pointsAfter", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "targetType", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "targetId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scopeFamily", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scopeId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentSnapshot", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reportId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "idempotencyKey", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "revokedAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "revokedById", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "revokeReason", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "blog_categories" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_categories" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_categories" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "color", + "entityType": "columns", + "schema": "public", + "table": "blog_categories" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "friendlyUrl", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenAt", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenBy", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "varchar(500)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenReason", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedBy", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "author", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "article", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "parent", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenBy", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "varchar(500)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenReason", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "coverImage", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "friendlyUrl", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "coverImageAlt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenAt", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenBy", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "varchar(500)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenReason", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedAt", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedBy", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "author", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "topic", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "parent", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenAt", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenBy", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "varchar(500)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenReason", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "author", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "forum", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "newsroom_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_articles" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "newsroom_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "newsroom_articles" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "newsroom_articles" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "newsroom_articles" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body", + "entityType": "columns", + "schema": "public", + "table": "newsroom_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "author", + "entityType": "columns", + "schema": "public", + "table": "newsroom_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "category", + "entityType": "columns", + "schema": "public", + "table": "newsroom_articles" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "featured", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins_translations" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins_translations" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins_translations" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins_translations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins_translations" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "newsroom_categories" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_categories" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_categories" + }, + { + "type": "varchar(80)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "newsroom_categories" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "newsroom_categories" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "newsroom_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_comments" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "newsroom_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenBy", + "entityType": "columns", + "schema": "public", + "table": "newsroom_comments" + }, + { + "type": "varchar(500)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenReason", + "entityType": "columns", + "schema": "public", + "table": "newsroom_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedBy", + "entityType": "columns", + "schema": "public", + "table": "newsroom_comments" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "newsroom_comments" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body", + "entityType": "columns", + "schema": "public", + "table": "newsroom_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "author", + "entityType": "columns", + "schema": "public", + "table": "newsroom_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "article", + "entityType": "columns", + "schema": "public", + "table": "newsroom_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "parent", + "entityType": "columns", + "schema": "public", + "table": "newsroom_comments" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": { + "type": "byDefault", + "name": "newsroom_events_id_seq", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": 1, + "cycle": false + }, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenBy", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events" + }, + { + "type": "varchar(500)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenReason", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "primaryTag", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "category", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_gallery" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_gallery" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_gallery" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_gallery" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_related_events" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_related_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_related_events" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_related_events" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_sessions" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_sessions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_sessions" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "startsAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_sessions" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_tags" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_tags" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_tags" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_tags" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_translations" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_translations" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_translations" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_translations" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_translations" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "syndicationIndexable", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "syndicationNoIndex", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "5", + "generated": null, + "identity": null, + "name": "syndicationPriority", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_categories" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_categories" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_categories" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_categories" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_faq" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_faq" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_faq" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_faq" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_faq" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "question", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_faq" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "answer", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_faq" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_related_explainers" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_related_explainers" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_related_explainers" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_related_explainers" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_tags" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_tags" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_tags" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_tags" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "seoTitle", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "seoDescription", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "newsroom_letters" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_letters" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_letters" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_letters" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "newsroom_letters" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "newsroom_letters" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_letters" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenBy", + "entityType": "columns", + "schema": "public", + "table": "newsroom_letters" + }, + { + "type": "varchar(500)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenReason", + "entityType": "columns", + "schema": "public", + "table": "newsroom_letters" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "newsroom_letters" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "newsroom_letters" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body", + "entityType": "columns", + "schema": "public", + "table": "newsroom_letters" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "author", + "entityType": "columns", + "schema": "public", + "table": "newsroom_letters" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "desk", + "entityType": "columns", + "schema": "public", + "table": "newsroom_letters" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "newsroom_pages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_pages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_pages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_pages" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "newsroom_pages" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "newsroom_pages" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "newsroom_pages" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "newsroom_pages" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "code", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "excerpt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "views", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "featured", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "noIndex", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "author", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "animation", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "category", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories_gallery" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories_gallery" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories_gallery" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories_gallery" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenBy", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags" + }, + { + "type": "varchar(500)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenReason", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "category", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags_related_tags" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags_related_tags" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags_related_tags" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags_related_tags" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags_translations" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags_translations" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags_translations" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags_translations" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "payments_demo_purchases" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "payments_demo_purchases" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "payments_demo_purchases" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "payments_demo_purchases" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "roleId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_permissions_role_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_permissions_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_permissions_updated_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_sessions_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "deviceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_sessions_device_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "expiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_sessions_expires_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "scopeKey", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "periodStart", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_budget_periods_scope_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "runId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_calls_run_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_calls" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "costSource", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "reconciledAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_calls_reconcile_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_calls" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "callId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_cost_adjustments_call_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "runId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "budgetPeriodId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_reservations_run_period_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_reservations_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_runs_user_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_runs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_runs_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_runs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_runs_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_runs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "actionKey", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_runs_action_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_runs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "leaseExpiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_runs_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_runs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "actorType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "idempotencyKey", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"idempotencyKey\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_runs_idempotency_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_runs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "locale", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "field", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_translation_sources_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_audit_logs_created_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_audit_logs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "actorId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_audit_logs_actor_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_audit_logs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_audit_logs_plugin_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_audit_logs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "action", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_audit_logs_action_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_audit_logs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "area", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_audit_logs_area_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_audit_logs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "targetType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "targetId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_audit_logs_target_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_audit_logs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "scopeFamily", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "scopeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_audit_logs_scope_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_audit_logs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "roleId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_access_rule_roles_role_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_access_rule_roles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "family", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "scopeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "action", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_access_rules_scope_action_key", + "entityType": "indexes", + "schema": "public", + "table": "core_content_access_rules" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "definitionId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_containers_definition_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "core_content_containers" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "definitionId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "parentId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_containers_definition_parent_position_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_containers" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "parentId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_containers_parent_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_containers" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "field", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "language", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_documents_doc_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_documents" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_documents_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_documents" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_documents_updated_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_documents" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "language", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_drafts_key_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_drafts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_drafts_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_drafts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_drafts_updated_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_drafts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "field", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "language", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_field_locks_key_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "expiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_field_locks_expires_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_field_locks_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"status\" = 'pending'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_submissions_pending_key", + "entityType": "indexes", + "schema": "public", + "table": "core_content_submissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "containerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_submissions_queue_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_submissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_submissions_item_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_submissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "authorId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_submissions_author_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_submissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "revisionId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "fileId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_file_refs_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "fileId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_file_refs_file_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "version", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"languageId\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_revisions_item_version_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_revisions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "version", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"languageId\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_revisions_translation_version_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_revisions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "version", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_revisions_language_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_revisions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_revisions_plugin_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_revisions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "actorUserId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_revisions_actor_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_revisions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "action", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "status = 'pending'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_schedules_active_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_schedules" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "scheduledFor", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_schedules_due_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_schedules" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_schedules_item_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_schedules" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_schedules_plugin_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_schedules" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_schedules_created_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_schedules" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"languageId\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_slug_history_shared_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"languageId\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_slug_history_locale_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_slug_history_item_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_slug_history_plugin_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "lastRun", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_cron_last_run_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_cron" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_dashboard_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_files_user_created_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_files_created_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "fileId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "languageCode", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_files_alt_file_language_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_files_alt" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "fileId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "fileFingerprint", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_files_alt_analysis_file_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_files_alt_analysis" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "status <> 'completed'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_files_alt_state_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_files_alt_state" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_languages_name_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_languages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageCode", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_languages_words_lang_code_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_languages_words" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tableName", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "pluginCode", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "variable", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_languages_words_lookup_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_languages_words" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_logs_created_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_logs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_logs_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_logs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "reportId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_report_events_report_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_report_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_report_reasons_position_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_report_reasons" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "targetType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "targetId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_reports_target_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_reports_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_reports_created_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "assigneeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_reports_assignee_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "reporterId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_reports_reporter_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "scopeFamily", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "scopeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_reports_scope_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "roleId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderators_permissions_role_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderators_permissions_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderators_permissions_updated_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "availableAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "status = 'pending'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_deliveries_claim_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_deliveries_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_deliveries_user_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "eventId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"eventId\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_deliveries_event_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "idempotencyKey", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_events_idempotency_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_events_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_events_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "notificationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "seq", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_receipts_notification_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_receipts_user_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"emailPending\" = true AND \"emailDeliveryId\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_receipts_email_pending_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "unreadCount", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_user_state_unread_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_user_state" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "groupKey", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "groupBucket", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notifications_group_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_notifications" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "lastActivityAt", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"archivedAt\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notifications_inbox_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notifications" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "lastActivityAt", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"archivedAt\" IS NULL AND \"readSeq\" < \"activitySeq\"", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notifications_unread_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notifications" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "subjectType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "subjectId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notifications_subject_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notifications" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "lastActivityAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notifications_last_activity_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notifications" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "latestEventId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notifications_latest_event_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notifications" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "expiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_passkey_challenges_expires_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_passkey_challenges_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_passkeys_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "webauthnUserId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_passkeys_webauthn_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "reference", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "status IN ('pending', 'processing')", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_payments_open_reference_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_payments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "providerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "checkoutId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"checkoutId\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_payments_checkout_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_payments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "reference", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_payments_reference_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_payments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_payments_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_payments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "status IN ('pending', 'processing')", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_payments_open_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_payments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "receivedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_payments_webhook_events_received_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_payments_webhook_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "availableAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_queue_status_available_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_queue" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_queue_created_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_queue" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "completedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"completedAt\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_queue_completed_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_queue" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "dedupeKey", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"dedupeKey\" IS NOT NULL AND status IN ('pending', 'processing')", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_queue_dedupe_active_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_queue" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_roles_updated_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_roles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "search_vector", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "core_search_index_search_vector_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "to_tsvector('simple'::regconfig, \"title\")", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "core_search_index_title_vector_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_search_index_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "authorIds", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "core_search_index_author_ids_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_search_index_item_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageCode", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_search_index_language_code_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "isPublic", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_search_index_is_public_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "imageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_search_index_image_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "containerType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "containerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_search_index_container_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "accessScopes", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "core_search_index_access_scopes_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "logScore", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_search_popularity_log_score_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_popularity" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_sessions_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "deviceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_sessions_device_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "expiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_sessions_expires_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "ipAddress", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_sessions_known_devices_ip_address_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_sessions_known_devices" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "expiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_sso_attempts_expires_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_sso_attempts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "expiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_sso_pending_expires_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_sso_pending" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "avatarId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_avatar_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "coverId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_cover_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_created_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "roleId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_role_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "roleId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_secondary_roles_role_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_sso_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_sso" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "providerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_sso_operations_user_provider_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "expiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_sso_operations_expires_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "revokedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "endsAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_sanctions_user_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_sanctions_created_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_warning_reasons_position_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_warning_reasons" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "points", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"enabled\" = true", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_warning_thresholds_enabled_points_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_warning_thresholds" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "issuedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_warnings_user_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "issuedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_warnings_issued_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "issuedById", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_warnings_issued_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "reasonId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_warnings_reason_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "targetType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "targetId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_warnings_target_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "reportId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_warnings_report_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_categories_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_categories_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_categories_translations_language_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "friendlyUrl", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_categories_translations_language_id_friendly_url_key", + "entityType": "indexes", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "article", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "parent", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_comments_article_parent_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_comments_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "blog_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "author", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_comments_author_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "article", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_comments_article_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "parent", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_comments_parent_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_comments_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_comments_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_comments_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "hiddenBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_comments_hidden_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "deletedBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_comments_deleted_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_status_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "coverImage", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_cover_image_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "hiddenBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_hidden_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_author_id_position_key", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_author_id_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_category_id_position_key", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_category_id_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_translations_language_id_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "friendlyUrl", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_translations_language_id_friendly_url_key", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "topic", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "parent", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_comments_topic_parent_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_comments_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "author", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_comments_author_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "topic", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_comments_topic_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "parent", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_comments_parent_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_comments_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_comments_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_comments_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "hiddenBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_comments_hidden_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "deletedBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_comments_deleted_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "forum", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_topics_forum_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_topics" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_topics_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "forum_topics" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "author", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_topics_author_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_topics" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_topics_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_topics" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_topics_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_topics" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_topics_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_topics" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "hiddenBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_topics_hidden_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_topics" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_articles_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "author", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_articles_author_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "category", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_articles_category_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_articles_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_articles_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_articles_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_bulletins_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_bulletins" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_bulletins_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_bulletins" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_bulletins_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_bulletins" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_bulletins_translations_language_id_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_bulletins_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_bulletins_translations_language_id_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_bulletins_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_categories_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_categories_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_categories_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_comments_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "author", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_comments_author_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "article", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_comments_article_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "parent", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_comments_parent_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_comments_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_comments_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_comments_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "hiddenBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_comments_hidden_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "deletedBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_comments_deleted_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "primaryTag", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_events_primary_tag_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "category", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_events_category_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_events_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_events_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_events_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "hiddenBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_events_hidden_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_events_gallery_position_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_events_gallery" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_events_gallery_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_events_gallery" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_events_related_events_position_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_events_related_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_events_related_events_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_events_related_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_events_sessions_position_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_events_sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_events_tags_position_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_events_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_events_tags_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_events_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_events_translations_language_id_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_events_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_events_translations_language_id_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_events_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "syndicationPriority", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_explainers_syndication_priority_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_explainers" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_explainers_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_explainers" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_explainers_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_explainers" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_explainers_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_explainers" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_explainers_categories_position_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_explainers_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_explainers_categories_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_explainers_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_explainers_faq_position_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_explainers_faq" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_explainers_related_explainers_position_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_explainers_related_explainers" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_explainers_related_explainers_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_explainers_related_explainers" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_explainers_tags_position_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_explainers_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_explainers_tags_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_explainers_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_explainers_translations_language_id_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_explainers_translations_language_id_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_letters_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_letters" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "author", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_letters_author_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_letters" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "desk", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_letters_desk_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_letters" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_letters_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_letters" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_letters_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_letters" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_letters_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_letters" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "hiddenBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_letters_hidden_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_letters" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_pages_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_pages_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_pages_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_pages_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_stories_status_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_stories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_stories_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_stories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "code", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_stories_code_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_stories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "author", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_stories_author_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_stories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "animation", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_stories_animation_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_stories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "category", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_stories_category_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_stories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_stories_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_stories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_stories_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_stories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_stories_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_stories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_stories_gallery_position_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_stories_gallery" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_stories_gallery_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_stories_gallery" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "category", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_tags_category_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_tags_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_tags_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_tags_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "hiddenBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_tags_hidden_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_tags_related_tags_position_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_tags_related_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_tags_related_tags_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_tags_related_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_tags_translations_language_id_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_tags_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_tags_translations_language_id_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_tags_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "payments_demo_purchases_user_item_unique", + "entityType": "indexes", + "schema": "public", + "table": "payments_demo_purchases" + }, + { + "nameExplicit": false, + "columns": [ + "roleId" + ], + "schemaTo": "public", + "tableTo": "core_roles", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_admin_permissions_roleId_core_roles_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_admin_permissions_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_admin_sessions_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "nameExplicit": false, + "columns": [ + "deviceId" + ], + "schemaTo": "public", + "tableTo": "core_sessions_known_devices", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_admin_sessions_deviceId_core_sessions_known_devices_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "nameExplicit": false, + "columns": [ + "runId" + ], + "schemaTo": "public", + "tableTo": "core_ai_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_ai_calls_runId_core_ai_runs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_ai_calls" + }, + { + "nameExplicit": false, + "columns": [ + "callId" + ], + "schemaTo": "public", + "tableTo": "core_ai_calls", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_ai_cost_adjustments_callId_core_ai_calls_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "nameExplicit": false, + "columns": [ + "runId" + ], + "schemaTo": "public", + "tableTo": "core_ai_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_ai_cost_adjustments_runId_core_ai_runs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "nameExplicit": false, + "columns": [ + "runId" + ], + "schemaTo": "public", + "tableTo": "core_ai_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_ai_reservations_runId_core_ai_runs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "nameExplicit": false, + "columns": [ + "budgetPeriodId" + ], + "schemaTo": "public", + "tableTo": "core_ai_budget_periods", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_ai_reservations_FClYpPXZwvsm_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "nameExplicit": false, + "columns": [ + "roleId" + ], + "schemaTo": "public", + "tableTo": "core_roles", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_ai_role_permissions_roleId_core_roles_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_ai_role_permissions" + }, + { + "nameExplicit": false, + "columns": [ + "roleId" + ], + "schemaTo": "public", + "tableTo": "core_roles", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_ai_role_policies_roleId_core_roles_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_ai_role_policies" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_ai_runs_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_ai_runs" + }, + { + "nameExplicit": false, + "columns": [ + "updatedById" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_ai_translation_sources_updatedById_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_ai_user_overrides_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_ai_user_overrides" + }, + { + "nameExplicit": false, + "columns": [ + "actorId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "core_audit_logs_actorId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_audit_logs" + }, + { + "nameExplicit": false, + "columns": [ + "ruleId" + ], + "schemaTo": "public", + "tableTo": "core_content_access_rules", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_content_access_rule_roles_AM3iX4qZcV8y_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_content_access_rule_roles" + }, + { + "nameExplicit": false, + "columns": [ + "roleId" + ], + "schemaTo": "public", + "tableTo": "core_roles", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_content_access_rule_roles_roleId_core_roles_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_content_access_rule_roles" + }, + { + "nameExplicit": false, + "columns": [ + "parentId" + ], + "schemaTo": "public", + "tableTo": "core_content_containers", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "RESTRICT", + "name": "core_content_containers_q6QIucJ4HJ7B_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_content_containers" + }, + { + "nameExplicit": false, + "columns": [ + "updatedBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "core_content_documents_updatedBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_content_documents" + }, + { + "nameExplicit": false, + "columns": [ + "updatedBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "core_content_drafts_updatedBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_content_drafts" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "core_content_field_locks_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "nameExplicit": false, + "columns": [ + "authorId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_content_submissions_authorId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_content_submissions" + }, + { + "nameExplicit": false, + "columns": [ + "reviewerId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_content_submissions_reviewerId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_content_submissions" + }, + { + "nameExplicit": false, + "columns": [ + "revisionId" + ], + "schemaTo": "public", + "tableTo": "core_content_revisions", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "core_content_file_refs_5get6SfhBPHr_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "nameExplicit": false, + "columns": [ + "fileId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "core_content_file_refs_fileId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "nameExplicit": false, + "columns": [ + "actorUserId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "core_content_revisions_actorUserId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_content_revisions" + }, + { + "nameExplicit": false, + "columns": [ + "createdBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "core_content_schedules_createdBy_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_content_schedules" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_admin_dashboard_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_files_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_files" + }, + { + "nameExplicit": false, + "columns": [ + "fileId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_files_alt_fileId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_files_alt" + }, + { + "nameExplicit": false, + "columns": [ + "languageCode" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "code" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_files_alt_languageCode_core_languages_code_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_files_alt" + }, + { + "nameExplicit": false, + "columns": [ + "runId" + ], + "schemaTo": "public", + "tableTo": "core_ai_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_files_alt_runId_core_ai_runs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_files_alt" + }, + { + "nameExplicit": false, + "columns": [ + "updatedById" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_files_alt_updatedById_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_files_alt" + }, + { + "nameExplicit": false, + "columns": [ + "fileId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_files_alt_analysis_fileId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_files_alt_analysis" + }, + { + "nameExplicit": false, + "columns": [ + "runId" + ], + "schemaTo": "public", + "tableTo": "core_ai_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_files_alt_analysis_runId_core_ai_runs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_files_alt_analysis" + }, + { + "nameExplicit": false, + "columns": [ + "fileId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_files_alt_state_fileId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_files_alt_state" + }, + { + "nameExplicit": false, + "columns": [ + "languageCode" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "code" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_languages_words_languageCode_core_languages_code_fk", + "entityType": "fks", + "schema": "public", + "table": "core_languages_words" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "core_logs_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_logs" + }, + { + "nameExplicit": false, + "columns": [ + "reportId" + ], + "schemaTo": "public", + "tableTo": "core_moderation_reports", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_moderation_report_events_4CqHOxfheaUm_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_report_events" + }, + { + "nameExplicit": false, + "columns": [ + "actorId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_moderation_report_events_actorId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_report_events" + }, + { + "nameExplicit": false, + "columns": [ + "reporterId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_moderation_reports_reporterId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "nameExplicit": false, + "columns": [ + "assigneeId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_moderation_reports_assigneeId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "nameExplicit": false, + "columns": [ + "resolvedById" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_moderation_reports_resolvedById_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "nameExplicit": false, + "columns": [ + "roleId" + ], + "schemaTo": "public", + "tableTo": "core_roles", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_moderators_permissions_roleId_core_roles_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_moderators_permissions_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_notification_deliveries_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "nameExplicit": false, + "columns": [ + "eventId" + ], + "schemaTo": "public", + "tableTo": "core_notification_events", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_notification_deliveries_vcf37BytIa0M_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "nameExplicit": false, + "columns": [ + "actorId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_notification_events_actorId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_notification_events" + }, + { + "nameExplicit": false, + "columns": [ + "eventId" + ], + "schemaTo": "public", + "tableTo": "core_notification_events", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_notification_receipts_UQ7ZEs0I6ijx_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_notification_receipts_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "nameExplicit": false, + "columns": [ + "notificationId" + ], + "schemaTo": "public", + "tableTo": "core_notifications", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_notification_receipts_Vxye2Cs6MgcL_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_notification_user_state_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_notification_user_state" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_notifications_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_notifications" + }, + { + "nameExplicit": false, + "columns": [ + "latestEventId" + ], + "schemaTo": "public", + "tableTo": "core_notification_events", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "RESTRICT", + "name": "core_notifications_I8QoGbTal5Wt_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_notifications" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_passkey_challenges_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_passkeys_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_payments_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_payments" + }, + { + "nameExplicit": false, + "columns": [ + "paymentId" + ], + "schemaTo": "public", + "tableTo": "core_payments", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_payments_webhook_events_paymentId_core_payments_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_payments_webhook_events" + }, + { + "nameExplicit": false, + "columns": [ + "imageId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_search_index_imageId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_sessions_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_sessions" + }, + { + "nameExplicit": false, + "columns": [ + "deviceId" + ], + "schemaTo": "public", + "tableTo": "core_sessions_known_devices", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_sessions_deviceId_core_sessions_known_devices_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_sessions" + }, + { + "nameExplicit": false, + "columns": [ + "roleId" + ], + "schemaTo": "public", + "tableTo": "core_roles", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "core_users_roleId_core_roles_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": false, + "columns": [ + "language" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "code" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET DEFAULT", + "name": "core_users_language_core_languages_code_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": false, + "columns": [ + "avatarId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_users_avatarId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": false, + "columns": [ + "coverId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_users_coverId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_confirm_emails_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_forgot_password_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_secondary_roles_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "nameExplicit": false, + "columns": [ + "roleId" + ], + "schemaTo": "public", + "tableTo": "core_roles", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_secondary_roles_roleId_core_roles_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_sso_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users_sso" + }, + { + "nameExplicit": false, + "columns": [ + "avatarFileId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_users_sso_avatarFileId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users_sso" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_sso_operations_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_sso_profile_sources_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "nameExplicit": true, + "columns": [ + "userId", + "providerId" + ], + "schemaTo": "public", + "tableTo": "core_users_sso", + "columnsTo": [ + "userId", + "providerId" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_sso_profile_sources_connection_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_moderation_sanctions_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "nameExplicit": false, + "columns": [ + "warningId" + ], + "schemaTo": "public", + "tableTo": "core_moderation_warnings", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_moderation_sanctions_rlFetAHphSkc_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "nameExplicit": false, + "columns": [ + "thresholdId" + ], + "schemaTo": "public", + "tableTo": "core_moderation_warning_thresholds", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_moderation_sanctions_ejX3lH3GN7oz_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "nameExplicit": false, + "columns": [ + "createdById" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_moderation_sanctions_createdById_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "nameExplicit": false, + "columns": [ + "revokedById" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_moderation_sanctions_revokedById_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_moderation_warnings_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "nameExplicit": false, + "columns": [ + "issuedById" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_moderation_warnings_issuedById_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "nameExplicit": false, + "columns": [ + "reasonId" + ], + "schemaTo": "public", + "tableTo": "core_moderation_warning_reasons", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_moderation_warnings_J1JvecLI20Z2_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "nameExplicit": false, + "columns": [ + "reportId" + ], + "schemaTo": "public", + "tableTo": "core_moderation_reports", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_moderation_warnings_0vlCswFpT46n_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "nameExplicit": false, + "columns": [ + "revokedById" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_moderation_warnings_revokedById_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "blog_categories", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "blog_categories_translations_itemId_blog_categories_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "nameExplicit": false, + "columns": [ + "languageId" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "blog_categories_translations_languageId_core_languages_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "nameExplicit": false, + "columns": [ + "hiddenBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "blog_comments_hiddenBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "blog_comments" + }, + { + "nameExplicit": false, + "columns": [ + "deletedBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "blog_comments_deletedBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "blog_comments" + }, + { + "nameExplicit": false, + "columns": [ + "author" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "blog_comments_author_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "blog_comments" + }, + { + "nameExplicit": false, + "columns": [ + "article" + ], + "schemaTo": "public", + "tableTo": "blog_posts", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "blog_comments_article_blog_posts_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "blog_comments" + }, + { + "nameExplicit": false, + "columns": [ + "parent" + ], + "schemaTo": "public", + "tableTo": "blog_comments", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "blog_comments_parent_blog_comments_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "blog_comments" + }, + { + "nameExplicit": false, + "columns": [ + "hiddenBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "blog_posts_hiddenBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": false, + "columns": [ + "coverImage" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "blog_posts_coverImage_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "blog_posts", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "blog_posts_author_id_itemId_blog_posts_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "blog_posts_author_id_relatedItemId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "blog_posts", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "blog_posts_category_id_itemId_blog_posts_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "blog_categories", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "blog_posts_category_id_relatedItemId_blog_categories_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "blog_posts", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "blog_posts_translations_itemId_blog_posts_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "nameExplicit": false, + "columns": [ + "languageId" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "blog_posts_translations_languageId_core_languages_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "nameExplicit": false, + "columns": [ + "hiddenBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "forum_replies_hiddenBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": false, + "columns": [ + "deletedBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "forum_comments_deletedBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": false, + "columns": [ + "author" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "forum_replies_author_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": false, + "columns": [ + "topic" + ], + "schemaTo": "public", + "tableTo": "forum_topics", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "forum_replies_topic_forum_topics_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": false, + "columns": [ + "parent" + ], + "schemaTo": "public", + "tableTo": "forum_comments", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "forum_replies_parent_forum_replies_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": false, + "columns": [ + "hiddenBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "forum_topics_hiddenBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "forum_topics" + }, + { + "nameExplicit": false, + "columns": [ + "author" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "forum_topics_author_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "forum_topics" + }, + { + "nameExplicit": false, + "columns": [ + "forum" + ], + "schemaTo": "public", + "tableTo": "core_content_containers", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "forum_topics_forum_core_content_containers_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "forum_topics" + }, + { + "nameExplicit": false, + "columns": [ + "author" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "newsroom_articles_author_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_articles" + }, + { + "nameExplicit": false, + "columns": [ + "category" + ], + "schemaTo": "public", + "tableTo": "newsroom_categories", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "newsroom_articles_category_newsroom_categories_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_articles" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_bulletins", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_bulletins_translations_kgMZwrpzwwU7_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_bulletins_translations" + }, + { + "nameExplicit": false, + "columns": [ + "languageId" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "newsroom_bulletins_translations_rkkiMJQiOrEo_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_bulletins_translations" + }, + { + "nameExplicit": false, + "columns": [ + "hiddenBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "newsroom_comments_hiddenBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_comments" + }, + { + "nameExplicit": false, + "columns": [ + "deletedBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "newsroom_comments_deletedBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_comments" + }, + { + "nameExplicit": false, + "columns": [ + "author" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "newsroom_comments_author_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_comments" + }, + { + "nameExplicit": false, + "columns": [ + "article" + ], + "schemaTo": "public", + "tableTo": "newsroom_articles", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_comments_article_newsroom_articles_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_comments" + }, + { + "nameExplicit": false, + "columns": [ + "parent" + ], + "schemaTo": "public", + "tableTo": "newsroom_comments", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "newsroom_comments_parent_newsroom_comments_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_comments" + }, + { + "nameExplicit": false, + "columns": [ + "hiddenBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "newsroom_events_hiddenBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_events" + }, + { + "nameExplicit": false, + "columns": [ + "primaryTag" + ], + "schemaTo": "public", + "tableTo": "newsroom_tags", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "newsroom_events_primaryTag_newsroom_tags_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_events" + }, + { + "nameExplicit": false, + "columns": [ + "category" + ], + "schemaTo": "public", + "tableTo": "newsroom_categories", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "newsroom_events_category_newsroom_categories_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_events" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_events", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_events_gallery_itemId_newsroom_events_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_events_gallery" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "newsroom_events_gallery_relatedItemId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_events_gallery" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_events", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_events_related_events_itemId_newsroom_events_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_events_related_events" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_events", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_events_related_events_0xHNSLM3JAJj_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_events_related_events" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_events", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_events_sessions_itemId_newsroom_events_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_events_sessions" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_events", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_events_tags_itemId_newsroom_events_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_events_tags" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_tags", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "newsroom_events_tags_relatedItemId_newsroom_tags_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_events_tags" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_events", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_events_translations_itemId_newsroom_events_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_events_translations" + }, + { + "nameExplicit": false, + "columns": [ + "languageId" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "newsroom_events_translations_languageId_core_languages_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_events_translations" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_explainers", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_explainers_categories_9dOmLv3k6xDX_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_explainers_categories" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_categories", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "newsroom_explainers_categories_mnxKvproEUm5_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_explainers_categories" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_explainers", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_explainers_faq_itemId_newsroom_explainers_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_explainers_faq" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_explainers", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_explainers_related_explainers_TFvIU3TogSGy_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_explainers_related_explainers" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_explainers", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_explainers_related_explainers_74JhaXF5PsDV_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_explainers_related_explainers" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_explainers", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_explainers_tags_itemId_newsroom_explainers_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_explainers_tags" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_tags", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "newsroom_explainers_tags_relatedItemId_newsroom_tags_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_explainers_tags" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_explainers", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_explainers_translations_hcPB0jdTm8MV_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "nameExplicit": false, + "columns": [ + "languageId" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "newsroom_explainers_translations_qqggtF6jhcsZ_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "nameExplicit": false, + "columns": [ + "hiddenBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "newsroom_letters_hiddenBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_letters" + }, + { + "nameExplicit": false, + "columns": [ + "author" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "newsroom_letters_author_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_letters" + }, + { + "nameExplicit": false, + "columns": [ + "desk" + ], + "schemaTo": "public", + "tableTo": "core_content_containers", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "newsroom_letters_desk_core_content_containers_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_letters" + }, + { + "nameExplicit": false, + "columns": [ + "author" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "newsroom_stories_author_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_stories" + }, + { + "nameExplicit": false, + "columns": [ + "animation" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "newsroom_stories_animation_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_stories" + }, + { + "nameExplicit": false, + "columns": [ + "category" + ], + "schemaTo": "public", + "tableTo": "newsroom_categories", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "newsroom_stories_category_newsroom_categories_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_stories" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_stories", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_stories_gallery_itemId_newsroom_stories_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_stories_gallery" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "newsroom_stories_gallery_relatedItemId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_stories_gallery" + }, + { + "nameExplicit": false, + "columns": [ + "hiddenBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "newsroom_tags_hiddenBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_tags" + }, + { + "nameExplicit": false, + "columns": [ + "category" + ], + "schemaTo": "public", + "tableTo": "newsroom_categories", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "newsroom_tags_category_newsroom_categories_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_tags" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_tags", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_tags_related_tags_itemId_newsroom_tags_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_tags_related_tags" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_tags", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_tags_related_tags_relatedItemId_newsroom_tags_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_tags_related_tags" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_tags", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_tags_translations_itemId_newsroom_tags_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_tags_translations" + }, + { + "nameExplicit": false, + "columns": [ + "languageId" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "newsroom_tags_translations_languageId_core_languages_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_tags_translations" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "payments_demo_purchases_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "payments_demo_purchases" + }, + { + "columns": [ + "roleId", + "permission" + ], + "nameExplicit": false, + "name": "core_ai_role_permissions_pkey", + "entityType": "pks", + "schema": "public", + "table": "core_ai_role_permissions" + }, + { + "columns": [ + "ruleId", + "roleId" + ], + "nameExplicit": false, + "name": "core_content_access_rule_roles_pkey", + "entityType": "pks", + "schema": "public", + "table": "core_content_access_rule_roles" + }, + { + "columns": [ + "eventId", + "userId" + ], + "nameExplicit": false, + "name": "core_notification_receipts_pkey", + "entityType": "pks", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "columns": [ + "providerId", + "eventId" + ], + "nameExplicit": false, + "name": "core_payments_webhook_events_pkey", + "entityType": "pks", + "schema": "public", + "table": "core_payments_webhook_events" + }, + { + "columns": [ + "itemType", + "itemId" + ], + "nameExplicit": false, + "name": "core_search_popularity_pkey", + "entityType": "pks", + "schema": "public", + "table": "core_search_popularity" + }, + { + "columns": [ + "userId", + "roleId" + ], + "nameExplicit": false, + "name": "core_users_secondary_roles_userId_roleId_pk", + "entityType": "pks", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "columns": [ + "userId", + "field" + ], + "nameExplicit": false, + "name": "core_users_sso_profile_sources_pkey", + "entityType": "pks", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "columns": [ + "itemId", + "languageId" + ], + "nameExplicit": true, + "name": "blog_categories_translations_item_id_language_id_pk", + "entityType": "pks", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "blog_posts_author_id_pk", + "entityType": "pks", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "blog_posts_category_id_pk", + "entityType": "pks", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "columns": [ + "itemId", + "languageId" + ], + "nameExplicit": true, + "name": "blog_posts_translations_item_id_language_id_pk", + "entityType": "pks", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "columns": [ + "itemId", + "languageId" + ], + "nameExplicit": true, + "name": "newsroom_bulletins_translations_item_id_language_id_pk", + "entityType": "pks", + "schema": "public", + "table": "newsroom_bulletins_translations" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "newsroom_events_gallery_pk", + "entityType": "pks", + "schema": "public", + "table": "newsroom_events_gallery" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "newsroom_events_related_events_pk", + "entityType": "pks", + "schema": "public", + "table": "newsroom_events_related_events" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "newsroom_events_tags_pk", + "entityType": "pks", + "schema": "public", + "table": "newsroom_events_tags" + }, + { + "columns": [ + "itemId", + "languageId" + ], + "nameExplicit": true, + "name": "newsroom_events_translations_item_id_language_id_pk", + "entityType": "pks", + "schema": "public", + "table": "newsroom_events_translations" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "newsroom_explainers_categories_pk", + "entityType": "pks", + "schema": "public", + "table": "newsroom_explainers_categories" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "newsroom_explainers_related_explainers_pk", + "entityType": "pks", + "schema": "public", + "table": "newsroom_explainers_related_explainers" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "newsroom_explainers_tags_pk", + "entityType": "pks", + "schema": "public", + "table": "newsroom_explainers_tags" + }, + { + "columns": [ + "itemId", + "languageId" + ], + "nameExplicit": true, + "name": "newsroom_explainers_translations_item_id_language_id_pk", + "entityType": "pks", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "newsroom_stories_gallery_pk", + "entityType": "pks", + "schema": "public", + "table": "newsroom_stories_gallery" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "newsroom_tags_related_tags_pk", + "entityType": "pks", + "schema": "public", + "table": "newsroom_tags_related_tags" + }, + { + "columns": [ + "itemId", + "languageId" + ], + "nameExplicit": true, + "name": "newsroom_tags_translations_item_id_language_id_pk", + "entityType": "pks", + "schema": "public", + "table": "newsroom_tags_translations" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_admin_permissions_pkey", + "schema": "public", + "table": "core_admin_permissions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_admin_sessions_pkey", + "schema": "public", + "table": "core_admin_sessions", + "entityType": "pks" + }, + { + "columns": [ + "actionKey" + ], + "nameExplicit": false, + "name": "core_ai_action_settings_pkey", + "schema": "public", + "table": "core_ai_action_settings", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_ai_budget_periods_pkey", + "schema": "public", + "table": "core_ai_budget_periods", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_ai_calls_pkey", + "schema": "public", + "table": "core_ai_calls", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_ai_cost_adjustments_pkey", + "schema": "public", + "table": "core_ai_cost_adjustments", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_ai_reservations_pkey", + "schema": "public", + "table": "core_ai_reservations", + "entityType": "pks" + }, + { + "columns": [ + "roleId" + ], + "nameExplicit": false, + "name": "core_ai_role_policies_pkey", + "schema": "public", + "table": "core_ai_role_policies", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_ai_runs_pkey", + "schema": "public", + "table": "core_ai_runs", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_ai_settings_pkey", + "schema": "public", + "table": "core_ai_settings", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_ai_translation_sources_pkey", + "schema": "public", + "table": "core_ai_translation_sources", + "entityType": "pks" + }, + { + "columns": [ + "userId" + ], + "nameExplicit": false, + "name": "core_ai_user_overrides_pkey", + "schema": "public", + "table": "core_ai_user_overrides", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_audit_logs_pkey", + "schema": "public", + "table": "core_audit_logs", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_audit_settings_pkey", + "schema": "public", + "table": "core_audit_settings", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_access_rules_pkey", + "schema": "public", + "table": "core_content_access_rules", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_containers_pkey", + "schema": "public", + "table": "core_content_containers", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_documents_pkey", + "schema": "public", + "table": "core_content_documents", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_drafts_pkey", + "schema": "public", + "table": "core_content_drafts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_field_locks_pkey", + "schema": "public", + "table": "core_content_field_locks", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_submissions_pkey", + "schema": "public", + "table": "core_content_submissions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_file_refs_pkey", + "schema": "public", + "table": "core_content_file_refs", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_revisions_pkey", + "schema": "public", + "table": "core_content_revisions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_schedules_pkey", + "schema": "public", + "table": "core_content_schedules", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_slug_history_pkey", + "schema": "public", + "table": "core_content_slug_history", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_cron_pkey", + "schema": "public", + "table": "core_cron", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_admin_dashboard_pkey", + "schema": "public", + "table": "core_admin_dashboard", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_files_pkey", + "schema": "public", + "table": "core_files", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_files_alt_pkey", + "schema": "public", + "table": "core_files_alt", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_files_alt_analysis_pkey", + "schema": "public", + "table": "core_files_alt_analysis", + "entityType": "pks" + }, + { + "columns": [ + "fileId" + ], + "nameExplicit": false, + "name": "core_files_alt_state_pkey", + "schema": "public", + "table": "core_files_alt_state", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_languages_pkey", + "schema": "public", + "table": "core_languages", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_languages_words_pkey", + "schema": "public", + "table": "core_languages_words", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_logs_pkey", + "schema": "public", + "table": "core_logs", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_moderation_report_events_pkey", + "schema": "public", + "table": "core_moderation_report_events", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_moderation_report_reasons_pkey", + "schema": "public", + "table": "core_moderation_report_reasons", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_moderation_reports_pkey", + "schema": "public", + "table": "core_moderation_reports", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_moderators_permissions_pkey", + "schema": "public", + "table": "core_moderators_permissions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_notification_deliveries_pkey", + "schema": "public", + "table": "core_notification_deliveries", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_notification_events_pkey", + "schema": "public", + "table": "core_notification_events", + "entityType": "pks" + }, + { + "columns": [ + "key" + ], + "nameExplicit": false, + "name": "core_notification_settings_pkey", + "schema": "public", + "table": "core_notification_settings", + "entityType": "pks" + }, + { + "columns": [ + "userId" + ], + "nameExplicit": false, + "name": "core_notification_user_state_pkey", + "schema": "public", + "table": "core_notification_user_state", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_notifications_pkey", + "schema": "public", + "table": "core_notifications", + "entityType": "pks" + }, + { + "columns": [ + "pageId" + ], + "nameExplicit": false, + "name": "core_page_layouts_pkey", + "schema": "public", + "table": "core_page_layouts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_users_passkey_challenges_pkey", + "schema": "public", + "table": "core_users_passkey_challenges", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_users_passkeys_pkey", + "schema": "public", + "table": "core_users_passkeys", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_payments_pkey", + "schema": "public", + "table": "core_payments", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_queue_pkey", + "schema": "public", + "table": "core_queue", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_roles_pkey", + "schema": "public", + "table": "core_roles", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_search_index_pkey", + "schema": "public", + "table": "core_search_index", + "entityType": "pks" + }, + { + "columns": [ + "name" + ], + "nameExplicit": false, + "name": "core_secrets_pkey", + "schema": "public", + "table": "core_secrets", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_sessions_pkey", + "schema": "public", + "table": "core_sessions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_sessions_known_devices_pkey", + "schema": "public", + "table": "core_sessions_known_devices", + "entityType": "pks" + }, + { + "columns": [ + "name" + ], + "nameExplicit": false, + "name": "core_update_lock_pkey", + "schema": "public", + "table": "core_update_lock", + "entityType": "pks" + }, + { + "columns": [ + "componentId" + ], + "nameExplicit": false, + "name": "core_update_results_pkey", + "schema": "public", + "table": "core_update_results", + "entityType": "pks" + }, + { + "columns": [ + "url" + ], + "nameExplicit": false, + "name": "core_update_sources_pkey", + "schema": "public", + "table": "core_update_sources", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_sso_attempts_pkey", + "schema": "public", + "table": "core_sso_attempts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_sso_pending_pkey", + "schema": "public", + "table": "core_sso_pending", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_users_pkey", + "schema": "public", + "table": "core_users", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_users_confirm_emails_pkey", + "schema": "public", + "table": "core_users_confirm_emails", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_users_forgot_password_pkey", + "schema": "public", + "table": "core_users_forgot_password", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_users_sso_operations_pkey", + "schema": "public", + "table": "core_users_sso_operations", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_moderation_sanctions_pkey", + "schema": "public", + "table": "core_moderation_sanctions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_moderation_warning_reasons_pkey", + "schema": "public", + "table": "core_moderation_warning_reasons", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_moderation_warning_thresholds_pkey", + "schema": "public", + "table": "core_moderation_warning_thresholds", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_moderation_warnings_pkey", + "schema": "public", + "table": "core_moderation_warnings", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "blog_categories_pkey", + "schema": "public", + "table": "blog_categories", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "blog_comments_pkey", + "schema": "public", + "table": "blog_comments", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "blog_posts_pkey", + "schema": "public", + "table": "blog_posts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "forum_replies_pkey", + "schema": "public", + "table": "forum_comments", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "forum_topics_pkey", + "schema": "public", + "table": "forum_topics", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "newsroom_articles_pkey", + "schema": "public", + "table": "newsroom_articles", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "newsroom_bulletins_pkey", + "schema": "public", + "table": "newsroom_bulletins", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "newsroom_categories_pkey", + "schema": "public", + "table": "newsroom_categories", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "newsroom_comments_pkey", + "schema": "public", + "table": "newsroom_comments", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "newsroom_events_pkey", + "schema": "public", + "table": "newsroom_events", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "newsroom_events_sessions_pkey", + "schema": "public", + "table": "newsroom_events_sessions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "newsroom_explainers_pkey", + "schema": "public", + "table": "newsroom_explainers", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "newsroom_explainers_faq_pkey", + "schema": "public", + "table": "newsroom_explainers_faq", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "newsroom_letters_pkey", + "schema": "public", + "table": "newsroom_letters", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "newsroom_pages_pkey", + "schema": "public", + "table": "newsroom_pages", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "newsroom_stories_pkey", + "schema": "public", + "table": "newsroom_stories", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "newsroom_tags_pkey", + "schema": "public", + "table": "newsroom_tags", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "payments_demo_purchases_pkey", + "schema": "public", + "table": "payments_demo_purchases", + "entityType": "pks" + }, + { + "nameExplicit": true, + "columns": [ + "itemType", + "itemId", + "languageCode" + ], + "nullsNotDistinct": false, + "name": "core_search_index_item_unique", + "entityType": "uniques", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + "providerId", + "providerAccountId" + ], + "nullsNotDistinct": false, + "name": "core_users_sso_provider_account_key", + "entityType": "uniques", + "schema": "public", + "table": "core_users_sso" + }, + { + "nameExplicit": true, + "columns": [ + "userId", + "providerId" + ], + "nullsNotDistinct": false, + "name": "core_users_sso_user_provider_key", + "entityType": "uniques", + "schema": "public", + "table": "core_users_sso" + }, + { + "nameExplicit": true, + "columns": [ + "issuedById", + "idempotencyKey" + ], + "nullsNotDistinct": false, + "name": "core_moderation_warnings_idempotency_key", + "entityType": "uniques", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "nameExplicit": false, + "columns": [ + "token" + ], + "nullsNotDistinct": false, + "name": "core_admin_sessions_token_unique", + "schema": "public", + "table": "core_admin_sessions", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "nullsNotDistinct": false, + "name": "core_admin_dashboard_userId_unique", + "schema": "public", + "table": "core_admin_dashboard", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "key" + ], + "nullsNotDistinct": false, + "name": "core_files_key_unique", + "schema": "public", + "table": "core_files", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "code" + ], + "nullsNotDistinct": false, + "name": "core_languages_code_unique", + "schema": "public", + "table": "core_languages", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "code" + ], + "nullsNotDistinct": false, + "name": "core_moderation_report_reasons_code_key", + "schema": "public", + "table": "core_moderation_report_reasons", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "openKey" + ], + "nullsNotDistinct": false, + "name": "core_moderation_reports_openKey_key", + "schema": "public", + "table": "core_moderation_reports", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "idempotencyKey" + ], + "nullsNotDistinct": false, + "name": "core_notification_deliveries_idempotencyKey_key", + "schema": "public", + "table": "core_notification_deliveries", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "tokenHash" + ], + "nullsNotDistinct": false, + "name": "core_users_passkey_challenges_tokenHash_key", + "schema": "public", + "table": "core_users_passkey_challenges", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "credentialId" + ], + "nullsNotDistinct": false, + "name": "core_users_passkeys_credentialId_key", + "schema": "public", + "table": "core_users_passkeys", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "token" + ], + "nullsNotDistinct": false, + "name": "core_sessions_token_unique", + "schema": "public", + "table": "core_sessions", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "publicId" + ], + "nullsNotDistinct": false, + "name": "core_sessions_known_devices_publicId_unique", + "schema": "public", + "table": "core_sessions_known_devices", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "stateHash" + ], + "nullsNotDistinct": false, + "name": "core_sso_attempts_stateHash_key", + "schema": "public", + "table": "core_sso_attempts", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "tokenHash" + ], + "nullsNotDistinct": false, + "name": "core_sso_pending_tokenHash_key", + "schema": "public", + "table": "core_sso_pending", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "nameCode" + ], + "nullsNotDistinct": false, + "name": "core_users_nameCode_unique", + "schema": "public", + "table": "core_users", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "name" + ], + "nullsNotDistinct": false, + "name": "core_users_name_unique", + "schema": "public", + "table": "core_users", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "email" + ], + "nullsNotDistinct": false, + "name": "core_users_email_unique", + "schema": "public", + "table": "core_users", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "token" + ], + "nullsNotDistinct": false, + "name": "core_users_confirm_emails_token_unique", + "schema": "public", + "table": "core_users_confirm_emails", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "nullsNotDistinct": false, + "name": "core_users_forgot_password_userId_unique", + "schema": "public", + "table": "core_users_forgot_password", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "token" + ], + "nullsNotDistinct": false, + "name": "core_users_forgot_password_token_unique", + "schema": "public", + "table": "core_users_forgot_password", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "tokenHash" + ], + "nullsNotDistinct": false, + "name": "core_users_sso_operations_tokenHash_key", + "schema": "public", + "table": "core_users_sso_operations", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "warningId" + ], + "nullsNotDistinct": false, + "name": "core_moderation_sanctions_warningId_key", + "schema": "public", + "table": "core_moderation_sanctions", + "entityType": "uniques" + }, + { + "value": "\"amount\" > 0", + "name": "core_payments_amount_positive", + "entityType": "checks", + "schema": "public", + "table": "core_payments" + }, + { + "value": "\"intent\" IN ('link', 'import', 'sync', 'preview')", + "name": "core_users_sso_operations_intent_check", + "entityType": "checks", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "value": "\"field\" IN ('avatar', 'firstName', 'lastName')", + "name": "core_users_sso_profile_sources_field_check", + "entityType": "checks", + "schema": "public", + "table": "core_users_sso_profile_sources" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/apps/web/content/docs/dev/audit-logs/index.mdx b/apps/web/content/docs/dev/audit-logs/index.mdx index a0569c6c02..43be8c2fac 100644 --- a/apps/web/content/docs/dev/audit-logs/index.mdx +++ b/apps/web/content/docs/dev/audit-logs/index.mdx @@ -50,8 +50,8 @@ after, an **Open** link to the target and **Show this person's activity**. | Content Engine | Create, edit, delete, duplicate, publish, unpublish, hide, unhide, restore, schedule and every translation change of every content type; access rule changes and protected files | | Roles and staff | Roles created, edited and deleted; administrator and moderator permissions added, changed and removed | | Users | Profile and role changes made in the AdminCP, password resets, email verification, signed-out devices | -| Settings | AI settings and access, notification types and danger-zone actions, report reasons, log retention and log exports | -| Moderation | Taking, assigning, resolving and reopening reports, report notes, acting on reported content, approving and rejecting submissions | +| Settings | AI settings and access, notification types and danger-zone actions, report reasons, warning reasons and thresholds, log retention and log exports | +| Moderation | Taking, assigning, resolving and reopening reports, report notes, acting on reported content, approving and rejecting submissions, [warnings and suspensions](/docs/dev/moderation/warnings) | | Background work | Scheduled publishing, cron jobs and queue tasks, attributed to the system; manual cron runs, attributed to the admin who started them | | Payments | Checkouts started by members, and every status change the provider reports (processing, succeeded, failed, expired) | diff --git a/apps/web/content/docs/dev/moderation/configure-warnings.mdx b/apps/web/content/docs/dev/moderation/configure-warnings.mdx index 21a429e79e..44579cc823 100644 --- a/apps/web/content/docs/dev/moderation/configure-warnings.mdx +++ b/apps/web/content/docs/dev/moderation/configure-warnings.mdx @@ -74,6 +74,8 @@ Warn a test member twice with **Spam** (4 points), then preview a third time in Changes apply to warnings issued from now on. Saving a lower threshold does not suspend members who are already above it, and raising one does not lift anything. +Every change to a reason or threshold, with its values before and after, is recorded in **AdminCP → Staff → Administrators → Logs**. + ## Related - [Warnings](/docs/dev/moderation/warnings) diff --git a/apps/web/content/docs/dev/moderation/issue-warnings.mdx b/apps/web/content/docs/dev/moderation/issue-warnings.mdx index 5ac4ed8563..32fc659091 100644 --- a/apps/web/content/docs/dev/moderation/issue-warnings.mdx +++ b/apps/web/content/docs/dev/moderation/issue-warnings.mdx @@ -7,6 +7,7 @@ icon: TriangleAlert import { ImgDocs } from "@/components/fumadocs/img"; import form from "./warning-form.webp"; import member from "./warning-member.webp"; +import memberLog from "./warning-member-log.webp"; Moderators warn members with one shared form, opened from wherever the problem is. This guide warns a member for a blog comment, then revokes the warning and lifts the suspension it caused. @@ -19,7 +20,7 @@ Warnings need [warning reasons](/docs/dev/moderation/configure-warnings) and a m | **Warn authors of this content** (`can_warn`) | The content type's module, for example **Comment** in the blog. Can be [scoped](/docs/dev/moderation/scoped-permissions) | Warn the author of that content | | **Warn members directly, without content** | Core → **Warnings** | Warn from a profile or the ModCP without content | | **Change a warning's points** / **Change when a warning's points expire** | Core → **Warnings** | Override the reason's defaults, with an explanation | -| **View every member's warnings** | Core → **Warnings** | See all warnings, not only those in their scope | +| **View every member's warnings** | Core → **Warnings** | See all warnings, not only those in their scope, and a member's **Moderation log** | | **Read staff notes on warnings** | Core → **Warnings** | Read private notes and override explanations | | **Revoke warnings** | Core → **Warnings** | Revoke a warning | | **View suspensions** / **Lift suspensions** | Core → **Warnings** | See and lift suspensions | @@ -83,6 +84,15 @@ Open the member in **Moderator CP → Warnings** (or **Warnings** on their profi Revoking a warning never lifts the suspension it caused. Lift it separately with **Lift** on the suspension. Other active suspensions keep applying. +The **Moderation log** at the bottom of the page lists who warned the member, revoked a warning or applied and lifted a suspension, newest first, with points before and after. It needs **View every member's warnings**, and suspension entries also need **View suspensions**. + + + 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 literal 0 HcmV?d00001 diff --git a/apps/web/content/docs/dev/moderation/warnings.mdx b/apps/web/content/docs/dev/moderation/warnings.mdx index 6384b95078..a4019e96e2 100644 --- a/apps/web/content/docs/dev/moderation/warnings.mdx +++ b/apps/web/content/docs/dev/moderation/warnings.mdx @@ -27,7 +27,7 @@ A **warning** records a moderator's decision about a member. It can carry **poin 1. A moderator opens the warn form. It previews the member's active points before and after, and the suspension a threshold would apply. 2. On submit, the server checks every permission again and locks that member's warnings for the length of one transaction. Two moderators warning the same member at once are handled one after the other, so the points add up and each threshold fires once. 3. A threshold applies when the warning moves active points from below its number to its number or above. When one warning crosses several, only the highest applies: 6 → 11 points with thresholds at 8 and 10 gives only the 10-point suspension. -4. The warning, the threshold decision, the suspension, the audit entries and the member's notifications commit together or not at all. +4. The warning, the threshold decision, the suspension, the [activity log](/docs/dev/audit-logs) entries and the member's notifications commit together or not at all. A temporary suspension ends at its own end time, even when the member still has the points. Expired points or a changed threshold never create a new suspension on their own; only a new warning does. Points that later fall below a threshold let it fire again on a future crossing. @@ -37,6 +37,10 @@ The server checks suspensions when it reads the session, for every request: core The member keeps their session. A strip under the header says the account is suspended and links to **Account status** at `/account-status`, which shows the suspension end time, active points and the warning history with each message. Members reach the same page from **Settings → Account status** at any time. It never shows staff notes or which moderator issued a warning. +## Where the history lives + +Every warning, revocation and suspension is written to the [Activity Logs](/docs/dev/audit-logs), in the same transaction as the change. Administrators see them in **AdminCP → Staff → Moderators → Logs**. Moderators with **View every member's warnings** see one member's entries as the **Moderation log** on that member's ModCP page. The entries hold points, threshold decisions and links to reports, never staff notes, override explanations or revocation reasons. Changes to warning reasons and thresholds land in the administrators' log. + ## Who is protected Nobody can warn themselves. A member who holds a root role cannot be warned at all, and other staff (admins and moderators) can only be warned by someone who holds a root role. These checks run before a threshold is evaluated, so a threshold can never suspend a protected account. @@ -45,7 +49,6 @@ Nobody can warn themselves. A member who holds a root role cannot be warned at a - Suspensions are the only sanction. There is no manual "suspend without a warning" action, no appeal workflow and no posting restriction per plugin yet; the data model leaves room for them. - Suspension state is cached for up to a minute per member. Applying or lifting a suspension clears that cache, and with Redis every instance sees it at once. Without Redis, other instances may take up to a minute. -- The audit trail is stored in `core_moderation_audit` but has no screen yet. ## Related diff --git a/packages/vitnode/src/api/lib/moderation/warnings/audit.ts b/packages/vitnode/src/api/lib/moderation/warnings/audit.ts new file mode 100644 index 0000000000..66e728d9d1 --- /dev/null +++ b/packages/vitnode/src/api/lib/moderation/warnings/audit.ts @@ -0,0 +1,47 @@ +import type { Context } from "hono"; + +import type { AuditDatabase } from "@/api/lib/audit/write"; +import type { AuditScope } from "@/lib/audit/const"; + +import { writeAuditLog } from "@/api/lib/audit/write"; + +/** The audit actions of warnings and sanctions, all about the warned member. */ +export type WarningAuditAction = + | "sanction.applied" + | "sanction.revoked" + | "warning.issued" + | "warning.revoked"; + +/** + * Writes one warning or sanction entry to the central audit log, in the + * moderators' log and in the same transaction as the change. The target is + * the warned member, so their history reads as one moderation log. Never pass + * staff notes, override explanations or revocation reasons in `metadata`. + */ +export const recordWarningAudit = async ( + c: Context, + tx: AuditDatabase, + { + action, + metadata, + scope = null, + user, + }: { + action: WarningAuditAction; + metadata: Record; + scope?: AuditScope | null; + user: { id: number; name: null | string }; + }, +): Promise => { + await writeAuditLog( + c, + { + action, + area: "moderator", + metadata, + scope, + target: { id: user.id, label: user.name, type: "user" }, + }, + tx, + ); +}; diff --git a/packages/vitnode/src/api/lib/moderation/warnings/reasons.ts b/packages/vitnode/src/api/lib/moderation/warnings/reasons.ts index 62e9d28878..f021c42223 100644 --- a/packages/vitnode/src/api/lib/moderation/warnings/reasons.ts +++ b/packages/vitnode/src/api/lib/moderation/warnings/reasons.ts @@ -3,14 +3,8 @@ import type { Context } from "hono"; import { z } from "@hono/zod-openapi"; import { and, asc, eq, inArray } from "drizzle-orm"; -import type { ContentDatabase } from "@/content/server/service"; -import type { ModerationAuditType } from "@/lib/moderation/warnings"; - import { core_languages_words } from "@/database/languages"; -import { - core_moderation_audit, - core_moderation_warning_reasons, -} from "@/database/warnings"; +import { core_moderation_warning_reasons } from "@/database/warnings"; export const WARNING_REASON_WORDS = { pluginCode: "core", @@ -92,28 +86,3 @@ export const listWarningReasons = async ( title: pick(row.id, "title"), })); }; - -/** - * Appends one entry to the warning system's audit trail. Never pass staff - * notes or override explanations in `data`. - */ -export const appendModerationAudit = async ( - db: ContentDatabase, - entry: { - actorId: null | number; - data?: Record; - sanctionId?: null | number; - type: ModerationAuditType; - userId?: null | number; - warningId?: null | number; - }, -): Promise => { - await db.insert(core_moderation_audit).values({ - actorId: entry.actorId, - data: entry.data ?? {}, - sanctionId: entry.sanctionId ?? null, - type: entry.type, - userId: entry.userId ?? null, - warningId: entry.warningId ?? null, - }); -}; diff --git a/packages/vitnode/src/api/lib/moderation/warnings/suspension.ts b/packages/vitnode/src/api/lib/moderation/warnings/suspension.ts index fb18b1211d..a126bb12bc 100644 --- a/packages/vitnode/src/api/lib/moderation/warnings/suspension.ts +++ b/packages/vitnode/src/api/lib/moderation/warnings/suspension.ts @@ -110,5 +110,5 @@ export const forgetSuspension = async ( { disconnect }: { disconnect: boolean }, ): Promise => { await c.get("cache").deleteSystem(suspensionCacheKey(userId)); - if (disconnect) c.get("realtime")?.closeUserConnections(userId); + if (disconnect) c.get("realtime").closeUserConnections(userId); }; diff --git a/packages/vitnode/src/api/lib/notifications/core-types.ts b/packages/vitnode/src/api/lib/notifications/core-types.ts index 7a6c5937f0..2a1a835944 100644 --- a/packages/vitnode/src/api/lib/notifications/core-types.ts +++ b/packages/vitnode/src/api/lib/notifications/core-types.ts @@ -1,5 +1,7 @@ import { z } from "zod"; +import { resolveLangValue } from "@/lib/helpers/multi-lang"; + import { buildNotificationType } from "./registry"; export const adminMessageNotification = buildNotificationType({ @@ -20,15 +22,6 @@ const reasonTextSchema = z .array(z.object({ languageCode: z.string(), value: z.string() })) .nullable(); -const pickText = ( - values: null | { languageCode: string; value: string }[], - locale: string, -): string => - values?.find(item => item.languageCode === locale)?.value ?? - values?.find(item => item.languageCode === "en")?.value ?? - values?.[0]?.value ?? - ""; - const ACCOUNT_STATUS_PATH = "/account-status"; /** Sent to a member when a moderator warns them. Never carries staff notes. */ @@ -48,7 +41,7 @@ export const warningIssuedNotification = buildNotificationType({ email: true, mandatory: true, present: ({ data, locale, t }) => { - const reason = pickText(data.reason, locale); + const reason = data.reason ? resolveLangValue(data.reason, { locale }) : ""; return { body: data.message, diff --git a/packages/vitnode/src/api/modules/admin/warnings/routes/warning-reasons.routes.ts b/packages/vitnode/src/api/modules/admin/warnings/routes/warning-reasons.routes.ts index 10f41be05d..ed36aabb72 100644 --- a/packages/vitnode/src/api/modules/admin/warnings/routes/warning-reasons.routes.ts +++ b/packages/vitnode/src/api/modules/admin/warnings/routes/warning-reasons.routes.ts @@ -4,8 +4,9 @@ import { z } from "@hono/zod-openapi"; import { count, eq, inArray, max, sql } from "drizzle-orm"; import { buildRoute } from "@/api/builders"; +import { multiLangAuditLabel } from "@/api/lib/audit/labels"; +import { writeAuditLog } from "@/api/lib/audit/write"; import { - appendModerationAudit, listWarningReasons, WARNING_REASON_WORDS, warningReasonSchema, @@ -13,6 +14,7 @@ import { import { saveLanguageWords } from "@/api/lib/save-language-words"; import { errorSchema, json } from "@/api/modules/moderation/lib/schema"; import { core_moderation_warning_reasons } from "@/database/warnings"; +import { auditDiff } from "@/lib/audit/diff"; import { WARNING_LIMITS } from "@/lib/moderation/warnings"; const wordsSchema = (maxLength: number) => @@ -138,11 +140,19 @@ export const createWarningReasonAdminRoute = buildRoute({ position: (lastPosition ?? -1) + 1, }) .returning({ id: core_moderation_warning_reasons.id }); - await appendModerationAudit(tx, { - actorId: c.get("admin")?.user.id ?? null, - data: { reasonId: row.id, ...auditedSettings(body) }, - type: "reason_created", - }); + const label = multiLangAuditLabel(body.title); + await writeAuditLog( + c, + { + action: "warning_reason.created", + changes: auditDiff({ + after: { ...auditedSettings(body), title: label }, + before: null, + }), + target: { id: row.id, label, type: "warning_reason" }, + }, + tx, + ); return row; }); @@ -190,7 +200,12 @@ export const updateWarningReasonAdminRoute = buildRoute({ const found = await c.get("db").transaction(async tx => { const [row] = await tx - .select({ id: core_moderation_warning_reasons.id }) + .select({ + enabled: core_moderation_warning_reasons.enabled, + expiresAfterHours: core_moderation_warning_reasons.expiresAfterHours, + id: core_moderation_warning_reasons.id, + points: core_moderation_warning_reasons.points, + }) .from(core_moderation_warning_reasons) .where(eq(core_moderation_warning_reasons.id, id)) .limit(1); @@ -201,15 +216,31 @@ export const updateWarningReasonAdminRoute = buildRoute({ .set(patch) .where(eq(core_moderation_warning_reasons.id, id)); } - await appendModerationAudit(tx, { - actorId: c.get("admin")?.user.id ?? null, - data: { - reasonId: id, - ...patch, - wordsChanged: body.title !== undefined, + const [stored] = await listWarningReasons(c, { ids: [id] }); + const label = multiLangAuditLabel(body.title ?? stored?.title); + await writeAuditLog( + c, + { + action: "warning_reason.updated", + changes: auditDiff({ + after: { + ...row, + ...patch, + ...(body.title === undefined ? {} : { title: label }), + }, + before: row, + exclude: ["id"], + }), + metadata: { + ...(body.description === undefined + ? {} + : { descriptionChanged: true }), + ...(body.message === undefined ? {} : { messageChanged: true }), + }, + target: { id, label, type: "warning_reason" }, }, - type: "reason_updated", - }); + tx, + ); return true; }); @@ -272,10 +303,21 @@ export const reorderWarningReasonsAdminRoute = buildRoute({ ), sql` `, ); - await db - .update(core_moderation_warning_reasons) - .set({ position: sql`case ${positionById} end` }) - .where(inArray(core_moderation_warning_reasons.id, ids)); + await db.transaction(async tx => { + await tx + .update(core_moderation_warning_reasons) + .set({ position: sql`case ${positionById} end` }) + .where(inArray(core_moderation_warning_reasons.id, ids)); + await writeAuditLog( + c, + { + action: "warning_reason.reordered", + metadata: { order: ids }, + target: { type: "warning_reason" }, + }, + tx, + ); + }); return c.body(null, 200); }, diff --git a/packages/vitnode/src/api/modules/admin/warnings/routes/warning-thresholds.routes.ts b/packages/vitnode/src/api/modules/admin/warnings/routes/warning-thresholds.routes.ts index 92ec137bea..0558d75524 100644 --- a/packages/vitnode/src/api/modules/admin/warnings/routes/warning-thresholds.routes.ts +++ b/packages/vitnode/src/api/modules/admin/warnings/routes/warning-thresholds.routes.ts @@ -1,14 +1,13 @@ -import type { Context } from "hono"; - import { z } from "@hono/zod-openapi"; import { and, asc, count, eq, ne, sql } from "drizzle-orm"; import type { ContentDatabase } from "@/content/server/service"; import { buildRoute } from "@/api/builders"; -import { appendModerationAudit } from "@/api/lib/moderation/warnings/reasons"; +import { writeAuditLog } from "@/api/lib/audit/write"; import { errorSchema, json } from "@/api/modules/moderation/lib/schema"; import { core_moderation_warning_thresholds } from "@/database/warnings"; +import { auditDiff } from "@/lib/audit/diff"; import { WARNING_LIMITS, WARNING_THRESHOLD_ACTIONS, @@ -72,7 +71,11 @@ const pointsTaken = async ( return row !== undefined; }; -const actorOf = (c: Context) => c.get("admin")?.user.id ?? null; +const TARGET = "warning_threshold"; + +/** A threshold has no name; its points are what admins tell it apart by. */ +const thresholdLabel = ({ points }: { points: number }): string => + String(points); const responses = { 400: json(errorSchema, "A temporary suspension needs a duration"), @@ -154,11 +157,15 @@ export const createWarningThresholdAdminRoute = buildRoute({ .insert(table) .values(body) .returning({ id: table.id }); - await appendModerationAudit(tx, { - actorId: actorOf(c), - data: { thresholdId: row.id, ...body }, - type: "threshold_created", - }); + await writeAuditLog( + c, + { + action: "warning_threshold.created", + changes: auditDiff({ after: body, before: null }), + target: { id: row.id, label: thresholdLabel(body), type: TARGET }, + }, + tx, + ); return { id: row.id }; }); @@ -202,7 +209,12 @@ export const updateWarningThresholdAdminRoute = buildRoute({ const result = await c.get("db").transaction(async tx => { await tx.execute(LOCK_THRESHOLDS); const [existing] = await tx - .select({ id: table.id }) + .select({ + action: table.action, + durationHours: table.durationHours, + enabled: table.enabled, + points: table.points, + }) .from(table) .where(eq(table.id, id)) .limit(1); @@ -212,11 +224,15 @@ export const updateWarningThresholdAdminRoute = buildRoute({ } await tx.update(table).set(body).where(eq(table.id, id)); - await appendModerationAudit(tx, { - actorId: actorOf(c), - data: { thresholdId: id, ...body }, - type: "threshold_updated", - }); + await writeAuditLog( + c, + { + action: "warning_threshold.updated", + changes: auditDiff({ after: body, before: existing }), + target: { id, label: thresholdLabel(body), type: TARGET }, + }, + tx, + ); return { id }; }); @@ -255,16 +271,22 @@ export const deleteWarningThresholdAdminRoute = buildRoute({ const { id } = c.req.valid("param"); const table = core_moderation_warning_thresholds; const deleted = await c.get("db").transaction(async tx => { - const [row] = await tx - .delete(table) - .where(eq(table.id, id)) - .returning({ id: table.id, points: table.points }); - if (!row) return false; - await appendModerationAudit(tx, { - actorId: actorOf(c), - data: { points: row.points, thresholdId: id }, - type: "threshold_deleted", + const [row] = await tx.delete(table).where(eq(table.id, id)).returning({ + action: table.action, + durationHours: table.durationHours, + enabled: table.enabled, + points: table.points, }); + if (!row) return false; + await writeAuditLog( + c, + { + action: "warning_threshold.deleted", + changes: auditDiff({ after: null, before: row }), + target: { id, label: thresholdLabel(row), type: TARGET }, + }, + tx, + ); return true; }); diff --git a/packages/vitnode/src/api/modules/moderation/lib/warnings-service.ts b/packages/vitnode/src/api/modules/moderation/lib/warnings-service.ts index 0dbf51326b..b448da35ed 100644 --- a/packages/vitnode/src/api/modules/moderation/lib/warnings-service.ts +++ b/packages/vitnode/src/api/modules/moderation/lib/warnings-service.ts @@ -11,6 +11,7 @@ import type { WarningThresholdAction, } from "@/lib/moderation/warnings"; +import { userAuditLabel } from "@/api/lib/audit/labels"; import { isUniqueViolation } from "@/api/lib/db-errors"; import { canOnReport } from "@/api/lib/moderation/access"; import { findReportTarget } from "@/api/lib/moderation/registry"; @@ -20,10 +21,8 @@ import { hasWarningPermission, warningTargetRefusal, } from "@/api/lib/moderation/warnings/access"; -import { - appendModerationAudit, - listWarningReasons, -} from "@/api/lib/moderation/warnings/reasons"; +import { recordWarningAudit } from "@/api/lib/moderation/warnings/audit"; +import { listWarningReasons } from "@/api/lib/moderation/warnings/reasons"; import { forgetSuspension } from "@/api/lib/moderation/warnings/suspension"; import { accountSuspendedNotification, @@ -576,59 +575,38 @@ export const issueWarning = async ( }) .returning({ id: core_moderation_warnings.id }); - await appendModerationAudit(tx, { - actorId: moderator.user.id, - data: { - content: draft.content - ? { - pluginId: draft.content.pluginId, - targetId: draft.content.targetId, - targetType: draft.content.targetType, - } - : null, - expiresAt: expiresAt?.toISOString() ?? null, - points: draft.points, - reasonId: draft.reason?.id ?? null, - reportId: report?.id ?? null, - }, - type: "warning_issued", - userId: draft.user.id, - warningId: warning.id, - }); - if (overridden) { - await appendModerationAudit(tx, { - actorId: moderator.user.id, - data: { - defaultExpiresAfterHours: draft.defaultExpiresAfterHours, - defaultPoints: draft.defaultPoints, - expiresAt: expiresAt?.toISOString() ?? null, - expiration: draft.overrides.expiration, - points: draft.points, - pointsOverridden: draft.overrides.points, - }, - type: "warning_overridden", - userId: draft.user.id, - warningId: warning.id, - }); - } - const rules = await loadEnabledThresholds(tx); const { applied, crossed } = evaluateWarningThresholds({ after, before, rules, }); - await appendModerationAudit(tx, { - actorId: moderator.user.id, - data: { - after, + const scope = + draft.content?.scopeFamily && draft.content.scopeId + ? { family: draft.content.scopeFamily, id: draft.content.scopeId } + : null; + await recordWarningAudit(c, tx, { + action: "warning.issued", + metadata: { appliedThresholdId: applied?.id ?? null, - before, + contentId: draft.content?.targetId ?? null, + contentPluginId: draft.content?.pluginId ?? null, + contentType: draft.content?.targetType ?? null, crossedThresholdIds: crossed.map(rule => rule.id), + defaultExpiresAfterHours: draft.defaultExpiresAfterHours, + defaultPoints: draft.defaultPoints, + expirationOverridden: draft.overrides.expiration, + expiresAt: expiresAt?.toISOString() ?? null, + points: draft.points, + pointsAfter: after, + pointsBefore: before, + pointsOverridden: draft.overrides.points, + reasonId: draft.reason?.id ?? null, + reportId: report?.id ?? null, + warningId: warning.id, }, - type: "threshold_evaluated", - userId: draft.user.id, - warningId: warning.id, + scope, + user: draft.user, }); let sanction: IssuedWarning["sanction"] = null; @@ -653,19 +631,19 @@ export const issueWarning = async ( }) .returning({ id: core_moderation_sanctions.id }); sanction = { endsAt, id: row.id, thresholdId: applied.id }; - await appendModerationAudit(tx, { - actorId: moderator.user.id, - data: { - action: applied.action, + await recordWarningAudit(c, tx, { + action: "sanction.applied", + metadata: { endsAt: endsAt?.toISOString() ?? null, + sanctionId: row.id, source: "threshold", + thresholdAction: applied.action, thresholdId: applied.id, thresholdPoints: applied.points, + warningId: warning.id, }, - sanctionId: row.id, - type: "sanction_applied", - userId: draft.user.id, - warningId: warning.id, + scope, + user: draft.user, }); } @@ -823,12 +801,17 @@ export const revokeWarning = async ( .returning({ id: core_moderation_warnings.id }); if (!row) return false; - await appendModerationAudit(tx, { - actorId: moderator.user.id, - data: { points: warning.points }, - type: "warning_revoked", - userId: warning.userId, - warningId: id, + await recordWarningAudit(c, tx, { + action: "warning.revoked", + metadata: { points: warning.points, warningId: id }, + scope: + warning.scopeFamily && warning.scopeId + ? { family: warning.scopeFamily, id: warning.scopeId } + : null, + user: { + id: warning.userId, + name: await userAuditLabel(c, warning.userId), + }, }); await c.get("notifications").publish({ actorId: null, @@ -895,13 +878,17 @@ export const revokeSanction = async ( .returning({ id: core_moderation_sanctions.id }); if (!row) return false; - await appendModerationAudit(tx, { - actorId: moderator.user.id, - data: { type: sanction.type }, - sanctionId: id, - type: "sanction_revoked", - userId: sanction.userId, - warningId: sanction.warningId, + await recordWarningAudit(c, tx, { + action: "sanction.revoked", + metadata: { + sanctionId: id, + type: sanction.type, + warningId: sanction.warningId, + }, + user: { + id: sanction.userId, + name: await userAuditLabel(c, sanction.userId), + }, }); await c.get("notifications").publish({ actorId: null, diff --git a/packages/vitnode/src/api/modules/moderation/moderation.module.ts b/packages/vitnode/src/api/modules/moderation/moderation.module.ts index afc46dc58f..f5517fb831 100644 --- a/packages/vitnode/src/api/modules/moderation/moderation.module.ts +++ b/packages/vitnode/src/api/modules/moderation/moderation.module.ts @@ -21,6 +21,7 @@ import { issueWarningRoute, listWarningReasonsRoute, listWarningsRoute, + memberLogRoute, memberStandingRoute, previewWarningRoute, revokeSanctionRoute, @@ -67,6 +68,7 @@ export const moderationModule = buildModule({ revokeSanctionRoute, searchMembersRoute, memberStandingRoute, + memberLogRoute, accountStatusRoute, ], }); diff --git a/packages/vitnode/src/api/modules/moderation/routes/account-status.route.ts b/packages/vitnode/src/api/modules/moderation/routes/account-status.route.ts index daf65a9522..320219770c 100644 --- a/packages/vitnode/src/api/modules/moderation/routes/account-status.route.ts +++ b/packages/vitnode/src/api/modules/moderation/routes/account-status.route.ts @@ -82,7 +82,8 @@ export const accountStatusRoute = buildRoute({ pluginId: row.pluginId, type: row.targetType, }); - // Link only to content that is still there and not hidden. + // Link only to content that is still there and not hidden. A plugin that + // fails to answer costs the member the link, never the page. const current = target ? await target.definition .current(c, { locale: null, targetId: row.targetId }) diff --git a/packages/vitnode/src/api/modules/moderation/routes/warnings.routes.ts b/packages/vitnode/src/api/modules/moderation/routes/warnings.routes.ts index a565b927b6..1ad469db2e 100644 --- a/packages/vitnode/src/api/modules/moderation/routes/warnings.routes.ts +++ b/packages/vitnode/src/api/modules/moderation/routes/warnings.routes.ts @@ -19,6 +19,7 @@ import { HTTPException } from "hono/http-exception"; import type { ModeratorContext } from "@/api/lib/moderation/access"; import { buildRoute } from "@/api/builders"; +import { listAuditLogsWithDetails } from "@/api/lib/audit/query"; import { requireModerator } from "@/api/lib/moderation/access"; import { canWarnAnywhere, @@ -33,6 +34,7 @@ import { findActiveSuspension } from "@/api/lib/moderation/warnings/suspension"; import { core_users } from "@/database/users"; import { core_moderation_warnings } from "@/database/warnings"; import { containsLikePattern } from "@/lib/api/like-pattern"; +import { AUDIT_CORE_PLUGIN_ID } from "@/lib/audit/const"; import { WARNING_LIMITS, WARNING_PERMISSIONS, @@ -579,6 +581,7 @@ export const memberStandingRoute = buildRoute({ revoke: z.boolean(), viewSanctions: z.boolean(), manageSanctions: z.boolean(), + viewLog: z.boolean(), }), }), "The member's standing", @@ -625,9 +628,93 @@ export const memberStandingRoute = buildRoute({ revoke: has(WARNING_PERMISSIONS.revoke), viewSanctions, manageSanctions: has(WARNING_PERMISSIONS.manageSanctions), + viewLog: has(WARNING_PERMISSIONS.view), }, }, 200, ); }, }); + +const LOG_PAGE_SIZE = 20; + +export const memberLogRoute = buildRoute({ + route: { + method: "get", + path: "/members/{id}/log", + description: + "A member's moderation log for the ModCP, newest first: warnings issued and revoked and, with `can_view_sanctions`, suspensions applied and lifted. Read from the central activity log; needs `can_view`. Never includes staff notes", + request: { + params: idParams, + query: z.object({ cursor: z.string().max(200).optional() }), + }, + responses: { + 200: json( + z.object({ + edges: z.array( + z.object({ + id: z.number(), + action: z.string(), + actor: reportPersonSchema.nullable(), + actorName: z.string().nullable(), + createdAt: z.date(), + metadata: z.record(z.string(), z.unknown()), + }), + ), + pageInfo: z.object({ + endCursor: z.string().nullable(), + hasNextPage: z.boolean(), + }), + }), + "One page of the member's moderation log", + ), + 401: commonResponses[401], + 403: { description: "Needs `can_view` warnings" }, + }, + }, + handler: async c => { + const moderator = await requireModerator(c); + if (!hasWarningPermission(moderator.set, WARNING_PERMISSIONS.view)) { + throw new HTTPException(403, { message: "Forbidden" }); + } + const { id } = c.req.valid("param"); + const { cursor } = c.req.valid("query"); + const viewSanctions = hasWarningPermission( + moderator.set, + WARNING_PERMISSIONS.viewSanctions, + ); + + const { edges, pageInfo } = await listAuditLogsWithDetails(c, { + cursor, + filters: { + area: "moderator", + pluginId: AUDIT_CORE_PLUGIN_ID, + targetId: String(id), + targetType: "user", + }, + first: LOG_PAGE_SIZE, + }); + const visible = edges.filter( + entry => viewSanctions || !entry.action.startsWith("sanction."), + ); + const people = await loadReportPeople( + c, + visible.map(entry => entry.actor?.id), + ); + + return c.json( + { + edges: visible.map(entry => ({ + id: entry.id, + action: entry.action, + actor: entry.actor ? (people.get(entry.actor.id) ?? null) : null, + actorName: entry.actorName, + createdAt: entry.createdAt, + metadata: entry.metadata, + })), + pageInfo, + }, + 200, + ); + }, +}); diff --git a/packages/vitnode/src/api/modules/moderation/warnings.test.ts b/packages/vitnode/src/api/modules/moderation/warnings.test.ts index 866870beb8..d043384733 100644 --- a/packages/vitnode/src/api/modules/moderation/warnings.test.ts +++ b/packages/vitnode/src/api/modules/moderation/warnings.test.ts @@ -15,6 +15,7 @@ import { warningIssuedNotification, } from "@/api/lib/notifications/core-types"; import { defineModerationScope, defineReportTarget } from "@/api/moderation"; +import { core_audit_logs } from "@/database/audit"; import { core_languages_words } from "@/database/languages"; import { core_moderation_report_events, @@ -24,7 +25,6 @@ import { import { core_roles } from "@/database/roles"; import { core_users } from "@/database/users"; import { - core_moderation_audit, core_moderation_sanctions, core_moderation_warning_reasons, core_moderation_warning_thresholds, @@ -408,9 +408,19 @@ interface StaffWarning { interface Standing { activePoints: number; hiddenWarnings: number; + permissions: { viewLog: boolean }; warnings: StaffWarning[]; } +interface MemberLog { + edges: { + action: string; + actor: null | { id: number }; + metadata: Record; + }[]; + pageInfo: { endCursor: null | string; hasNextPage: boolean }; +} + interface AccountStatus { activePoints: number; sanctions: { active: boolean; id: number }[]; @@ -500,9 +510,15 @@ describe("issuing a warning", () => { expect( (row.expiresAt as Date).getTime() - (row.issuedAt as Date).getTime(), ).toBe(48 * HOUR); - expect(h.memory.rows(core_moderation_audit).map(e => e.type)).toContain( - "warning_overridden", - ); + expect( + h.memory + .rows(core_audit_logs) + .find(entry => entry.action === "warning.issued"), + ).toMatchObject({ + area: "moderator", + metadata: { defaultPoints: 2, points: 4, pointsOverridden: true }, + targetType: "user", + }); }); it("lets a root moderator warn other staff", async () => { @@ -810,6 +826,80 @@ describe("revoking", () => { }); }); +describe("member moderation log", () => { + it.each([ + ["with can_view_sanctions", SENIOR, true], + ["without can_view_sanctions", VIEWER, false], + ])( + "lists the member's warning activity newest first %s", + async (_name, viewer, viewSanctions) => { + const h = await harness(); + await h.seedPoints(MEMBER, 6); + const { sanction, warningId } = await h.issued(SENIOR); + await h.issued(SENIOR, { reasonId: REASON_ZERO, userId: OTHER_MEMBER }); + expect((await h.revoke(SENIOR, "warnings", warningId)).status).toBe(200); + const sanctionId = sanction?.id ?? 0; + expect((await h.revoke(SENIOR, "sanctions", sanctionId)).status).toBe( + 200, + ); + + const res = await h.call(viewer, "GET", `/members/${MEMBER}/log`); + + expect(res.status).toBe(200); + const log = (await res.json()) as MemberLog; + expect(log.edges).toEqual( + [ + { action: "sanction.revoked", metadata: { sanctionId, warningId } }, + { action: "warning.revoked", metadata: { points: 2, warningId } }, + { + action: "sanction.applied", + metadata: { sanctionId, thresholdId: THRESHOLD_EIGHT, warningId }, + }, + { + action: "warning.issued", + metadata: { + appliedThresholdId: THRESHOLD_EIGHT, + points: 2, + pointsAfter: 8, + pointsBefore: 6, + }, + }, + ] + .filter(entry => viewSanctions || entry.action.startsWith("warning.")) + .map(({ action, metadata }) => + expect.objectContaining({ + action, + actor: expect.objectContaining({ id: SENIOR }), + metadata: expect.objectContaining(metadata), + }), + ), + ); + expect(log.pageInfo).toEqual({ + endCursor: expect.any(String), + hasNextPage: false, + }); + }, + ); + + it.each([ + ["can_view", 200, VIEWER], + ["only core can_warn", 403, BASIC], + ["only content-scoped can_warn", 403, SCOPED], + ])( + "answers a moderator with %s with %i and says so in the standing", + async (_name, status, moderator) => { + const h = await harness(); + + const res = await h.call(moderator, "GET", `/members/${MEMBER}/log`); + + expect(res.status).toBe(status); + expect((await h.standing(moderator)).permissions.viewLog).toBe( + status === 200, + ); + }, + ); +}); + describe("reports", () => { it("links a warning to a report and resolves it only when asked", async () => { const h = await harness(); @@ -911,7 +1001,7 @@ describe("staff notes", () => { expect(basic.hiddenWarnings).toBe(1); }); - it("never reach the member's own status, notifications, events or audit trail", async () => { + it("never reach the member's own status, notifications, events, audit trail or moderation log", async () => { const h = await harness(); await h.seedPoints(MEMBER, 6); const { sanction, warningId } = await h.issued(SENIOR, { @@ -935,11 +1025,19 @@ describe("staff notes", () => { "moderation.warning.revoked", "moderation.sanction.revoked", ]); + const log = await h.call(SENIOR, "GET", `/members/${MEMBER}/log`); + expect(log.status).toBe(200); + const trail = JSON.stringify([ + await log.text(), + h.memory.rows(core_audit_logs), + ]); + expect(trail).not.toContain("Issued by mistake"); + const leaked = JSON.stringify([ status, h.emit.mock.calls, h.publish.mock.calls.map(([args]) => args.data), - h.memory.rows(core_moderation_audit), + trail, ]); expect(leaked).not.toContain(STAFF_NOTE); expect(leaked).not.toContain(OVERRIDE_NOTE); diff --git a/packages/vitnode/src/database/warnings.ts b/packages/vitnode/src/database/warnings.ts index 6712101838..a32d195320 100644 --- a/packages/vitnode/src/database/warnings.ts +++ b/packages/vitnode/src/database/warnings.ts @@ -2,7 +2,6 @@ import { sql } from "drizzle-orm"; import { camelCase, index, unique, uniqueIndex } from "drizzle-orm/pg-core"; import type { - ModerationAuditType, SanctionSource, SanctionThresholdSnapshot, SanctionType, @@ -12,7 +11,6 @@ import type { } from "@/lib/moderation/warnings"; import { - MODERATION_AUDIT_TYPES, SANCTION_SOURCES, SANCTION_TYPES, WARNING_THRESHOLD_ACTIONS, @@ -181,38 +179,3 @@ export const core_moderation_sanctions = camelCase.table.withRLS( index("core_moderation_sanctions_created_at_id_idx").on(t.createdAt, t.id), ], ); - -/** - * The warning system's audit trail. Never holds staff notes or override - * explanations - those stay on the warning, behind `can_view_notes`. - */ -export const core_moderation_audit = camelCase.table.withRLS( - "core_moderation_audit", - t => ({ - id: t.serial().primaryKey(), - type: t - .varchar({ enum: MODERATION_AUDIT_TYPES, length: 40 }) - .$type() - .notNull(), - actorId: t.integer().references(() => core_users.id, { - onDelete: "set null", - }), - userId: t.integer().references(() => core_users.id, { - onDelete: "set null", - }), - warningId: t.integer().references(() => core_moderation_warnings.id, { - onDelete: "set null", - }), - sanctionId: t.integer().references(() => core_moderation_sanctions.id, { - onDelete: "set null", - }), - data: t.jsonb().$type>().notNull().default({}), - createdAt: t.timestamp().notNull().defaultNow(), - }), - t => [ - index("core_moderation_audit_user_idx").on(t.userId, t.createdAt), - index("core_moderation_audit_created_at_id_idx").on(t.createdAt, t.id), - index("core_moderation_audit_warning_idx").on(t.warningId), - index("core_moderation_audit_sanction_idx").on(t.sanctionId), - ], -); diff --git a/packages/vitnode/src/lib/audit/actions.ts b/packages/vitnode/src/lib/audit/actions.ts index ce9caf14ec..82d6fe4617 100644 --- a/packages/vitnode/src/lib/audit/actions.ts +++ b/packages/vitnode/src/lib/audit/actions.ts @@ -137,6 +137,16 @@ export const CORE_AUDIT_ACTIONS = defineAuditActions([ { action: "report.resolved", targetType: "report" }, { action: "report.reopened", targetType: "report" }, { action: "report.action_taken", targetType: "report" }, + { action: "warning.issued", targetType: "user" }, + { action: "warning.revoked", targetType: "user" }, + { action: "sanction.applied", targetType: "user" }, + { action: "sanction.revoked", targetType: "user" }, + { action: "warning_reason.created", targetType: "warning_reason" }, + { action: "warning_reason.updated", targetType: "warning_reason" }, + { action: "warning_reason.reordered", targetType: "warning_reason" }, + { action: "warning_threshold.created", targetType: "warning_threshold" }, + { action: "warning_threshold.updated", targetType: "warning_threshold" }, + { action: "warning_threshold.deleted", targetType: "warning_threshold" }, { action: "submission.approved", targetType: "submission" }, { action: "submission.rejected", targetType: "submission" }, { action: "cron.run", targetType: "cron_job" }, diff --git a/packages/vitnode/src/lib/moderation/warnings.ts b/packages/vitnode/src/lib/moderation/warnings.ts index 83cafdf343..92da40eb2e 100644 --- a/packages/vitnode/src/lib/moderation/warnings.ts +++ b/packages/vitnode/src/lib/moderation/warnings.ts @@ -1,3 +1,5 @@ +import type { MultiLangValue } from "@/lib/helpers/multi-lang"; + /** Moderator permissions of core's `warnings` module. */ export const WARNING_PERMISSIONS = { view: "can_view", @@ -46,33 +48,13 @@ export const SANCTION_TYPES = ["suspension"] as const; export type SanctionType = (typeof SANCTION_TYPES)[number]; -/** What created a sanction. */ export const SANCTION_SOURCES = ["threshold", "manual"] as const; export type SanctionSource = (typeof SANCTION_SOURCES)[number]; -export const MODERATION_AUDIT_TYPES = [ - "warning_issued", - "warning_overridden", - "warning_revoked", - "threshold_evaluated", - "sanction_applied", - "sanction_revoked", - "reason_created", - "reason_updated", - "threshold_created", - "threshold_updated", - "threshold_deleted", -] as const; - -export type ModerationAuditType = (typeof MODERATION_AUDIT_TYPES)[number]; - -/** A multi-language value, as stored in `core_languages_words`. */ -export type WarningReasonText = { languageCode: string; value: string }[]; - /** What a warning remembers about its reason, so renaming the reason never rewrites history. */ export interface WarningReasonSnapshot { - title: WarningReasonText; + title: MultiLangValue; } /** What a warning remembers about the content it was issued for. */ @@ -181,13 +163,3 @@ export const isSanctionActiveAt = ( !sanction.revokedAt && sanction.startsAt.getTime() <= now.getTime() && (sanction.endsAt === null || sanction.endsAt.getTime() > now.getTime()); - -/** Picks the reason text in `locale`, else English, else the first one. Pure. */ -export const warningReasonText = ( - values: null | undefined | WarningReasonText, - locale: string, -): string => - values?.find(item => item.languageCode === locale)?.value ?? - values?.find(item => item.languageCode === "en")?.value ?? - values?.[0]?.value ?? - ""; diff --git a/packages/vitnode/src/locales/en.json b/packages/vitnode/src/locales/en.json index c583e589f0..c6c1653ab8 100644 --- a/packages/vitnode/src/locales/en.json +++ b/packages/vitnode/src/locales/en.json @@ -2923,7 +2923,7 @@ "title": "Warn {name}", "title_loading": "Warn member", "desc": "The member gets your message in a notification. Points count across the whole site.", - "no_reasons": "No warning reasons are set up yet - an administrator can add them in the AdminCP.", + "no_reasons": "No warning reasons are set up yet. An administrator can add them in the AdminCP.", "cancel": "Cancel", "submit": "Issue warning", "points": "{count, plural, one {# point} other {# points}}", @@ -2982,7 +2982,7 @@ "suspended_until": "Their account is suspended until {endsAt}.", "suspended_indefinitely": "Their account is suspended indefinitely.", "report_resolved": "The report is closed.", - "differs": "That's different from the preview - their points changed in the meantime." + "differs": "Their points changed after you opened the form, so this differs from the preview." }, "errors": { "title": "Could not issue the warning", @@ -3089,6 +3089,23 @@ "source": "Source", "from_threshold": "Reached {points, plural, one {# point} other {# points}}", "manual": "Applied by staff" + }, + "log": { + "title": "Moderation log", + "loading": "Loading the moderation log", + "error": "Could not load the moderation log.", + "empty": "Nothing has happened to this account yet.", + "load_more": "Show older", + "someone": "Someone", + "warning_issued": " warned them for {points, plural, one {# point} other {# points}} ({before} → {after} active)", + "warning_revoked": " revoked a warning of {points, plural, one {# point} other {# points}}", + "sanction_applied_until": "'s warning suspended the account until {endsAt} (the {threshold}-point threshold)", + "sanction_applied_indefinite": "'s warning suspended the account indefinitely (the {threshold}-point threshold)", + "sanction_revoked": " lifted a suspension", + "other": ": {action}", + "points_overridden": "Points changed from the reason's {points, plural, one {# point} other {# points}}", + "expiration_overridden": "Expiry changed", + "report": "From report #{id}" } }, "account_status": { @@ -3107,7 +3124,7 @@ "points_title": "Active points", "points_desc": "Points from warnings that haven't expired or been revoked. Reaching a limit can suspend your account.", "warnings_title": "Your warnings", - "no_warnings": "You haven't received any warnings. Keep it up!", + "no_warnings": "You haven't received any warnings.", "expires": "Points expire:", "never": "Never", "content": "About:", @@ -3303,6 +3320,24 @@ "succeeded": "Payment succeeded", "failed": "Payment failed", "expired": "Payment expired" + }, + "warning": { + "issued": "Warned member", + "revoked": "Revoked a warning" + }, + "sanction": { + "applied": "Suspended account", + "revoked": "Lifted a suspension" + }, + "warning_reason": { + "created": "Created warning reason", + "updated": "Updated warning reason", + "reordered": "Reordered warning reasons" + }, + "warning_threshold": { + "created": "Created warning threshold", + "updated": "Updated warning threshold", + "deleted": "Deleted warning threshold" } }, "fields": { @@ -3330,7 +3365,12 @@ "access_view": "View access", "access_read": "Read access", "access_comment": "Comment access", - "access_downloadAttachments": "Download access" + "access_downloadAttachments": "Download access", + "points": "Points", + "expiresAfterHours": "Points expire after (hours)", + "enabled": "Enabled", + "action": "Action", + "durationHours": "Duration (hours)" } }, "access": { diff --git a/packages/vitnode/src/views/admin/views/core/moderation/warnings/warnings-admin-api.ts b/packages/vitnode/src/views/admin/views/core/moderation/warnings/warnings-admin-api.ts index 6948a85208..79b9968935 100644 --- a/packages/vitnode/src/views/admin/views/core/moderation/warnings/warnings-admin-api.ts +++ b/packages/vitnode/src/views/admin/views/core/moderation/warnings/warnings-admin-api.ts @@ -94,7 +94,7 @@ export const saveAdminWarningReason = async ({ values, }: { id?: number; - values: Partial; + values: AdminWarningReasonInput; }): Promise> => await runAdminApiMutation({ expected: id === undefined ? 201 : 200, @@ -103,7 +103,7 @@ export const saveAdminWarningReason = async ({ id === undefined ? await fetcherClient({ plugin: manifest.id, - args: { body: values as AdminWarningReasonInput }, + args: { body: values }, method: "post", module: "admin/warnings", options, diff --git a/packages/vitnode/src/views/moderation/warnings/member-log.tsx b/packages/vitnode/src/views/moderation/warnings/member-log.tsx new file mode 100644 index 0000000000..e76590a8ec --- /dev/null +++ b/packages/vitnode/src/views/moderation/warnings/member-log.tsx @@ -0,0 +1,215 @@ +import type { LucideIcon } from "lucide-react"; + +import { useInfiniteQuery } from "@tanstack/react-query"; +import { Link } from "@tanstack/react-router"; +import { + ChevronDownIcon, + HistoryIcon, + ShieldAlertIcon, + ShieldOffIcon, + TriangleAlertIcon, + Undo2Icon, +} from "lucide-react"; +import React from "react"; +import { useFormatter, useTranslations } from "use-intl"; + +import { DateFormat } from "@/components/date-format"; +import { Button } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; + +import type { ModcpMemberLogEntry } from "./warnings-api"; + +import { PersonName } from "../modcp/report-tone"; +import { memberLogQueryOptions } from "./warnings-api"; + +const ICONS: Record = { + "sanction.applied": ShieldAlertIcon, + "sanction.revoked": ShieldOffIcon, + "warning.issued": TriangleAlertIcon, + "warning.revoked": Undo2Icon, +}; + +const numberOf = (value: unknown): null | number => + typeof value === "number" ? value : null; + +const EntryText = ({ + entry, +}: { + entry: ModcpMemberLogEntry; +}): React.ReactNode => { + const t = useTranslations("core.moderation.warn.log"); + const format = useFormatter(); + const { metadata } = entry; + const actor = () => + entry.actor ? ( + + ) : ( + (entry.actorName ?? t("someone")) + ); + const points = numberOf(metadata.points) ?? 0; + + switch (entry.action) { + case "sanction.applied": { + const endsAt = + typeof metadata.endsAt === "string" ? new Date(metadata.endsAt) : null; + const values = { + actor, + threshold: numberOf(metadata.thresholdPoints) ?? 0, + }; + + return endsAt + ? t.rich("sanction_applied_until", { + ...values, + endsAt: format.dateTime(endsAt, { + dateStyle: "medium", + timeStyle: "short", + }), + }) + : t.rich("sanction_applied_indefinite", values); + } + case "sanction.revoked": + return t.rich("sanction_revoked", { actor }); + case "warning.issued": + return t.rich("warning_issued", { + actor, + after: numberOf(metadata.pointsAfter) ?? points, + before: numberOf(metadata.pointsBefore) ?? 0, + points, + }); + case "warning.revoked": + return t.rich("warning_revoked", { actor, points }); + default: + return t.rich("other", { action: entry.action, actor }); + } +}; + +const EntryDetails = ({ entry }: { entry: ModcpMemberLogEntry }) => { + const t = useTranslations("core.moderation.warn.log"); + const { metadata } = entry; + const reportId = numberOf(metadata.reportId); + const details = [ + metadata.pointsOverridden === true + ? t("points_overridden", { + points: numberOf(metadata.defaultPoints) ?? 0, + }) + : null, + metadata.expirationOverridden === true ? t("expiration_overridden") : null, + ].filter((detail): detail is string => detail !== null); + + if (details.length === 0 && reportId === null) return null; + + return ( +

+ {details.map(detail => ( + {detail} + ))} + {reportId === null ? null : ( + + {t("report", { id: reportId })} + + )} +

+ ); +}; + +const PendingRows = () => ( + +); + +/** + * The member's moderation log: who warned them, revoked a warning, or applied + * and lifted a suspension, from the central activity log. Never shows staff + * notes. Only for moderators with `can_view`. + */ +export const MemberLog = ({ userId }: { userId: number }) => { + const t = useTranslations("core.moderation.warn.log"); + const titleId = React.useId(); + const query = useInfiniteQuery(memberLogQueryOptions(userId)); + const entries = query.data?.pages.flatMap(page => page.edges) ?? []; + + return ( +
+

+ {t("title")} +

+ +
+ {query.isPending ? ( + <> + + {t("loading")} + + + + ) : query.isError ? ( +

+ {t("error")} +

+ ) : entries.length === 0 ? ( +

+

+ ) : ( +
    + {entries.map(entry => { + const Icon = ICONS[entry.action] ?? HistoryIcon; + + return ( +
  1. + + +
    +

    + +

    + +

    + +

    +
    +
  2. + ); + })} +
+ )} + + {query.hasNextPage ? ( + + ) : null} +
+
+ ); +}; diff --git a/packages/vitnode/src/views/moderation/warnings/modcp-member-content.tsx b/packages/vitnode/src/views/moderation/warnings/modcp-member-content.tsx index 7c767c2b57..c0c52cad93 100644 --- a/packages/vitnode/src/views/moderation/warnings/modcp-member-content.tsx +++ b/packages/vitnode/src/views/moderation/warnings/modcp-member-content.tsx @@ -8,6 +8,7 @@ import { PageTitle } from "@/components/ui/page-title"; import { WarnDialog } from "@/tanstack/moderation/warn-button"; import { Person } from "../modcp/report-tone"; +import { MemberLog } from "./member-log"; import { SanctionCard, StaffWarningCard } from "./warning-list"; import { memberStandingQueryOptions } from "./warnings-api"; @@ -132,6 +133,8 @@ export const ModcpMemberContent = ({ userId }: { userId: number }) => { )}
+ {permissions.viewLog ? : null} + ); diff --git a/packages/vitnode/src/views/moderation/warnings/warning-list.tsx b/packages/vitnode/src/views/moderation/warnings/warning-list.tsx index 07e2134f20..05c96f1387 100644 --- a/packages/vitnode/src/views/moderation/warnings/warning-list.tsx +++ b/packages/vitnode/src/views/moderation/warnings/warning-list.tsx @@ -177,6 +177,7 @@ export const StaffWarningCard = ({ ? resolveReasonText(warning.reason, locale) : t("custom_reason"); const titleId = React.useId(); + const { revokedAt } = warning; return (
) : null} - {warning.revokedAt ? ( + {revokedAt ? (

{t.rich("list.revoked", { by: () => , - date: () => , + date: () => , })} {warning.revokeReason ? ` ${warning.revokeReason}` : null}

) : null} - {warning.sanctionId && warning.revokedAt ? ( + {warning.sanctionId && revokedAt ? (

{t("list.sanction_stays")}

@@ -343,6 +344,7 @@ export const SanctionCard = ({ }) => { const t = useTranslations("core.moderation.warn"); const [lifting, setLifting] = React.useState(false); + const { revokedAt } = sanction; return (
@@ -410,11 +412,11 @@ export const SanctionCard = ({ ) : null} - {sanction.revokedAt ? ( + {revokedAt ? (

{t.rich("list.lifted", { by: () => , - date: () => , + date: () => , })} {sanction.revokeReason ? ` ${sanction.revokeReason}` : null}

diff --git a/packages/vitnode/src/views/moderation/warnings/warnings-api.ts b/packages/vitnode/src/views/moderation/warnings/warnings-api.ts index 9ea7595444..cc715a7ede 100644 --- a/packages/vitnode/src/views/moderation/warnings/warnings-api.ts +++ b/packages/vitnode/src/views/moderation/warnings/warnings-api.ts @@ -1,4 +1,8 @@ -import { keepPreviousData, queryOptions } from "@tanstack/react-query"; +import { + infiniteQueryOptions, + keepPreviousData, + queryOptions, +} from "@tanstack/react-query"; import { fetcherClient } from "@/lib/fetcher-client"; import { manifest } from "@/manifest"; @@ -78,6 +82,39 @@ export const memberStandingQueryOptions = (userId: number) => staleTime: 10_000, }); +const fetchMemberLog = async (userId: number, cursor: null | string) => { + const response = await fetcher({ + plugin: manifest.id, + args: { + params: { id: userId }, + query: cursor ? { cursor } : {}, + }, + method: "get", + module: "moderation", + path: "/members/{id}/log", + }); + if (!response.ok) { + throw new ModcpRequestError(response.status, "the member's log"); + } + + return await response.json(); +}; + +export type ModcpMemberLogEntry = Awaited< + ReturnType +>["edges"][number]; + +export const memberLogQueryOptions = (userId: number) => + infiniteQueryOptions({ + getNextPageParam: (last: Awaited>) => + last.pageInfo.hasNextPage ? last.pageInfo.endCursor : undefined, + initialPageParam: null as null | string, + queryFn: async ({ pageParam }) => await fetchMemberLog(userId, pageParam), + queryKey: [...WARNINGS_QUERY_ROOT, "member-log", userId], + retry: false, + staleTime: 10_000, + }); + export interface ModcpWarningsFilter { issuedById?: number; page: number; From c6347d991a8292b9a2e3087ba24b19148fce46a2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 19:50:00 +0000 Subject: [PATCH 4/9] =?UTF-8?q?refactor(moderation):=20=E2=99=BB=EF=B8=8F?= =?UTF-8?q?=20tighten=20warning=20copy=20and=20React=20Doctor=20findings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 Claude-Session: https://claude.ai/code/session_01CnAiFc4WPMdVa99L7jsTXY --- .../dev/moderation/configure-warnings.mdx | 2 +- .../content/docs/dev/moderation/warnings.mdx | 2 +- .../src/api/lib/notifications/core-types.ts | 23 ++- .../warnings/routes/warning-reasons.routes.ts | 22 ++- .../moderation/warnings.postgres.test.ts | 3 + .../moderation/warnings/warning-list.tsx | 177 ++++++++++-------- 6 files changed, 134 insertions(+), 95 deletions(-) diff --git a/apps/web/content/docs/dev/moderation/configure-warnings.mdx b/apps/web/content/docs/dev/moderation/configure-warnings.mdx index 44579cc823..3780b7f4c4 100644 --- a/apps/web/content/docs/dev/moderation/configure-warnings.mdx +++ b/apps/web/content/docs/dev/moderation/configure-warnings.mdx @@ -53,7 +53,7 @@ Open **AdminCP → Moderation → Warning thresholds** and choose **Add threshol | Active points | Action | Duration | | --- | --- | --- | | 8 | Temporary suspension | 24 hours | -| 10 | Indefinite suspension | - | +| 10 | Indefinite suspension | None | (); + +/** Emails have no viewer time zone, so suspension ends are written in UTC. */ +const utcDateTimeFormat = (locale: string): Intl.DateTimeFormat => { + let format = utcDateTimeFormats.get(locale); + if (!format) { + format = new Intl.DateTimeFormat(locale, { + dateStyle: "medium", + timeStyle: "short", + timeZone: "UTC", + }); + utcDateTimeFormats.set(locale, format); + } + + return format; +}; + /** Sent to a member when a moderator warns them. Never carries staff notes. */ export const warningIssuedNotification = buildNotificationType({ id: "core.warning_issued", @@ -99,11 +116,7 @@ export const accountSuspendedNotification = buildNotificationType({ present: ({ data, locale, t }) => ({ body: data.endsAt ? t("core.notifications.types.account_suspended.body_until", { - endsAt: new Intl.DateTimeFormat(locale, { - dateStyle: "medium", - timeStyle: "short", - timeZone: "UTC", - }).format(new Date(data.endsAt)), + endsAt: utcDateTimeFormat(locale).format(new Date(data.endsAt)), }) : t("core.notifications.types.account_suspended.body_indefinite"), target: ACCOUNT_STATUS_PATH, diff --git a/packages/vitnode/src/api/modules/admin/warnings/routes/warning-reasons.routes.ts b/packages/vitnode/src/api/modules/admin/warnings/routes/warning-reasons.routes.ts index ed36aabb72..97acbfc00f 100644 --- a/packages/vitnode/src/api/modules/admin/warnings/routes/warning-reasons.routes.ts +++ b/packages/vitnode/src/api/modules/admin/warnings/routes/warning-reasons.routes.ts @@ -61,16 +61,18 @@ const saveReasonWords = async ( reasonId: number, body: Partial>, ) => { - for (const variable of ["title", "description", "message"] as const) { - const values = body[variable]; - if (values === undefined) continue; - await saveLanguageWords(c, { - ...WARNING_REASON_WORDS, - itemId: reasonId, - values: values.filter(item => item.value.length > 0), - variable, - }); - } + await Promise.all( + (["title", "description", "message"] as const).map(async variable => { + const values = body[variable]; + if (values === undefined) return; + await saveLanguageWords(c, { + ...WARNING_REASON_WORDS, + itemId: reasonId, + values: values.filter(item => item.value.length > 0), + variable, + }); + }), + ); }; /** What an audit entry keeps of a reason: settings, never the words. */ diff --git a/packages/vitnode/src/api/modules/moderation/warnings.postgres.test.ts b/packages/vitnode/src/api/modules/moderation/warnings.postgres.test.ts index 20a80c2a6e..3ba0f92380 100644 --- a/packages/vitnode/src/api/modules/moderation/warnings.postgres.test.ts +++ b/packages/vitnode/src/api/modules/moderation/warnings.postgres.test.ts @@ -59,6 +59,9 @@ describe.skipIf(!POSTGRES_TEST_URL)("issuing warnings on PostgreSQL", () => { c.set("notifications", { publish, } as unknown as Context["var"]["notifications"]); + c.set("realtime", { + closeUserConnections: () => undefined, + } as unknown as Context["var"]["realtime"]); c.set("core", { moderation: { scopes: [], targets: [] }, permissionStaff: [], diff --git a/packages/vitnode/src/views/moderation/warnings/warning-list.tsx b/packages/vitnode/src/views/moderation/warnings/warning-list.tsx index 05c96f1387..00597f9317 100644 --- a/packages/vitnode/src/views/moderation/warnings/warning-list.tsx +++ b/packages/vitnode/src/views/moderation/warnings/warning-list.tsx @@ -159,6 +159,103 @@ const PersonOrDeleted = ({ person }: { person: null | ReportPerson }) => { return person ? : <>{t("deleted_user")}; }; +/** Who, when, what for: the facts of one warning. */ +const WarningFacts = ({ + member, + warning, +}: { + member?: null | ReportPerson; + warning: ModcpStaffWarning; +}) => { + const t = useTranslations("core.moderation.warn"); + + return ( +
+ {member !== undefined ? ( + + {member ? ( + + + + ) : ( + + )} + + ) : null} + + + + + {warning.expiresAt ? ( + + ) : ( + t("list.never") + )} + + {warning.defaultPoints !== null && + warning.defaultPoints !== warning.points ? ( + + {t("points", { count: warning.defaultPoints })} + + ) : null} + {warning.content ? ( + + {warning.content.url ? ( + + {warning.content.title ?? t("list.untitled")} + + ) : ( + (warning.content.title ?? t("list.untitled")) + )} + + ) : null} + {warning.reportId ? ( + + + + ) : null} +
+ ); +}; + +/** Staff notes and override explanations - `null` from the API unless the moderator may read them. */ +const StaffNotes = ({ warning }: { warning: ModcpStaffWarning }) => { + const t = useTranslations("core.moderation.warn"); + if (!warning.staffNote && !warning.overrideNote) return null; + + return ( +
+

+

+ {warning.staffNote ? ( + + {warning.staffNote} + + ) : null} + {warning.overrideNote ? ( + + {warning.overrideNote} + + ) : null} +
+ ); +}; + /** One warning as staff see it, with Revoke when allowed. */ export const StaffWarningCard = ({ canRevoke, @@ -216,89 +313,13 @@ export const StaffWarningCard = ({
-
- {member !== undefined ? ( - - {member ? ( - - - - ) : ( - - )} - - ) : null} - - - - - {warning.expiresAt ? ( - - ) : ( - t("list.never") - )} - - {warning.defaultPoints !== null && - warning.defaultPoints !== warning.points ? ( - - {t("points", { count: warning.defaultPoints })} - - ) : null} - {warning.content ? ( - - {warning.content.url ? ( - - {warning.content.title ?? t("list.untitled")} - - ) : ( - (warning.content.title ?? t("list.untitled")) - )} - - ) : null} - {warning.reportId ? ( - - - - ) : null} -
+ {warning.message} - {warning.staffNote || warning.overrideNote ? ( -
-

-

- {warning.staffNote ? ( - - {warning.staffNote} - - ) : null} - {warning.overrideNote ? ( - - - {warning.overrideNote} - - - ) : null} -
- ) : null} + {revokedAt ? (

From 83ee169b045a6d2aaefae12810cbd34fa2b8f1fc Mon Sep 17 00:00:00 2001 From: aXenDeveloper Date: Fri, 6 Nov 2026 12:11:09 +0100 Subject: [PATCH 5/9] =?UTF-8?q?feat(warnings):=20=E2=9C=A8=20add=20standin?= =?UTF-8?q?g=20confirmation=20and=20management=20components?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 1 + .../docs/dev/moderation/issue-warnings.mdx | 6 +- .../content/docs/dev/moderation/warnings.mdx | 10 + apps/web/src/page-cache.gen.ts | 1 + packages/vitnode/src/locales/en.json | 57 ++ .../vitnode/src/tanstack/profile/screen.tsx | 69 +-- .../src/views/profile/profile-content.tsx | 14 +- .../profile/warnings/profile-warnings.tsx | 536 ++++++++++++++++ .../warnings/standing-confirm-content.tsx | 179 ++++++ .../views/profile/warnings/standing-parts.tsx | 95 +++ .../warnings/standing-sheet-content.tsx | 571 ++++++++++++++++++ .../src/views/profile/warnings/standing.ts | 185 ++++++ 12 files changed, 1674 insertions(+), 50 deletions(-) create mode 100644 packages/vitnode/src/views/profile/warnings/profile-warnings.tsx create mode 100644 packages/vitnode/src/views/profile/warnings/standing-confirm-content.tsx create mode 100644 packages/vitnode/src/views/profile/warnings/standing-parts.tsx create mode 100644 packages/vitnode/src/views/profile/warnings/standing-sheet-content.tsx create mode 100644 packages/vitnode/src/views/profile/warnings/standing.ts diff --git a/AGENTS.md b/AGENTS.md index ba42eb4a6f..850a4165af 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -117,6 +117,7 @@ Pick the error view by what failed: - If you override a background color, you MUST override its text color for contrast. - Prefer semantic design tokens (`bg-background`, `text-foreground`). - Every action button gets a `lucide-react` icon before its label (`aria-hidden`) - Save, Delete, Reply, Share, Try again, Sign in, submit buttons and confirm buttons in dialogs. The only exception is Cancel (and other dismiss buttons that just close something). +- Text links underline on hover (`underline-offset-4 hover:underline`), never just a color change. Links styled as buttons (`buttonVariants`) are the exception. - Use the Tailwind spacing scale (`p-4`, `mx-2`) - never arbitrary values (`p-[16px]`). - Use `gap-*` classes for spacing. Never use `space-*`, and never mix margin/padding with gap on the same element. - Use semantic (`items-center`, `justify-between`) and responsive (`md:grid-cols-2`) classes. diff --git a/apps/web/content/docs/dev/moderation/issue-warnings.mdx b/apps/web/content/docs/dev/moderation/issue-warnings.mdx index 32fc659091..8bd4e6e982 100644 --- a/apps/web/content/docs/dev/moderation/issue-warnings.mdx +++ b/apps/web/content/docs/dev/moderation/issue-warnings.mdx @@ -39,7 +39,7 @@ Pick the entry point that matches what you are looking at: | --- | --- | | A comment | **More actions** (`…`) → **Warn author** | | A report | **Warn author** under the status button in the ModCP report | -| A profile | **Warn** next to **Warnings** on the member's profile | +| A profile | **Warn member** on the warnings card in the profile's side column | | The ModCP | **Moderator CP → Warnings → Find a member**, then **Warn member** | Each button appears only when the server says you may use it. @@ -80,7 +80,9 @@ Choose **Issue warning**. The toast says how many active points the member has n ### Revoke a warning or lift a suspension -Open the member in **Moderator CP → Warnings** (or **Warnings** on their profile). **Revoke** on a warning asks for a reason; its points stop counting at once and the member is notified. The warning stays in the history, marked **Revoked**. +Open the member in **Moderator CP → Warnings**, or use the warnings card on their profile. On the card, the ↺ button next to a warning revokes it and the shield button next to an active suspension lifts it. Click a warning or suspension to open its details in a side sheet, with the same action at the bottom. + +**Revoke** asks for a reason, which is kept for staff. The warning's points stop counting at once and the member is notified. The warning stays in the history, marked **Revoked**. Revoking a warning never lifts the suspension it caused. Lift it separately with **Lift** on the suspension. Other active suspensions keep applying. diff --git a/apps/web/content/docs/dev/moderation/warnings.mdx b/apps/web/content/docs/dev/moderation/warnings.mdx index 9744d25189..04ca26dda2 100644 --- a/apps/web/content/docs/dev/moderation/warnings.mdx +++ b/apps/web/content/docs/dev/moderation/warnings.mdx @@ -37,6 +37,16 @@ The server checks suspensions when it reads the session, for every request: core The member keeps their session. A strip under the header says the account is suspended and links to **Account status** at `/account-status`, which shows the suspension end time, active points and the warning history with each message. Members reach the same page from **Settings → Account status** at any time. It never shows staff notes or which moderator issued a warning. +## On a member's profile + +The profile's side column shows a warnings card with the member's active points, any active suspension and their latest warnings and suspensions. Click any of them to read the full details in a side sheet. + +Who sees it: + +- **The member**, on their own profile. They see their messages, points and suspensions, never staff notes or which moderator warned them. +- **Moderators** who may view or issue warnings, on every member's profile. They also get **Warn member**, revoke and lift buttons, each shown only when their permissions allow it. +- **Everyone else** sees nothing. The card isn't part of the cached page, so a warning never leaks to visitors. + ## Where the history lives Every warning, revocation and suspension is written to the [Activity Logs](/docs/dev/audit-logs), in the same transaction as the change. Administrators see them in **AdminCP → Staff → Moderators → Logs**. Moderators with **View every member's warnings** see one member's entries as the **Moderation log** on that member's ModCP page. The entries hold points, threshold decisions and links to reports, never staff notes, override explanations or revocation reasons. Changes to warning reasons and thresholds land in the administrators' log. diff --git a/apps/web/src/page-cache.gen.ts b/apps/web/src/page-cache.gen.ts index 2ccdea3b66..70d1b19cd3 100644 --- a/apps/web/src/page-cache.gen.ts +++ b/apps/web/src/page-cache.gen.ts @@ -55,6 +55,7 @@ export const pageCacheRoutes: PageCacheRouteTable = [ { path: '/blog/article/:slug', maxAge: 300, staleWhileRevalidate: 60 }, { path: '/blog/category/:slug', maxAge: 300, staleWhileRevalidate: 60 }, { path: '/blog/comments/:slug' }, + { path: '/blog/preview/:token' }, { path: '/forum/comments/:slug' }, { path: '/forum/topics/:slug', maxAge: 60, staleWhileRevalidate: 30 }, { path: '/login/sso/:providerId' }, diff --git a/packages/vitnode/src/locales/en.json b/packages/vitnode/src/locales/en.json index c6c1653ab8..252be58574 100644 --- a/packages/vitnode/src/locales/en.json +++ b/packages/vitnode/src/locales/en.json @@ -3140,6 +3140,63 @@ "indefinite": "Your account is suspended.", "details": "See details" } + }, + "profile_card": { + "label": "Warnings", + "points": "{count, plural, one {# warning point} other {# warning points}}", + "no_restrictions": "No restrictions being applied", + "suspended_until": "Suspended · ends {relative}", + "suspended_indefinitely": "Suspended indefinitely", + "warn": "Warn member", + "lift": "Lift suspension", + "revoke": "Revoke warning", + "revoke_label": "Revoke warning: {reason}", + "see_all": "See all", + "history": "Warnings and suspensions", + "empty": "No warnings yet. Anything issued shows up here, newest first.", + "empty_own": "You have no warnings. Keep it up!", + "hidden": "{count, plural, one {# more warning is} other {# more warnings are}} outside your moderation scope.", + "loading": "Loading warnings", + "load_error": { + "title": "Warnings didn't load", + "desc": "Check your connection, then try again." + }, + "retry": "Try again", + "ends": "ends {relative}", + "sheet": { + "warning_desc": "Warning · issued {relative}", + "suspension_desc": "Suspension · started {relative}", + "message": "Message to the member", + "message_own": "Message from the moderators", + "points": "Points", + "not_counting": "not counting", + "issued": "Issued", + "expires": "Expires", + "expired": "Expired", + "never": "Never", + "stops_counting": "Stops counting {relative}", + "staff_note": "Staff note", + "staff_only": "Only staff can see this", + "led_to": "Led to", + "caused_by": "Caused by", + "revoked": "Revoked", + "time_served": "Time served", + "served": "{percent}% served", + "no_end_desc": "The account stays suspended until a moderator lifts it.", + "started": "Started", + "ended": "Ended", + "why": "Why", + "blocks": "What it blocks", + "blocks_desc": "The member can browse like a visitor, but can't post, comment or change anything on their account." + }, + "confirm": { + "reason_desc": "Staff only. Saved in the moderation log.", + "reason_required": "Add a short reason so other staff know why.", + "reason_min": "Use at least {count} characters.", + "reason_max": "Keep it under {count} characters.", + "revoke_failed": "Could not revoke the warning", + "lift_failed": "Could not lift the suspension" + } } }, "activity_logs": { diff --git a/packages/vitnode/src/tanstack/profile/screen.tsx b/packages/vitnode/src/tanstack/profile/screen.tsx index f49c0aebc2..9f2eca890d 100644 --- a/packages/vitnode/src/tanstack/profile/screen.tsx +++ b/packages/vitnode/src/tanstack/profile/screen.tsx @@ -6,24 +6,27 @@ import { } from "@tanstack/react-query"; import { Link } from "@tanstack/react-router"; import { cn } from "cn"; -import { ShieldIcon, TriangleAlertIcon, UserRoundPenIcon } from "lucide-react"; +import { UserRoundPenIcon } from "lucide-react"; import React from "react"; import { useTranslations } from "use-intl"; -import { Button, buttonVariants } from "@/components/ui/button"; +import { buttonVariants } from "@/components/ui/button"; import { UserPresenceStatus } from "@/components/user-presence-status"; import { USER_HEADER_HREF } from "@/views/layouts/theme/header/user/user-header-model"; -import { memberStandingQueryOptions } from "@/views/moderation/warnings/warnings-api"; import { userPresenceQueryOptions } from "@/views/presence/presence-query"; import { ProfileContent } from "@/views/profile/profile-content"; import { userProfileQueryKey } from "@/views/profile/profile-query"; import { profileTimelineQueryOptions } from "@/views/profile/profile-timeline-query"; +import { + ProfileWarnings, + ProfileWarningsSkeleton, +} from "@/views/profile/warnings/profile-warnings"; import { SearchFeedList } from "@/views/search/search-feed-content"; import { useSessionQuery } from "../auth/session-query"; import { useLocale } from "../i18n/locale"; import { RouteMessages } from "../i18n/route-messages"; -import { WarnDialog } from "../moderation/warn-button"; +import { REPORT_NAMESPACES } from "../moderation/report-button"; import { useOwnUserImageEditor } from "./own-image-editor"; import { userProfileQuery } from "./query"; import { PROFILE_NAMESPACES } from "./route"; @@ -74,42 +77,19 @@ const ProfilePresence = ({ ); }; -/** - * Warn and View warnings for moderators. Shown only when the server's - * standing route answers for this member - it decides who may warn directly. - */ -const ModeratorActions = ({ userId }: { userId: number }) => { - const t = useTranslations("core.profile"); - const [open, setOpen] = React.useState(false); - const { data } = useQuery(memberStandingQueryOptions(userId)); - if (!data) return null; - - return ( -

- -
- ); -}; +const ProfileWarningsSlot = ({ + isOwner, + userId, +}: { + isOwner: boolean; + userId: number; +}) => ( + }> + + + + +); const ProfileTimeline = ({ userId, @@ -155,11 +135,10 @@ const ProfileScreen = ({ children, nameCode }: ProfileRouteProps) => { return ( - ) : session?.user?.isModerator ? ( - + action={isOwner ? : null} + aside={ + isOwner || session?.user?.isModerator ? ( + ) : null } editor={editor} diff --git a/packages/vitnode/src/views/profile/profile-content.tsx b/packages/vitnode/src/views/profile/profile-content.tsx index 39a2bce489..42ac43b9fd 100644 --- a/packages/vitnode/src/views/profile/profile-content.tsx +++ b/packages/vitnode/src/views/profile/profile-content.tsx @@ -73,6 +73,8 @@ const SecondaryRoles = ({ roles }: { roles: ProfileRole[] }) => { export interface ProfileContentProps { action?: React.ReactNode; + /** Rendered at the top of the side column, above the About card - the warnings card, for example. */ + aside?: React.ReactNode; /** Rendered beside the About card on large screens - a feed, posts, anything the host adds. */ children?: React.ReactNode; editor?: UserImageEditor; @@ -83,6 +85,7 @@ export interface ProfileContentProps { export const ProfileContent = ({ action, + aside, children, editor, presence, @@ -97,7 +100,7 @@ export const ProfileContent = ({ year: "numeric", }); const joinedIso = joinedAt.toISOString(); - const hasAside = user.secondaryRoles.length > 0; + const hasAside = Boolean(aside) || user.secondaryRoles.length > 0; return (
@@ -183,12 +186,17 @@ export const ProfileContent = ({ hasAside && children ? "lg:grid-cols-3" : null, )} > - + {hasAside ? ( +
+ {aside} + +
+ ) : null} {children ? (
diff --git a/packages/vitnode/src/views/profile/warnings/profile-warnings.tsx b/packages/vitnode/src/views/profile/warnings/profile-warnings.tsx new file mode 100644 index 0000000000..d2c01639db --- /dev/null +++ b/packages/vitnode/src/views/profile/warnings/profile-warnings.tsx @@ -0,0 +1,536 @@ +import { useQuery } from "@tanstack/react-query"; +import { Link } from "@tanstack/react-router"; +import { cn } from "cn"; +import { + ChevronRightIcon, + MessageSquareWarningIcon, + RotateCcwIcon, + RotateCwIcon, + ShieldOffIcon, + TriangleAlertIcon, +} from "lucide-react"; +import React from "react"; +import { useFormatter, useLocale, useNow, useTranslations } from "use-intl"; + +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardFooter } from "@/components/ui/card"; +import { Dialog, DialogContent } from "@/components/ui/dialog"; +import { + Empty, + EmptyContent, + EmptyDescription, + EmptyHeader, + EmptyMedia, + EmptyTitle, +} from "@/components/ui/empty"; +import { FEEDBACK_TONE_TEXT } from "@/components/ui/feedback-tone"; +import { Sheet, SheetContent } from "@/components/ui/sheet"; +import { Skeleton } from "@/components/ui/skeleton"; +import { TooltipWithContent } from "@/components/ui/tooltip"; +import { WarnDialog } from "@/tanstack/moderation/warn-button"; +import { ModcpRequestError } from "@/views/moderation/modcp/modcp-query"; +import { + accountStatusQueryOptions, + memberStandingQueryOptions, +} from "@/views/moderation/warnings/warnings-api"; + +import type { + ProfileStanding, + StandingConfirm, + StandingEntry, + StandingTarget, +} from "./standing"; + +import { + activeSuspensionOf, + standingEntries, + standingFromAccountStatus, + standingFromModcp, +} from "./standing"; +import { + reasonTextOf, + SuspensionLength, + SuspensionNode, + SuspensionTitle, + WarningNode, +} from "./standing-parts"; + +const StandingSheetContent = React.lazy( + async () => await import("./standing-sheet-content"), +); + +const StandingConfirmContent = React.lazy( + async () => await import("./standing-confirm-content"), +); + +const VISIBLE_ENTRIES = 4; + +const RowAction = ({ + entry, + onConfirm, + standing, +}: { + entry: StandingEntry; + onConfirm: (confirm: StandingConfirm) => void; + standing: ProfileStanding; +}) => { + const t = useTranslations("core.moderation.profile_card"); + const tWarn = useTranslations("core.moderation.warn"); + const locale = useLocale(); + + if ( + entry.kind === "warning" && + standing.can.revoke && + entry.warning.status !== "revoked" + ) { + const { warning } = entry; + + return ( + + + + ); + } + + if ( + entry.kind === "suspension" && + standing.can.lift && + entry.suspension.active + ) { + const { suspension } = entry; + + return ( + + + + ); + } + + return null; +}; + +const EntryRow = ({ + entry, + last, + onConfirm, + onOpen, + standing, +}: { + entry: StandingEntry; + last: boolean; + onConfirm: (confirm: StandingConfirm) => void; + onOpen: (target: StandingTarget) => void; + standing: ProfileStanding; +}) => { + const t = useTranslations("core.moderation.profile_card"); + const tWarn = useTranslations("core.moderation.warn"); + const tStatus = useTranslations("core.moderation.warn.statuses"); + const format = useFormatter(); + const now = useNow({ updateInterval: 60_000 }); + const locale = useLocale(); + const revoked = + entry.kind === "warning" && entry.warning.status === "revoked"; + + return ( +
  • +
  • + ); +}; + +const StatusLine = ({ standing }: { standing: ProfileStanding }) => { + const t = useTranslations("core.moderation.profile_card"); + const format = useFormatter(); + const now = useNow({ updateInterval: 60_000 }); + const suspension = activeSuspensionOf(standing); + + if (!suspension) { + return ( +

    + {t("no_restrictions")} +

    + ); + } + + return ( +

    + {suspension.endsAt + ? t("suspended_until", { + relative: format.relativeTime(suspension.endsAt, now), + }) + : t("suspended_indefinitely")} +

    + ); +}; + +const StandingCard = ({ + standing, + userId, +}: { + standing: ProfileStanding; + userId: number; +}) => { + const t = useTranslations("core.moderation.profile_card"); + const titleId = React.useId(); + const [target, setTarget] = React.useState(null); + const [shownTarget, setShownTarget] = React.useState(target); + const [confirm, setConfirm] = React.useState(null); + const [shownConfirm, setShownConfirm] = React.useState(confirm); + const [warning, setWarning] = React.useState(false); + if (target && target !== shownTarget) setShownTarget(target); + if (confirm && confirm !== shownConfirm) setShownConfirm(confirm); + + const suspension = activeSuspensionOf(standing); + const entries = standingEntries(standing); + const visible = entries.slice(0, VISIBLE_ENTRIES); + const showLift = standing.can.lift && suspension; + + return ( + + +
    +

    + {t("points", { count: standing.activePoints })} +

    + +
    + {showLift || standing.can.warn ? ( +
    + {showLift ? ( + + ) : null} + {standing.can.warn ? ( + + ) : null} +
    + ) : null} +
    + + + {visible.length === 0 ? ( +

    + {standing.own ? t("empty_own") : t("empty")} +

    + ) : ( +
      + {visible.map((entry, index) => ( + + ))} +
    + )} + {standing.hiddenWarnings > 0 ? ( +

    + {t("hidden", { count: standing.hiddenWarnings })} +

    + ) : null} +
    + + {entries.length > 0 ? ( + + + {t("see_all")} + + {entries.length} + + + ) : null} + + { + if (!open) setTarget(null); + }} + open={target !== null} + > + + {shownTarget ? ( + + + + + +
    + } + > + + + ) : null} + + + + { + if (!open) setConfirm(null); + }} + open={confirm !== null} + > + + {shownConfirm ? ( + + + + +
    + } + > + { + setConfirm(null); + }} + /> + + ) : null} + + + + {standing.can.warn ? ( + + ) : null} + + ); +}; + +export const ProfileWarningsSkeleton = ({ label }: { label?: string }) => ( + + {label ? ( + + {label} + + ) : null} + +
    + + +
    + +
    + + {[0, 1, 2].map(row => ( +
    + +
    + + +
    +
    + ))} +
    +
    +); + +const forbidden = (error: unknown) => + error instanceof ModcpRequestError && + (error.status === 401 || error.status === 403); + +export const ProfileWarnings = ({ + isOwner, + userId, +}: { + isOwner: boolean; + userId: number; +}) => { + const t = useTranslations("core.moderation.profile_card"); + const own = useQuery({ ...accountStatusQueryOptions(), enabled: isOwner }); + const staff = useQuery({ + ...memberStandingQueryOptions(userId), + enabled: !isOwner, + }); + const query = isOwner ? own : staff; + + if (query.isPending) return ; + + if (query.isError) { + if (forbidden(query.error)) return null; + + return ( + + + + + {t("load_error.title")} + {t("load_error.desc")} + + + + + + ); + } + + const standing = isOwner + ? own.data + ? standingFromAccountStatus(own.data) + : null + : staff.data + ? standingFromModcp(staff.data) + : null; + + if (!standing) return null; + + return ; +}; diff --git a/packages/vitnode/src/views/profile/warnings/standing-confirm-content.tsx b/packages/vitnode/src/views/profile/warnings/standing-confirm-content.tsx new file mode 100644 index 0000000000..f415d476df --- /dev/null +++ b/packages/vitnode/src/views/profile/warnings/standing-confirm-content.tsx @@ -0,0 +1,179 @@ +import { useQueryClient } from "@tanstack/react-query"; +import { RotateCcwIcon, ShieldOffIcon } from "lucide-react"; +import { toast } from "sonner"; +import { useFormatter, useLocale, useNow, useTranslations } from "use-intl"; +import { z } from "zod"; + +import type { AutoFormOnSubmit } from "@/components/form/auto-form"; + +import { AutoForm } from "@/components/form/auto-form"; +import { AutoFormTextarea } from "@/components/form/fields/textarea"; +import { + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { WARNING_LIMITS } from "@/lib/moderation/warnings"; +import { + ACCOUNT_STATUS_QUERY_KEY, + revokeSanction, + revokeWarning, + WARNINGS_QUERY_ROOT, +} from "@/views/moderation/warnings/warnings-api"; + +import type { StandingConfirm } from "./standing"; + +import { + reasonTextOf, + SuspensionLength, + SuspensionNode, + SuspensionTitle, + WarningNode, +} from "./standing-parts"; + +const Subject = ({ confirm }: { confirm: NonNullable }) => { + const tWarn = useTranslations("core.moderation.warn"); + const format = useFormatter(); + const now = useNow({ updateInterval: 60_000 }); + const locale = useLocale(); + + if (confirm.kind === "revoke") { + const { warning } = confirm; + + return ( +
    + +
    +

    + {reasonTextOf(warning, locale, tWarn("custom_reason"))} +

    +

    + {tWarn("points", { count: warning.points })} + {warning.issuedBy ? ` · ${warning.issuedBy.name}` : null} + {` · ${format.relativeTime(warning.issuedAt, now)}`} +

    +
    +
    + ); + } + + const { suspension } = confirm; + + return ( +
    + +
    +

    + +

    +

    + +

    +
    +
    + ); +}; + +const StandingConfirmContent = ({ + confirm, + onClose, +}: { + confirm: NonNullable; + onClose: () => void; +}) => { + const t = useTranslations("core.moderation.profile_card.confirm"); + const tWarn = useTranslations("core.moderation.warn"); + const queryClient = useQueryClient(); + const revoking = confirm.kind === "revoke"; + const formSchema = z.object({ + reason: z + .string({ error: t("reason_required") }) + .trim() + .min(WARNING_LIMITS.explanationMinLength, { + error: t("reason_min", { count: WARNING_LIMITS.explanationMinLength }), + }) + .max(WARNING_LIMITS.noteMaxLength, { + error: t("reason_max", { count: WARNING_LIMITS.noteMaxLength }), + }), + }); + + const onSubmit: AutoFormOnSubmit = async values => { + const result = + confirm.kind === "revoke" + ? await revokeWarning({ id: confirm.warning.id, reason: values.reason }) + : await revokeSanction({ + id: confirm.suspension.id, + reason: values.reason, + }); + + if (!result.ok) { + toast.error(revoking ? t("revoke_failed") : t("lift_failed"), { + description: tWarn( + `errors.${result.failure === "conflict" ? "unknown" : result.failure}`, + ), + }); + + return; + } + + toast.success( + revoking ? tWarn("revoke.done") : tWarn("sanctions.lifted_done"), + { + description: revoking + ? tWarn("revoke.done_desc") + : tWarn("sanctions.lifted_done_desc"), + }, + ); + await Promise.all([ + queryClient.invalidateQueries({ queryKey: WARNINGS_QUERY_ROOT }), + queryClient.invalidateQueries({ queryKey: ACCOUNT_STATUS_QUERY_KEY }), + ]); + onClose(); + }; + + return ( + <> + + + {revoking ? tWarn("revoke.title") : tWarn("sanctions.lift_title")} + + + {revoking ? tWarn("revoke.desc") : tWarn("sanctions.lift_desc")} + + + + ( + + ), + }, + ]} + formSchema={formSchema} + onSubmit={onSubmit} + submitButtonProps={{ + children: revoking ? ( + <> +