Fix draft timer: broadcasts, increments, reconnect sync, overnight pause #72

Merged
chrisp merged 2 commits from claude/website-draft-polling-plan-oIkCh into main 2026-06-06 05:57:47 +00:00
Owner

Summary

  • Timer bank broadcasts: emit timer-bank-updated after every pick so all clients immediately see the updated bank instead of waiting for the next timer-pick-started
  • Increment accuracy: capture pickMadeAt at route entry (before auth/DB overhead) and use Math.ceil so credited seconds always match the client countdown display
  • Race condition fix: hold schedulingInProgress lock for the full timer callback to prevent the recovery interval from scheduling a duplicate timeout mid-pick
  • force-autopick fix: call rescheduleTimer so the next team's clock starts immediately instead of waiting for the old timeout to naturally expire
  • adjust-time-bank fix: for on-clock teams, shift picksExpiresAt by the adjustment and reschedule so the client countdown updates; block adjustments that would reduce the bank to zero
  • New socket events: timer-pick-started, timer-overnight-paused, timer-bank-updated with full type definitions; removed dead timer-update event
  • Reconnect sync: draft-state-sync now includes expiresAt for the active timer and isOvernightPause state so reconnecting clients see accurate countdown and pause banner immediately without a page reload
  • Room closure countdown: capture client-side timestamp when draft completes so the "Room closes in X" countdown actually ticks down before the loader revalidates with draftCompletedAt
  • Countdown interval: run at 500ms with Math.ceil to prevent skipped seconds under event loop pressure
  • Overnight pause UX: canPick only blocks on commissioner pause — overnight pause freezes the timer but the on-clock player can still pick early
  • Overnight pause refactor: extract checkOvernightPause to server/overnight-pause-check.ts, breaking the timer↔socket circular import and sharing the timezone cache across both callers with correct eviction
  • PostgreSQL type fix: cast varchar owner ID to uuid in getTeamTimezone join

Test plan

  • Manual pick: all clients see bank increment immediately after pick
  • Timeout pick: all clients see bank update (0 → increment); next clock starts within ~1s
  • Force-autopick: next team's clock starts immediately; no "Pick already made" log
  • Force-manual-pick: all clients see bank increment
  • Pause while clock running: countdown freezes on all clients
  • Resume: clock continues from frozen value
  • adjust-time-bank on on-clock team: countdown shifts immediately
  • adjust-time-bank to zero: returns 400 error
  • Reconnect (socket disconnect/connect): countdown resumes for correct team
  • Hard refresh mid-draft: on-clock indicator and countdown correct immediately
  • Draft complete: "Room closes in X" counts down
  • Overnight pause: banner shows, pick buttons still enabled, timer frozen
  • npm run test:run — all 158 files / 2351 tests pass

🤖 Generated with Claude Code

## Summary - **Timer bank broadcasts**: emit `timer-bank-updated` after every pick so all clients immediately see the updated bank instead of waiting for the next `timer-pick-started` - **Increment accuracy**: capture `pickMadeAt` at route entry (before auth/DB overhead) and use `Math.ceil` so credited seconds always match the client countdown display - **Race condition fix**: hold `schedulingInProgress` lock for the full timer callback to prevent the recovery interval from scheduling a duplicate timeout mid-pick - **force-autopick fix**: call `rescheduleTimer` so the next team's clock starts immediately instead of waiting for the old timeout to naturally expire - **adjust-time-bank fix**: for on-clock teams, shift `picksExpiresAt` by the adjustment and reschedule so the client countdown updates; block adjustments that would reduce the bank to zero - **New socket events**: `timer-pick-started`, `timer-overnight-paused`, `timer-bank-updated` with full type definitions; removed dead `timer-update` event - **Reconnect sync**: `draft-state-sync` now includes `expiresAt` for the active timer and `isOvernightPause` state so reconnecting clients see accurate countdown and pause banner immediately without a page reload - **Room closure countdown**: capture client-side timestamp when draft completes so the "Room closes in X" countdown actually ticks down before the loader revalidates with `draftCompletedAt` - **Countdown interval**: run at 500ms with `Math.ceil` to prevent skipped seconds under event loop pressure - **Overnight pause UX**: `canPick` only blocks on commissioner pause — overnight pause freezes the timer but the on-clock player can still pick early - **Overnight pause refactor**: extract `checkOvernightPause` to `server/overnight-pause-check.ts`, breaking the `timer↔socket` circular import and sharing the timezone cache across both callers with correct eviction - **PostgreSQL type fix**: cast `varchar` owner ID to `uuid` in `getTeamTimezone` join ## Test plan - [ ] Manual pick: all clients see bank increment immediately after pick - [ ] Timeout pick: all clients see bank update (0 → increment); next clock starts within ~1s - [ ] Force-autopick: next team's clock starts immediately; no "Pick already made" log - [ ] Force-manual-pick: all clients see bank increment - [ ] Pause while clock running: countdown freezes on all clients - [ ] Resume: clock continues from frozen value - [ ] adjust-time-bank on on-clock team: countdown shifts immediately - [ ] adjust-time-bank to zero: returns 400 error - [ ] Reconnect (socket disconnect/connect): countdown resumes for correct team - [ ] Hard refresh mid-draft: on-clock indicator and countdown correct immediately - [ ] Draft complete: "Room closes in X" counts down - [ ] Overnight pause: banner shows, pick buttons still enabled, timer frozen - [ ] `npm run test:run` — all 158 files / 2351 tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code)
chrisp added 2 commits 2026-06-06 05:42:58 +00:00
Eliminates the setInterval(1s) + per-second DB write by storing picksExpiresAt
in draft_timers and using a targeted setTimeout per pick. Clients count down
locally from the expiresAt timestamp, removing server-pushed timer-update events.

Key changes:
- database/schema.ts: add picksExpiresAt and picksStartedAt to draft_timers
- server/timer.ts: full rewrite — schedulePickForSeason, rescheduleTimer,
  30s recovery interval instead of 1s tick, overnight-pause resume scheduling
- server/socket.ts: new timer-pick-started / timer-overnight-paused events,
  updated draft-state-sync to include expiresAt for reconnect recovery
- draft.make-pick / draft.force-manual-pick: compute actual remaining from
  picksExpiresAt at pick time; call rescheduleTimer after the autodraft chain
- useDraftSocketEvents: handle new timer events, restore countdown on reconnect
- $leagueId.draft.$seasonId: client-side countdown useEffect from expiresAt
- plans/zero-downtime-scaling.md: full 4-phase scaling plan for future reference

Resolves 2351 unit tests (all passing).

https://claude.ai/code/session_019k5J6Ty7uP5HxSx6CsbiBK
Fix draft timer bugs: broadcasts, increments, reconnect sync, and overnight pause
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 2m39s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 1m24s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
46f8552f60
- Broadcast timer-bank-updated after every pick so all connected clients
  immediately see the updated time bank (was only visible on next timer-pick-started)
- Capture pickMadeAt at route entry (before auth/DB overhead) and use Math.ceil
  so credited seconds always match the client countdown display
- Clear picksExpiresAt on every pick so _schedulePickForSeason starts fresh
- Hold schedulingInProgress lock for full timer callback to prevent the recovery
  interval from scheduling a duplicate timeout mid-pick
- Fix force-autopick route: call rescheduleTimer so the next team's clock
  starts immediately instead of waiting for the old timeout to fire naturally
- Fix draft.adjust-time-bank for on-clock teams: shift picksExpiresAt by the
  adjustment and reschedule, so the client countdown updates; block adjustments
  that would reduce the bank to zero
- Add timer-pick-started / timer-overnight-paused / timer-bank-updated socket
  events with full type definitions; replace dead timer-update event
- Fix draft-state-sync to include expiresAt for the active timer and
  isOvernightPause state so reconnecting clients see accurate countdown and
  pause banner immediately
- Fix room-closure countdown: capture client-side timestamp when draft completes
  so countdown runs even before the loader revalidates with draftCompletedAt
- Run countdown interval at 500ms with Math.ceil to prevent skipped seconds
- Add draft-started socket handler to transition pre-draft UI without a refresh
- Fix overnight pause: canPick only blocks on commissioner pause, not overnight
  pause (timer freezes but player can still pick early)
- Extract checkOvernightPause to server/overnight-pause-check.ts, breaking the
  timer↔socket circular import and ensuring the timezone cache is shared and
  evicted correctly across both callers
- Fix PostgreSQL varchar=uuid type mismatch in getTeamTimezone join

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
chrisp force-pushed claude/website-draft-polling-plan-oIkCh from 46f8552f60 to 0eb6715655 2026-06-06 05:51:50 +00:00 Compare
chrisp merged commit f96c8f5244 into main 2026-06-06 05:57:47 +00:00
chrisp deleted branch claude/website-draft-polling-plan-oIkCh 2026-06-06 05:57:52 +00:00
Sign in to join this conversation.
No description provided.