feat(webapp): no Discord token in Admin, Tests on typed values, prices, images in the backup, the cap preview, a queue verdict (lane 9 B2) #859

Merged
claude-bot merged 7 commits from build/lane9-b2-server into main 2026-09-26 23:43:35 +00:00
Collaborator

Lane 9 (#735), server PR B2. It covers the owner's D139 (A2), D143 (A6), D144 (A7) and D147 (A10), plus the smaller §3.2 items. No pages.

A2 / D139: no Discord token field

  • The token field is gone from the settings API (KEY_BOT_TOKEN, bot_token), from the legacy Admin form and from the setup wizard.
  • The wizard's Discord step now runs the identity check (#849), and the step settles when key_ok is true and discord is "ready".
  • records_sessions on invites now means "a bot URL is set".
  • Migration b3d5f7a9c1e4 deletes the stored token row.
  • The e2e seed settles the step through /identity.

A10 / D147: "Test it" on the typed values

  • The LLM, Whisper and test-email endpoints take an optional body holding the form's values, with the same URL and SSRF policy as saving.
  • The saved key only ever goes to the saved address. A different address with a blank key is probed without any key, so keyless Ollama, Whisper and SMTP relays still test. The result then carries "No key was sent — type it to test this address." whenever a stored key was held back.
  • A test proves the outgoing request has no Authorization header.
  • The email test now runs inline and returns {sent, error}.

A7 / D144: prices

GET and PUT /admin/settings/plan-pricing. The PUT is hosted-only (501 otherwise) and audited.

A6 / D143: images in the nightly backup, on by default

  • include_media defaults to true.
  • The worker mounts media:/app/media:ro in all three compose files, and the media is tarred beside the dump and shipped with it.
  • backup_logs.media_archive_path (migration b3d5f7a9c1e4) is read back by verify.
  • Self-hosters: the worker now needs read access to the media volume; this is in the CHANGELOG and OPERATIONS.

Also

  • Instance cap preview: GET /admin/settings/retention/preview lists the campaigns a proposed cap would cut.
  • Provider queue: each provider gets last_finished_at and a verdict of free, busy or stuck. Stuck means work is waiting and nothing has finished in 40 minutes; a provider that has never finished anything counts as busy.
  • Notice contact: a published flag. Unpublished, the public read looks withdrawn, and a new admin read lets the address still be edited.
  • Test-isolation fix: the KEY_BACKUP_CONFIG leak in test_backup_offsite.py. A similar latent BackupLog row leak remains and is noted for follow-up.

Tests

  • Backend: 155 plus 574 targeted, all passing.
  • Frontend: 2,475 passed; eslint has 0 errors.
  • ruff is clean.
  • Migration up/down/up passes.
  • J1 needs its CI run (the seed's discord settle changed).

Merge notes

  • A (#855) also adds a migration on c4e6; whichever lands second is re-chained.
  • After #854, the new Depends(get_db) calls become scope="function".

🤖 Generated with Claude Code

Lane 9 (#735), server PR B2. It covers the owner's D139 (A2), D143 (A6), D144 (A7) and D147 (A10), plus the smaller §3.2 items. No pages. ## A2 / D139: no Discord token field - The token field is gone from the settings API (`KEY_BOT_TOKEN`, `bot_token`), from the legacy Admin form and from the setup wizard. - The wizard's Discord step now runs the identity check (#849), and the step settles when `key_ok` is true and `discord` is `"ready"`. - `records_sessions` on invites now means "a bot URL is set". - Migration `b3d5f7a9c1e4` deletes the stored token row. - The e2e seed settles the step through `/identity`. ## A10 / D147: "Test it" on the typed values - The LLM, Whisper and test-email endpoints take an optional body holding the form's values, with the same URL and SSRF policy as saving. - **The saved key only ever goes to the saved address.** A different address with a blank key is **probed without any key**, so keyless Ollama, Whisper and SMTP relays still test. The result then carries "No key was sent — type it to test this address." whenever a stored key was held back. - A test proves the outgoing request has no `Authorization` header. - The email test now runs inline and returns `{sent, error}`. ## A7 / D144: prices `GET` and `PUT /admin/settings/plan-pricing`. The PUT is hosted-only (501 otherwise) and audited. ## A6 / D143: images in the nightly backup, on by default - `include_media` defaults to **true**. - The worker mounts `media:/app/media:ro` in all three compose files, and the media is tarred beside the dump and shipped with it. - `backup_logs.media_archive_path` (migration `b3d5f7a9c1e4`) is read back by verify. - **Self-hosters:** the worker now needs read access to the media volume; this is in the CHANGELOG and OPERATIONS. ## Also - **Instance cap preview:** `GET /admin/settings/retention/preview` lists the campaigns a proposed cap would cut. - **Provider queue:** each provider gets `last_finished_at` and a `verdict` of `free`, `busy` or `stuck`. Stuck means work is waiting and nothing has finished in 40 minutes; a provider that has never finished anything counts as busy. - **Notice contact:** a `published` flag. Unpublished, the public read looks withdrawn, and a new admin read lets the address still be edited. - **Test-isolation fix:** the `KEY_BACKUP_CONFIG` leak in `test_backup_offsite.py`. A similar latent `BackupLog` row leak remains and is noted for follow-up. ## Tests - Backend: 155 plus 574 targeted, all passing. - Frontend: 2,475 passed; eslint has 0 errors. - ruff is clean. - Migration up/down/up passes. - **J1** needs its CI run (the seed's discord settle changed). ## Merge notes - A (#855) also adds a migration on `c4e6`; whichever lands second is re-chained. - After #854, the new `Depends(get_db)` calls become `scope="function"`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
cap preview, health verdict, notice publish, media in the backup

Six server-side pieces from the lane 9 survey's §3.2/§3.3, all built
against the bot identity check and D67 removals already on main
(#849) and the backup-credentials work already on main (#848):

- D139/A2: drop the stored instance-wide Discord bot token. The bot
  never read it — it signs in with its own environment's
  DISCORD_BOT_TOKEN — so the setup "Discord" step now settles on a
  successful identity check (admin_bot_identity) instead of a typed
  token, and an invite preview's "records sessions" reads "a bot URL
  is set and the campaign has a guild". bot_token is dropped from
  PUT /api/admin/settings/bot's request and bot_token_configured from
  its response.
- A10/D147: POST …/llm/test, …/whisper/test and
  …/notifications/test-email accept an optional body and probe
  exactly what is typed, saved or not. The saved secret is only
  reused for the saved address; a different typed address with a
  blank key sends no key and says so. The email test now runs
  inline with a short timeout and reports {sent, error} instead of
  queuing a Celery task nobody ever saw the outcome of again (#380's
  failure mode).
- A7/D144: GET/PUT /api/admin/settings/plan-pricing over PlanPricing's
  four fields. The PUT is hosted only (501, NotHostedError); the GET
  answers on every instance since reading defaults back prices
  nothing.
- A6/D143: uploaded media joins the backup, on by default
  (include_media, unlike include_audio) — the worker now mounts
  media:/app/media:ro. Migration b3d5f7a9c1e4 adds
  backup_logs.media_archive_path (recorded and read back the same way
  as the audio archive's, but never derived — no naming scheme
  predates the column) and, in the same migration, deletes the
  now-orphaned stored bot-token row.
- The instance retention cap preview (IS-311's confirm): GET
  /api/admin/settings/retention/preview diffs the sweep's own
  selection under today's cap against a proposed one, per campaign,
  skipping the trash (#405).
- The AI queue's last_finished_at and a server-computed verdict
  (free/busy/stuck, ADMIN-14) — a provider that has never finished
  anything is busy, never stuck, so a first-ever request in flight is
  never a false alarm.
- The rightsholder-notice contact gets a published flag: the public
  GET answers as fully withdrawn while it is false, and the new
  admin-only GET (beside the existing PUT) always sees the real
  address.

Migration chain: a2c4e6f8b0d1 -> c4e6a8b0d2f1 -> b3d5f7a9c1e4 (new
head). Verified upgrade/downgrade/upgrade on a fresh Postgres.

Not in scope: A1 (retention semantics) — the owner hasn't decided it.
Server PR A (owner/audit/overview) is a parallel PR on the same base;
expect the orchestrator to re-chain at merge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
backup media checkbox (lane 9 B2, D139/A2, A6)

Setup.jsx's Discord step (IS-256) no longer asks for a token — there
is nothing to type any more (D139/A2) — and its "Check the bot
answers" button becomes "Check the bot is signed in", calling the
new getBotIdentity instead of pingBot and reporting who the bot is
signed in as, or why it isn't, using the server's own detail text.

The legacy Admin backup form (BackupSection) gets an "Include
uploaded media" checkbox beside "Include audio recordings", checked
by default (A6).

e2e/seed/seed.py's bot setup no longer sends bot_token (silently
ignored by the server now) and instead calls
GET /api/admin/settings/bot/identity to settle the setup "Discord"
step, the same way a real bot signing in would — the fake bot's own
/identity already answers ready. J1 (e2e/j1-first-campaign.spec.js)
walks this seed's Discord settlement indirectly and needs a run to
confirm; not run here per the delegation's rules (no
scripts/journeys.sh).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(webapp): A10's withheld key probes instead of refusing; drop the
Some checks failed
CI / Backend migration, tests, and audit (pull_request) Failing after 2m50s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 54s
CI / Backend lint (ruff) (pull_request) Successful in 33s
CI / Feedback site tests (pull_request) Successful in 40s
CI / Bot/backend version sync (pull_request) Successful in 24s
CI / Does this change touch the recording path? (pull_request) Successful in 17s
CI / Docker image build (pull_request) Successful in 26s
CI / Bot tests and audit (pull_request) Successful in 3m50s
CI / Frontend tests, audit, and build (pull_request) Successful in 18m58s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been cancelled
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Has been cancelled
d7cf303ff7
orphaned Discord token input from legacy Admin.jsx

Two follow-ups on review of the lane 9 B2 server PR:

1. A different typed address no longer short-circuits llm/test,
   whisper/test and notifications/test-email when it cannot reuse the
   stored secret. Self-hosted Ollama, llama.cpp and mail relays are
   usually keyless, so refusing the probe outright meant a GM could
   never test a fresh address at all. The probe now always runs, with
   no key (never the stored one — confirmed by inspecting the
   outgoing request, not just the resolved config), and a `detail`
   note — "No key was sent — type it to test this address." — is
   attached to the result, on success and on failure alike, only when
   there was a stored secret the address mismatch is why it wasn't
   reused. A server with nothing stored at all reports plainly, with
   no note: that is the ordinary state of a keyless endpoint, not a
   warning.

   Applied to all three A10 endpoints (llm/test, whisper/test,
   notifications/test-email) for consistency — they share the same
   `_typed_api_key` helper and the same design flaw, and the email
   endpoint's own scenario (a self-hosted, unauthenticated relay) is
   the same shape as Ollama's.

2. Removed the "Discord Bot Token" input, its form state and its
   place in the save payload from the legacy Admin.jsx Discord
   section. D139 dropped the field everywhere on the server; an input
   that saves nothing and reads nothing back is exactly what D20/D36
   rule out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat(webapp): the server has an owner (D68, IS-397, lane 9 A)
Some checks failed
CI / Backend migration, tests, and audit (pull_request) Failing after 6m6s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 2m46s
CI / Backend lint (ruff) (pull_request) Successful in 32s
CI / Bot tests and audit (pull_request) Successful in 4m52s
CI / Bot/backend version sync (pull_request) Successful in 30s
CI / Docker image build (pull_request) Successful in 17s
CI / Does this change touch the recording path? (pull_request) Successful in 27s
CI / Feedback site tests (pull_request) Successful in 2m0s
CI / Frontend tests, audit, and build (pull_request) Successful in 19m57s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 13m36s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 18m56s
b2293c526d
Migration f1a3c5e7b9d2 adds users.is_server_owner (partial unique index)
and server_ownership_offers, backfilled from the earliest
admin.setup.finished actor if still an admin, otherwise the
earliest-created admin, otherwise nobody. The owner cannot be demoted,
deleted, or delete their own account without handing the server on
first; the offer flow mirrors a campaign's own (offer/accept/decline/
cancel, a bell notification, the outcome told to the previous owner);
the CLI set_owner moves ownership without an offer for a vanished
owner.

Also in this PR: D56's member-search-scope write, About this server's
GET/PUT, the admin campaign list's delivery_failing_since/
owner_last_seen_at/uses_own_ai fields plus a members read for the
hand-on picker, notifying both owners on an admin's campaign
reassignment (campaign_handed_on), the audit log's name filters
(actor/campaign/category) and per-page name resolution, and one cheap
GET /admin/overview for the future Admin index.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Merge origin/main into lane 9 A (#855): scope="function" for the branch's get_db, and the inventory's metadata keys
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m1s
CI / Backend lint (ruff) (pull_request) Successful in 35s
CI / Feedback site tests (pull_request) Successful in 53s
CI / Docker image build (pull_request) Successful in 25s
CI / Bot/backend version sync (pull_request) Successful in 1m7s
CI / Does this change touch the recording path? (pull_request) Successful in 31s
CI / Bot tests and audit (pull_request) Successful in 2m12s
CI / Backend migration, tests, and audit (pull_request) Failing after 27m1s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been cancelled
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Has been cancelled
CI / Frontend tests, audit, and build (pull_request) Has been cancelled
41bbc62c51
- CHANGELOG: kept lane 9 A's entry and main's D131-D178 line.
- Every Depends(get_db) the branch adds in admin.py and users.py is now
  scope="function" (#854), so test_commit_before_response's walk passes.
- test_admin_campaign_inventory: EXPECTED_KEYS gains delivery_failing_since,
  owner_last_seen_at and uses_own_ai, each commented as metadata rather
  than campaign content.

f1a3c5e7b9d2 already sat on c4e6a8b0d2f1; one head.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- CHANGELOG: kept B2's entries, lane 9 A's and main's D131-D178 line.
- admin.py: B2's plan-pricing endpoints and lane 9 A's /admin/overview both
  appended at the end; kept both.
- b3d5f7a9c1e4 now revises f1a3c5e7b9d2 (it was written on c4e6a8b0d2f1).
  The one-head pin is test_media_archive_path_and_bot_token_removal_migration's;
  test_server_owner_migration's becomes test_this_migration_is_still_in_the_chain.
  c4e6's own chain test keeps f1a3 as its successor; test_notifications'
  history docstring gains both steps.
- B2's four new Depends(get_db) (admin.py, content_notices.py) are
  scope="function".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test(backend): the retention test leaves the media snapshot out (#859, A6)
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 59s
CI / Backend lint (ruff) (pull_request) Successful in 30s
CI / Feedback site tests (pull_request) Successful in 44s
CI / Bot tests and audit (pull_request) Successful in 2m0s
CI / Bot/backend version sync (pull_request) Successful in 26s
CI / Docker image build (pull_request) Successful in 28s
CI / Does this change touch the recording path? (pull_request) Successful in 33s
CI / Frontend tests, audit, and build (pull_request) Successful in 17m52s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been cancelled
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Has been cancelled
CI / Backend migration, tests, and audit (pull_request) Has been cancelled
05efd1f987
The full suite at the top of the merge train failed
test_retention_never_deletes_a_manual_dump_or_the_one_just_taken: B2 turns
the media snapshot on by default, and once an earlier test has left a file
in the media root the run also writes questboard_media_<ts>.tar.gz, which the
test's listing of the scheduled files did not expect. Order-dependent, so
B2's targeted run passed. The test is about the dumps; it now says
include_media: false.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(webapp): a server handover clears the old owner before setting the new (#855)
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 2m57s
CI / Backend lint (ruff) (pull_request) Successful in 1m17s
CI / Bot tests and audit (pull_request) Successful in 2m3s
CI / Bot/backend version sync (pull_request) Successful in 27s
CI / Docker image build (pull_request) Successful in 20s
CI / Does this change touch the recording path? (pull_request) Successful in 30s
CI / Feedback site tests (pull_request) Successful in 2m2s
CI / Backend migration, tests, and audit (pull_request) Successful in 13m58s
CI / Frontend tests, audit, and build (pull_request) Successful in 41m31s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 19m15s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 33m6s
7d12eb3be0
uq_users_one_server_owner is a partial unique index, checked row by row and
not deferrable, and one flush writes both rows in primary-key order - so the
handover failed whenever the new owner's id sorted first (CI hit it; local runs
happened not to). The CLI's set_owner no longer clears its own target either.
A parametrized test forces both id orders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merge branch 'train/1-855' into train/2-859
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m2s
CI / Bot/backend version sync (pull_request) Successful in 26s
CI / Docker image build (pull_request) Successful in 19s
CI / Backend lint (ruff) (pull_request) Successful in 1m11s
CI / Does this change touch the recording path? (pull_request) Successful in 21s
CI / Bot tests and audit (pull_request) Successful in 2m1s
CI / Feedback site tests (pull_request) Successful in 1m54s
CI / Frontend tests, audit, and build (pull_request) Successful in 18m18s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Failing after 55s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been cancelled
CI / Backend migration, tests, and audit (pull_request) Has been cancelled
3e87535fc7
fix(e2e): the seed reads the inline test email's answer (D147, #859)
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m1s
CI / Bot tests and audit (pull_request) Successful in 2m3s
CI / Backend lint (ruff) (pull_request) Successful in 33s
CI / Feedback site tests (pull_request) Successful in 46s
CI / Bot/backend version sync (pull_request) Successful in 28s
CI / Docker image build (pull_request) Successful in 29s
CI / Does this change touch the recording path? (pull_request) Successful in 27s
CI / Backend migration, tests, and audit (pull_request) Successful in 14m33s
CI / Frontend tests, audit, and build (pull_request) Successful in 19m33s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 13m24s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 18m49s
6bb1da69bd
B2 made the admin Test Email send inline and answer 200 {sent, error}
instead of queueing (202). The seed still expected the 202 and failed
the journeys job before any test ran.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign in to join this conversation.
No description provided.