feat: lane 6, prep and planning: the prep sheet, the tool margin, History and Planning, J3 and the seed (#732) #858

Merged
claude-bot merged 34 commits from build/lane6-all into main 2026-09-27 00:03:40 +00:00
Collaborator

Lane 6 (#732), complete. It follows the owner's answers P1–P12 (D177, D178, D182–D191) and the accepted departure lists b6a, b6b and b6c (D214–D216).

What's in it

  • Server:
    • 1a: prep sheets;
    • 1b: items and the plan;
    • 2: History and the tools, each with an output schema, plus "Suggest the next beat" (P5);
    • 3: Planning.
  • Pages:
    • 4a: the prep sheet at /sessions/:id/prep, with redirects from the old /planning links (P1);
    • 4b: the tool margin, and the phone's Tools sheet;
    • 4c: History and Planning.
  • J3, "a GM preps": in its own campaign, on desktop, phone and keyboard, with axe checks throughout.
  • The demo seed: 6 prep sheets across their states, 14 items, pins that are open, riding a shelf or ended, 17 History runs, 6 arcs (one soft-deleted), and a scene made for a session.

P12 (D191)

  • Failed runs that used to be pinned now clear after 30 days, like any other.
  • Migration d6f8b0c2e4a7 drops generation_results.pinned.
  • The retired /pin and /unpin routes are gone; nothing in the frontend called them.

Migrations

The chain is re-chained onto main: c4e6a8b0d2f1 → b8c0d2e4f6a7 → f6a8b0c2d4e5 → a7b9c1d3e5f6 → d6f8b0c2e4a7. There is one head, and up/down/up runs cleanly.

Bugs J3 found along the way

  • The plan could land after its run already read "ready". land_plan now does it in one commit.
  • The vote bugs. This branch takes main's versions, and #851/#854 fixes them properly across the whole app.
  • A direct .delay() in prep_service.draft_plan. It now uses enqueue (#850's guard).

Tests

  • Backend, full suite: 4,862 passed, 13 skipped.
  • Frontend: 2,488 passed; eslint has 0 errors.
  • ruff is clean.
  • coverage/lane5.py (lane 6's checklist) is fully classified. Its P2 Foundry rows are now deferred to v5.x.
  • Journeys: two full local runs were green, 107 passed, before the merge from main. CI runs them again here.

Before merge

Once #854 (#851) is on main, main gets merged in again and lane 6's new Depends(get_db) calls become scope="function".

🤖 Generated with Claude Code

Lane 6 (#732), complete. It follows the owner's answers P1–P12 (D177, D178, D182–D191) and the accepted departure lists b6a, b6b and b6c (D214–D216). ## What's in it - **Server:** - 1a: prep sheets; - 1b: items and the plan; - 2: History and the tools, each with an output schema, plus "Suggest the next beat" (P5); - 3: Planning. - **Pages:** - 4a: the prep sheet at `/sessions/:id/prep`, with redirects from the old `/planning` links (P1); - 4b: the tool margin, and the phone's Tools sheet; - 4c: History and Planning. - **J3, "a GM preps":** in its own campaign, on desktop, phone and keyboard, with axe checks throughout. - **The demo seed:** 6 prep sheets across their states, 14 items, pins that are open, riding a shelf or ended, 17 History runs, 6 arcs (one soft-deleted), and a scene made for a session. ## P12 (D191) - Failed runs that used to be pinned now clear after 30 days, like any other. - Migration `d6f8b0c2e4a7` drops `generation_results.pinned`. - The retired `/pin` and `/unpin` routes are gone; nothing in the frontend called them. ## Migrations The chain is re-chained onto main: `c4e6a8b0d2f1 → b8c0d2e4f6a7 → f6a8b0c2d4e5 → a7b9c1d3e5f6 → d6f8b0c2e4a7`. There is one head, and up/down/up runs cleanly. ## Bugs J3 found along the way - **The plan could land after its run already read "ready".** `land_plan` now does it in one commit. - **The vote bugs.** This branch takes main's versions, and #851/#854 fixes them properly across the whole app. - **A direct `.delay()` in `prep_service.draft_plan`.** It now uses `enqueue` (#850's guard). ## Tests - Backend, full suite: **4,862 passed**, 13 skipped. - Frontend: **2,488 passed**; eslint has 0 errors. - ruff is clean. - `coverage/lane5.py` (lane 6's checklist) is fully classified. Its P2 Foundry rows are now deferred to v5.x. - Journeys: two full local runs were green, 107 passed, before the merge from main. CI runs them again here. ## Before merge Once #854 (#851) is on main, main gets merged in again and lane 6's new `Depends(get_db)` calls become `scope="function"`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Tests first: GET/PATCH /sessions/{id}/prep with the sheet's own version,
migration 3b7e1d9c5a26 (prep_sheets, prep_items, shelf_pins'
shelf start and its backfill), and reconcile_pins filling shelves only
from a pin's shelf start (P3a). The one-head pin moves to the new
migration's test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Migration 3b7e1d9c5a26 on c3d5e7f9a1b2: prep_sheets (one per session,
made on the first save, with its own version for autosave), prep_items
(the table only; 1b writes it), and shelf_pins.shelf_start_session_id,
backfilled to each pin's start once, when the column is made.

GET and PATCH /api/sessions/{id}/prep, GMs of the campaign only (P7).
A stale save is a 409 carrying the current sheet. The beats stay in
sessions.beat_notes and are saved under the sheet's version; the old
beat-notes endpoint moves that version, and a beats save from the sheet
moves the old endpoint's token, so the two editors meet as conflicts.

P3(a): a pin rides every later sheet from its start, but reconcile_pins
fills shelves only from its shelf start. The table's pin sets both. The
cancel prune and the cold open's carried count treat the shelf start
as where a card originated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane 5's PR 4b took e5f7a9b1c3d4, the survey's first choice, and the
coordinator assigned the survey's spare. down_revision stays on main's
head, c3d5e7f9a1b2; it is re-chained after 4b at merge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tests first: insert (a run under Generated, a rule under Rules, a wiki
article under Wiki, P10), remove and undo, For later and moving it onto a
sheet (P11), pin / unpin / pin again with P3(a)'s two starts, Add to
shelf into a scene from an item or a carried block, and the plan: draft,
the worker's write into the sheet, redraft, use this draft, and each
part to the shelf.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Items: Insert into sheet (a run under Generated, a rule under Rules,
a wiki article under Wiki: P10a), one per source per sheet, Remove with
an undo; Keep for later (P11a) and moving it onto a sheet. The item
routes live at /api/prep-items/{id}, For later at
/api/campaigns/{id}/prep/later.

Pins (D39, P3a): pinning an item makes one pin that rides every later
sheet, and rides the shelves only if the item is already on this shelf.
Add to shelf, from an item or a carried block, sets the pin's shelf
start there and files the card (and the pin) in the scene. Unpin and
Pin again reopen the same pin; the table's Unpin tail is now
shelf_service.end_pin, shared.

The plan (D38): POST .../prep/plan queues session_prep with the sheet's
want as its direction and names the run on the sheet. The worker
(run_workbench_generation_core, on task_session) writes the plan into
the sheet if the sheet still names that run, dated by when the tool
began, so a change made while drafting leaves it stale. Use this draft,
and each scene, secret or NPC onto the shelf as a note or wiki card.
The plan and items never move the sheet's autosave version. No
migration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tests first: migration f6a8b0c2d4e5 (runs' soft delete, P4a's move of
pinned runs into their session's sheet or For later), the sweep sparing
what a sheet keeps, History rows (kept_by, clears_at, replaced, label,
tool lists), delete with an undo, the sheet's History section; an
output schema per tool; next_beat (P5a); names and warm draws leaving
runs (#693); editing the plan under the sheet's version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The migration a7b9c1d3e5f6 (an arc's soft delete, the session a scene was
made for) is here with its tests, which take the one-head pin from
test_prep_sheets_migration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Migration f6a8b0c2d4e5 on b8c0d2e4f6a7: generation_results.deleted_at,
and P4(a)'s move: each pinned, finished run becomes an item under
Generated in its session's sheet (For later with no session), kept, not
carried, no pin; its flag is cleared. A pinned run that never finished
keeps its pin. Nothing is deleted. Downgrade re-pins every run a live
item keeps.

The sweep deletes a run only when nothing keeps it (a live item, a
removal within 30 days, a sheet's plan, or the retired pin), and purges
History's soft deletes after a day. History rows gain label, kept_by,
clears_at, replaced and deleted_at, a repeatable tool_id and a
session_id filter; Delete is soft with /restore, and 409 for a run a
sheet keeps. GET /prep gains the sheet's History section.

Every tool declares a strict output schema, sent as json_schema (names
through generate_name_options). next_beat (P5a) proposes one beat from
what the GM wants, the beats, the last recap and the open arcs and
quests. Cached names and every Instant NPC draw leave a run, named by
result_id (#693). PATCH /prep takes the plan under the sheet's version,
naming the draft it was made on, so a draft that lands meanwhile is a
409, not overwritten.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- GET /campaigns/{id}/planning: the sessions ahead and what their sheets
  hold, every pin and what it rides, the scene library, the arcs, the open
  quests and For later, in one read that writes nothing.
- P8(a): POST /campaigns/{id}/pins/{pin}/unpin stops a pin at the last
  session played or being played (or where it was pinned, if that is later)
  and names the session; /pin-again is its Undo.
- Arcs: a soft delete with POST .../restore, and PUT .../arcs/order.
- scenes.session_id, the session a scene was made for.
- prep_session on the campaign list, and This week's prep line gains beats,
  plan_drafted and has_sheet, both from the rule Planning uses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings lane 6's four server PRs (1a, 1b, 2, 3) onto main, which now has
lane 5's Table mode (d4e6f8a0b2c3, e5f7a9b1c3d4), lane 7's safety
(7e5afe7a1b2c) and R1/R2.

- The chain is now c3d5e7f9a1b2 -> d4e6f8a0b2c3 -> 7e5afe7a1b2c ->
  e5f7a9b1c3d4 -> b8c0d2e4f6a7 -> f6a8b0c2d4e5 -> a7b9c1d3e5f6 (one head).
  test_planning_migration keeps the one-head pin; test_drop_scene_text_migration
  becomes a still-in-the-chain test; test_shelf_cards_migration takes main's
  d4e6f8a0b2c3 pin.
- app/main.py mounts prep, planning and safety.
- docs/API.md and CHANGELOG keep both sides' sections.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The session's prep sheet as a document that saves itself (D38-D40):
Where we left it, Carried forward (pin, unpin with its undo, pin again,
add to shelf in a scene), What I want, Beats (a numbered list written in
place), The plan (draft, drafting, still drafting, failed, drafted, stale,
redraft, edit on the draft it was made on, each part to the shelf),
Generated, Rules & bestiary, Wiki (P10), the Shelf section by scene (make,
rename, file; no Foundry, P2; no "shows at the start", P6) and History
(insert, use this draft).

Autosave: a second after typing stops, only the fields typed, on the
sheet's version; a failed save keeps the words and backs off; the 409 is
"changed in another window" with Keep mine and Use the other window's; the
leave guard asks only when a save has failed or met another window.

The address is the session's (P1): old /planning links naming a session
and the plan or beats go to its sheet; /campaigns/:id/prep is the next
sheet, or "No session to prep". The Contents' Running it entry is
"Prep · Session 14", current on every sheet with the sheet's sections under
it; the header's chip says "GMs only" (P7). Sessions, This week, the
session page and the old campaign page link to the sheet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Tailwind's preflight took the markers off Beats and the plan's lists:
  numbered and bulleted again.
- A tool's output of several lines ran on from its title after a dash:
  it starts under the title now, one line a line.
- The sheet column is capped at the measure, so a section's note lines up
  with the blocks under it rather than with the far edge of the frame.
- The header's time and tally are one run of text, so a phone does not
  leave the dot alone at the end of a line.
- The leave guard's callers list names the sheet's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seven drawers beside the sheet (Story arcs, Quests, NPCs, Names, Improv,
Rules & bestiary, Wiki), and on a narrow screen the Tools sheet (D42), which
takes Open the table's place on a phone. Every tool's result row is Insert
into sheet, Add to shelf in a scene (IS-341, TB-085), Append to an article,
Save to the wiki and Copy. Names are runs now (#693); Instant NPC draws are
runs made for the sheet. Suggest the next beat (P5) under Beats and in the
Story arcs drawer: one beat, Add or Another, nothing in without Add. Suggest
a title on a recurring session's heading (IS-342). History's Re-open opens
the run's drawer. The two lookups insert into Rules & bestiary and Wiki
(P10); no Convert stat blocks in bulk (P9); no Foundry (P2).

Rule snapshots carry the pack's scope and whether the record is GM-only, so
the sheet's rules blocks can say so. Old /planning links to a margin tool
open its drawer on the sheet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Add to shelf opens its scenes in place inside the margin: the margin
  scrolls on its own, and a popover hung off it was cut off at its edge.
- The Tools sheet's Close sits beside its title, not a long scroll down.
- The header's meta row ends at the margin's edge, not the frame's, at 2560.
- Tests for the margin: the drawers, a required field sending nothing,
  Insert and Add to shelf in a scene, a failure, rumours, Instant NPC and
  its empty pool, ready-made names and not-ready, both lookups, the arcs and
  quests drawers, Suggest the next beat, Suggest a title, Re-open and the
  Tools sheet. The Shelf section's test waits for all its groups at once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/campaigns/:id/planning is Planning now (P1, D41): sessions ahead with each
sheet's state; everything pinned with Unpin (P8(a), stops at the last session
played) and its Undo; ended pins; the scene library (sessions used, cards,
made for, rename, new; no Foundry, P2); story arcs edited in place (move
either way by key or button, delete with Undo, add, suggest, PR-054's
failure); and For later (IS-352), with the tool margin beside it keeping
what it makes for later (P11(a), IS-353). Old links naming a session or a
sheet's tool still go to the sheet.

/campaigns/:id/planning/history is Generation history (PREP-3): the groups
there are, where each run lives and when it clears, Re-open (into its sheet
or Planning), Try again, Insert into the next session, Use this draft, and
Delete with Undo; a run a sheet keeps says so in the server's words (409).

The sheet's History links to it (All history); No session to prep offers
Plan without a date; Edit the arcs goes to Planning's arcs. The Contents'
Running it has Planning above Prep. The old Prep page and what only it used
(GeneratorPanel, workbenchTools, BeatNotesEditor, GenerationHistory,
LorePicker, WorkbenchResultActions) are gone, with their tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The render check: on a phone the first arc's row squeezed its title and
notes into a one-word column beside Up, Down and Edit, and History's rows
did the same beside their actions. A row's actions now go under it there,
and a row with none (a run still writing) leaves no gap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #833 (J4, lane 5's seed, the stateful fake bot) and #837 (the
last-admin guard). Git resolved it without conflicts; main added no
migrations, so lane 6's chain stays on e5f7a9b1c3d4 with one head.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sheet found on Planning, what she wants and the beats saving
themselves, the plan drafted and a secret put on the shelf, a read-aloud,
a creature and a quest from the margin (the Tools sheet on a phone), a pin
carried to the next sheet, a scene with the creature filed in it, Unpin
and Undo on Planning, and History's keep and delete, on all three
projects with axe clean.

J3 preps in its own campaign, The Saltglass Survey, with no players and
no Discord, so the sessions each project makes tell nobody. The fake AI
scripts the plan, the read-aloud and names by their output schema, and
aiRequests()/aiNow() read what it was asked.

Fix what J3 found: the worker marked the plan's run ready in one commit
and wrote the plan in another, so a sheet polling between the two
stopped asking and said "Nothing drafted yet" beside a written plan.
land_plan now marks the run ready in the commit that lands its plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Six prep sheets, one per state (complete, stale, what I want and beats
only, two played records, and a blank one in The Tuesday Tide), with
Session 27 left without one; fourteen items across Generated, Rules &
bestiary and Wiki, For later (and one moved from it) and one removed;
three pins made on sheets (sheets only, on the shelves a session later,
ended); eleven more History runs, none pending; the old keep-pinned runs
as the items P4(a)'s migration makes them; arcs in the GM's order with
one soft-deleted; the empty scene made for Session 24.

test_demo_seed reads it back through prep_service, history_service and
planning_service, and lane 5's rule-card tripwire now counts only the
unpinned cards. The README moves Planning to Seeded and says the GM pages
don't show to the owner's account, which plays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An empty Read-aloud's "nothing reached the model" now filters the fake
AI's log by the read-aloud's scripted answer, so a pipeline another
journey left running cannot answer for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
notify_vote_cast queued send_vote_notification without catching a
failure, though the broker fails fast (a two-second socket timeout) on
the promise that request-path enqueues are non-fatal. A broker slow for
three seconds made a player's vote a 500 and rolled it back; J2's grid
then showed the earlier answer. Found by a full journeys run on a loaded
host; the notification is now best-effort and logged.

J3 also waits for the campaign list before asking the Contents for
Planning: the rail is drawn before it knows Dana is a GM here, and on a
slow stack the Running it group arrived after goVia gave up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
get_db commits when the request's dependencies close, and FastAPI closes
them after the response has been sent. So the vote grid's second press
could reach the server before the first was committed, find no vote,
insert one, and collide with the first in uq_votes_slot_user: a 500, and
the grid put the earlier answer back. J2 met it twice in lane 6's full
journeys runs.

upsert_vote and delete_vote now commit (the codebase's "Commits."
convention), so the answer only goes once the vote is durable, and the
vote-update notification is queued after it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #838, #842, #847, #848, #849, #850 and #852.

- Migrations: lane 6's first migration, b8c0d2e4f6a7, is re-chained from
  e5f7a9b1c3d4 onto main's head c4e6a8b0d2f1, so the chain is
  e5f7 -> a2c4 -> c4e6 -> b8c0 -> f6a8 -> a7b9. c4e6's one-head pin becomes
  still-in-the-chain; test_drop_scene_text_migration takes main's side;
  test_notifications' history names both sides.
- vote_service: main's versions (ON CONFLICT upsert, enqueue), per #847 and
  #850. Lane 6's own fixes (9f1b3c01, f8875f46: a best-effort vote
  notification, a commit before the route answers) are superseded by #851's
  fix in flight (#854), and their two tests are dropped with them, along
  with the Fixed bullet that described them.
- campaigns.py: the Instant NPC draw's refill goes through enqueue (#850)
  and still returns lane 6's result_id.
- prep_service.draft_plan queued the plan with a direct .delay() from an
  async def; it now uses await enqueue(...) (#850).
- CHANGELOG: both sides kept, and a stray "=======" lane 6's last merge
  left behind is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The owner's answer to P12: the old pinned runs that P4's migration left
pinned, the ones that failed or never finished, stay ordinary unkept runs
and clear 30 days after they were made, like any other. With nothing left
to read or write it, the column goes.

- Migration d6f8b0c2e4a7, after a7b9c1d3e5f6, drops the column; its
  downgrade re-adds it as NOT NULL DEFAULT false, and f6a8b0c2d4e5's own
  downgrade then re-pins what a live item keeps. The one-head pin moves to
  test_drop_generation_pinned_migration; test_planning_migration's becomes
  still-in-the-chain; test_notifications' history names it.
- The sweep's exemption goes from kept_clause, so the failed runs clear.
- Removed with it: the model column, set_pinned, the list's pinned
  filter, POST .../workbench/history/{id}/pin and /unpin (the History
  panel that called them is gone; nothing in the frontend did), the
  pinned field on History's rows, the demo seed's pinned=False, and the
  frontend's unused pinResult/unpinResult.
- Tests: the retired routes are 404s, a failed run clears 30 days after
  it was made, the sweep takes an old failed run, and the migration's
  up, down, up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs(design): lane 6's checklist defers the eight Foundry rows (#732, P2)
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m0s
CI / Bot tests and audit (pull_request) Successful in 2m16s
CI / Backend lint (ruff) (pull_request) Successful in 1m28s
CI / Feedback site tests (pull_request) Successful in 39s
CI / Bot/backend version sync (pull_request) Successful in 1m14s
CI / Docker image build (pull_request) Successful in 59s
CI / Does this change touch the recording path? (pull_request) Successful in 42s
CI / Backend migration, tests, and audit (pull_request) Successful in 15m35s
CI / Frontend tests, audit, and build (pull_request) Successful in 38m38s
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
19b7b2ce84
P2 was answered as recommended: draw none of Foundry until the connection
exists (v5.x), as D113 ruled for Table mode, and move the eight Foundry
rows to deferred so the lane can be finished. The pages already leave it
out and test for it; the checklist still counted the rows as drawn.

IS-277, IS-282 to IS-286, IS-288 and IS-350 are deferred to v5.x; IS-349
stays drawn without its pairing. coverage/lane5.py: 148 drawn, 40
replaced, 9 behaviour, 16 elsewhere, 10 deferred; none unclassified.

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>
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>
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>
- CHANGELOG: kept lane 6's entries and lane 9 A/B2's.
- campaigns.py: kept lane 6's get_workbench_result (returns _history_row);
  the pin/unpin endpoints main had scoped stay deleted (P12, d6f8b0c2e4a7).
- b8c0d2e4f6a7 now revises b3d5f7a9c1e4 (it was on c4e6a8b0d2f1). The one-head
  pin stays with test_drop_generation_pinned_migration; the B2 migration's pin
  becomes test_this_migration_is_still_in_the_chain; c4e6's chain test keeps
  f1a3 as its successor; test_notifications' history docstring gains lane 6's
  steps after B2's.
- Lane 6's 24 Depends(get_db) (campaigns.py, planning.py, prep.py) are
  scope="function".
- vote_service, votes.py and bot.py are byte-identical to main (#854).

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>
Merge train/2-859 into lane 6 (#858): the retention test's media fix
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 57s
CI / Bot tests and audit (pull_request) Successful in 2m4s
CI / Backend lint (ruff) (pull_request) Successful in 32s
CI / Feedback site tests (pull_request) Successful in 48s
CI / Bot/backend version sync (pull_request) Successful in 28s
CI / Docker image build (pull_request) Successful in 33s
CI / Does this change touch the recording path? (pull_request) Successful in 29s
CI / Backend migration, tests, and audit (pull_request) Successful in 15m41s
CI / Frontend tests, audit, and build (pull_request) Successful in 19m32s
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
c429ab0898
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
Merge branch 'train/2-859' into train/3-858
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 49s
CI / Backend lint (ruff) (pull_request) Successful in 32s
CI / Bot/backend version sync (pull_request) Successful in 25s
CI / Feedback site tests (pull_request) Successful in 40s
CI / Docker image build (pull_request) Successful in 17s
CI / Does this change touch the recording path? (pull_request) Successful in 25s
CI / Bot tests and audit (pull_request) Successful in 5m7s
CI / Backend migration, tests, and audit (pull_request) Successful in 14m40s
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
baae63ac1c
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>
Merge branch 'train/2-859' into train/3-858
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 2m7s
CI / Backend lint (ruff) (pull_request) Successful in 32s
CI / Feedback site tests (pull_request) Successful in 48s
CI / Bot/backend version sync (pull_request) Successful in 27s
CI / Does this change touch the recording path? (pull_request) Successful in 26s
CI / Docker image build (pull_request) Successful in 31s
CI / Frontend tests, audit, and build (pull_request) Successful in 19m25s
CI / Backend migration, tests, and audit (pull_request) Successful in 43m8s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 35m42s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 37m13s
2805a3e766
Sign in to join this conversation.
No description provided.