feat: auto-fill attendance from recording speakers (#114) #195

Merged
claude-bot merged 3 commits from feat/114-attendance-autofill into main 2026-07-18 02:42:12 +00:00
Contributor

Closes #114. Fourth feature of v3.7.0 Table Tools.

Proposes session attendance from who was in the voice channel, so the GM doesn't hand-enter it. Manual attendance is never overwritten.

Backend + bot (80f14f8)

  • New AttendanceProposal side table (source spoke|in_channel_silent), distinct from SessionAttendance; migration c6d7e8f9a0b1 (round-trips). Campaign gains attendance_autofill (propose|auto_apply|off, default propose); Session gains attendance_unmatched_speakers (JSONB).
  • generate_attendance_proposals: resolves Discord IDs via verified discord PlatformLinks only; captured speakers → spoke, in-channel-silent → in_channel_silent (spoke wins), unlinked speakers → unmatched (never applied). Idempotent (delete-then-reinsert).
  • THE INVARIANT — a pre-existing attendance row is never overwritten, in any mode — is enforced at one chokepoint, bulk_insert_attendance_absent (insert-if-absent), used by both apply and auto_apply. The overwriting upsert_attendance is untouched/unused here.
  • process_audio hook is savepoint-isolated + try/except (a failure never fails audio processing or changes status) and mode-aware (off writes nothing).
  • Endpoints (GM): GET .../attendance-proposals, POST .../apply ({applied, skipped_existing}, then clears proposals), DELETE (dismiss).
  • Bot (additive — no BOT_CONTRACT_VERSION bump): sends uncaptured_member_ids (silent voice members) in the audio-notify payload; backend persists them to presence.json in the session dir so a reprocess still has them.

Frontend (<head>)

  • GM-only confirm banner on completed sessions: "Recording detected N attendees — apply?" listing members + sources and unmatched speakers; Apply inserts + refreshes the attendance list, Dismiss clears.
  • attendance_autofill campaign setting with helper text.

Tests

  • Backend: +8 (test_attendance_proposals.py) — spoke/silent/unmatched split, verified-link-only, idempotent regen, off, the apply/auto_apply invariant (manual row survives), endpoint counts, non-GM 403. Full suite 486 pass; migration round-trip verified.
  • Frontend: +10 (sessions api 4, banner 5, campaign setting 1). 239 pass, eslint clean, vite build green.

Notes

  • Resolution uses verified links only, matching the bot-auth security posture elsewhere — an unverified/unclaimed Discord link can never drive attendance.
  • The hook logs counts only (never member names), per the repo's no-PII-in-logs rule.

🤖 Generated with Claude Code

Closes #114. Fourth feature of **v3.7.0 Table Tools**. Proposes session attendance from who was in the voice channel, so the GM doesn't hand-enter it. Manual attendance is never overwritten. ## Backend + bot (`80f14f8`) - New `AttendanceProposal` side table (source `spoke`|`in_channel_silent`), distinct from `SessionAttendance`; migration `c6d7e8f9a0b1` (round-trips). Campaign gains `attendance_autofill` (`propose`|`auto_apply`|`off`, default propose); Session gains `attendance_unmatched_speakers` (JSONB). - `generate_attendance_proposals`: resolves Discord IDs via **verified** discord `PlatformLink`s only; captured speakers → `spoke`, in-channel-silent → `in_channel_silent` (spoke wins), unlinked speakers → `unmatched` (never applied). Idempotent (delete-then-reinsert). - **THE INVARIANT** — a pre-existing attendance row is never overwritten, in any mode — is enforced at one chokepoint, `bulk_insert_attendance_absent` (insert-if-absent), used by both `apply` and `auto_apply`. The overwriting `upsert_attendance` is untouched/unused here. - `process_audio` hook is **savepoint-isolated + try/except** (a failure never fails audio processing or changes status) and mode-aware (`off` writes nothing). - Endpoints (GM): `GET .../attendance-proposals`, `POST .../apply` (`{applied, skipped_existing}`, then clears proposals), `DELETE` (dismiss). - Bot (**additive — no `BOT_CONTRACT_VERSION` bump**): sends `uncaptured_member_ids` (silent voice members) in the audio-notify payload; backend persists them to `presence.json` in the session dir so a reprocess still has them. ## Frontend (`<head>`) - GM-only confirm banner on completed sessions: "Recording detected N attendees — apply?" listing members + sources and unmatched speakers; Apply inserts + refreshes the attendance list, Dismiss clears. - `attendance_autofill` campaign setting with helper text. ## Tests - Backend: +8 (`test_attendance_proposals.py`) — spoke/silent/unmatched split, verified-link-only, idempotent regen, `off`, the **apply/auto_apply invariant** (manual row survives), endpoint counts, non-GM 403. Full suite **486 pass**; migration round-trip verified. - Frontend: +10 (sessions api 4, banner 5, campaign setting 1). **239 pass**, eslint clean, vite build green. ## Notes - Resolution uses verified links only, matching the bot-auth security posture elsewhere — an unverified/unclaimed Discord link can never drive attendance. - The hook logs counts only (never member names), per the repo's no-PII-in-logs rule. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Proposes session attendance from who was in voice: captured speakers →
"spoke", in-channel-but-silent members → "in_channel_silent", unlinked
Discord users → surfaced as "unmatched" (never applied). Manual attendance
is never overwritten.

Backend:
- AttendanceProposal side table (source spoke|in_channel_silent) distinct
  from SessionAttendance; migration c6d7e8f9a0b1 (round-trips). Campaign
  gains attendance_autofill (propose|auto_apply|off, default propose);
  Session gains attendance_unmatched_speakers (JSONB) for the banner.
- attendance_service.generate_attendance_proposals: resolves Discord IDs via
  VERIFIED discord PlatformLinks only; spoke takes precedence over silent;
  idempotent (delete-then-reinsert). THE INVARIANT lives in one chokepoint,
  bulk_insert_attendance_absent — inserts only where no attendance row exists,
  used by both apply and auto_apply; upsert_attendance (overwriting) is unused
  here.
- process_audio hook: savepoint-isolated + try/except so a failure never fails
  audio processing or changes status; mode-aware (off writes nothing).
- Endpoints (GM): GET .../attendance-proposals (proposals + unmatched),
  POST .../apply (insert-if-absent, returns {applied, skipped_existing},
  clears proposals), DELETE (dismiss).

Bot (additive — no BOT_CONTRACT_VERSION bump):
- post_audio_tracks sends optional uncaptured_member_ids; recording snapshots
  silent voice members before disconnect. Backend persists them to
  presence.json in session_dir so a reprocess still has them.

Tests: +8 (test_attendance_proposals.py) — spoke/silent/unmatched split,
verified-link-only, idempotent regen, off, the auto_apply/apply invariant
(manual row survives), endpoint counts, non-GM 403. Full suite 486 pass;
migration round-trip verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(frontend): attendance auto-fill banner + campaign setting (#114)
Some checks failed
CI / Docker image build (pull_request) Successful in 19s
CI / Backend lint (ruff) (pull_request) Successful in 52s
CI / Bot tests and audit (pull_request) Failing after 1m28s
CI / Frontend tests, audit, and build (pull_request) Successful in 1m43s
CI / Backend migration, tests, and audit (pull_request) Has been cancelled
59b5db13c7
- api/sessions.js: fetch/apply/dismiss attendance-proposals wrappers.
- SessionDetail: GM-only confirm banner (completed sessions) — "Recording
  detected N attendees — apply?" listing each proposed member + source
  (spoke / in channel, silent) and any unmatched (unlinked) speakers as an
  informational note. Apply inserts attendance (never overwriting), shows
  "Applied N, skipped M already-set", and refreshes the attendance list;
  Dismiss clears the proposals. Gated on isGm + status===completed.
- CampaignDetail: attendance_autofill setting (propose/auto_apply/off) with
  helper text noting auto_apply never overwrites manual attendance.

Tests: +10 (sessions api 4, SessionDetail banner 5, CampaignDetail 1).
239 pass, eslint clean, vite build succeeds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
test(bot): update post_audio_tracks assertion for uncaptured_member_ids (#114)
All checks were successful
CI / Docker image build (pull_request) Successful in 40s
CI / Backend lint (ruff) (pull_request) Successful in 48s
CI / Frontend tests, audit, and build (pull_request) Successful in 1m34s
CI / Bot tests and audit (pull_request) Successful in 1m54s
CI / Backend migration, tests, and audit (pull_request) Successful in 4m10s
76d4fe2d0b
The additive uncaptured_member_ids field (#114) is always present in the
audio-notify body (defaults to []); update the exact-call assertion and add
a case exercising a populated list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
claude-bot deleted branch feat/114-attendance-autofill 2026-07-18 02:42:13 +00:00
Sign in to join this conversation.
No description provided.