V1 Web PWA · Journey

Journey: Public scene · anonymous abuse report → operator triage

A source-reconciled journey record for V1 Web PWA — shipped behavior, state boundaries, failure modes, and the automation evidence available today.

automated · 2 specs verdict: partial· 2026-05-29
9sections5 minread

On this page
Journey at a glance
ActorsAnonymous viewerSigned-in reporterTrust-and-safety operatorAPI operator
Automation2 E2E spec(s) · verdict: partial· 2026-05-29

Journey flow#

Generated from the authored steps below — click a node to jump to that section.

flowchart TD s1["1. Open and submit the public form"] s2["2. Record and route the report"] s3["3. Inspect the operator projection"] s4["4. Resolve through the report API"] s5["5. Apply a separate takedown when warranted"] s1 --> s2 --> s3 --> s4 --> s5 click s1 href "#1-open-and-submit-the-public-form" click s2 href "#2-record-and-route-the-report" click s3 href "#3-inspect-the-operator-projection" click s4 href "#4-resolve-through-the-report-api" click s5 href "#5-apply-a-separate-takedown-when-warranted" s5 --> v(["verdict: partial"]) click v href "../results/public-scene-abuse-report.html" style v stroke:#d6a531,color:#d6a531

This record follows the shipped report path for a public Living Offering. The public form, 202 receipt, operator-list projection, reporter read-back, and report-specific resolve/dismiss APIs are real. The join stops at two important boundaries: reports live in a process-local singleton with no snapshot sink, and the operator console's visible controls write to generic incident stores rather than the report-specific resolution endpoints.

Personas#

  • Anonymous viewer — reports a public or revoked /scene/<shortCode> link without needing an account.
  • Signed-in reporter — submits the same form and can later read their receipt on /profile/safety.
  • Trust-and-safety operator — sees the report projected into /operator/admin and opens the affected offering.
  • API operator — calls the report-specific resolve or dismiss endpoint; this is the only shipped way to mutate the report's own status.

Pre-conditions#

  • A Living Offering share exists. The report endpoint looks up the share by short code but does not require it to remain active.
  • The public form at /scene/<shortCode>/report offers the fixed reasons privacy, self-harm, harassment, copyright, misinformation, and other. Detail is optional and capped at 2,000 characters.
  • Public abuse protection permits at most 120 requests in 60 seconds for this route group.
  • A bearer token is optional. When one is valid, the BFF derives the reporter identity from it; the request-body id is not authoritative for that case.

Steps#

1. Open and submit the public form#

PublicSceneReportForm preserves the offering back link, requires one reason, allows an empty detail, and posts to POST /v1/living-scenes/public/:shortCode/report. The page exposes an explicit failure status when the request is rejected and a success message only after an OK response.

2. Record and route the report#

The BFF validates the short code, normalizes the reason/detail, records the report, appends living_scene.public_report_created to the admin audit stream, and returns 202 with the report receipt. Every reason uses the same routed queue, lilith-living-scene-public-reports; self-harm becomes severity S1, harassment/copyright/misinformation S2, and privacy/other S3 in the admin-list projection. Severity is not a separate crisis-review workflow.

3. Inspect the operator projection#

GET /v1/admin/abuse-reports maps reports to INC-* rows with category auto, the calculated severity, report status, and a /scene/<shortCode> deep link. /operator/admin prepends those rows to AdminInboxConsole, where the operator can inspect the summary and open the offering.

4. Resolve through the report API#

POST /v1/admin/abuse-reports/:id/resolve and /dismiss update the report record and append living_scene.public_report_resolved or living_scene.public_report_dismissed audit events. A signed-in reporter reads the resulting status through GET /v1/living-scenes/public-reports/mine.

The visible AdminInboxDecisionPanel and AdminInboxIncidentActions do not call those endpoints. They update the generic operator-decision and incident lifecycle stores, so using them does not resolve or dismiss the abuse report.

5. Apply a separate takedown when warranted#

The Living Offering deletion/takedown API can tombstone public URLs and revoke download grants, and browser coverage proves that cascade. It is a separate operator/API action; resolving an abuse report does not automatically take the offering down, warn a sharer, or suspend an account.

Post-conditions#

  • The submitter receives a 202 report receipt and, when authenticated, can read its current status on /profile/safety.
  • The operator list contains an incident-shaped projection with the correct severity and offering deep link.
  • Resolve/dismiss status and actor/note are readable for as long as the BFF process remains alive; corresponding admin audit events are appended.
  • No claim is made that the report itself, generic operator decision, incident action, and Living Offering takedown are one atomic lifecycle.

Failure modes#

  • Process restartlivingSceneAbuseReportStore is a module singleton over an in-memory map. It has no durable snapshot wiring, so report-list and reporter-receipt state disappear on restart even when admin audit persistence is configured.
  • False operator closure — generic verdict/incident buttons can succeed while the abuse report remains open; only the report-specific API changes that record.
  • Revoked share — a known revoked share still accepts a report and returns 202; this is intentional coverage, not proof that the share is playable.
  • Missing detail — valid and accepted. A reason is the minimum useful payload.
  • Unknown report id — resolve/dismiss returns 404 and creates no terminal record.
  • Rate limit — the 121st request in the covered window returns 429.
  • Unavailable BFF — the public form keeps the viewer on the page and shows “Report could not be sent”; there is no offline report queue.

E2E coverage#

  • apps/oshun/web/e2e/public-scene-abuse-report.spec.ts drives the form constraints and failure state, anonymous 202, optional detail, S1 projection, API resolve/dismiss read-back, unknown-id rejection, and the signed-in /profile/safety receipt.
  • apps/oshun/web/e2e/scene-password-and-grants.spec.ts covers report creation on active and revoked shares, routed-queue output, rate limiting, and the separate deletion/tombstone/download-grant cascade.
  • Coverage depth: partial. The API lifecycle is deep; the missing browser join is a report-specific resolve/dismiss/takedown control in the operator UI.

Per-view files touched#

Cross-references#

  • scene-public-viewer-anonymous.md — the public surface from which the report link opens.
  • scene-keep-and-share.md — share and revocation lifecycle.
  • incident-triage.md — generic operator incident lifecycle, which is adjacent but not report-specific.
  • Sources: apps/oshun/web/src/app/scene/[id]/report/PublicSceneReportForm.tsx, apps/oshun/bff/src/living-scenes/abuse-report-store.ts, and apps/oshun/bff/src/routes/living-scenes.ts.

Open questions#

  • Which operator control should call report-specific resolve/dismiss, and should it optionally compose with a Living Offering takedown?
  • Which durable store should own abuse reports and reporter receipts across restart?
  • Should S1 self-harm reports route to a distinct safety queue rather than only receiving a higher severity label in the shared queue?
  • What explicit account/sharer enforcement actions, if any, belong in this workflow?