[Table Tools] Auto-fill attendance from recording speakers #114
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Context / Motivation
The recording pipeline already knows who was at the table, but attendance is entered by hand:
SessionAttendance(webapp/backend/app/models/session_attendance.py—session_id,user_id,attendedbool, unique per session+user), set manually GM-only viaPUT /sessions/{session_id}/attendance/{user_id}(set_attendance,routers/sessions.py:391; serviceattendance_service.upsert_attendance,services/attendance_service.py:54). The model docstring even anticipates the bot setting attendance via the same endpoint.process_audio(webapp/backend/app/tasks/reminder_tasks.py:1461) reads per-user WAV tracks plusspeakers.json(discord_user_id → display_name, read at:1519) from the audio_temp volume, then resolves Discord ids → users → members viaPlatformLink+CampaignMember(:1541-1580).build_recording_status(bot/questboard_bot/services/recording_status.py:10) returns bothspeakers(per-user seconds captured) anduncaptured_members(voice-channel members with no captured audio).Spec
Hook: at the end of successful
process_audio(the point where member identity is already resolved,reminder_tasks.py:1541-1580/ success path:1631-1646) — not at transcript approval, so proposals are ready when the GM reviews the transcript.Proposals, not writes:
AttendanceProposal(session_id,user_id,source:spoke|in_channel_silent,created_at) — distinct fromSessionAttendanceso manual data is never touched.speakers.json+PlatformLinkresolution) → proposed present (spoke).in_channel_silent). Requires the bot to persist the finalspeakers/uncaptured_memberssnapshot at/record/stoptime into the payload it sends the backend (extend the audio-notify contract inwebapp/backend/app/routers/bot.py+bot/questboard_bot/api_client.pyin the same commit per the repo rule).PlatformLink) are skipped and listed in the banner as "unmatched speakers".GM UX: session page shows a one-click confirm banner ("Recording detected 4 attendees — apply?") listing proposed members and sources. Confirm applies all proposals through the existing
upsert_attendancepath. Dismiss discards.Campaign setting:
attendance_autofill:propose(default) |auto_apply|off.auto_applywrites attendance directly but still never overwrites an existing manually setSessionAttendancerow (existing row wins in all modes).Out of scope
Acceptance criteria
upsert_attendancesemantics.offdisables generation entirely; reprocessing audio does not duplicate proposals (idempotent upsert).References
webapp/backend/app/models/session_attendance.pywebapp/backend/app/services/attendance_service.py:54(upsert_attendance),routers/sessions.py:391(set_attendance)webapp/backend/app/tasks/reminder_tasks.py:1461(process_audio),:1519(speakers.json),:1541-1580(PlatformLink/CampaignMember resolution)bot/questboard_bot/services/recording_status.py:10(build_recording_status—speakers/uncaptured_members)webapp/backend/app/routers/bot.py,bot/questboard_bot/api_client.py(API contract pair)Filed from the July 2026 full-project review.
Done — merged in PR #195 (backend+bot
80f14f8, bot-test fix76d4fe2, frontend). CI green.Shipped:
AttendanceProposalside table (spoke/in_channel_silent) distinct fromSessionAttendance; migrationc6d7e8f9a0b1(round-trips). Campaignattendance_autofill(propose/auto_apply/off); Sessionattendance_unmatched_speakers(JSONB).generate_attendance_proposals: verified-discord-link resolution only; captured→spoke, silent→in_channel_silent (spoke wins), unlinked→unmatched. Idempotent. THE INVARIANT (never overwrite a manual attendance row, any mode) enforced at one chokepointbulk_insert_attendance_absent.process_audiohook: savepoint-isolated + non-fatal + mode-aware.{applied, skipped_existing}), DELETE dismiss.uncaptured_member_ids; backend persists topresence.json. Additive — noBOT_CONTRACT_VERSIONbump.attendance_autofillsetting.Tests: backend 486 pass (+8, incl. the auto_apply/apply invariant); frontend 239 (+10); bot 188 (+1 — updated the exact-call assertion for the additive
uncaptured_member_idsfield, which is what tripped CI once before the fix).Acceptance criteria met: N linked speakers + M silent → N spoke + M in_channel_silent proposals, unlinked surface as unmatched; manual rows unchanged by proposals and auto_apply; confirm applies in one action;
offdisables generation; reprocessing is idempotent.Closing.