docs: plan report/block/hide, decide M8's open items, plan demo content

- website/USER_SAFETY.md (new): report, block and hide on the website and
  in the app, S1-S5 decided by the org lead, five questions open.
- android/PLAN.md: M8's three open items decided (keep the rating and add
  report/block/hide; conservative data safety; UnifiedPush on specialUse
  with restart on boot), the order to the cutover, and M21 (Teams and the
  team forum in the app, planned, not scheduled).
- website/DEMO_MODE.md: DM32 and section 11, the demo content plan (two
  seed passes; moderation, Teams/forum/reports, event history, Rust stats).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-29 07:27:21 -05:00
parent 6aeb8afa52
commit df3d43f355
4 changed files with 278 additions and 13 deletions

View File

@@ -498,6 +498,7 @@ down.
| DM29 | (step-5 walk, W2, 2026-10-28) `SECRET_WORDS` also hides harmless fields such as "Allow Master Key" and "TC Auth Only". Narrow it? | **No, leave it as is** |
| DM30 | (step 6, 2026-10-28) How does runicgateway.com `/beta` publish the logins? | **Live from the demo.** `/beta` reads this site's `GET /public/settings` `demo.logins` on its server, caches it for a minute, and links to `/account/login` when the demo does not answer (runicgateway.com D82) |
| DM31 | (step 6) runicgateway.com's "See it running" links were UO routes, which 404 here. What do they do? | **Per game.** A deep link names its game, and that site's mounted `demoGames` (`["rust"]` for this demo) says which games' links show. Rust's point at `/rust` and `/rust/servers/demo?tab=…`, so that site now quotes the fixture's server id `demo` (runicgateway.com D83) |
| DM32 | (2026-10-29) The moderator tour shows empty queues, and other surfaces are empty too (§11). Which get seeded? | **All four of §11's groups:** moderation history, Teams with their forum and reports, event history with the calendar, and Rust player stats |
DNS and HTTPS for `demo.runicgateway.com` come from an external reverse proxy, which the org lead is
setting up. The app sits behind it on plain HTTP; core already trusts one proxy hop for
@@ -647,3 +648,71 @@ through that proxy.
locks). The org lead types the password, ticks the full-access attestation and saves. The closed
track does not exist yet (Test and release shows *Get started* for closed and internal testing), so
there is no opt-in URL for `betaOptInUrl`, and `/beta`'s first gate stays shut.
7. **Demo content** (DM32, §11): the seed's two passes and the rig's clans. Planned 2026-10-29.
One PR in `website` (core seed, the wipe's second pass), one in `Module-Rust` (`onDemoSeed`
stats and feed lines, into `edge`), a new baseline, and a walk of every surface §11 names after
the next wipe.
## 11. Demo content (DM32)
**Why.** A read-only count of the live demo on 2026-10-29 found these surfaces empty after a wipe:
every moderation queue (recent actions, filter hits, spam hits, member events, appeals, reports),
Teams (public, the player's own, staff's review queue), event runs and history, the public calendar
(the three seeded events are never scheduled), `demo-player`'s inbox, and on the Rust side the
leaderboard, wipes, clans and the events feed. Only the four demo accounts exist. A reviewer touring
as `demo-moderator` finds nothing to moderate.
### 11.1 Two seed passes
Some of it can be written the moment the site is up. The rest depends on the rig: Teams are
**module-authoritative** and re-synced every 15 minutes (`TEAMS.md`), so a Team written straight
into core would be archived by the next sync, and the Rust stats need the wipe the rig is on. So:
- **Pass 1, `seed:demo`, as today**: accounts, settings, content, events, and the new core rows
below that need nothing from the rig.
- **Pass 2, `seed:demo --after-rig`**, a new step in `demo-wipe.yml` after the rig reports in and
the first Teams sync has run: Team forum content, reports, Rust stats and feed lines. It waits for
the Teams it names, with a time limit, and fails the wipe if they never appear, as the rig smoke
check does.
The rig's wipe id comes from the restored world save, so it is the same on every wipe
(`w-20261005T165054Z` today). Pass 2 reads it from the server state rather than the fixture, so a
new baseline with a new map still works.
### 11.2 What each group seeds
- **Moderation history** (pass 1, core). Discord-shaped rows for six invented members: a few
`mod_actions` (warn, timeout, ban), `warnings`, `filter_hits` and `spam_hits`, join and leave
`member_events`, staff `mod_notes` (one admin-only), and three `appeals`: one pending to claim,
one under review, one resolved. Times are relative to the seed, so the 24 h / 7 d / 30 d counts
are never zero. There is no bot on the demo, so approving an appeal records the reversal as
failed. That is the shipped behaviour when the bot is down, and the appeal still closes.
- **Teams, forum and reports.** Two or three clans **in the rig's baseline** (the Clans plugin's
data, which already reports an empty board), with stand-in members and `demo-player`'s stand-in
Steam ID in one, so sync brings them in as Teams. One clan's name contains a reserved term, so it
lands in staff's review queue. Pass 2 then writes forum threads and posts in them, and two open
reports. Once report, block and hide exist (`USER_SAFETY.md`), it also writes reports on chat
lines.
- **Event history and the calendar** (pass 1, core). Two finished runs of the seeded events, with
their steps, log, participants (stand-ins, and `demo-player` through its link) and published
results, so the history and results pages fill and `demo-player`'s inbox has the results
notification. The three seeded events are scheduled in the coming days, so the public calendar
shows them.
- **Rust player stats** (pass 2, Module-Rust `onDemoSeed`). About ten stand-in players with wipe
stats (kills, deaths, NPC kills, playtime, gather totals, weapon kills) on the rig's current
wipe, an earlier wipe for the Wipes tab, and a day of feed lines (kills, joins, chat), so the
leaderboard, profiles and feed are populated. The online list stays honest at zero.
### 11.3 Open questions
1. **Stand-in Steam IDs.** Every 17-digit `7656119…` number can belong to a real person, and these
would appear on public leaderboards. *Recommended:* check each candidate against Steam's public
profile page before using it, keep only IDs with no profile, and list them in the fixture with
that check recorded. `demo-player`'s existing stand-in gets the same check.
2. **Invented Discord IDs.** The moderation pages show Discord user ids. *Recommended:* synthetic
ids from a block well below any issued since 2016, named in the fixture as demo data. Nothing on
the demo links them to Discord.
3. **Names.** *Recommended:* invented gamer tags ("Rusty_Pete", "Scrap Baron", …), never a real
person's name, and nothing offensive even in the seeded filter hits, which show a placeholder
word.

138
website/USER_SAFETY.md Normal file
View File

@@ -0,0 +1,138 @@
# Report, block and hide — user safety on the website and in the app
**Status:** plan of record, 2026-10-29. Nothing built yet. Each phase stops for the org lead
before the next one starts.
**Why now.** Google Play's content-rating questionnaire for the Android app (`android/PLAN.md` M8)
was answered "users or user-generated content can be reported". The app shows other players' chat
(the Rust live feed) and has no way to report it or to stop seeing someone. Play's user-generated
content policy expects both wherever that answer is given. The org lead chose to make the answer
true rather than retake the questionnaire (S1).
## 1. What a member can do
| Action | Who | Where | Effect |
|---|---|---|---|
| **Report** | any signed-in member | a chat line (web + app), a forum thread, post or upload (web; app later, §6) | a row in the existing **Reports** queue (`TEAMS.md` §5.6). Changes nothing on its own |
| **Hide for me** | any signed-in member | the same items | that one item disappears from **their own** view, on every device. No report is filed |
| **Block** | any signed-in member | the author of any of those items | everything that author writes (chat lines and forum content) disappears from the blocker's view, on the website and in the app, on every device. Undone from *Account → Blocked* |
| **Hide for everyone** | staff (and, for their own forum, Team leaders, as today) | the Reports queue, the forum's existing moderation menu, and the item's own menu | the item disappears for everybody except staff, who see it with a *Hidden by staff* marker and can undo it |
A report and a hide are separate on purpose, as `TEAMS.md` §5.6 already says: filing a report must
never be a way to hide something. *Hide for me* is the member's own tool, and *Hide for everyone* is
staff's.
## 2. Decisions (org lead, 2026-10-29)
| # | Question | Answer |
|---|---|---|
| S1 | The questionnaire says chat is reportable, and it is not. Retake it, drop chat from the app, or add report/block? | **Add report, block and hide.** The questionnaire stays as answered |
| S2 | Where does a block live? | **On the account, everywhere.** Saved on the site against the blocker's account, so it applies on the website and in the app, on every device |
| S3 | "Hide": who hides, and from whom? | **Both.** A member can hide an item for themself, and staff can hide it for everyone |
| S4 | The app has no Teams or forum screens. Does the app get the forum? | **Plan it** (§6, `android/PLAN.md` M21). In the app, report/block/hide go on chat lines now |
| S5 | When does the Android `edge` → `main` cutover happen? | **After this feature ships**, so the first Play bundle already has it |
## 3. What exists today
- **Reports.** `content_reports` (`TEAMS.md` §5.6) is generic by design, but
`contentReports.model.js` accepts only `team_forum_thread`, `team_forum_post` and
`team_forum_upload`, and finds the owning Team for each. Filing is
`POST /player/teams/:slug/forum/report`. The queue is `GET /admin/moderation/reports`, and
`POST /admin/moderation/reports/:id/handle` claims, actions or dismisses a report. "Actioned" only
records a decision; it does nothing to the content.
- **Staff hide in the forum.** `team_forum_moderation` already records `hide`/`unhide` (and pin,
lock, delete) by a leader or staff, through `POST …/forum/threads/:id/moderate` and
`…/posts/:id/moderate`.
- **Chat lines.** Each one is a `rust_events` row in Module-Rust (`kind = 'player.chat'`, with an
`id`, `server_id`, `steam_id`, time and raw frame). The website and the app both read them from
the public `GET /public/rust/servers/:id/events`. The app polls it signed out or in.
- **No blocks or per-member hides** exist anywhere.
## 4. Design (recommendations; §5 lists what needs an answer)
### 4.1 Core (`website`)
- **Module-owned report targets.** A new optional module hook, `reportTargets`, declares the types
a module's content can be reported as (for Rust: `rust.chat`). Each type answers two questions for
core: `snapshot(id)` (author, text, time, where it was said), and `hide(id, actor)` /
`unhide(id, actor)`. Core files the report, stores the snapshot and runs the queue. The module owns
the content and its hidden flag. This is a `MODULE_API` minor bump, **1.20.0**. The Integration
Kit re-pins and gains a paragraph.
- **One filing route for every target**: `POST /player/reports {targetType, targetId, reason,
detail?}`. Forum types keep working through the existing forum route, which then calls the same
model.
- **`content_reports.snapshot JSON`**: what the item said when it was reported. Chat lines are
pruned with the rest of `rust_events`, and a queue row must still show what was said after that.
Forum reports fill it too, so an author editing a post after the report cannot change what staff
read.
- **The queue gains *Hide for everyone*** beside actioned and dismissed. It calls the forum's
existing hide or the module's `hide`, and records the report as actioned with that action named.
- **Blocks**: `user_blocks (blocker_user_id, kind 'user'|'game', blocked_user_id NULL, module_id
NULL, external_id NULL, display_name snapshot, created_at)`. A game block names a game identity
(for Rust, a Steam ID), because most people in chat have no site account. When that identity is
linked to an account, blocking either one hides both.
- **Per-member hides**: `user_hidden_items (user_id, target_type, target_id, created_at)`.
- **Routes**: `GET/POST/DELETE /auth/me/blocks`, `GET/POST/DELETE /auth/me/hidden`. Their
OpenAPI annotations and spec are regenerated as usual.
- **Filtering.** Signed-in forum reads drop blocked authors and hidden items on the server. The
public chat feed is shared and cached, so it cannot be filtered per reader on the server. The
website and the app filter it with the member's lists instead, read once per session and kept
current after each block or hide. Module client chunks reach those lists through a new client
module-API call.
- **Web UI.** The forum's thread and post menus gain *Hide for me* and *Block author*, beside the
existing *Report*. A new *Account → Blocked* page lists blocks and undoes them.
### 4.2 Module-Rust
- `reportTargets['rust.chat']` with `snapshot` (the speaker's name and Steam ID, the line, server,
time, and the linked account if there is one) and `hide`/`unhide`.
- **Hide for everyone** sets a flag on the `rust_events` row (`hidden_at`, `hidden_by`). The events
endpoint leaves hidden chat lines out for everybody except staff, who get them marked.
- **The live feed's chat lines gain a menu**: *Report*, *Hide for me*, *Block player*, and for
staff *Hide for everyone*.
- On the demo, all of this stays **open**: it is what a Play reviewer tests, and the wipe resets it.
### 4.3 Android app (chat lines only, S4)
- **Long-press a chat line** for the same menu. Report opens a sheet with the six reasons and an
optional detail. A member who is signed out gets *Sign in to report or block*.
- **Account → Blocked** lists blocks and undoes them, mirroring the web page.
- The feed filters with the member's lists, as in §4.1.
## 5. Questions for the org lead
Each has a recommendation; nothing is built until they are answered.
1. **Signed out.** Can a signed-out visitor report a chat line? *Recommended: no.* Reporting, hiding
and blocking all need an account, as forum reports do today. Otherwise the queue is open to
anyone with `curl`. Play needs the mechanism to exist, not to be anonymous.
2. **Blocking someone who has no account.** Can a member block a Steam ID that is linked to nobody?
*Recommended: yes* (§4.1's game block). Almost everyone in a Rust chat feed is unlinked.
3. **Blocking staff.** Can a member block a moderator or admin? *Recommended: yes.* A block only
hides content from the blocker. It changes nothing about staff powers, and it doesn't stop staff
seeing the blocker.
4. **What a block does to the blocked person.** Nothing beyond hiding, *recommended*. There are no
direct messages to stop, and telling someone they were blocked invites the harassment the block
was for.
5. **Staff view of hidden chat.** Do staff still see a line hidden for everyone? *Recommended: yes,
marked*, so a hide can be checked and undone, as the forum already treats its own hidden posts.
## 6. The team forum in the app (planned, not scheduled)
The app has no Teams screens. `android/PLAN.md` M21 plans them: the member's Teams, a Team's page,
the forum's threads and posts (read, reply, new thread), the same report/hide/block menu on posts
and threads, and the forum's push stream. When it ships, the app lets members post content, so the
Play declarations gain "user-generated content: collected". M21 says so.
## 7. Build order
1. **This plan** (docs). Answers to §5 recorded here.
2. **Core** (`website`): §4.1, `MODULE_API` 1.20.0, tests, OpenAPI, `BACKEND_DESIGN.md`,
`MODULE_API.md`, `TEAMS.md` §5.6 amended. One PR.
3. **Module-Rust** (`edge`): §4.2 plus the core-ref pin. One PR. **Integration-kit**: the re-pin and
a paragraph on `reportTargets`. One PR.
4. **Android** (`edge`): §4.3. One PR.
5. **A walk** on the demo after the next wipe: report, hide and block, each on the web and in the
app; *Hide for everyone* from the queue; a blocked Steam ID that later links to an account stays
blocked.
6. Then `android/PLAN.md` M8's cutover (S5).