feat: poll countdown timer, game-selection sync, source tracking, and multi-admin fixes

Work spanning May 7-10 across multiple sessions:

Poll winner detection + source column (May 7):
- Fix race condition in handleEndPolling where WS voting.ended cleared
  leadingGame before the setTimeout could capture the winner
- Add pollActiveRef guard to prevent late poll.leading messages from
  re-activating an ended poll
- Add 'source' column to session_games (dice/manual/poll) with backward-
  compatible fallback from manually_added flag
- Show indigo "Poll" badge in game lists (Picker, Home, SessionDetail)
- Include source in session export (JSON and text formats)

Multi-admin poll state sync (May 9):
- Enrich poll.start broadcast with pollStartedAt timestamp so all admin
  clients can start their timers from the correct time
- Enrich voting.ended broadcast with winnerGameId/Label/Votes so all
  admins see the winner prompt, not just the one who clicked End Poll
- Add poll.start WS handler in SessionInfo so Admin B sees polls started
  by Admin A without refreshing
- Make handleStartPolling optimistic with rollback on failure

WebSocket keepalive + auto-reconnect (May 9):
- Add 30s ping interval to SessionInfo WS connection (matching server's
  60s timeout) to prevent silent disconnects
- Add auto-reconnect on close with 3s delay
- Proper cleanup of ping interval, reconnect timeout, and onclose handler

Sync selected game across admin clients (May 10):
- New POST/DELETE /sessions/:id/game-selection endpoints with DB
  persistence (pending_game_id, pending_game_source columns)
- Broadcast game.picked/game.dismissed WS events to session subscribers
- handleDismissGame replaces inline setSelectedGame(null) calls
- Restore pending game selection on page load for late-joining admins
- Clear pending selection when game is formally added to session

Poll ending countdown timer (May 10):
- POST /:id/voting/end now accepts optional { delay } (0-300 seconds)
- New POST /:id/voting/cancel-end to abort a scheduled end
- New poll.ending and poll.ending.cancelled WS events
- poll_ending_at column on sessions table for crash recovery
- rescheduleEndingPolls() called on server startup to resume countdowns
- End Poll button opens popover with End Now / 5s / 10s / 30s / custom
- Red "Poll Ending" card with countdown display and Cancel button
- Document new WS events in docs/api/websocket.md

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
cottongin
2026-05-10 20:33:00 -04:00
parent a1078e0cc7
commit 59db8f6ed7
7 changed files with 596 additions and 68 deletions

View File

@@ -143,6 +143,8 @@ Must be authenticated.
| `voting.ended` | Host ended the voting/polling period (broadcast to subscribers) |
| `poll.start` | Host started a new poll (broadcast to subscribers) |
| `poll.leading` | Current poll leader updated (broadcast to subscribers) |
| `poll.ending` | Poll is ending after a countdown (broadcast to subscribers) |
| `poll.ending.cancelled`| Scheduled poll end was cancelled (broadcast to subscribers) |
---
@@ -391,12 +393,15 @@ Possible `reason` values: `room_closed`, `room_not_found`, `connection_failed`,
### voting.ended
- **Broadcast to:** Clients subscribed to the session
- **Triggered by:** `POST /api/sessions/:id/voting/end` (host ends the voting/polling period)
- **Triggered by:** `POST /api/sessions/:id/voting/end` (host ends the voting/polling period, either immediately or when a countdown reaches zero)
**Data:**
```json
{
"sessionId": 3
"sessionId": 3,
"winnerGameId": 42,
"winnerLabel": "Quiplash 3",
"winnerVotes": 7
}
```
@@ -427,6 +432,42 @@ Possible `reason` values: `room_closed`, `room_not_found`, `connection_failed`,
}
```
### poll.ending
- **Broadcast to:** Clients subscribed to the session
- **Triggered by:** `POST /api/sessions/:id/voting/end` with `{ "delay": N }` where N > 0
Signals that a countdown has started and the poll will automatically end when the timer reaches zero. All connected admin clients should display the countdown. The `endsAt` timestamp is authoritative; derive the remaining time on each client by comparing against the local clock.
**Data:**
```json
{
"sessionId": 3,
"endsAt": "2026-05-10T20:15:30.000Z",
"delaySeconds": 30
}
```
| Field | Type | Description |
|----------------|---------|--------------------------------------------------|
| `sessionId` | number | The session the poll belongs to |
| `endsAt` | string | ISO 8601 timestamp when the poll will auto-end |
| `delaySeconds` | number | Original delay requested (seconds, 1-300) |
### poll.ending.cancelled
- **Broadcast to:** Clients subscribed to the session
- **Triggered by:** `POST /api/sessions/:id/voting/cancel-end`
The scheduled poll end was cancelled by an admin. Clients should stop displaying the countdown and revert to the normal "Voting In Progress" state.
**Data:**
```json
{
"sessionId": 3
}
```
---
## 7. Error Handling