[Scheduling] Standing availability, absences, and session-at-risk warnings #104

Closed
opened 2026-07-14 19:49:31 +00:00 by claude-bot · 2 comments
Contributor

Context / Motivation

Voting (webapp/backend/app/models/vote.py, models/timeslot.py, grid UI webapp/frontend/src/components/VotingGrid.jsx) handles per-session polling, but nothing captures standing facts like "Sarah is out for all of August" or a group's minimum viable table size. GMs discover conflicts only after proposing times, and a confirmed session can quietly fall below quorum with no warning.

Spec

(a) Per-user availability

  • UserWeeklyAvailability: user_id, weekday (0-6), start_time, end_time, stored with the user's own timezone (user.timezone, models/user.py:34) — campaign-zone-agnostic; converted at query time.
  • UserAbsence: user_id, start_date, end_date, optional note ("away 8/1–8/15").
  • Editable on the Profile page; optionally surfaced per-campaign for quick entry.

(b) Scheduling overlay

When a GM picks a time (direct/tentative) or creates vote slots, the UI shows who is unavailable for each candidate time: absences (date overlap) and weekly availability (time-range mismatch, converted between user zone and campaign zone). Backend: an endpoint like GET /campaigns/{id}/availability?times=... returning per-member availability verdicts, consumed by the session create form and VotingGrid.jsx.

(c) Campaign quorum

campaign.quorum_min_players (nullable int, default off) in campaign settings.

(d) Session-at-risk warnings

For upcoming confirmed sessions, compute likely attendance = members − (absences overlapping the session time + explicit declines where vote data exists). When it drops below quorum:

  • notify the GM through the existing bot event path — publish_bot_event / publish_bot_event_async in webapp/backend/app/services/bot_pubsub.py:29/:38 (Redis channel qb:bot:notifications, consumed by the dispatch map in bot/questboard_bot/cogs/notifications.py:169) — with a new event_type: "session_at_risk" and a matching bot handler;
  • flag the session on the web dashboard.

Evaluate inside the existing minutely poller (poll_session_reminders, webapp/backend/app/tasks/reminder_tasks.py:998) or a sibling Beat task; dedupe with a sent-marker like SessionReminderSent (models/session_reminder_sent.py) so exactly one at-risk notification fires per session (re-arm only if the session is rescheduled).

Out of scope

  • Auto-rescheduling or suggesting alternative times.
  • Reading availability from external calendars (ties into the calendar-sync issue).
  • Vote-mode changes beyond the unavailability overlay.

Acceptance criteria

  • A recorded absence shows as a conflict on the session create form and voting grid for times inside the range.
  • Weekly availability conflicts render correctly across timezone boundaries (test with user zone ≠ campaign zone).
  • With quorum set and attendance dropping below it, the GM receives exactly one bot notification per session (idempotency test), and the dashboard flags the session.
  • Quorum off (default) produces no warnings.

References

  • webapp/backend/app/models/user.py:34, models/campaign.py:41 (timezone columns)
  • webapp/backend/app/models/vote.py, models/timeslot.py, models/session_attendance.py
  • webapp/backend/app/services/bot_pubsub.py:29 (publish_bot_event, channel qb:bot:notifications)
  • webapp/backend/app/tasks/reminder_tasks.py:998 (poll_session_reminders), models/session_reminder_sent.py (dedupe pattern)
  • bot/questboard_bot/cogs/notifications.py:169 (event dispatch map)
  • webapp/frontend/src/components/VotingGrid.jsx

Filed from the July 2026 full-project review.

## Context / Motivation Voting (`webapp/backend/app/models/vote.py`, `models/timeslot.py`, grid UI `webapp/frontend/src/components/VotingGrid.jsx`) handles per-session polling, but nothing captures standing facts like "Sarah is out for all of August" or a group's minimum viable table size. GMs discover conflicts only after proposing times, and a confirmed session can quietly fall below quorum with no warning. ## Spec **(a) Per-user availability** - `UserWeeklyAvailability`: `user_id`, `weekday` (0-6), `start_time`, `end_time`, stored with the **user's own timezone** (`user.timezone`, `models/user.py:34`) — campaign-zone-agnostic; converted at query time. - `UserAbsence`: `user_id`, `start_date`, `end_date`, optional `note` ("away 8/1–8/15"). - Editable on the Profile page; optionally surfaced per-campaign for quick entry. **(b) Scheduling overlay** When a GM picks a time (direct/tentative) or creates vote slots, the UI shows who is unavailable for each candidate time: absences (date overlap) and weekly availability (time-range mismatch, converted between user zone and campaign zone). Backend: an endpoint like `GET /campaigns/{id}/availability?times=...` returning per-member availability verdicts, consumed by the session create form and `VotingGrid.jsx`. **(c) Campaign quorum** `campaign.quorum_min_players` (nullable int, default off) in campaign settings. **(d) Session-at-risk warnings** For upcoming **confirmed** sessions, compute likely attendance = members − (absences overlapping the session time + explicit declines where vote data exists). When it drops below quorum: - notify the GM through the existing bot event path — `publish_bot_event` / `publish_bot_event_async` in `webapp/backend/app/services/bot_pubsub.py:29/:38` (Redis channel `qb:bot:notifications`, consumed by the dispatch map in `bot/questboard_bot/cogs/notifications.py:169`) — with a new `event_type: "session_at_risk"` and a matching bot handler; - flag the session on the web dashboard. Evaluate inside the existing minutely poller (`poll_session_reminders`, `webapp/backend/app/tasks/reminder_tasks.py:998`) or a sibling Beat task; dedupe with a sent-marker like `SessionReminderSent` (`models/session_reminder_sent.py`) so exactly **one** at-risk notification fires per session (re-arm only if the session is rescheduled). ## Out of scope - Auto-rescheduling or suggesting alternative times. - Reading availability from external calendars (ties into the calendar-sync issue). - Vote-mode changes beyond the unavailability overlay. ## Acceptance criteria - A recorded absence shows as a conflict on the session create form and voting grid for times inside the range. - Weekly availability conflicts render correctly across timezone boundaries (test with user zone ≠ campaign zone). - With quorum set and attendance dropping below it, the GM receives exactly one bot notification per session (idempotency test), and the dashboard flags the session. - Quorum off (default) produces no warnings. ## References - `webapp/backend/app/models/user.py:34`, `models/campaign.py:41` (timezone columns) - `webapp/backend/app/models/vote.py`, `models/timeslot.py`, `models/session_attendance.py` - `webapp/backend/app/services/bot_pubsub.py:29` (`publish_bot_event`, channel `qb:bot:notifications`) - `webapp/backend/app/tasks/reminder_tasks.py:998` (`poll_session_reminders`), `models/session_reminder_sent.py` (dedupe pattern) - `bot/questboard_bot/cogs/notifications.py:169` (event dispatch map) - `webapp/frontend/src/components/VotingGrid.jsx` _Filed from the July 2026 full-project review._
Author
Contributor

Work-in-progress checkpoint (pausing mid-build)

Backend + bot: DONE and verified. Committed on branch feat/104-availability (eca2357, pushed). Not yet merged — no PR yet because the frontend is unfinished.

What's implemented and passing:

  • Models + migration b9c0d1e2f3a4 (down_revision a8b9c0d1e2f3): user_weekly_availability, user_absences (both in the user's own timezone), campaigns.quorum_min_players (nullable, off by default), and session_at_risk_notified (sent-marker keyed on (session_id, confirmed_time) so a reschedule re-arms). Migration verified to apply and downgrade -1 → upgrade head round-trip against a real Postgres.
  • availability_service: timezone-correct verdicts (a candidate/session UTC instant is converted into each user's own zone, then matched against weekly windows / absences; no windows = unknown, not unavailable). evaluate_session_at_risk = members − |absent ∪ explicit-"no"-voters|, at-risk only when quorum is set and attendance is below it.
  • Endpoints: GET /api/campaigns/{id}/availability?times=… (per-member verdicts), GET/PUT /api/me/availability, GET/POST/DELETE /api/me/absences. quorum_min_players on the campaign PATCH/response. at_risk: bool computed on the confirmed-session read.
  • Beat task poll_session_at_risk (5 min): fires exactly one session_at_risk bot event per (session_id, confirmed_time); idempotent, re-arms on reschedule. Bot: new session_at_risk handler in the notifications dispatch → GM-facing embed. Additive event → no BOT_CONTRACT_VERSION bump.

Verified: backend 421 passed (9 new), bot 187 passed (1 new), ruff check + format clean, migration round-trips.

Remaining (to resume)

  • Frontend (not started): availability + absences editor on Profile; quorum input on campaign settings; availability overlay on the voting grid (primary) + session-create form; at-risk banner on SessionDetail + Dashboard badge. API contract for these is documented in the branch commit message.
  • Then: open the #104 PR, CI, merge.

Nothing is deployed; main is unaffected.

## Work-in-progress checkpoint (pausing mid-build) **Backend + bot: DONE and verified.** Committed on branch `feat/104-availability` (`eca2357`, pushed). **Not yet merged** — no PR yet because the frontend is unfinished. What's implemented and passing: - **Models + migration** `b9c0d1e2f3a4` (down_revision `a8b9c0d1e2f3`): `user_weekly_availability`, `user_absences` (both in the user's own timezone), `campaigns.quorum_min_players` (nullable, off by default), and `session_at_risk_notified` (sent-marker keyed on `(session_id, confirmed_time)` so a reschedule re-arms). Migration verified to apply **and** `downgrade -1 → upgrade head` round-trip against a real Postgres. - **`availability_service`**: timezone-correct verdicts (a candidate/session UTC instant is converted into each user's own zone, then matched against weekly windows / absences; no windows = `unknown`, not unavailable). `evaluate_session_at_risk` = members − |absent ∪ explicit-"no"-voters|, at-risk only when quorum is set and attendance is below it. - **Endpoints**: `GET /api/campaigns/{id}/availability?times=…` (per-member verdicts), `GET/PUT /api/me/availability`, `GET/POST/DELETE /api/me/absences`. `quorum_min_players` on the campaign PATCH/response. `at_risk: bool` computed on the confirmed-session read. - **Beat task** `poll_session_at_risk` (5 min): fires exactly one `session_at_risk` bot event per `(session_id, confirmed_time)`; idempotent, re-arms on reschedule. **Bot**: new `session_at_risk` handler in the notifications dispatch → GM-facing embed. Additive event → no `BOT_CONTRACT_VERSION` bump. Verified: **backend 421 passed** (9 new), **bot 187 passed** (1 new), `ruff check` + `format` clean, migration round-trips. ## Remaining (to resume) - **Frontend** (not started): availability + absences editor on Profile; quorum input on campaign settings; availability overlay on the voting grid (primary) + session-create form; at-risk banner on SessionDetail + Dashboard badge. API contract for these is documented in the branch commit message. - Then: open the #104 PR, CI, merge. Nothing is deployed; `main` is unaffected.
Author
Contributor

Update to the checkpoint above: the frontend delegate finished right as I was pausing, so the frontend is now committed as WIP (559c30d, pushed) — but flagged unreviewed by me. Both commits are on feat/104-availability.

Delegate self-report (not yet orchestrator-verified): lint clean, 160/160 frontend tests (20 new), build green. It built: availability + absences editor on Profile, quorum_min_players on campaign settings, the voting-grid availability overlay + a session-create conflict hint, and the at-risk banner on SessionDetail. Dashboard at-risk badge deliberately skipped (the next-session list payload doesn't carry at_risk, and the spec said not to add a per-card request).

On resume: review 559c30d, re-run lint/tests/build myself, then open the #104 PR → CI → merge. Nothing deployed; main unaffected.

**Update to the checkpoint above:** the frontend delegate finished right as I was pausing, so the frontend is now committed as **WIP (`559c30d`, pushed)** — but flagged **unreviewed by me**. Both commits are on `feat/104-availability`. Delegate self-report (not yet orchestrator-verified): lint clean, **160/160 frontend tests** (20 new), build green. It built: availability + absences editor on Profile, `quorum_min_players` on campaign settings, the voting-grid availability overlay + a session-create conflict hint, and the at-risk banner on SessionDetail. Dashboard at-risk badge deliberately skipped (the next-session list payload doesn't carry `at_risk`, and the spec said not to add a per-card request). **On resume:** review `559c30d`, re-run lint/tests/build myself, then open the #104 PR → CI → merge. Nothing deployed; `main` unaffected.
rbrooks referenced this issue from a commit 2026-07-17 22:29:39 +00:00
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Quest-Board#104
No description provided.