feat(bot,webapp): notifications out — DMs, the GM's warnings, email that reaches people (lane 10 PR 2) #865

Merged
claude-bot merged 10 commits from build/lane10-2-notify-out into main 2026-09-27 08:13:19 +00:00
Collaborator

Closes #840. Lane 10 (#736) PR 2, covering D151, D152, D153, D159 (server side only) and D161. Coverage rows: IS-167, IS-168, EX-030, EX-031, EX-034, IS-213, EX-040, EX-041.

What's in it

  • Discord DMs (D152):
    • A delivery publishes notification_dm on the durable stream: one line, a link, and the time for session news.
    • The bot DMs the person and reports closed or no_shared_server to a new route, POST /api/bot/users/discord/{id}/dm-status, which records it on the link. That route is additive, so the contract stays at v2.
    • DMs are offered only when the bot's /identity lists dm. /identity gains features.
  • The GM's warnings go to the bell (D151). "Session at risk" and "audio about to be deleted" become GM notifications. The channel post happens only when no GM can get them by DM. GM tasks × DM defaults to on.
  • Groups without Discord get email (D153, O6, #840). Session news (vote, confirmed, reminder, cancelled) is emailed to each member by default unless they've turned it off. The old from-address email backends are removed. Reminders become per-member notifications.
  • Reader's time: the email's "When:" and the calendar invite use each reader's time zone.
  • The reminder line says "You said you're in" or "Are you in?".
  • The star and wish by DM (D161) follows the player's own setting. feedback_requested is written when a session completes.
  • D159: campaigns.voice_channel_id is stored and exposed to GMs. No pages yet.
  • A real bug found along the way: a Notifications save by someone who can't receive DMs was refused, and echoing the grid overwrote D153's default. The PUT now refuses only cells turning on, and writes only cells that change.

Migration

10a1b2c3d4e5_outbound_notifications:

  • adds 4 kinds, keeping the union with lanes 7 and 9;
  • adds notifications.in_bell;
  • adds platform_links.dm_failed_*;
  • adds campaigns.voice_channel_id.

It re-chains onto main's head at merge.

Tests

  • Bot full suite: 505 passed.
  • Backend: 54 new tests, and a broad sweep of 1,531 passing.
  • Frontend: 2,476 passed; eslint 0 errors.
  • ruff is clean; the version sync is OK.

For the owner (on the questions page):

  • Does a webhook-only campaign count as "has Discord"?
  • Should "the summary couldn't be written" also get a fallback post?
  • Is star-and-wish its own grid row?

PR 8 will give the fake bot features: ["dm"].

🤖 Generated with Claude Code

Closes #840. Lane 10 (#736) PR 2, covering D151, D152, D153, D159 (server side only) and D161. Coverage rows: IS-167, IS-168, EX-030, EX-031, EX-034, IS-213, EX-040, EX-041. ## What's in it - **Discord DMs (D152):** - A delivery publishes `notification_dm` on the durable stream: one line, a link, and the time for session news. - The bot DMs the person and reports `closed` or `no_shared_server` to a new route, `POST /api/bot/users/discord/{id}/dm-status`, which records it on the link. That route is additive, so the **contract stays at v2**. - DMs are offered only when the bot's `/identity` lists `dm`. `/identity` gains `features`. - **The GM's warnings go to the bell (D151).** "Session at risk" and "audio about to be deleted" become GM notifications. The channel post happens only when no GM can get them by DM. GM tasks × DM defaults to on. - **Groups without Discord get email (D153, O6, #840).** Session news (vote, confirmed, reminder, cancelled) is emailed to each member by default unless they've turned it off. The old from-address email backends are removed. Reminders become per-member notifications. - **Reader's time:** the email's "When:" and the calendar invite use each reader's time zone. - **The reminder line** says "You said you're in" or "Are you in?". - **The star and wish by DM (D161)** follows the player's own setting. `feedback_requested` is written when a session completes. - **D159:** `campaigns.voice_channel_id` is stored and exposed to GMs. No pages yet. - **A real bug found along the way:** a Notifications save by someone who can't receive DMs was refused, and echoing the grid overwrote D153's default. The PUT now refuses only cells turning on, and writes only cells that change. ## Migration `10a1b2c3d4e5_outbound_notifications`: - adds 4 kinds, keeping the **union** with lanes 7 and 9; - adds `notifications.in_bell`; - adds `platform_links.dm_failed_*`; - adds `campaigns.voice_channel_id`. It re-chains onto main's head at merge. ## Tests - Bot full suite: **505 passed**. - Backend: 54 new tests, and a broad sweep of **1,531** passing. - Frontend: 2,476 passed; eslint 0 errors. - ruff is clean; the version sync is OK. **For the owner** (on the questions page): - Does a webhook-only campaign count as "has Discord"? - Should "the summary couldn't be written" also get a fallback post? - Is star-and-wish its own grid row? **PR 8** will give the fake bot `features: ["dm"]`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
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>
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
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>
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>
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
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
Author
Collaborator

CI run 11143 failed two journeys on all three projects. Both are new with this PR; its base, #868's run 11142, passed them.

  • J0b:131. It expects "Discord DMs arrive with a later release." on the notifications grid. This PR ships DMs, so the test is stale. It will be updated to assert the new DM column rather than just dropping the line.
  • J2:464, "a group without Discord schedules the same". The Session Confirmed email never arrives; only the earlier test email does. This may be a real regression in D153's default email for a campaign with no Discord.

A fix is being diagnosed on a fix865 branch, with J0b and J2 run locally. It will be fast-forwarded onto this PR and merged up to #864.

CI run 11143 failed two journeys on all three projects. Both are new with this PR; its base, #868's run 11142, passed them. - **J0b:131.** It expects "Discord DMs arrive with a later release." on the notifications grid. This PR ships DMs, so the test is stale. It will be updated to assert the new DM column rather than just dropping the line. - **J2:464, "a group without Discord schedules the same".** The Session Confirmed email never arrives; only the earlier test email does. This may be a real regression in D153's default email for a campaign with no Discord. A fix is being diagnosed on a `fix865` branch, with J0b and J2 run locally. It will be fast-forwarded onto this PR and merged up to #864.
test(e2e): J6 gets a budget of its own, as J1 and J2 have
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 57s
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 28s
CI / Feedback site tests (pull_request) Successful in 48s
CI / Docker image build (pull_request) Successful in 32s
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 / Frontend tests, audit, and build (pull_request) Successful in 20m51s
CI / Backend migration, tests, and audit (pull_request) Successful in 41m50s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Failing after 1h21m22s
be5bb0af2c
J6 is five acts and twelve axe scans in one test. On the phone project it
has run 26-50 s on recent runs and ran past the default 60 s twice in a row
on #866 (run 11141), with every step still progressing: no single step over
2 s, and 32 s of the 60 in axe. The keyboard and desktop projects, and every
other phone test, were flat against main. A budget, not a race.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
deliver_notification runs its body under asyncio.run and called the email
backend from inside it. The backend's _load_config starts a second event
loop to read the SMTP settings, which raises "Cannot run the event loop
while another loop is running" in a thread whose loop is running; that was
caught, logged as "Failed to load SMTP config" and taken as "no SMTP", so
every per-person notification email was dropped after its delivery row had
been claimed. Latent since #655 because every delivery test patches
send_notification away; it surfaced in J2 once lane 10 (D153) made this the
path a group without Discord hears about its sessions on.

The task now reads the SMTP config in its own session, passes it to
send_notification (new `smtp` keyword), and runs the blocking send on a
thread. A new test drives the real backend with only smtplib.SMTP faked,
from inside a running loop, and fails without the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
J0b: the Discord DM grid line "arrive with a later release" is gone now
that DMs ship. The fake bot's /identity lists `features: ["dm"]` like the
real bot's BOT_FEATURES, so the stack is an instance whose bot can DM.
Dana, unlinked, gets no DM column and the kit's "once your Discord is
linked, under Connected accounts" line (D36, SH-157); a new read-only test
has Priya, linked, see the column with Sessions x DM off (D151's only
default-on cell is GM tasks).

J2: a group without Discord is emailed its session news per person
(D153), with the bell's line as the subject (#736) — not one "Session
Confirmed — <title>" to the server's from-address. The no-Discord leg now
waits for the vote and the confirmation in Priya's own mail, matched by
the session link in the body (each project's campaign has a "Session 1"),
and the Discord leg's "not also by email" asks the same way instead of for
a subject that no longer exists. A `mailMessage` fixture reads one
Mailpit message in full.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merge fix/j6-budget (#880) into lane 9 page 4a
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 57s
CI / Bot tests and audit (pull_request) Successful in 2m6s
CI / Backend lint (ruff) (pull_request) Successful in 30s
CI / Feedback site tests (pull_request) Successful in 45s
CI / Docker image build (pull_request) Successful in 18s
CI / Bot/backend version sync (pull_request) Successful in 27s
CI / Does this change touch the recording path? (pull_request) Successful in 21s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Backend migration, tests, and audit (pull_request) Successful in 16m57s
CI / Frontend tests, audit, and build (pull_request) Successful in 20m21s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 19m8s
e670186a73
Merge lane 9 page 4a (with main, #880, #882) into lane 8 PR 1
All checks were successful
CI / Backend lint (ruff) (pull_request) Successful in 29s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 57s
CI / Bot/backend version sync (pull_request) Successful in 28s
CI / Feedback site tests (pull_request) Successful in 49s
CI / Docker image build (pull_request) Successful in 26s
CI / Does this change touch the recording path? (pull_request) Successful in 22s
CI / Bot tests and audit (pull_request) Successful in 1m56s
CI / Backend migration, tests, and audit (pull_request) Successful in 42m26s
CI / Frontend tests, audit, and build (pull_request) Successful in 45m10s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 12m37s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 40m27s
098b807616
Merge lane 8 PR 1 (with main, #880, #882) into lane 10 PR 2
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 52s
CI / Bot tests and audit (pull_request) Successful in 2m26s
CI / Feedback site tests (pull_request) Successful in 44s
CI / Bot/backend version sync (pull_request) Successful in 26s
CI / Docker image build (pull_request) Successful in 34s
CI / Does this change touch the recording path? (pull_request) Successful in 17s
CI / Backend lint (ruff) (pull_request) Successful in 2m29s
CI / Frontend tests, audit, and build (pull_request) Successful in 19m26s
CI / Backend migration, tests, and audit (pull_request) Successful in 19m2s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 12m44s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 42m4s
b7e84bcf03
Author
Collaborator

This is fixed, and the fix has been merged up train 2 (the branch is now at b7e84bcf).

J2 was a real product bug, and it has been there since #655. Every per-person notification email was silently dropped:

  • the delivery task ran inside asyncio.run;
  • the email backend's _load_config started a second event loop, which raised an error;
  • that error was treated as "no SMTP".

#865 is the first change that sends session news down this path, so it's the first to expose the bug.

  • The fix: _deliver reads SMTP in its own session and passes it through, then sends via asyncio.to_thread. This is the same pattern the SMTP test route uses.
  • Regression test: test_the_email_reaches_smtp_from_inside_the_delivery_tasks_loop, which fails without the fix.
  • J2 now checks the new per-person subjects ([Quest Board] Session N is on: …). It matches mail by its session link.

J0b was an expected change. The e2e fake bot now reports features: ["dm"], like the real bot. J0b asserts both cases:

  • an unlinked user (Dana) sees the "once your Discord is linked" line;
  • a linked user (Priya) sees the Discord DM column.

Locally, J0b and J2 passed 21 of 21 across the three projects.

Also merged into the train: main (#882's J5 fix and #870) and #880 (the J6 budget), in one pass.

Follow-ups are in #887: the confirming GM is notified of their own confirmation, and J2:36 depends on the bell's first page.

This is fixed, and the fix has been merged up train 2 (the branch is now at b7e84bcf). **J2 was a real product bug,** and it has been there since #655. Every per-person notification email was silently dropped: - the delivery task ran inside `asyncio.run`; - the email backend's `_load_config` started a second event loop, which raised an error; - that error was treated as "no SMTP". #865 is the first change that sends session news down this path, so it's the first to expose the bug. - **The fix:** `_deliver` reads SMTP in its own session and passes it through, then sends via `asyncio.to_thread`. This is the same pattern the SMTP test route uses. - **Regression test:** `test_the_email_reaches_smtp_from_inside_the_delivery_tasks_loop`, which fails without the fix. - **J2** now checks the new per-person subjects (`[Quest Board] Session N is on: …`). It matches mail by its session link. **J0b was an expected change.** The e2e fake bot now reports `features: ["dm"]`, like the real bot. J0b asserts both cases: - an unlinked user (Dana) sees the "once your Discord is linked" line; - a linked user (Priya) sees the Discord DM column. Locally, J0b and J2 passed 21 of 21 across the three projects. **Also merged into the train:** main (#882's J5 fix and #870) and #880 (the J6 budget), in one pass. **Follow-ups are in #887:** the confirming GM is notified of their own confirmation, and J2:36 depends on the bell's first page.
Sign in to join this conversation.
No description provided.