Skip to main content

Reports

Feedback is not criticism — it is the system talking to itself.

A report is a signed-in record that one topic, one branch, or one user should be reviewed. Each user can file one report per target. Reports are not public — there is no .md, no .json, and no entry in the sitemap.

Why reports exist​

Lorediv is built and maintained by its community. As the tree grows, some content will break the rules — spam, impersonation, misleading claims, or material that harms other users. The community needs a way to flag that content so moderators can act on it. Moderators can dismiss a false report, remove the offending topic or branch, or in the case of users who persistently violate good practices, close their account. The goal is to protect the integrity of the tree and the people who contribute to it, without silencing honest disagreement.


1. How it works​

Report sits on the topic menu (⋯) and on the branch menu. If the menu is on a branch, the target is that branch. If it is on the topic, the target is the topic. The author of a topic does not see Report on that topic. The author of a branch does not see it on that branch. Report only appears for signed-in users — a guest does not see it.

flowchart TD
U["Signed-in user clicks Report"] --> D["Dialog"]
D --> R["Select reason<br/>Spam · Duplicate · Misleading<br/>Offensive · Other"]
R --> N["Optional note<br/>max 500 characters"]
N --> S["Confirm"]
S --> DB["Row stored<br/>status = open"]
DB --> W["Discord webhook<br/>if configured"]

The dialog requires one reason: Spam, Duplicate, Misleading, Offensive, or Other. A note is optional and stops at 500 characters. Nothing is stored until the user confirms. The menu then reads Reported and does not send a second report. A repeat from the server responds You already reported this.

Targets the API accepts​

The menu only sends topic or branch. The API also accepts user if that user exists and is not the reporter.

TargetGuard
topicMust exist. Cannot report your own topic if the reporter is the author (topics.user_id).
branchMust exist. Cannot report your own branch if the reporter is the author (branches.proposed_by).
userMust exist. Cannot report yourself if the target is the reporter.

The author check on a branch uses the UUID proposed_by, not the visible author label.


2. What is stored​

One row per user and target. The unique index is (reporter_id, target, target_id).

FieldValue
targettopic, branch, or user
reasonspam, duplicate, misleading, offensive, or other
noteText, or null
statusopen at creation. Then dismissed or removed.
reviewed_by, reviewed_atWho closed the report and when.

A second report from the same user on the same target is rejected at the database level — the unique index catches it before the application layer responds You already reported this.

Discord notification​

After the row is stored, the system sends a webhook notification. The row stays even if the notification fails.

The webhook URL is set through an environment variable. When it is empty, the report is saved and no notification is sent. When it is set, it must be HTTPS on discord.com or discordapp.com, with a path that starts with /api/webhooks/. Timeout is 2.5 seconds. A failure is logged without killing the report.

The message includes: reason, target type, title, note, the reporter's handle (X first, then Google, then GitHub), a link to the topic, and a link to the review queue.


3. Who can review​

A list of user IDs controls who can open the review queue. When that list is empty, the moderation route does not exist.

reports.access returns { allowed }. If it is true, the user's profile menu shows Moderation.

The queue​

The queue lives at /moderation, outside the app group. Non-moderators receive the same result as an unknown route — notFound, not 403. robots.txt disallows /moderation. The page carries noindex.

The queue shows open reports, newest first, up to 100. Each row displays: reason, note, handle, target type, title, link to the topic, date, and how many open reports share that same destination.

flowchart TD
Q["Queue<br/>open reports"] --> D["Dismiss"]
Q --> R["Remove"]
D --> DF["Leaves the content<br/>closes that row<br/>status = dismissed"]
R --> RF["Deletes the topic or branch<br/>other open reports on that<br/>target pass to removed"]
RF --> P["Poll edge case<br/>removing a branch that leaves<br/>a poll at two options or fewer<br/>removes the whole topic"]
ActionWhat happens
DismissLeaves the content in place. Closes only that report row (dismissed).
RemoveDeletes the topic (same as author delete) or the branch (same rules as author delete). All other open reports on that target pass to removed.
Remove on a userBlocked. A user report can only be dismissed. No accounts are deleted.

Removing a branch from a poll that has two root options or fewer removes the whole topic — the same rule that applies when an author deletes a branch. This applies even when the branch is a reply.


4. Limits​

Requires a session. Each operation spends two budgets: the action-specific budget and the shared IP write budget.

ActionUser budgetIP budget
Create a reportreport:create — 5 per 10 minuteswrite:ip — 60 per minute
Review a reportreport:review — 30 per hourwrite:ip — 60 per minute

In production, without Redis, writes are closed. The /api/trpc gateway also charges query:read on these calls. reports.queue and reports.access only spend that gateway token.

There is no public .md or .json for a report. A report does not appear in the sitemap. The moderation page is not a public address on the tree.


Code​

PiecePath
Routerapps/frontend/src/server/routers/reports.ts
Dialogapps/frontend/src/components/ui/ReportDialog.tsx
Menuapps/frontend/src/components/ui/ThreeDotMenu.tsx
Queueapps/frontend/src/app/moderation/page.tsx
Queue UIapps/frontend/src/components/moderation/ModerationQueue.tsx
Remove logicapps/frontend/src/server/lib/moderate-target.ts
Discord webhookapps/frontend/src/lib/discord-report.ts
Moderator checkapps/frontend/src/lib/moderator.ts
Tablepackages/shared/src/schema/reports.ts