feat: cases (reports, rating reviews, removal and deletion requests, appeals) and notifications
Pull request / title (pull_request) Successful in 1s
CI / test (pull_request) Successful in 1m5s
CI / image (apps/api/Dockerfile, t5de-hub-api) (pull_request) Skipped
CI / image (apps/web/Dockerfile, t5de-hub-web) (pull_request) Skipped

Cases are one queue for what someone has to decide, each with a thread (internal notes for those deciding), an
assignee and a decision. Reports about a product go to the teams of the catalogs hosting it and to moderators, and
only the reporter and moderators know who reported. A creator lowering a rating opens a rating review; a catalog's
owner asking to delete it opens a deletion request; a catalog's team can ask for a product's removal everywhere; the
person an action affected can appeal it once, decided by another moderator (an admin may decide their own, flagged).
Resolving carries the case out: the rating is lowered, the product removed, the catalog deleted.

Notifications reach people on the website (a bell with the unread count) and their proxies (realtime): moderators'
edits to their products, removals and restores, decisions and replies on their cases, membership decisions.

Also: moderators can restore a removed product, and a product's creator and moderators can see it when it's adult
without an adult account (getting and wearing it still need one).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
dhkatzandClaude Opus 5.5 committed 2026-11-05 07:03:59 -08:00
1 parent e07a6c6dac
commit 8af4b7b6a9
33 files changed
+5228 -44

No files matched your search

+27 -3
View File
@@ -131,8 +131,22 @@ log), who opened it, a status (`open`, `resolved`, `declined`), an assignee, a r
| `catalog-deletion` | a catalog's owner (deleting goes to review for now) | moderators (`catalog:delete-review`) |
| `appeal` | the person affected by a removal, ban, delisting or decline (one per action) | a different moderator than the one who acted |
- People see cases they opened and cases about what they control (`case:read` at a covering scope).
- People see cases they opened or are assigned, and cases about what they control (`case:read` at a covering scope),
except reports: those only the reporter, the hosting catalogs' teams (reports about products) and moderators see,
and only the reporter and moderators see who reported (a report's audit entry isn't tagged with what it's about).
- Who decides: moderators, by the kind's permission (`case:resolve`; `product:rate-lower` for rating reviews,
`product:remove` for removal requests, `catalog:delete-review` for deletions); reports about a product also the
teams of the catalogs hosting it (`case:resolve` there). An appeal goes to another moderator than the one who acted;
an admin may decide one of their own, which the audit log flags (`ownAction`).
- Resolving a rating review lowers the rating (as an edit, in the product's history); a removal request removes the
product; a deletion request deletes the catalog (status `deleted`; its changes feed says so). Reports and appeals
record the decision, and the moderator acts where it's done: a removed product is restored (`POST
/api/v1/products/:id/unremove`), an edit undone from the history, a catalog reinstated, a member unbanned.
- Appealable actions: `product.remove`, `product.edit`/`lock`/`revert` by others (the creator), `catalog.delist`
(the product's creator), `catalog.suspend`/`reject` (the owner), `catalog.member_ban`/`member_decline` (the
person). The audit log marks the entries a person may appeal (`appealable`).
- Reports are limited per person (10 an hour), one open report per person and resource.
- Not built yet: canned reasons, moderator notes and second approval (below).
- **Canned reasons** (admins edit the list) for removals, bans, declines and edits; free text can be added.
- **Moderator notes**: private notes on people and products (`person:notes`), shown in the console only.
- **Second approval** for the most destructive actions: banning a person, removing a catalog and transferring a
@@ -144,6 +158,10 @@ log), who opened it, a status (`open`, `resolved`, `declined`), an assignee, a r
One table; shown on the website (a bell, unread count), on the proxy's realtime connection
(`{"type": "notification"}`, so the client can show them later), and by email for types the person opts into.
Built (2026-11-05): `product_edited` (by someone other than the creator, with the reason), `product_removed`,
`product_restored`, `case_decided`, `case_note`, `membership_decided`. Not yet: email (the hub sends none yet) and its
settings, and the import, grant, new-revision and request-digest types.
Types: a moderator edited your product (with the reason); a case about you or yours was opened or decided; your
membership request was decided; your import finished or failed; your catalog's deletion request was decided; a
product in your library has a new revision; a grant was made or withdrawn; someone requested your products
@@ -196,8 +214,14 @@ for meaning.
| `GET /api/v1/console/permissions?scope=...` | the person's permissions at a scope (the website uses it to show actions) |
| `GET /api/v1/console/audit?tag=&actor=&action=&from=&to=&q=` | entries the person may see, newest first |
| `GET /api/v1/console/stats?scope=&metric=&from=&to=` | `{series: [{day, value}]}` |
| `GET/POST /api/v1/cases`, `GET/PATCH /api/v1/cases/:id`, `POST /api/v1/cases/:id/notes` | cases |
| `GET /api/v1/notifications`, `POST /api/v1/notifications/read`, `GET/PUT /api/v1/notifications/settings` | notifications |
| `GET /api/v1/cases?status=&kind=&mine=1&before=&limit=` | `{cases, before, moderator}`: the cases the person may see (below), newest first |
| `POST /api/v1/cases` | `{kind, about?, catalog?, audit?, reason}` → 201 `{case}`. `report`: `about` names one product, catalog, room (its number) or person (username); 10 an hour (429 `rate_limited`), one open per person and resource (409 `already_reported`), not yourself. `removal-request`: `about.product`, hosted by `catalog`, whose `hosting:delist` the person needs. `appeal`: `audit`, an entry of an action that affected the person (409 `appealed` if they appealed it already) |
| `GET /api/v1/cases/:id` | `{case, notes, canDecide, canAssign}`; internal notes only for those deciding and moderators |
| `POST /api/v1/cases/:id/notes` | `{body, internal?}`: a note; the opener's reach the assignee and others' reach the opener as notifications (internal ones reach nobody) |
| `POST /api/v1/cases/:id/assign` | (`case:assign`) `{to: username or null}`: a moderator |
| `POST /api/v1/cases/:id/decide` | `{decision: "resolve" or "decline", resolution}`: by those who may decide it (below); resolving carries out a rating review, removal request or deletion request; the opener is told |
| `GET /api/v1/notifications?before=` | `{notifications: [{id, type, tags, data, at, read}], unread, before}`, newest first, 50 a page |
| `POST /api/v1/notifications/read` | `{ids?}` (all if left out) → `{unread}` |
| `GET/POST /api/v1/account/tokens`, `DELETE /api/v1/account/tokens/:id` | API tokens |
| `GET/PUT/DELETE /api/v1/roles/:scopeType/:scopeId/:person` | role assignments at a scope |
| `POST /api/v1/console/view-as/:person` | (admins) start viewing as someone, read-only |