Loading…
Reference in a new issue
No description provided.
Delete branch "build/lane1-notifications"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Lane 1's server PR (series item 1 on #727; #655's core). Backend,
docs/API.md, and one string in the shell's search client.Notifications. A per-person history of what happened, with recipients worked out on the server and text built when it is read.
notificationsstores a recipient, a campaign, a kind, and a reference to its subject, never text;render()loads the subject with the reader's current permissions and says "No longer available" when they may no longer see it (left the campaign, a GM-only kind for a player, the subject gone). Nobody is told about their own action. Kinds and where they fire:session_confirmed(RSVP folded in: one row for one moment, linking to the page the RSVP control is on),vote_opened(a vote-mode session's creation, a reopened vote, a changed slate to those yet to vote),session_scheduled(a tentative session),session_cancelled,transcript_ready(GMs),summary_ready(members, first time only),wiki_suggestions(GMs, one row per batch),member_joined(GMs, from an invite, an approval or being added),ownership_offerandownership_answered.calendar_inviteis defined but not fired, since the same confirm already rings the bell. Every hook sits beside the existing bot post and email, inside a savepoint, so a notification failing never breaks the thing it is about; and every bot emit returns early without a guild, which is exactly the gap these rows close.Delivery. In-app always. Email through the existing SMTP path, carrying a line and a link and never the content, at most once per notification per channel (the delivery row is claimed before the send). Settings are a grid of category (sessions, discussions, wiki, GM tasks) against channel (email; Discord DM listed only when the bot is configured and not delivered yet), plus a mute per campaign. Nobody's current email changes: the migration carries
recap_email_opt_in = trueover as the(sessions, email)row, the recap email itself is untouched and now reads that one setting, andtests/test_recap_email.pyproves the pre-#655 shape still gets its mail. A mute also silences the recap email; no existing user has one.The API.
GET /api/me/notifications(keyset paging, rendered for the reader),GET /api/me/notifications/unread,POST /api/me/notifications/{id}/read,POST /api/me/notifications/read-all,GET/PUT /api/me/notification-settings,PUT/DELETE /api/me/notification-settings/mutes/{campaign_id},GET /api/campaigns/{id}/search-all,GET /api/campaigns/{id}/live,GET /api/meta, andpronounsonGET/PATCH /api/me. Documented indocs/API.md.Search (SH-9). One box, results tagged by kind: sessions (title and summary), people (members and their characters, aliases included), wiki (title, body, aliases; a hit found only in GM material is marked
gmand returned to GMs alone), rules (loaded content packs;hrefnull until there is a page), and withtranscripts=1for a GM the transcript segments with the speaker and timestamp. Two characters minimum ("One more letter."). The path is/search-allbecause/searchis the old campaign page's session-memory search, which returns a bare array; two shapes cannot share a path without breaking that page on main, so this one moves to/searchwhen lane 3 rebuilds the page. The shell's client points at it.Live (SH-6). The campaign's session in progress and its recording state from the cached bot status, cache only:
recording(heard within 90 s),stale,none. The second commit makesstalereachable: the bot's status key lives 15 s, so a crash was indistinguishable from a clean stop and/livesaidnoneover a session nobody was capturing, the one answer IS-001 forbids. The backend now keeps a ten-minute memo of the last active payload it saw, refreshed by every read that sees one and deleted by an explicit stop; the live key gone and the memo present isstale, withlast_heard_at. The residual gap, documented: a stop published while nobody has the app open can read as stale for a few minutes, chosen over the opposite error.Findings for the owner (also on #727): there is no session numbering anywhere in Quest Board, so where the prototype says "Session 14" the app says the session's title; a real ordinal is its own decision.
campaigns.owner_idis the billing anchor and stays; nothing under/api/bot/*changed, so no contract bump.Migration
f5a6b7c8d9eb(raw SQL, TEXT + CHECK, chained on the true single heade4f5a6b7c8da), verified up, down and up against Postgres and matching the models' constraint names.session_numberdoes not exist;users.pronounsdoes now.Tests: 3,684 backend (98 new),
ruffclean; the frontend'ssrc/apisuite with a newsearch.test.js.🤖 Generated with Claude Code
Two follow-ups to the lane 1 server PR. A bot that dies mid-recording must not read as "not being recorded" (IS-001), and until now it did. The bot's status key lives fifteen seconds and is republished every two, so fifteen seconds after a crash there is nothing left and a crash is indistinguishable from a clean stop — /live said `none` over a session nobody was capturing, and `stale` was unreachable in practice. The backend now keeps its own memo, `qb:bot:recording:last_active:<guild_id>`, with a ten-minute TTL. It is written wherever the backend sees a payload — `get_cached_recording_status`, `get_cached_recording_status_with_age`, and the pub/sub subscriber, which is what keeps it warm while a GM watches the recording dashboard. An active payload refreshes it; an inactive one deletes it, so a clean stop does not read as a crash; nothing at all leaves it alone, because absence is exactly the case it exists for. /live reads the live key first and keeps the 90-second rule for a payload that is present but old, and falls back to the memo only when the key is gone entirely. The memo is not part of the bot contract — the bot neither writes nor reads it — and it can only record what the backend actually saw. One residual gap, and it errs the safe way: a stop published while nobody has the app open expires unseen, so a clean stop can read as `stale` for a few minutes. Also: the shell's search client now asks `/api/campaigns/{id}/search-all`. `/search` is the old campaign page's session-memory search, which returns a bare array and is still what that page calls; the two shapes cannot share a path until lane 3 rebuilds it. `src/api/search.test.js` pins the path, because a one-character slip there reaches a real endpoint and gets a shape the caller cannot read. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>