feat(webapp): web RSVP, post-session task statuses, and a "Needs you" dashboard (phase 4 of v4.3.0) #526
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/phase4-attention-surface"
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?
Closes #370, closes #380, closes #368. Phase 4 of the v4.3.0 build order (#514): the attention surface. Two migrations, re-chained after lane A's so alembic has one head:
f0ab1c2d3e4f → fa0b1c2d3e4f (session RSVP) → 0b1c2d3e4f5a (post_session_tasks), proven by applying all of them on a fresh Postgres 16 before the test files ran. No bot change, no contract bump.#370 web RSVP. Reuses the row the Discord ✅/❌ reaction already writes (
session_attendance), adding a nullablersvp_statusenum andrsvp_at, becauseattendedisNOT NULL DEFAULT falseand cannot tell "said no" from "never answered".GET /api/sessions/{id}/rsvps(one entry per member,nullfor unanswered, any member) andPUT /api/sessions/{id}/rsvp(members, for themselves only; open on confirmed sessions and on tentative-mode sessions still proposed; vote-mode proposed sessions are excluded because the slot vote already asks). The bot endpoint now also writesrsvp_status, so a Discord reaction shows in the web roster. Deliberately, the RSVP path does not writeattended: that would assert attendance at a session that has not happened, and sincebulk_insert_attendance_absentnever overwrites an existing row it would silently disable the #114 recording auto-fill for everyone who RSVPed. No notification is published, because the Discord path publishes none either. UI:SessionRsvp.jsx, a Yes/Maybe/No segmented control, the party roster with an explicit "No answer" state, inline confirmation.#380 the four invisible tasks.
sessions.post_session_tasksJSONB,{task: {status, error, at}}for the storyline rebuild, journal entry, title suggestions and recap email. Every write is oneCOALESCE(post_session_tasks,'{}') || :patchstatement because the four tasks run concurrently in separate workers and a read-modify-write would lose someone's failure. Queued marks are written at fan-out from both call sites viasummary_events.fanout_task_names(), so the fan-out list and the status map cannot drift (tested); a refused enqueue isfailed, notqueued. Absent, failed and skipped are distinct. UI:PostSessionChecklist.jsxwith all six rows; failure reasons GM-only, the state word for everyone; the recap row reads Queued / Sent / Failed.#368 "Needs you".
GET /api/me/attentionreturns five kinds (in_progress,vote,rsvp,approve_transcript,wiki_suggestions) in six round trips regardless of campaign count, with the "already answered?" tests as correlatedEXISTSsubqueries; GM-only kinds are skipped when the user GMs nothing; archived and trashed campaigns contribute nothing.NeedsYou.jsxrenders above the campaign list only when there is something, deep-links each row, and takes its labels fromvocabulary.js. The countdown line is now a link to the session, which meant the campaign card became a container with the campaign link inside it.Backend full suite 2415 passed / 13 skipped before the rebase and the two new test files (39 tests) pass on the re-chained schema; frontend 555 passed (+32); ruff clean. Follow-ups filed: #524 (
count_bot_proposalsignoresdeleted_at), #525 (RSVP "no" into the at-risk evaluation).🤖 Generated with Claude Code
One summary event fans out six tasks. Audio processing and lore proposals each write a status column and appear on the session page; the campaign storyline rebuild, the journal entry, the next-session title suggestions and the party recap email wrote nothing anywhere. An unconfigured LLM, a broker that dropped the enqueue and an SMTP host that refused every address all looked identical to a task that had simply not run yet. `sessions.post_session_tasks` (JSONB, nullable, migration 0b1c2d3e4f5a) holds `{task: {status, error, at}}` for those four. One column rather than eight scalars: they are four instances of one shape, read together or not at all, and a fifth fan-out task should cost a dict key. `app/services/post_session_tasks.py` owns every write and does it as a single `post_session_tasks = COALESCE(...,'{}') || :patch` statement. The four tasks run concurrently in separate workers, and a read-modify-write from four processes loses updates — the one it loses being somebody's failure. Absent is not failed. A NULL column (a fan-out that predates this) and a missing key (the recap email on a summary *edit*, which must never re-send) both read as "no record", and `skipped` — journal entries off, campaign archived, storyline hand-edited — is distinct from `failed`. A refused enqueue is recorded as failed rather than queued, for the reason #413 gave for lore generation. `generate_session_title_suggestions` gains an optional `session_id`: it generates for a campaign, but a GM looks for "did this run?" on the session whose summary set it off. The manual regenerate endpoint omits it and records nothing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>