An exact preview of what a retention change would delete, an exact erase count, and a fix for over-cap campaigns (#778) #792
Loading…
Reference in a new issue
No description provided.
Delete branch "build/retention-preview"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #778. Owner, 2026-09-23 (B3): build it before v4.5.0.
This is the confirmation in front of deleting people's recorded voices, so the rule throughout is: the thing that counts and the thing that deletes are the same function.
The retention preview
GET /api/campaigns/{id}/retention-preview?audio_mode=&audio_days=&transcript_mode=&transcript_months=is GM-only and takes a proposed pair, the value the GM has typed and not saved yet. It returns, per side, the sessions that would actually lose audio and those that would lose a transcript, newest first. Nothing is written.The preview and the nightly sweep share one selection.
app/retention.pynow holdsselect_retention_catch, andenforce_retention's audio and transcript passes simply act on what it returns. The preview calls the same function, with:effective_retention_from, split out ofget_effective_retention, which now calls it.The agreement is tested, not asserted.
test_retention_preview.py:Mutation-checked: making the preview skip the clamp fails the capped case.
"Has audio" and "has a transcript" mean what the sweep keys on: the database columns (
audio_file_path,transcriptand their anchors), not files on disk. The trashed-campaign cache the sweep keeps is untouched.On the page (
IS-313), the hedge is gone. It now reads, for example, "Three sessions have audio older than that — Kettlebridge, … and …". It asks about a side only if that side changed, previews as the GM types (250 ms debounce, abortable), and shows the over-cap refusal before anyone presses Save.An exact count for erasing a member's recordings
GET /api/campaigns/{id}/members/{user_id}/recordings→{sessions, recordings, transcripts, quotes}, behind the erase's own authorisation.Same principle:
run_member_erasureno longer decides as it goes.plan_member_erasurebuilds a plan, reading only. The erase applies exactly that plan, and the count counts it. Tested: count, then erase and confirm the erase matches the count exactly, then count again and get all zeros.The warning on Members now reads: "It cannot be undone: it takes Ade's audio from 11 sessions, their lines from 9 transcripts and 2 quotes attributed to them."
A bug in a page shipped this week, fixed here
A campaign whose stored retention is above a cap set later couldn't save anything on the Recording page. The page PATCHed all four retention fields on every save, and the cap check treated a present field as a change, so flipping a toggle got a 400. D24 deliberately keeps such an override for the day the cap is raised, so this state is expected, not rare.
Fix: the server checks a retention field against the cap only if it differs from the stored value. An over-cap value still can't be newly set, and the stored one is still clamped on read, which is what the sweep obeys. The page also sends only the fields the GM changed.
Tests:
Also
The member search ("Add someone", "Attach to an account") is debounced 250 ms and abortable, and never shows a stale answer for the current text.
Checks
--maxWorkers=2); eslint 0 errors, and the warning count didn't rise.Reported, not changed
transcript_updated_at = now), so their audio and words are kept longer. That's the wrong direction for a privacy control. Filed as #791.🤖 Generated with Claude Code
The confirmation in front of deleting people's recorded voices stops hedging. IS-313 used to count sessions "old enough to be caught" and say "any audio they still have goes to the bin", because nothing on the wire could say what a session still held. GET /api/campaigns/{id}/retention-preview takes the pair a GM has typed and not saved and answers with the sessions that would actually lose audio and those that would actually lose a transcript. It is the sweep's own selection: enforce_retention and the preview now both call retention.select_retention_catch, the policy is resolved and clamped by the same settings_service.effective_retention_from, and an over-cap pair gets the PATCH's own 400 and sentence (the same _check_retention_within_cap call). A test previews, saves, runs the sweep at the same pinned instant, and checks both took the same sessions, across capped and uncapped pairs. The member erase is counted the same way. run_member_erasure now applies a plan built by plan_member_erasure, which decides and changes nothing; GET .../members/{user_id}/recordings counts that plan, so "Ade's audio from 11 sessions" is the erase's own number. A test counts, erases, compares, and counts again to find nothing left. The page asks while the GM types (debounced like the app bar's search), shows an over-cap refusal before anyone saves, asks only about the side that changed, and says exactly what goes, naming it as the drawing does. The member search on Members is debounced the same way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>Owner, 2026-09-23: Option A for "Bin it once transcribed", the second item under Reported, not changed above.
Choosing
trash_after_processingnow also clears all already-transcribed audio at the next sweep, not only backlog older than the instance's default days. "Already transcribed" means exactly what the post-processing step would have binned at the time. Audio from a failed or unfinished transcription is not caught, because it's kept so it can be reprocessed.It goes in the shared
select_retention_catch, so the sweep and the preview change together and the agreement test covers it. It's being added to this PR as a further commit, with the test cases: a transcribed session younger than the window is now caught; a failed run and already-trashed audio are not.🤖 Generated with Claude Code