# Vote Tracking API Design ## Overview Extend the REST and WebSocket APIs so clients can track votes at both session and global levels. The primary consumer is a stream overlay (ticker-style display) that already has the admin JWT. ## Approach **Approach B — Split by resource ownership.** Session-scoped vote data lives under the session resource. Global vote history lives under the vote resource. The WebSocket emits real-time events for live votes only. ## WebSocket: `vote.received` Event **Trigger:** `POST /api/votes/live` — fires after the vote transaction succeeds, before the HTTP response. Only live votes emit this event; chat-import does not. **Broadcast target:** Session subscribers via `broadcastEvent('vote.received', data, sessionId)`. **Payload:** ```json { "type": "vote.received", "timestamp": "2026-03-15T20:30:00.000Z", "data": { "sessionId": 5, "game": { "id": 42, "title": "Quiplash 3", "pack_name": "Party Pack 7" }, "vote": { "username": "viewer123", "type": "up", "timestamp": "2026-03-15T20:29:55.000Z" }, "totals": { "upvotes": 14, "downvotes": 3, "popularity_score": 11 } } } ``` **Implementation notes:** - `votes.js` needs access to the WebSocket manager singleton via `getWebSocketManager()`. - The existing session games JOIN needs to select `pack_name` from the `games` table. ## REST: `GET /api/sessions/:id/votes` Per-game vote breakdown for a specific session. **Location:** `backend/routes/sessions.js` **Auth:** None (matches `GET /api/sessions/:id/games`). **Response:** ```json { "session_id": 5, "votes": [ { "game_id": 42, "title": "Quiplash 3", "pack_name": "Party Pack 7", "upvotes": 14, "downvotes": 3, "net_score": 11, "total_votes": 17 } ] } ``` **Query:** ```sql SELECT lv.game_id, g.title, g.pack_name, SUM(CASE WHEN lv.vote_type = 1 THEN 1 ELSE 0 END) AS upvotes, SUM(CASE WHEN lv.vote_type = -1 THEN 1 ELSE 0 END) AS downvotes, SUM(lv.vote_type) AS net_score, COUNT(*) AS total_votes FROM live_votes lv JOIN games g ON lv.game_id = g.id WHERE lv.session_id = ? GROUP BY lv.game_id ORDER BY net_score DESC ``` **Error handling:** - Session not found → 404 - Session exists, no votes → 200 with empty `votes` array ## REST: `GET /api/votes` Paginated global vote history with flexible filtering. **Location:** `backend/routes/votes.js` **Auth:** None. **Query parameters:** | Param | Type | Default | Description | |-------|------|---------|-------------| | `session_id` | integer | — | Filter by session | | `game_id` | integer | — | Filter by game | | `username` | string | — | Filter by voter | | `vote_type` | `up` or `down` | — | Filter by direction | | `page` | integer | 1 | Page number | | `limit` | integer | 50 | Results per page (max 100) | **Response:** ```json { "votes": [ { "id": 891, "session_id": 5, "game_id": 42, "game_title": "Quiplash 3", "pack_name": "Party Pack 7", "username": "viewer123", "vote_type": "up", "timestamp": "2026-03-15T20:29:55.000Z", "created_at": "2026-03-15T20:29:56.000Z" } ], "pagination": { "page": 1, "limit": 50, "total": 237, "total_pages": 5 } } ``` **Design notes:** - `vote_type` returned as `"up"` / `"down"`, not raw `1` / `-1`. - `game_title` and `pack_name` included via JOIN. - Ordered by `timestamp DESC`. - `limit` capped at 100 server-side. **Error handling:** - Invalid filter values → 400 - No results → 200 with empty array and `total: 0` ## Testing Strategy ### Phase 1: Regression tests (pre-implementation) Written and passing before any code changes to lock down existing behavior. **`tests/api/regression-votes-live.test.js`** — existing `POST /api/votes/live`: - Returns 200 with correct response shape (`success`, `session`, `game`, `vote`) - `game` includes `id`, `title`, `upvotes`, `downvotes`, `popularity_score` - Increments `upvotes`/`popularity_score` for upvote - Increments `downvotes`/decrements `popularity_score` for downvote - 400 for missing fields, invalid vote value, invalid timestamp - 404 when no active session or timestamp doesn't match a game - 409 for duplicate within 1-second window - 401 without JWT **`tests/api/regression-games.test.js`** — game aggregate fields: - `GET /api/games` returns `upvotes`, `downvotes`, `popularity_score` - `GET /api/games/:id` returns same fields - Aggregates accurate after votes **`tests/api/regression-sessions.test.js`** — session endpoints: - `GET /api/sessions/:id` returns session object - `GET /api/sessions/:id` returns 404 for nonexistent session - `GET /api/sessions/:id/games` returns game list with expected shape **`tests/api/regression-websocket.test.js`** — existing WebSocket events: - Auth flow (auth → auth_success) - Subscribe/unsubscribe flow - `session.started` broadcast on session create - `session.ended` broadcast on session close - `game.added` broadcast on game add ### Phase 2: New feature tests (TDD — written before implementation) - **`tests/api/votes-get.test.js`** — `GET /api/votes` history endpoint - **`tests/api/sessions-votes.test.js`** — `GET /api/sessions/:id/votes` breakdown - **`tests/api/votes-live-websocket.test.js`** — `vote.received` WebSocket event ### Workflow 1. Write Phase 1 regression tests → run → all green 2. Write Phase 2 feature tests → run → all red 3. Implement features 4. Run all tests → Phase 1 still green, Phase 2 now green