feat(webapp): email — one template, stop and answer tokens, invites, the recap on approval; D232's sub-row (lane 10 PR 3) — stacked on #864 #871

Merged
claude-bot merged 6 commits from build/lane10-3-email into main 2026-09-27 10:55:16 +00:00
Collaborator

Lane 10 (#736), PR 3. Stacked on the second merge train, on top of #864. It covers D154, D155, D157, D162, D164 and D224, plus D232 (O29).

Email

  • One template: app/notifications/templates.py, producing HTML and text, with RFC 8058 one-click unsubscribe headers. Every notification email is built on it.
  • Stateless HMAC tokens (email_tokens.py) for the two things an email lets you do while signed out:
    • Stop: "stop these emails" stops that kind of email everywhere (D224). The token has no expiry.
    • Answer: the answer buttons, checked live against the session. They work until it starts.
    • The tokens are typed and signed with a key derived from SECRET_KEY, never the raw secret. The orchestrator added that, since issuing no tokens yet means changing the key costs nothing.
  • The public API, rate-limited (the pages come in 7b):
    • GET|POST /api/email/stop and POST /api/email/stop/undo;
    • GET|POST /api/email/answer, which checks campaign membership live.
  • Invites:
    • the subject is Session {n} · {campaign}, with the title in the description;
    • URL: is added;
    • RSVP=FALSE, and the invite says "Answer in Quest Board";
    • the reason reaches the .ics.
  • The recap email goes out on approval (D162): never twice, including across approve, unapprove and re-approve. Sessions already emailed are left alone.
  • The dev email preview (D164), GET /api/dev/email-preview/{kind}, only runs when ENVIRONMENT=development. The default is production.
  • Answer buttons appear on confirmed and reminder emails; a time vote opens the session instead (D155).

D232 (O29)

  • A new category, session_feedback, for the star-and-wish ask, which gets its own row on the grid.
  • Migration 10d4e5f6a7b8 (on 10c3) widens the category CHECK and copies everyone's existing Sessions settings to the new row.

Tests

  • 40 new tests, plus updates: 301 backend tests passing across every touched file, plus 77 re-run after the key change.
  • Frontend: 2,605 passed; eslint 0 errors.
  • Migration up, down and up.
  • ruff is clean. Version sync is OK (no bot changes).

🤖 Generated with Claude Code

Lane 10 (#736), PR 3. **Stacked on the second merge train, on top of #864.** It covers D154, D155, D157, D162, D164 and D224, plus D232 (O29). ## Email - **One template:** `app/notifications/templates.py`, producing HTML and text, with RFC 8058 one-click unsubscribe headers. Every notification email is built on it. - **Stateless HMAC tokens** (`email_tokens.py`) for the two things an email lets you do while signed out: - **Stop:** "stop these emails" stops **that kind of email everywhere** (D224). The token has no expiry. - **Answer:** the answer buttons, checked live against the session. They work until it starts. - The tokens are typed and signed with **a key derived from `SECRET_KEY`**, never the raw secret. The orchestrator added that, since issuing no tokens yet means changing the key costs nothing. - **The public API**, rate-limited (the pages come in 7b): - `GET|POST /api/email/stop` and `POST /api/email/stop/undo`; - `GET|POST /api/email/answer`, which checks campaign membership live. - **Invites:** - the subject is `Session {n} · {campaign}`, with the title in the description; - `URL:` is added; - **`RSVP=FALSE`**, and the invite says "Answer in Quest Board"; - the reason reaches the `.ics`. - **The recap email goes out on approval (D162):** never twice, including across approve, unapprove and re-approve. Sessions already emailed are left alone. - **The dev email preview (D164)**, `GET /api/dev/email-preview/{kind}`, only runs when `ENVIRONMENT=development`. The default is production. - **Answer buttons** appear on confirmed and reminder emails; a time vote opens the session instead (D155). ## D232 (O29) - A new category, `session_feedback`, for the star-and-wish ask, which gets its own row on the grid. - **Migration `10d4e5f6a7b8`** (on `10c3`) widens the category CHECK and **copies** everyone's existing Sessions settings to the new row. ## Tests - 40 new tests, plus updates: 301 backend tests passing across every touched file, plus 77 re-run after the key change. - Frontend: 2,605 passed; eslint 0 errors. - Migration up, down and up. - ruff is clean. Version sync is OK (no bot changes). 🤖 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
Also D232 (O29): the star-and-wish ask gets its own sub-row under Sessions
in the notification settings grid (migration 10d4e5f6a7b8).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ruff format on the new/changed files
- fix miscounted rows in the session_feedback carry-over migration test
- update tests/test_enqueue_failure.py and test_notifications.py for the
  recap email leaving the summary fan-out (D162) and the widened category
  CHECK becoming a historical fact once 10d4e5f6a7b8 lands
- fix test_sessions.py's calendar .ics SUMMARY/DESCRIPTION assertion
- the email-answer lookup uses the session's campaign for the reader's zone

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(webapp): email tokens sign with a key of their own, derived from SECRET_KEY
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 2m4s
CI / Bot tests and audit (pull_request) Successful in 3m40s
CI / Backend lint (ruff) (pull_request) Successful in 55s
CI / Feedback site tests (pull_request) Successful in 46s
CI / Docker image build (pull_request) Successful in 40s
CI / Bot/backend version sync (pull_request) Successful in 46s
CI / Does this change touch the recording path? (pull_request) Successful in 23s
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
c7e643dda7
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
Merge branch 'train2/4-864' into w3-871
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 2m35s
CI / Bot tests and audit (pull_request) Successful in 2m7s
CI / Feedback site tests (pull_request) Successful in 43s
CI / Backend lint (ruff) (pull_request) Successful in 1m7s
CI / Bot/backend version sync (pull_request) Successful in 28s
CI / Does this change touch the recording path? (pull_request) Successful in 17s
CI / Docker image build (pull_request) Successful in 1m13s
CI / Backend migration, tests, and audit (pull_request) Successful in 18m15s
CI / Frontend tests, audit, and build (pull_request) Successful in 20m40s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 12m57s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Failing after 26m0s
1d030bb4bc
Merge main into lane 10 PR 3 (train 2 landed)
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m1s
CI / Backend lint (ruff) (pull_request) Successful in 32s
CI / Bot/backend version sync (pull_request) Successful in 3m30s
CI / Feedback site tests (pull_request) Successful in 3m44s
CI / Does this change touch the recording path? (pull_request) Successful in 20s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Bot tests and audit (pull_request) Successful in 5m3s
CI / Docker image build (pull_request) Successful in 3m7s
CI / Backend migration, tests, and audit (pull_request) Successful in 18m28s
CI / Frontend tests, audit, and build (pull_request) Successful in 48m0s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Failing after 38m33s
35cdee2a5f
Conflicts: the notification email call. Kept both sides: #871's stop_url
and answer buttons, and #865's smtp= handed in from the task's own session
(the fix for notification emails being silently dropped). The docstring
carries all three.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(webapp): the Recap email row before approval says the truth (D162, #871)
All checks were successful
CI / Backend lint (ruff) (pull_request) Successful in 33s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 56s
CI / Feedback site tests (pull_request) Successful in 42s
CI / Bot/backend version sync (pull_request) Successful in 23s
CI / Does this change touch the recording path? (pull_request) Successful in 17s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Docker image build (pull_request) Successful in 25s
CI / Bot tests and audit (pull_request) Successful in 1m59s
CI / Backend migration, tests, and audit (pull_request) Successful in 17m37s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 23m58s
CI / Frontend tests, audit, and build (pull_request) Successful in 46m50s
70433ceae4
J5 saw "No record" instead of "Skipped" on lane 10 PR 3's checklist: D162
moved the recap email to fire on approval, so `post_session_tasks.recap_email`
has no record until then, and the checklist read that as if nothing were
known rather than as the truthful "goes out when you approve" or "skipped"
D162 promised.

Adds `recap_email_forecast` to `GET /sessions/{id}` (GM-only, only while there
is no real record yet): the same skip checks `send_recap_email` makes for
real — the campaign has recaps off, or nobody on the attendance list has
opted in — made read-only, plus how many people it would reach. checklist.js
reads it for the "Recap email" row's word and note; the recap task's default
branch is otherwise unchanged.

J5 needed no changes: the seed's campaign has no attendance rows recorded
before Dana approves (attendance auto-fill defaults to "propose", not
"auto_apply"), so the truthful pre-approval state is "Skipped" — exactly what
the test already asserted. Verified with `scripts/journeys.sh j5` — 24/24
across desktop, phone and keyboard, no retries.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Author
Collaborator

Fixed J5's "Recap email" row (70433cea), and merged it up lane 10's stack.

D162 sends the recap on approval, so before approval there's no task record, and the Review checklist said "No record". GET /sessions/{id} now carries a GM-only recap_email_forecast, which runs the same checks send_recap_email makes, read-only:

  • Skipped when the campaign has recaps turned off;
  • Skipped when nobody on the attendance list has opted in;
  • otherwise "To N people, once you approve."

Once a real record exists, it takes over.

Tests:

  • J5 passed 24 of 24 across the three projects locally, with no retries. The :105 "Accurate" knock-on is gone.
  • 5 new backend tests and 4 checklist tests.

Left out on purpose: the archived-campaign and no-SMTP checks. They're rare admin states, and each would mean an extra read on every session view.

**Fixed J5's "Recap email" row (70433cea), and merged it up lane 10's stack.** D162 sends the recap on approval, so before approval there's no task record, and the Review checklist said "No record". `GET /sessions/{id}` now carries a GM-only `recap_email_forecast`, which runs the same checks `send_recap_email` makes, read-only: - **Skipped** when the campaign has recaps turned off; - **Skipped** when nobody on the attendance list has opted in; - otherwise **"To N people, once you approve."** Once a real record exists, it takes over. **Tests:** - J5 passed 24 of 24 across the three projects locally, with no retries. The `:105` "Accurate" knock-on is gone. - 5 new backend tests and 4 checklist tests. **Left out on purpose:** the archived-campaign and no-SMTP checks. They're rare admin states, and each would mean an extra read on every session view.
Sign in to join this conversation.
No description provided.