feat(webapp): Admin's Recording & retention, Backup, Import and Audit log (lane 9 page 4b) — stacked on #864 #873

Merged
claude-bot merged 4 commits from build/lane9-4b-pages into main 2026-09-27 09:34:55 +00:00
Collaborator

Lane 9 (#735), page 4b. Stacked on the second merge train, on top of #864. This is frontend only: the server work (A, B2, C and D179's retention) is already underneath it.

What's rebuilt

  • ADMIN-5/6, Recording & retention, with D179's meanings:
    • the default and the instance cap;
    • the cap confirm, via GET /admin/settings/retention/preview, which lists the campaigns a new cap would cut;
    • the on-disk recordings table, which polls while any row is processing.
  • ADMIN-7, Backup:
    • "Back up now" runs with no confirm, and polls while it runs;
    • the write-only remote, with credentials_set;
    • uploaded images on by default (D143).
  • ADMIN-9, Import: it shares a new ImportCampaignForm with the New Campaign page's import (D46).
  • ADMIN-10, the audit log: filters by name and category, "What happened" labels, expandable detail, and Earlier/Later paging.
  • J1's Backup leg walks the new page.

Coverage

ADMIN-5 (AD-077–AD-084, IS-311), ADMIN-6 (AD-084–AD-088), ADMIN-7 (AD-089–AD-098, IS-081, IS-396), ADMIN-9 (AD-129–AD-133), ADMIN-10 (AD-134–AD-141). coverage/lane7.py reports "unclassified: none".

Departures

  • The recordings table shows plainer lines than the drawing's "Kept 90 days … until 23 November" and "3 of 6 voices". The date follows in #872; the per-voice progress is deferred.
  • The cap confirm lists every campaign it would cut, with no export shortcut, because an administrator may not be a member of them.
  • The audit log shows the server's own description for each event.
  • The legacy Admin.jsx sections stay until page 4d deletes the old page.

The render check

There was no seeded stack, so the four pages were mounted against realistic fixture data and screenshotted with Playwright:

  • 2560, 1440 and 1024, in light and dark;
  • a true 390 width, with no horizontal overflow;
  • five extra states.

The only fault found is shared with 4a's tables: they wrap at 390 instead of scrolling. That's filed as #872.

Tests

  • 4 new test files (14 tests). The full vitest suite: 2,619 passed. eslint: 0 errors.
  • The New Campaign page's 19 tests still pass after the shared-form refactor.
  • A changelog.d/ fragment is included.

🤖 Generated with Claude Code

Lane 9 (#735), page 4b. **Stacked on the second merge train, on top of #864.** This is frontend only: the server work (A, B2, C and D179's retention) is already underneath it. ## What's rebuilt - **ADMIN-5/6, Recording & retention**, with D179's meanings: - the default and the instance cap; - **the cap confirm**, via `GET /admin/settings/retention/preview`, which lists the campaigns a new cap would cut; - the on-disk recordings table, which polls while any row is processing. - **ADMIN-7, Backup:** - "Back up now" runs with no confirm, and polls while it runs; - **the write-only remote**, with `credentials_set`; - uploaded images **on by default** (D143). - **ADMIN-9, Import:** it shares a new `ImportCampaignForm` with the New Campaign page's import (D46). - **ADMIN-10, the audit log:** filters by name and category, "What happened" labels, expandable detail, and Earlier/Later paging. - **J1's Backup leg** walks the new page. ## Coverage ADMIN-5 (AD-077–AD-084, IS-311), ADMIN-6 (AD-084–AD-088), ADMIN-7 (AD-089–AD-098, IS-081, IS-396), ADMIN-9 (AD-129–AD-133), ADMIN-10 (AD-134–AD-141). `coverage/lane7.py` reports "unclassified: none". ## Departures - **The recordings table shows plainer lines** than the drawing's "Kept 90 days … until 23 November" and "3 of 6 voices". The date follows in #872; the per-voice progress is deferred. - **The cap confirm lists every campaign it would cut,** with no export shortcut, because an administrator may not be a member of them. - **The audit log shows the server's own description** for each event. - **The legacy `Admin.jsx` sections stay** until page 4d deletes the old page. ## The render check There was no seeded stack, so the four pages were mounted against realistic fixture data and screenshotted with Playwright: - 2560, 1440 and 1024, in light and dark; - a true 390 width, with no horizontal overflow; - five extra states. The only fault found is shared with 4a's tables: they wrap at 390 instead of scrolling. That's filed as #872. ## Tests - 4 new test files (14 tests). The full vitest suite: **2,619 passed**. eslint: 0 errors. - The New Campaign page's 19 tests still pass after the shared-form refactor. - A `changelog.d/` fragment is included. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
WIP milestone: the five discussion tables (model + migration 7d15c0a1b2e3)
and app/services/audience_service.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Written before the router; they fail until it lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WIP milestone: the endpoints, before the first green run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The audience service (app/services/audience_service.py) is #593's one
definition of who can see a thing: table, party, mine, chosen people and
characters, or GMs only, every GM always inside, resolved when read. It
answers for one reader, for everyone, and as a SQL predicate; a test holds
the three equal. Lanes 8 and 11 reuse it.

Discussions (migration 7d15c0a1b2e3, five tables) with the board, starting,
reading and replying, GM notes for fewer people (refused when they reach
outside the thread, IS-145; thread and post audiences intersected on every
read), answers, verdicts, audience changes with a gains/losses preview,
follows, read markers, lock, soft delete with restore, post edit/hide/delete
(T9(a)) and the Talk counts. A thread or post a reader can't see is a 404
identical to a missing one. A crawl walks every endpoint as every kind of
reader and finds nothing leaked; a test pins which modules may read the
tables (never AI context, #654).

The one-head pin moves to test_discussions_migration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The migrations chain d4e6f8a0b2c3 -> 7e5afe7a1b2c -> 7d15c0a1b2e3: the
discussions migration is re-chained onto the safety one, and the one-head
pin moves to test_discussions_migration. CHANGELOG and docs/API.md keep
both sides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Written before the code; they fail until it lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WIP milestone before the first run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Also: talk-counts' safety_waiting in PR 1's count test, and viewers_for held
equal to Roster.viewer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	CHANGELOG.md
# Conflicts:
#	CHANGELOG.md
The page at /campaigns/:id/safety (CAMP-18): lines and veils for everyone;
for a GM, Edit, Remove with Undo (D12), "+ Add", and the waiting queue with
Add as a veil / Add as a line (in the GM's wording) and Dismiss. A player gets
the anonymous suggest card with the drawn words (T3(a)) and every GM named
(T10(a)), and IS-017's Sent card.

The flag dialog, "Flag something to Dana", behind four doors: This week's
card (IS-205, about no session), the foot of a live or played session's page
(T2(a), IS-210), and Table mode's players' face ("Flag something quietly").
The GM's Table-mode face gets the quiet "N waiting in Table rules & safety"
in its place, polled from GET …/safety/waiting (T4(a)).

The Contents gains the Table rules & safety entry, with the GM's count
(IS-204, IS-212); a player is never asked for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The flag's wrapper on Table mode's players' face carried .qb's paper, which
drew a strip across the old face's own background in both themes (found by
the render check). CHANGELOG bullets for Table rules & safety and its doors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The kit's "who can see this": AudiencePill, AudienceMenu and
  AudienceControl beside the chip row, with the drawn words, T10(a)'s
  naming of every GM, a Players | Characters switch and a checklist, and a
  keyboard-operable dialog that traps focus.
- The Discussions board (CAMP-19) with its filters as the address and
  T12(a)'s sentence only while empty; starting a thread (all three kinds,
  About search, a rumour's quest); a thread (CAMP-20) with answers,
  verdicts, notes for fewer people with IS-145's refusal said before
  posting, follow, moderation, widen/narrow, delete with Undo, and canon.
- Talk on every session that is not cancelled (T1(a)) and on an article,
  with the article's Read · Talk · History bar on today's page (T8(a)).
- The Contents' Discussions entry with its unread count (IS-157), Home's
  Needs you rows (IS-152), and the bell's four discussion kinds and
  safety_waiting, with a test that every server kind has an entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The reply box is the drawing's: a placeholder, not a label above it;
  a refused note stops the button without saying the refusal twice.
- A thread's title at the drawing's h2 step, so a question fits a phone.
- The article's tab bar without the band `.qb` painted on the old page;
  the Talk page and the new-thread form read the article from `entry`.
- A question's Needs you row says its day, not a table time.
- The picker is taller than a plain menu, so its people fit.
- The leave-guard list and a player's Read tabs now include lane 7's.
- CHANGELOG.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re-chains 7d15c0a1b2e3 (discussions) onto e5f7a9b1c3d4 (lane 5 PR 4b's
scene-text drop), which reached main first; 7f2d0c1a3b4e stays on top of it
and keeps the one-head pin. test_drop_scene_text_migration's pin becomes a
still-in-the-chain one, and test_notifications' history names both moves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CHANGELOG: kept main's Table-mode entries (lane 5's #832 had since redrawn
the players' face and added Start/Stop to the bar) under A's new entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The players' flag door moves from SessionTable (deleted by lane 5's #832)
  onto the new players' face, PlayersTable: a block button at the foot of its
  one column, as lane4.html draws it, spaced by .tmp's own gap. The Players'
  view keeps it, being what players see. .qb-flag-table goes.
- The GM's waiting count sits at the foot of lane 5's aside, after the
  preview, "Shown to the table" and the quick note, where the drawing had
  "Flag something quietly".
- One read for the Contents count: the Table rules & safety entry takes
  talk.safety_waiting from the frame's …/talk-counts read (A's useTalkCounts),
  still gated on the viewer being a GM. useSafetyWaiting and its second
  request go; the Talk counts re-read after a write changesSafetyWaiting names.
  AppFrame.test's two safety-count tests now drive fetchTalkCounts, the
  player's with a number the server would never send.
- Notifications: the GM tasks row says "and Table rules & safety", since
  safety_waiting is filed there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Amberwake Accord talks: 15 threads and 51 posts across every kind,
state and audience — an open question with two replies the owner has not
read, an answer made canon into The Tidewatch Ledger, Dana's question to the
owner (Home's "A question for you"), rumours open, confirmed and denied,
threads about Sessions 23, 13 and 4 and two articles, a locked thread, a
deleted one, a hidden post, GM notes, and four threads the owner cannot see.
Follows, reads (the Contents says 2) and the bell's rows, written as rows.

Table rules & safety: the drawing's three lines and three veils, one line
removed, and three anonymous submissions waiting — day-only, no sender —
with Dana's one "Something is waiting" row.

The Tuesday Tide is left empty on purpose. test_demo_seed checks the counts,
the owner's view (11 of 15, 2 unread, the question in Needs you), Dana's
Needs you and waiting count, and that every seeded bell row renders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
J2, at its lane 7 TODO: on the session she has just said yes to, Priya
opens Talk (T1(a)), starts a question that defaults to "Just you and Dana"
(asserted), walks the picker (a dialog; Escape hands focus back to the
pill) and asks. Dana is told in the bell, sees who can see it, replies; the
reply box names exactly that audience. Priya is told, marks the answer, and
the thread reads Answered. axe on the Talk tab, the form with the picker
open, the thread for both, and the bell with the new kind in it.

J2's safety block (#121), in a campaign of its own since the count is per
campaign: Priya suggests a veil with no name on it; Dana's Contents says
"Table rules & safety 1", the bell's generic row takes her to the queue, she
adds it as a veil in her own words, and Priya reads it.

J6's act 4: the board, the thread Priya asked about Session 2 and its Talk
tab, with a seeded GMs-only thread and GM note absent on the page and in the
API answers; and the table's line and veil. J5 and J6 expect a player's
Read and Talk tabs (IS-202) where they expected none.

The seed writes canon.talk through the product's API, and pencils Session 3
in, confirming it as its last act: J2 reads "Session 3 is on" among Priya's
twenty newest bell rows in the third project, and the question leg's reply
row per project had pushed it off (J4 met the same wall).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
J1 went straight from "Create the session" to the Contents' This week while
the session page was still arriving. In the keyboard project the Enter was
lost two runs in five once lane 7 gave that page one more request and its
Talk tab: focus ended on the session's heading, the address unchanged. It
now waits for the heading, as the rest of the suite waits for a page before
leaving it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Make it canon" on a party thread wrote the answer, unblocked, into a wiki
page the whole table reads, so members outside the party read it there
(found by the lane 8 survey). CANON_AUDIENCES is ("table",) until lane 8's
party and visibility blocks (IS-148) can carry the thread's audience into
the article; a party answer is now a 422, like a narrower one. The thread
page follows: no "Make it canon" on a party thread. Tests hold both sides;
API.md and the CHANGELOG say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merge main (#833: J4 and lane 5's demo seed) into lane 7's integration (#733)
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 52s
CI / Backend lint (ruff) (pull_request) Successful in 31s
CI / Feedback site tests (pull_request) Successful in 46s
CI / Bot/backend version sync (pull_request) Successful in 51s
CI / Bot tests and audit (pull_request) Successful in 2m4s
CI / Does this change touch the recording path? (pull_request) Successful in 37s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Docker image build (pull_request) Successful in 2m28s
CI / Backend migration, tests, and audit (pull_request) Successful in 15m15s
CI / Frontend tests, audit, and build (pull_request) Successful in 20m33s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 22m2s
e0ec155c4e
demo/engine.py: both lanes' calls, the rules pack before Session 24's shelf
(lane 5) and lane 7's talking and table rules after it. test_demo_seed: lane
7's discussion and safety checks and lane 5's _the_table_is_seeded, both.
demo/README.md: both lanes' "What it touches" bullets. The e2e seed,
identities.json and README merged cleanly: J4's campaign and Joaquín, and
Session 3 pencilled in and confirmed last, both kept. Preview.jsx takes
#833's `article.entry` fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Applies the owner's two lane 7 decisions on top of the built T10(a)/board work:

- D174 (T10(b)): every audience sentence that named a GM now says "the GM"
  (one) or "the GMs" (more than one) instead — the picker's labels and
  foot, the board's default-audience hint, a session note's readers, the
  Table rules & safety flag dialog and its "Not even ... can see who sent
  it" promise, and who may change the lists. A reader who is themself a
  GM still reads "you". Also fixes `rosterFromMembers`'s "party" sentence,
  which could name a GM as "everyone but <GM>" when they have no active
  character — GMs are now excluded from that count entirely.
- D176 (T12(a)): the Discussions board's opening sentence shows only while
  the board is completely empty, in place of the separate empty-state line
  (which is removed) rather than alongside it. A non-empty board shows
  neither.

Updates the J2 journey spec to the new wording, the CHANGELOG's lane 7
bullets, docs/API.md's Audience shape note, and the lane6 coverage
script's description text. Coverage script still prints "unclassified:
none".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(table): the safety waiting count under a phone GM's stage too (T15, #733)
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 2m36s
CI / Bot tests and audit (pull_request) Successful in 5m3s
CI / Backend lint (ruff) (pull_request) Successful in 1m15s
CI / Bot/backend version sync (pull_request) Successful in 1m13s
CI / Feedback site tests (pull_request) Successful in 2m12s
CI / Does this change touch the recording path? (pull_request) Successful in 41s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Docker image build (pull_request) Successful in 1m19s
CI / Backend migration, tests, and audit (pull_request) Successful in 15m57s
CI / Frontend tests, audit, and build (pull_request) Successful in 20m35s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 20m28s
293ec95271
Addendum from the owner's answers to the lane 7 questions: T13 and T14 are
already built as recommended (verified, no change needed) — the private
question's bell line already says "you", never a GM's name, and the flag
doors on This week and a session's page are already player-only. T15 had a
real gap: the desktop aside's "N waiting in Table rules & safety" quiet
count (T4(a)) had no equivalent on a phone, since GmPhone has no aside to
put it in.

Reuses the existing WaitingCount component under the phone stage, with the
same words and the same nothing-when-none-wait rule as desktop. Two new
tests in TableMode.test.jsx cover it showing and staying silent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Conflicts, both sides kept: CHANGELOG; docs/API.md (Discussions beside
  Prep Sheets and Planning, and the contents); main.py (discussions, prep
  and planning routers); demo/engine.py (the discussions and prep/Planning
  seed methods); useContents.js (a session address takes talk and prep);
  leaveGuardCallers.test.js (NewThread and the prep sheet).
- 7d15c0a1b2e3 now revises d6f8b0c2e4a7 (it was on e5f7a9b1c3d4). The one-head
  pin returns to test_discussion_notifications_migration; lane 6's
  test_drop_generation_pinned_migration becomes "still in the chain", and
  test_drop_scene_text_migration keeps a2c4e6f8b0d1 as its successor.
- ck_notifications_kind / _subject_type: 7f2d0c1a3b4e was written on
  7e5afe7a1b2c's lists, so an upgrade would have dropped lane 9 A's
  server_ownership_offer and campaign_handed_on (and the server_ownership_offer
  subject type) from the CHECKs. Its _BEFORE lists are now f1a3c5e7b9d2's, so
  the CHECK is the union and the downgrade narrows to exactly f1a3's. The
  model's enum and SUBJECT_TYPES follow chain order; CATEGORY_FOR_KIND has
  every kind. test_server_owner_migration, test_safety_migration and
  test_notifications hold their lists as history; the discussions migration
  test checks a server-owner row survives its downgrade.
- Lane 7's 20 Depends(get_db) in discussions.py are scope="function". Lane 7
  adds no direct .delay()/.apply_async(): it notifies through main's
  notification_service.notify, which already goes off-loop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merge train/3-858 into lane 7 (#846): the retention test's media fix
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 59s
CI / Backend lint (ruff) (pull_request) Successful in 32s
CI / Feedback site tests (pull_request) Successful in 45s
CI / Bot/backend version sync (pull_request) Successful in 28s
CI / Bot tests and audit (pull_request) Successful in 2m5s
CI / Docker image build (pull_request) Successful in 34s
CI / Does this change touch the recording path? (pull_request) Successful in 29s
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
CI / Backend migration, tests, and audit (pull_request) Has been cancelled
4ed619bc14
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merge branch 'train/3-858' into train/4-846
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 2m17s
CI / Bot tests and audit (pull_request) Successful in 4m9s
CI / Backend lint (ruff) (pull_request) Successful in 1m14s
CI / Bot/backend version sync (pull_request) Successful in 28s
CI / Docker image build (pull_request) Successful in 22s
CI / Feedback site tests (pull_request) Successful in 1m50s
CI / Does this change touch the recording path? (pull_request) Successful in 28s
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
CI / Frontend tests, audit, and build (pull_request) Has been cancelled
846b997e71
Merge branch 'train/3-858' into train/4-846
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 59s
CI / Backend lint (ruff) (pull_request) Successful in 32s
CI / Feedback site tests (pull_request) Successful in 44s
CI / Bot/backend version sync (pull_request) Successful in 30s
CI / Docker image build (pull_request) Successful in 31s
CI / Does this change touch the recording path? (pull_request) Successful in 24s
CI / Bot tests and audit (pull_request) Successful in 5m21s
CI / Backend migration, tests, and audit (pull_request) Successful in 16m36s
CI / Frontend tests, audit, and build (pull_request) Failing after 20m50s
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
183b8d9cb5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The backend's recording_tests table, endpoints, handoff route,
process_recording_test and sweep, with their tests; the bot's /record/test,
tests/ capture directory, test status and handoff, not yet tested. The bot's
tests and the changelog follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
app/services/wiki_text.py parses :::gm, :::for <characters, @players>
and :::party blocks, renders a text for one reader (every block they are
outside removed without a trace), and merges a writer's edit of their
view back without ever losing, moving into view, or re-scoping a block
they couldn't see. Reading fails closed; writing is strict, with a line
number on every refusal. Example tests plus a seeded fuzzer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bot's half is tested: /record/test announces "Recording a test" before
it captures (and records anyway if the post fails, D171), captures with the
same sink into audio_temp/tests/{id}/ with absolute placement intact, hands
over through POST /api/bot/recording-tests/{id}/audio, deletes the audio if
the handoff does not land (D150), and crash recovery and the startup sweep
never take tests/ for a session. The session page's SSE keys a test's
"other" by its own id. API.md documents the routes; the changelog says it.

Closes #824.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs: CLAUDE.md's recording architecture names the test path (D124)
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 2m43s
CI / Backend lint (ruff) (pull_request) Successful in 35s
CI / Feedback site tests (pull_request) Successful in 48s
CI / Bot/backend version sync (pull_request) Successful in 30s
CI / Docker image build (pull_request) Successful in 38s
CI / Does this change touch the recording path? (pull_request) Successful in 34s
CI / Bot tests and audit (pull_request) Successful in 5m12s
CI / Backend migration, tests, and audit (pull_request) Successful in 14m32s
CI / Frontend tests, audit, and build (pull_request) Successful in 18m34s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 12m56s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Has been cancelled
5e8d2485cc
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat: notifications out — DMs, the GM's warnings, email that reaches people (#736, lane 10 PR 2)
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 2m26s
CI / Backend lint (ruff) (pull_request) Successful in 32s
CI / Bot/backend version sync (pull_request) Successful in 27s
CI / Bot tests and audit (pull_request) Successful in 2m2s
CI / Docker image build (pull_request) Successful in 28s
CI / Does this change touch the recording path? (pull_request) Successful in 36s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Feedback site tests (pull_request) Successful in 2m3s
CI / Frontend tests, audit, and build (pull_request) Successful in 18m19s
CI / Backend migration, tests, and audit (pull_request) Successful in 37m40s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Has been cancelled
06f441e4ab
Server and bot halves together; no pages.

- Discord DMs (D152): `notification_dm` on the durable stream, claimed before
  publish, the bell's line + link + time for session news only; sent only to a
  bot whose GET /identity lists "dm" in its new `features`. The bot's
  handler is exempt from the channel check and reports refusals to the new
  POST /api/bot/users/discord/{id}/dm-status (closed | no_shared_server | ok);
  the settings carry a `warning` beside the DM column until the next DM lands.
- The GM's warnings (D151): session_at_risk and audio_deletion_pending are
  GM-only bell rows, DM'd per the GM's grid (GM tasks x DM on by default), and
  posted to the channel only when no GM is reachable by DM. The at-risk poller
  runs for campaigns without a guild.
- A group without Discord (D153, #840): session news is emailed to every
  member unless Sessions email is explicitly off; the from-address email
  backends are retired. Reminders are `session_reminder` rows per member (only
  the last in the bell), reading the reader's RSVP.
- The reader's time: users.timezone, else the campaign's, else UTC, with the
  abbreviation, in notification emails and calendar invites.
- One star, one wish (D161): `feedback_requested`, written on completion for
  the players Home asks, never in the bell.
- D159: campaigns.voice_channel_id, stored and shown to GMs.
- PUT notification-settings writes only cells that change and never refuses an
  unchanged echo, so the page's whole-grid save keeps the defaults.
- Migration 10a1b2c3d4e5 on c4e6a8b0d2f1: the kind CHECK is the union with
  lane 9 A's and lane 7's kinds; notifications.in_bell; platform_links
  dm_failed_at/dm_failed_reason; campaigns.voice_channel_id.

Bot contract unchanged (additive).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Admin becomes a place with sections in Home's Contents rather than a page
of ten tabs, reusing the same shape lane 3 gave Settings:

- adminSections.js (16 sections, the survey's addresses); ADMIN_SECTIONS'
  `built` flag follows settingsSections.js's own rollout rule — three
  sections with no legacy screen at all (Content notices, Instance health,
  Plans & entitlements) stay unmounted for their own PR (4c/4d).
- /admin/* routes; `?tab=` redirects to the new address (paths are
  contracts, R1-8); `bot` (ambiguous) goes to AI models.
- Home's Contents "This instance" group, the crumb ("Admin › <section>"),
  and IS-377's Stop for a non-administrator (AdminDoc.jsx).
- The index (GET /admin/overview, already on this branch from lane 9 A):
  grouped status lines, D142's quiet leftovers box, and D141's accept/
  decline banner for an offered server.
- ADMIN-15 About this server: the admin GET/PUT this lane's server work
  added, now with a form.
- ADMIN-1 Users: the owner's chip, "Hand the server on…" (an offer, D68),
  Make/Remove administrator (a dialog, then an undo), delete with the
  typed name and the blocked-by-campaigns state, and D56's search-scope
  radio cards (new GET endpoint — the setter existed, nothing read it back).
- ADMIN-2 Campaigns: the instance-wide list, and an immediate (never an
  offer) hand-on for a campaign whose owner is unreachable or just moved
  on — the admin's own recovery path either way.
- D141's server-scoped attention item: AttentionItem.campaign_id/
  campaign_name become optional for the one kind that isn't a campaign's
  (server_owner_offer), checked ahead of the membership scope so it
  reaches an administrator with no campaigns of their own.
- Every other built section keeps working at its new address by mounting
  pages/Admin.jsx's existing component unchanged (the tab bar is gone);
  AI models and Discord share one interim mount until page 4c splits
  BotSettings along the new addresses — a documented departure.
- J1's Admin leg walked for real: the leftovers list, Backup's own
  heading and Contents-current row, a whole-page axe scan (no longer
  scoped to [role="alert"]), the desktop project settling the step, and
  the keyboard project reaching Admin through the account menu and its
  Contents sub-list. A new IS-377 journey test (Priya, stopped, axed).

Backend: GET /api/admin/settings/member-search-scope (D56's setter has
existed since #855; nothing read it back before this).

Not in this PR (see the survey's §7 row 4a and the file's own notes):
AI models/AI usage & speed/Discord's real split, Recording & retention,
Backup, Content packs, Content notices, Audit log's names/categories, and
Plans & entitlements — pages 4b/4c/4d.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
fix(webapp): name the offering owner on D141's accept/decline banner
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 2m47s
CI / Bot tests and audit (pull_request) Successful in 2m8s
CI / Backend lint (ruff) (pull_request) Successful in 1m9s
CI / Feedback site tests (pull_request) Successful in 47s
CI / Docker image build (pull_request) Successful in 33s
CI / Bot/backend version sync (pull_request) Successful in 1m8s
CI / Does this change touch the recording path? (pull_request) Successful in 32s
CI / Backend migration, tests, and audit (pull_request) Successful in 16m33s
CI / Frontend tests, audit, and build (pull_request) Failing after 21m1s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Failing after 1m3s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been cancelled
4fb7b928d0
The overview's still-current owner_name is who is offering — the offer
has not resolved yet — so the banner reads "Dana is offering you this
server" rather than the anonymous fallback, matching the drawing without
a second request.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- lore_entries gain an audience (lane 7's model) and a people table;
  migration 8a1b2c3d4e5f moves gm_notes and player_private_notes into
  every page's and every version's text as blocks, escaping old bodies'
  ':::' lines first.
- The wiki's routes read through wiki_audience: a page outside the
  reader's audience is 403 'This page isn't shared with you' and nothing
  else (D167); lists are one query; facts and timeline rows read as
  audiences; ?as_user= / ?as_character= for GMs (IS-128).
- The old article page keeps working: its three boxes are read out of,
  and written back into, the text (wiki_text's legacy split).
- Search, /ask, the summary, a shelf card's face, the vocabulary, the
  timeline, lane 7's about chip and canon, drafts and proposals' merges
  all read the renderer's output; a test lists every raw read.
- Export schema 14: notes travel inside the body, with the audience.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(webapp): the bell knows lane 9's two ownership kinds (#846)
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 53s
CI / Backend lint (ruff) (pull_request) Successful in 1m7s
CI / Bot tests and audit (pull_request) Successful in 1m59s
CI / Bot/backend version sync (pull_request) Successful in 28s
CI / Docker image build (pull_request) Successful in 32s
CI / Does this change touch the recording path? (pull_request) Successful in 47s
CI / Feedback site tests (pull_request) Successful in 1m59s
CI / Backend migration, tests, and audit (pull_request) Successful in 16m28s
CI / Frontend tests, audit, and build (pull_request) Successful in 48m33s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 14m46s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 24m23s
2c4f2ff68b
Lane 7's test asserts the bell's NOTE_KIND covers every kind the server
defines; lane 9 A (#855) added server_ownership_offer and
campaign_handed_on after lane 7 was written.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The one-head pin moves to tests/wiki/test_wiki_migration.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tests/wiki/test_wiki_crawl.py walks every wiki-touching read as every
reader; /ask's context is one function now (ask_lore_context) so the
crawl can read it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs: CHANGELOG for the wiki's text and page audiences; ruff format (#734, lane 8)
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m3s
CI / Bot tests and audit (pull_request) Successful in 2m20s
CI / Backend lint (ruff) (pull_request) Successful in 31s
CI / Bot/backend version sync (pull_request) Successful in 25s
CI / Docker image build (pull_request) Successful in 20s
CI / Does this change touch the recording path? (pull_request) Successful in 32s
CI / Feedback site tests (pull_request) Successful in 2m1s
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
CI / Backend migration, tests, and audit (pull_request) Has been cancelled
cce85bb474
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	CHANGELOG.md
# Conflicts:
#	CHANGELOG.md
#	docs/API.md
#	webapp/backend/app/models/notification.py
#	webapp/backend/app/services/notification_service.py
#	webapp/backend/app/services/notification_settings_service.py
#	webapp/backend/tests/conftest.py
#	webapp/backend/tests/test_discord_webhook_fallback_removal_migration.py
#	webapp/backend/tests/test_notifications.py
#	webapp/backend/tests/test_safety_migration.py
#	webapp/frontend/src/shell/AppFrame.jsx
#	webapp/frontend/src/shell/AppFrame.test.jsx
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re-chain 10c3d4e5f6a7 onto 10a1b2c3d4e5; the one-head pin moves to
test_recording_tests_migration. The bot lists record_test in BOT_FEATURES
(additive) so page 7a can tell a bot too old for Test the bot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merge remote-tracking branch 'origin/main' into train2/1-866
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 59s
CI / Backend lint (ruff) (pull_request) Successful in 27s
CI / Bot tests and audit (pull_request) Successful in 2m4s
CI / Bot/backend version sync (pull_request) Successful in 24s
CI / Feedback site tests (pull_request) Successful in 43s
CI / Does this change touch the recording path? (pull_request) Successful in 15s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Docker image build (pull_request) Successful in 34s
CI / Frontend tests, audit, and build (pull_request) Successful in 21m33s
CI / Backend migration, tests, and audit (pull_request) Successful in 16m56s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Failing after 24m50s
70f794db34
Merge branch 'train2/1-866' into train2/2-868
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m0s
CI / Backend lint (ruff) (pull_request) Successful in 31s
CI / Feedback site tests (pull_request) Successful in 46s
CI / Bot tests and audit (pull_request) Successful in 2m2s
CI / Bot/backend version sync (pull_request) Successful in 28s
CI / Docker image build (pull_request) Successful in 20s
CI / Does this change touch the recording path? (pull_request) Successful in 17s
CI / Backend migration, tests, and audit (pull_request) Successful in 17m28s
CI / Frontend tests, audit, and build (pull_request) Successful in 21m8s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Failing after 25m10s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 31m26s
80fad5de6b
Merge branch 'train2/2-868' into train2/3-865
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 59s
CI / Backend lint (ruff) (pull_request) Successful in 42s
CI / Feedback site tests (pull_request) Successful in 57s
CI / Bot tests and audit (pull_request) Successful in 2m6s
CI / Bot/backend version sync (pull_request) Successful in 43s
CI / Does this change touch the recording path? (pull_request) Successful in 18s
CI / Docker image build (pull_request) Successful in 59s
CI / Backend migration, tests, and audit (pull_request) Successful in 16m47s
CI / Frontend tests, audit, and build (pull_request) Successful in 20m57s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 13m44s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Has been cancelled
d318ab15b4
Merge branch 'train2/3-865' into train2/4-864
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 53s
CI / Backend lint (ruff) (pull_request) Successful in 28s
CI / Bot/backend version sync (pull_request) Successful in 33s
CI / Feedback site tests (pull_request) Successful in 46s
CI / Does this change touch the recording path? (pull_request) Successful in 16s
CI / Docker image build (pull_request) Successful in 51s
CI / Bot tests and audit (pull_request) Successful in 3m25s
CI / Backend migration, tests, and audit (pull_request) Successful in 17m32s
CI / Frontend tests, audit, and build (pull_request) Successful in 37m42s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 13m13s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Has been cancelled
aae9943ca6
feat(webapp): Recording & retention, Backup, Import and Audit log rebuilt (lane 9 page 4b, #735)
Some checks failed
CI / Frontend tests, audit, and build (pull_request) Has been cancelled
CI / Backend migration, tests, and audit (pull_request) Has been cancelled
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Has been cancelled
CI / Bot tests and audit (pull_request) Has been cancelled
CI / Backend lint (ruff) (pull_request) Has been cancelled
CI / Feedback site tests (pull_request) Has been cancelled
CI / Bot/backend version sync (pull_request) Has been cancelled
CI / Docker image build (pull_request) Has been cancelled
CI / Does this change touch the recording path? (pull_request) Has been cancelled
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
ef9fc93f02
ADMIN-5/6 gets the default/cap split (D24) with IS-311's confirm against
GET /admin/settings/retention/preview before a tighter cap catches a
campaign's sessions, and says D179's now-fixed retention promises
correctly (D179 itself shipped server-side in lane 9 B2). ADMIN-7 queues
a backup without asking first (D12), polls the last runs while one is in
progress, and drops the now-false "uploaded images are not in the
backup" line since D143 already put them in. ADMIN-9 shares its form
(ImportCampaignForm.jsx) with Home's New campaign -> Import a file
(D46), gaining "Open its Members" since an administrator need not be a
member of what they import. ADMIN-10 filters by name and category
(server support already in main), and reads a row's detail as labelled
lines with the record as stored one click further.

Departures, each recorded in the page's own docstring:
- the on-disk detail lines needing per-recording expiry/progress fields
  the server doesn't return are left as plainer wording rather than
  adding new backend fields;
- the audit log's description column shows the server's own
  `description` string verbatim rather than reconstructing bespoke
  per-event sentences from `context`;
- Admin.jsx's legacy RetentionSection/RecordingsSection/BackupSection/
  AuditLogSection/AdminImportSection are left in place rather than
  deleted, since the legacy default-exported page and its own test
  still use them and deleting the whole file is page 4d's job.

Coverage: docs/design/redesign-surfaces/coverage/lane7.py reports
{'replaced': 19, 'drawn': 165, 'elsewhere': 9} total 193 - unclassified: none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(webapp): one Admin door in the phone Contents, Backup's axe scan scoped again (#866)
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 2m1s
CI / Bot tests and audit (pull_request) Successful in 4m12s
CI / Backend lint (ruff) (pull_request) Successful in 1m5s
CI / Feedback site tests (pull_request) Successful in 1m40s
CI / Bot/backend version sync (pull_request) Successful in 54s
CI / Docker image build (pull_request) Successful in 1m13s
CI / Does this change touch the recording path? (pull_request) Successful in 28s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Backend migration, tests, and audit (pull_request) Successful in 18m17s
CI / Frontend tests, audit, and build (pull_request) Successful in 41m41s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Failing after 27m48s
892477cf11
Home's "This instance" group (lane 9 page 4a, d69f412a) put a second
"Admin" link in the phone's Contents sheet alongside the account rows'
own — both land in the one `nav` there (SH-4), so `accountRow(you,
"Admin")` resolved to two. The desktop identity disc's dropdown is a
separate landmark and was never affected. Fixed by dropping the account
row's Admin whenever `contents.groups` already carries "This instance",
which only the sheet ever sees both of at once.

4a also restored J1's whole-page axe scan on `/admin/backup`, which
still mounts the legacy `BackupSection` (`.bg-indigo-600` at 2.69:1,
an unlabelled `input[min="1"]`). Scoped the scan away from that
section's own markup rather than off the whole page, since 4a did
rebuild the frame around it; page 4b (#873) replaces the section and
restores the whole-page scan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merge branch 'train2/1-866' into train2/2-868
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m2s
CI / Bot tests and audit (pull_request) Successful in 2m15s
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 29s
CI / Docker image build (pull_request) Successful in 33s
CI / Does this change touch the recording path? (pull_request) Successful in 19s
CI / Backend migration, tests, and audit (pull_request) Successful in 17m30s
CI / Frontend tests, audit, and build (pull_request) Successful in 43m47s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 13m37s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 22m30s
b7ee17ec0c
Merge branch 'train2/2-868' into train2/3-865
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 59s
CI / Bot tests and audit (pull_request) Successful in 2m3s
CI / Backend lint (ruff) (pull_request) Successful in 31s
CI / Bot/backend version sync (pull_request) Successful in 26s
CI / Docker image build (pull_request) Successful in 33s
CI / Does this change touch the recording path? (pull_request) Successful in 17s
CI / Feedback site tests (pull_request) Successful in 1m53s
CI / Backend migration, tests, and audit (pull_request) Successful in 19m1s
CI / Frontend tests, audit, and build (pull_request) Successful in 22m59s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 13m37s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Failing after 31m55s
1863ce33b8
Merge branch 'train2/3-865' into train2/4-864
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 2m28s
CI / Backend lint (ruff) (pull_request) Successful in 29s
CI / Feedback site tests (pull_request) Successful in 45s
CI / Bot/backend version sync (pull_request) Successful in 24s
CI / Docker image build (pull_request) Successful in 26s
CI / Does this change touch the recording path? (pull_request) Successful in 14s
CI / Bot tests and audit (pull_request) Successful in 4m21s
CI / Backend migration, tests, and audit (pull_request) Successful in 18m57s
CI / Frontend tests, audit, and build (pull_request) Successful in 46m40s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 35m5s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Failing after 58m33s
4fb314fa4c
test(e2e): J1 scans the whole Backup page again, now 4b rebuilt it (#873)
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 2m27s
CI / Backend lint (ruff) (pull_request) Successful in 29s
CI / Feedback site tests (pull_request) Successful in 46s
CI / Docker image build (pull_request) Successful in 34s
CI / Does this change touch the recording path? (pull_request) Successful in 20s
CI / Bot tests and audit (pull_request) Successful in 5m10s
CI / Bot/backend version sync (pull_request) Failing after 11m35s
CI / Frontend tests, audit, and build (pull_request) Successful in 22m27s
CI / Backend migration, tests, and audit (pull_request) Successful in 45m31s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 13m2s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Failing after 29m54s
7d972b9e65
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merge main into build/lane9-4b-pages
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m30s
CI / Backend lint (ruff) (pull_request) Successful in 57s
CI / Feedback site tests (pull_request) Successful in 1m18s
CI / Bot/backend version sync (pull_request) Successful in 56s
CI / Bot tests and audit (pull_request) Successful in 3m41s
CI / Docker image build (pull_request) Successful in 43s
CI / Does this change touch the recording path? (pull_request) Successful in 24s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Backend migration, tests, and audit (pull_request) Successful in 18m6s
CI / Frontend tests, audit, and build (pull_request) Successful in 24m37s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 39m48s
039226b576
Sign in to join this conversation.
No description provided.