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
Loading…
Reference in a new issue
No description provided.
Delete branch "build/lane10-3-email"
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 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
app/notifications/templates.py, producing HTML and text, with RFC 8058 one-click unsubscribe headers. Every notification email is built on it.email_tokens.py) for the two things an email lets you do while signed out:SECRET_KEY, never the raw secret. The orchestrator added that, since issuing no tokens yet means changing the key costs nothing.GET|POST /api/email/stopandPOST /api/email/stop/undo;GET|POST /api/email/answer, which checks campaign membership live.Session {n} · {campaign}, with the title in the description;URL:is added;RSVP=FALSE, and the invite says "Answer in Quest Board";.ics.GET /api/dev/email-preview/{kind}, only runs whenENVIRONMENT=development. The default is production.D232 (O29)
session_feedback, for the star-and-wish ask, which gets its own row on the grid.10d4e5f6a7b8(on10c3) widens the category CHECK and copies everyone's existing Sessions settings to the new row.Tests
🤖 Generated with Claude Code
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>"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>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>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>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>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-onlyrecap_email_forecast, which runs the same checkssend_recap_emailmakes, read-only:Once a real record exists, it takes over.
Tests:
:105"Accurate" knock-on is gone.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.