docs(teams): phase 5 — discussion, the edit window, and reports that route around leadership
TEAMS.md §5.4, §5.5.7 (new), §5.6 and the Part 12 phase entry; BACKEND_DESIGN.md's schema and admin route tables. **The largest change is a decision, not a description.** §5.6's first rule said "a leader may also see and act on reports for their own Team, but staff always receive them". The org lead settled on 2026-08-18 that the leader half is **decided against, not deferred**: the gap the section exists to close is that leaders moderate their own forum and a Team's leaders are exactly the people who will not report their own Team, so a leader-visible queue hands a complaint about a leader back to them — and a read-only leader view still tells them who reported what. Recorded as an amendment rather than by editing the sentence away, because the reasoning for the original is what makes the correction legible. **§5.6's `content_reports` DDL does not work as written, and the amendment says so rather than quietly swapping it.** With `status` in the unique key, CLOSED rows collide with each other: dismiss a report, let the behaviour recur, dismiss the second one, and the UPDATE lands on a tuple that already exists — so the queue starts throwing duplicate-key errors on the first repeat reporter. The shipped table keys on a generated `open_marker`, the same encoding `team_forum_grants.active_marker` uses. Three smaller departures are recorded beside it: `handled_note`, the two username snapshots §2.10 asks for everywhere else, and a real CASCADE on `team_id`. **New §5.5.7 for `teams_forum_edit_window_minutes`** (0–1440, default 15), and the rule under it: the window is resolved on the server TWICE — the read path stamps `canEdit`/`editableUntil` so a client knows whether to draw the control, the write re-derives it from `created_at` before allowing anything. The read is advice and the write is enforcement, because a time-bounded permission must not take its clock from the party it bounds. That is also why the key is not published: the client needing the number is the admin screen, and the client needing the decision already has it per post. **§5.4 gains three notes its route table does not carry**: thread creation splits authority by TYPE rather than widening the leader gate (and reports it as two booleans, since one would make a client guess which right it described); post moderation is its own route whose validator accepts all eight actions so the model can say "pin applies to a thread, not to a post"; and a reply's three refusal codes are chosen to be distinguishable — 404 absent, 400 announcement, 409 locked — with locked refusing staff too. The Part 12 entry records what the phase disproved, its four acceptance criteria, that it spans ONE repo where phase 4 needed two, and the single defect the live rig found. It also notes that phase 4 shipped `uploads` with the default off, so §5.6's "pull reports forward if uploads is enabled anywhere" never triggered. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -525,7 +525,12 @@ have been found earlier, because until then no caller had ever passed a non-null
|
||||
Design of record: [`MODULE_SYSTEM.md`](MODULE_SYSTEM.md) §2.4; the loader's obligations are
|
||||
[`MODULE_API.md`](MODULE_API.md) Part 4.
|
||||
|
||||
### The eleven Team tables — core's, populated by a module (Teams phases 2–4)
|
||||
### The eleven Team tables — core's, populated by a module (Teams phases 2–5)
|
||||
|
||||
*Twelve rows in the table below: `content_reports` is listed here because Team forum content is its
|
||||
first consumer, and it is deliberately **not** one of the eleven — it carries no `team_*` prefix, its
|
||||
`target_type` is an open VARCHAR, and a wiki page or a news comment is meant to become a value in it
|
||||
rather than a table of its own.*
|
||||
|
||||
A Team is a **core** entity that a **module** answers for. The module says what Teams exist and who is
|
||||
in them, through the team provider; core stores that answer, gates it and displays it. Every table
|
||||
@@ -547,6 +552,37 @@ core's.
|
||||
| `team_forum_posts` | post bodies, sanitised on write through the forum's **own** profile (`utils/forumHtml.js`) and served without re-sanitising. No stored body ever contains an `<img>` |
|
||||
| `team_forum_moderation` | append-only, per Team, recording `actor_role` — WHICH authority was exercised. Deliberately not merged with `mod_actions`/`appeals`, which is Discord-sanction-shaped |
|
||||
| `team_forum_uploads` | attribution for `uploads` mode: who uploaded what, when, how big, and to which post. Also the sweep's worklist |
|
||||
| `content_reports` | member-raised abuse reports (phase 5). **Not a `team_*` table and not named for the forum** — `target_type` is a plain VARCHAR so a wiki page or a news comment becomes a value rather than a table. Team forum content is only the first consumer |
|
||||
|
||||
**Core had no user-facing report flow of any kind before `content_reports`.** `moderation`,
|
||||
`mod_notes` and `appeals` are all either staff-initiated or Discord-sanction-shaped; nothing anywhere
|
||||
let a *member* say "this is a problem". That was survivable while every piece of content on the site
|
||||
came from staff, and stops being the moment a Team forum lets players write to each other. Four
|
||||
properties are worth carrying:
|
||||
|
||||
- **Reports reach site staff and nobody else.** A Team's leaders moderate their own forum, so a
|
||||
leader-visible queue would route a complaint *about* a leader back to that leader. There is one
|
||||
queue, mounted at `/admin/moderation/reports` beside appeals — a staffer working a queue should have
|
||||
one place to work — and no leader-facing counterpart anywhere
|
||||
([`TEAMS.md`](TEAMS.md) §5.6, org lead 2026-08-18).
|
||||
- **A report is not a moderation action.** Filing one changes nothing about the content; it opens a
|
||||
queue item. That keeps it clear of `team_forum_moderation`, which records things that actually
|
||||
happened, and stops "report" becoming a way for any participant to hide anything.
|
||||
- **One OPEN report per (target, reporter)**, enforced by a unique key over a generated `open_marker`
|
||||
that is `1` while open and `NULL` once closed — the same encoding as
|
||||
`team_forum_grants.active_marker`, and for the same reason: only the *live* rows may collide. A
|
||||
closed report frees the slot, so a member whose first report was dismissed may raise the same target
|
||||
again if the behaviour recurs.
|
||||
- **Every transition writes `activity_log`, `dismissed` included.** A queue where acting is audited and
|
||||
declining to act is not is one where the cheapest way to make a report vanish leaves no trace.
|
||||
|
||||
**`teams_forum_edit_window_minutes`** (0–1440, default 15) bounds how long an author may edit their own
|
||||
post; staff are not bound by it. It is resolved on the server **twice** — the read path stamps each
|
||||
post with `canEdit`/`editableUntil` so a client knows whether to draw the control, and the write
|
||||
re-derives it from `created_at` before allowing anything. The read is advice, the write is enforcement,
|
||||
and the split exists because a time-bounded permission must not take its clock from the party it
|
||||
bounds. It is deliberately **not** in `settings.getPublic()`: the client that needs the number is the
|
||||
admin screen, and the client that needs the decision already has it per post.
|
||||
|
||||
**The forum's tables are guarded at the ROUTE and never at the data.** `teams_forums_enabled` off
|
||||
means every forum route answers **404** — not 403, which would advertise a feature the operator
|
||||
@@ -960,6 +996,7 @@ file a route sits in — that is the property the route manifest freezes.
|
||||
| POST | `/teams/:id/unhide` · `/teams/:id/display-name` | the two **gated** actions (§2.9): an admin applies at once, a **moderator** files a pending request and nothing changes publicly. The caller does not choose — the server decides from the role it re-validates on the request |
|
||||
| GET | `/teams/:id/grants` | the full forum-grant ledger, revoked rows included. Read-only in this phase; the grant flow lands with the forums |
|
||||
| POST | `/teams/:id/leader-override` · DELETE `…/:memberKey` | set or clear a staff leadership decision, applied **on top of** the synced value at read time. Not gated: it publishes no game-sourced string |
|
||||
| GET | `/moderation/reports` · POST `…/:id/handle` | the member-raised content-report queue (phase 5, [`TEAMS.md`](TEAMS.md) §5.6) and the staff decision on one. Mounted under **moderation**, not under Teams: a staffer working a queue should have one place to work, and `target_type` is open-ended so the next reportable thing arrives as a row rather than as a screen. Each row carries its target already resolved — a post's excerpt and author, a thread's title, or an upload's uploader, byte size and **sniffed** mimetype — in three batched reads, never one per row. A target hard-deleted since reporting comes back `null` and the row still lists. **There is no leader-facing counterpart to either route**, deliberately |
|
||||
| GET | `/teams/review` | the reserved-name review queue — Teams auto-hidden because their name matched, each showing which term |
|
||||
| GET | `/teams/requests` · POST `…/:id/decide` | the approval queue, and the decision. **Admin only** to decide, checked live rather than from a token claim; a request already decided returns `409`, so two admins deciding at once cannot double-apply |
|
||||
| — | `/shard/*` · `/uo-link/*` | **Served by `module-uo`, not by core** (33 routes). Documented in [`../modules/uo/API.md`](../modules/uo/API.md) |
|
||||
|
||||
Reference in New Issue
Block a user