feat: per-player erasure from transcripts & recordings (#118) #202

Merged
claude-bot merged 2 commits from feat/118-member-erasure into main 2026-07-18 08:10:45 +00:00
Contributor

Closes #118. Third issue of v3.8.0 Privacy & Data Lifecycle.

A GM (their campaign) or admin can scrub a departed player's contributions from recorded material — leaning on the speaker-attribution already in the pipeline.

Backend (653dacc)

  • POST /campaigns/{id}/members/{member_id}/erase-recordings (GM-of-campaign OR admin; 403/404 otherwise) → enqueues the erasure task, 202. {regenerate_summaries: bool = false}.
  • run_member_erasure (testable core): resolves the member's speaker labels (character name + Discord display name) and per session rewrites their transcript lines to [HH:MM:SS] <label>: [removed]preserving the timeline skeleton; the generated FTS tsvector reindexes so erased words become unsearchable. Deletes the member's {discord_id}.wav (via verified PlatformLink), purges LoreExtractCache for affected sessions, deletes their attributed quote highlights (#116), and appends an erasure note to each affected session (new erasure_notes JSONB; migration a2b3c4d5e6f7). Summaries left as-is unless the regenerate flag is set.
  • One member.recordings_erased audit entry attributed to the acting GM/admin (actor threaded endpoint→task→log_event), campaign/target + counts. Idempotent — a repeat run changes nothing and writes no second entry.

Frontend (<head>)

  • GM-only "Erase recordings" action on member rows → type-to-confirm modal (must type the member's display name) + optional "regenerate summaries" checkbox; success shows a "queued" banner.
  • SessionDetail renders an amber "Recordings erased" notice (visible to all members) from erasure_notes.

Tests

  • Backend: +11 (test_member_erasure.py) — transcript scrub + FTS-unsearchable (spoken content), quote deletion, cache-purge scoping, WAV deletion, no-link/missing-dir no-ops, erasure note + single actor-attributed audit + idempotent repeat, endpoint authz. Full suite 561 pass; migration round-trip verified.
  • Frontend: +10. 296 pass, eslint clean, vite build green.

Notes

  • Two fixes applied during review: threaded the acting user into the audit entry (was system-actor — but this is a destructive action that needs accountability), and fixed a test bug whose FTS token sat inside the preserved speaker label rather than the redacted spoken text (the scrub code was correct).
  • Builds on this milestone/session's own work: #116 highlights, #114's verified-link resolution, #120's campaign-scoped audit.

🤖 Generated with Claude Code

Closes #118. Third issue of **v3.8.0 Privacy & Data Lifecycle**. A GM (their campaign) or admin can scrub a departed player's contributions from recorded material — leaning on the speaker-attribution already in the pipeline. ## Backend (`653dacc`) - `POST /campaigns/{id}/members/{member_id}/erase-recordings` (GM-of-campaign OR admin; 403/404 otherwise) → enqueues the erasure task, 202. `{regenerate_summaries: bool = false}`. - `run_member_erasure` (testable core): resolves the member's speaker labels (**character name + Discord display name**) and per session rewrites their transcript lines to `[HH:MM:SS] <label>: [removed]` — **preserving the timeline skeleton**; the generated FTS tsvector reindexes so erased words become **unsearchable**. Deletes the member's `{discord_id}.wav` (via **verified** PlatformLink), purges `LoreExtractCache` for affected sessions, deletes their attributed **quote highlights** (#116), and appends an erasure note to each affected session (new `erasure_notes` JSONB; migration `a2b3c4d5e6f7`). Summaries left as-is unless the regenerate flag is set. - One `member.recordings_erased` audit entry **attributed to the acting GM/admin** (actor threaded endpoint→task→`log_event`), campaign/target + counts. **Idempotent** — a repeat run changes nothing and writes no second entry. ## Frontend (`<head>`) - GM-only "Erase recordings" action on member rows → **type-to-confirm modal** (must type the member's display name) + optional "regenerate summaries" checkbox; success shows a "queued" banner. - SessionDetail renders an amber "Recordings erased" notice (visible to all members) from `erasure_notes`. ## Tests - Backend: +11 (`test_member_erasure.py`) — transcript scrub + **FTS-unsearchable** (spoken content), quote deletion, cache-purge scoping, WAV deletion, no-link/missing-dir no-ops, erasure note + single **actor-attributed** audit + idempotent repeat, endpoint authz. Full suite **561 pass**; migration round-trip verified. - Frontend: +10. **296 pass**, eslint clean, vite build green. ## Notes - Two fixes applied during review: **threaded the acting user into the audit entry** (was system-actor — but this is a destructive action that needs accountability), and **fixed a test bug** whose FTS token sat inside the preserved speaker label rather than the redacted spoken text (the scrub code was correct). - Builds on this milestone/session's own work: #116 highlights, #114's verified-link resolution, #120's campaign-scoped audit. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
A GM (their campaign) or admin can scrub a member's contributions from
recorded material — leaning on the speaker-attribution that already runs
end to end.

- POST /campaigns/{id}/members/{member_id}/erase-recordings (GM-of-campaign
  OR admin; 403/404 otherwise) → enqueues the erasure task, 202. Body
  {regenerate_summaries: bool = false}.
- erase_member_recordings / run_member_erasure (testable core): resolves the
  member's speaker labels (character_name + Discord display name) and, per
  campaign session, rewrites transcript lines under those labels to
  "[HH:MM:SS] <label>: [removed]" — preserving the timeline skeleton; the
  generated FTS tsvector reindexes so erased words become unsearchable.
  Deletes the member's per-speaker WAV ({discord_id}.wav, via verified
  PlatformLink) from any retained audio dir; purges LoreExtractCache for
  affected sessions; deletes the member's attributed quote highlights (#116);
  appends an erasure note to each affected session (new erasure_notes JSONB,
  migration a2b3c4d5e6f7). Summaries left as-is unless regenerate flag set.
- One member.recordings_erased audit entry — attributed to the acting
  GM/admin (actor threaded endpoint→task→log_event) with campaign/target +
  counts. Idempotent: a repeat run changes nothing and writes no second entry.

Tests: +11 (test_member_erasure.py) — transcript scrub + FTS-unsearchable
(spoken content), quote deletion, cache purge scoping, WAV deletion,
no-Discord-link/missing-dir no-ops, erasure note + single actor-attributed
audit + idempotent repeat, endpoint authz. Full suite 561 pass; migration
round-trip verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(frontend): member erasure — type-to-confirm + erasure notice (#118)
All checks were successful
CI / Docker image build (pull_request) Successful in 20s
CI / Backend lint (ruff) (pull_request) Successful in 2m19s
CI / Frontend tests, audit, and build (pull_request) Successful in 2m28s
CI / Bot tests and audit (pull_request) Successful in 4m39s
CI / Backend migration, tests, and audit (pull_request) Successful in 6m2s
be6f647629
- api/campaigns.js: eraseMemberRecordings wrapper.
- CampaignDetail: GM-only "Erase recordings" action on member rows → a
  type-to-confirm modal (must type the member's display name to enable
  confirm) explaining the irreversible scrub, with an optional "regenerate
  summaries" checkbox (default off). Success shows a "queued — runs in the
  background" banner.
- SessionDetail: amber "Recordings erased" notice listing each erasure_notes
  entry + date, visible to all members; nothing when null/empty.

Tests: +10 (wrapper 2, CampaignDetail erase flow 5, SessionDetail notice 3).
296 pass, eslint clean, vite build succeeds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
claude-bot deleted branch feat/118-member-erasure 2026-07-18 08:10:45 +00:00
Sign in to join this conversation.
No description provided.