Admin: a recording's "kept until" date, the tables on a phone, and binned campaigns (#872) #1021

Merged
claude-bot merged 5 commits from fix/872-admin-kept-until-phone-tables into main 2026-10-02 01:09:55 +00:00
Collaborator

Closes #872, and folds in a leftover: Admin → Campaigns never asked for binned campaigns and had no test.

1. "Kept until" on the recordings table (ADMIN-6)

  • retention.audio_expires_at(session, policy) is the date the nightly sweep acts on, and audio_window_catches now reads it, so the table's date and the sweep's decision are one piece of arithmetic. The refactor is behaviour-preserving: the same guards in the same order, and anchor + days < now for anchor < now - days. Tests assert the sweep catches a recording one second after the date and not one second before.
  • GET /api/admin/recordings rows gain audio_expires_at, audio_retention_mode, audio_retention_days, audio_retention_source (campaign, instance or cap) and content_approved.
  • The row reads as the drawing does: "Kept 90 days, as the campaign asks: until 23 November". The drawing has only that one; the others follow its shape:
    • "Kept 30 days, as the instance asks: until …"
    • "Kept 60 days, as the instance allows: until …" (a campaign's own setting narrowed by the cap)
    • "Kept for good, as the campaign asks." / "… as the instance asks."
    • "Waiting to be approved." and "Approved, so it goes to the bin at the next nightly clean-up." (bin once approved)
  • The live per-voice progress ("3 of 6 voices") is still deferred: it needs a counter from the transcription pipeline.

2. The Admin tables at phone width

Audit, Users, Campaigns and the recordings table each have a minimum width and scroll sideways inside their own named, focusable region; the page never scrolls sideways. Dates and short cells stay on one line; prose wraps. Minimums are tuned so nothing scrolls at 1024.

3. Admin → Campaigns

  • The page now asks for binned campaigns and shows them inline, dimmed, as "In the bin, gone 3 October" (drawn). The row's date is the purge's own (retention.trash_purge_at).
  • Fixed: "Keeps its own" never showed a campaign's settings. It compared against days/forever; the server's values are retain_days, retain_indefinitely, trash_after_approval. It now reads "Audio 30 days · Transcripts 6 months", "Audio for good", "Audio binned once approved", "Transcripts for good", and "The instance's" with no override.
  • Fixed: "Hand it on…" sent an empty member unless the picker was touched, because the select displayed the first member while its state was empty.
  • AdminCampaigns.test.jsx is new: the list, archived, binned with and without a date, every retention mode, hand-on, empty, failed load.

Also fixed

A failed recording's line doubled its punctuation when the error ended in a full stop (…did not answer.”.).

Checked

  • Full backend suite: 6733 passed, 14 skipped; ruff 0.4.4 clean. Full vitest at the final commit: 3617 passed, 1 skipped; eslint 0 errors. Every coverage/lane*.py passes. Catalogue rows IS-470 and IS-471.
  • Rendered twice in e2e stacks at 390×844 (touch), 1024 and 1440, light and dark, and looked at. Every "kept until" date matched the API's day for its row, across the campaign's own days, the instance's, a cap, kept for good, bin once approved before and after approval, a binned and a failed recording. At 390 each table scrolls inside its region (document 390/390) with the last column reachable and a visible focus ring; at 1024 and 1440 none scrolls; Audit's rows are 45–85px at 390 (were up to 126). No console errors or failed calls.
  • No migration, no bot API change. docs/API.md updated.

Noted, not changed

"Hand it on…" is offered on a binned and on an archived campaign, as the drawing has it. On a phone the recordings table's status and action are reached by scrolling the region right.

🤖 Generated with Claude Code

Closes #872, and folds in a leftover: Admin → Campaigns never asked for binned campaigns and had no test. ## 1. "Kept until" on the recordings table (ADMIN-6) - `retention.audio_expires_at(session, policy)` is the date the nightly sweep acts on, and `audio_window_catches` now reads it, so the table's date and the sweep's decision are one piece of arithmetic. The refactor is behaviour-preserving: the same guards in the same order, and `anchor + days < now` for `anchor < now - days`. Tests assert the sweep catches a recording one second after the date and not one second before. - `GET /api/admin/recordings` rows gain `audio_expires_at`, `audio_retention_mode`, `audio_retention_days`, `audio_retention_source` (`campaign`, `instance` or `cap`) and `content_approved`. - The row reads as the drawing does: "Kept 90 days, as the campaign asks: until 23 November". The drawing has only that one; the others follow its shape: - "Kept 30 days, as the instance asks: until …" - "Kept 60 days, as the instance allows: until …" (a campaign's own setting narrowed by the cap) - "Kept for good, as the campaign asks." / "… as the instance asks." - "Waiting to be approved." and "Approved, so it goes to the bin at the next nightly clean-up." (bin once approved) - The live per-voice progress ("3 of 6 voices") is still deferred: it needs a counter from the transcription pipeline. ## 2. The Admin tables at phone width Audit, Users, Campaigns and the recordings table each have a minimum width and scroll sideways inside their own named, focusable region; the page never scrolls sideways. Dates and short cells stay on one line; prose wraps. Minimums are tuned so nothing scrolls at 1024. ## 3. Admin → Campaigns - The page now asks for binned campaigns and shows them inline, dimmed, as "In the bin, gone 3 October" (drawn). The row's date is the purge's own (`retention.trash_purge_at`). - **Fixed: "Keeps its own" never showed a campaign's settings.** It compared against `days`/`forever`; the server's values are `retain_days`, `retain_indefinitely`, `trash_after_approval`. It now reads "Audio 30 days · Transcripts 6 months", "Audio for good", "Audio binned once approved", "Transcripts for good", and "The instance's" with no override. - **Fixed: "Hand it on…" sent an empty member** unless the picker was touched, because the select displayed the first member while its state was empty. - `AdminCampaigns.test.jsx` is new: the list, archived, binned with and without a date, every retention mode, hand-on, empty, failed load. ## Also fixed A failed recording's line doubled its punctuation when the error ended in a full stop (`…did not answer.”.`). ## Checked - Full backend suite: **6733 passed, 14 skipped**; ruff 0.4.4 clean. Full vitest at the final commit: **3617 passed, 1 skipped**; eslint 0 errors. Every `coverage/lane*.py` passes. Catalogue rows IS-470 and IS-471. - Rendered twice in e2e stacks at 390×844 (touch), 1024 and 1440, light and dark, and looked at. Every "kept until" date matched the API's day for its row, across the campaign's own days, the instance's, a cap, kept for good, bin once approved before and after approval, a binned and a failed recording. At 390 each table scrolls inside its region (document 390/390) with the last column reachable and a visible focus ring; at 1024 and 1440 none scrolls; Audit's rows are 45–85px at 390 (were up to 126). No console errors or failed calls. - No migration, no bot API change. `docs/API.md` updated. ## Noted, not changed "Hand it on…" is offered on a binned and on an archived campaign, as the drawing has it. On a phone the recordings table's status and action are reached by scrolling the region right. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
audio_expires_at comes from retention.audio_expires_at, the function
audio_window_catches now reads too, so the date shown is the date the
sweep acts on. The row also carries the effective mode, days and whose
setting it is, for "Kept 90 days, as the campaign asks: until 23 November".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The tables had no minimum width, so at 390px every cell wrapped instead of
scrolling inside .tscroll. They get a width of their own, short cells stay
on one line, and the scroll region is a named, focusable region.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The page never asked for soft-deleted campaigns, so its binned-row branch
was dead. It now requests include_deleted and says "In the bin, gone 3
October" from a new purge_at (retention.trash_purge_at, the purge's own
arithmetic). Hand it on… sends the member shown selected instead of an
empty choice. Adds the page's first test.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Admin tables: keeps-its-own words, recordings table, widths, error stop (#872)
Some checks failed
CI / Backend lint (ruff) (pull_request) Successful in 28s
CI / Bot/backend version sync (pull_request) Successful in 31s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m39s
CI / Feedback site tests (pull_request) Successful in 50s
CI / Bot tests and audit (pull_request) Successful in 1m56s
CI / Does this change touch the recording path? (pull_request) Successful in 21s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Docker image build (pull_request) Successful in 4m39s
CI / Frontend (shard 2/2) (pull_request) Successful in 7m45s
CI / Frontend (shard 1/2) (pull_request) Successful in 9m18s
CI / Backend migration, tests, and audit (pull_request) Has been cancelled
CI / End-to-end journeys (keyboard) (pull_request) Has been cancelled
CI / End-to-end journeys (desktop) (pull_request) Has been cancelled
CI / End-to-end journeys (phone) (pull_request) Has been cancelled
51d16cce2b
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merge main: the ledger keeps both branches' rows (#872)
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m9s
CI / Backend lint (ruff) (pull_request) Successful in 39s
CI / Bot/backend version sync (pull_request) Successful in 32s
CI / Feedback site tests (pull_request) Successful in 50s
CI / Does this change touch the recording path? (pull_request) Successful in 33s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Bot tests and audit (pull_request) Successful in 1m58s
CI / Docker image build (pull_request) Successful in 2m44s
CI / Frontend (shard 2/2) (pull_request) Successful in 7m59s
CI / Frontend (shard 1/2) (pull_request) Successful in 10m2s
CI / End-to-end journeys (phone) (pull_request) Successful in 9m17s
CI / End-to-end journeys (desktop) (pull_request) Successful in 10m9s
CI / Backend migration, tests, and audit (pull_request) Successful in 22m32s
CI / End-to-end journeys (keyboard) (pull_request) Successful in 22m38s
924f7aec1e
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign in to join this conversation.
No description provided.