diff --git a/docs/api/endpoints/sessions.md b/docs/api/endpoints/sessions.md index 50d94d5..8f34a4d 100644 --- a/docs/api/endpoints/sessions.md +++ b/docs/api/endpoints/sessions.md @@ -15,6 +15,7 @@ Sessions represent a gaming night. Only one session can be active at a time. Gam | POST | `/api/sessions/{id}/close` | Bearer | Close a session | | DELETE | `/api/sessions/{id}` | Bearer | Delete a closed session | | GET | `/api/sessions/{id}/games` | No | List games in a session | +| GET | `/api/sessions/{id}/votes` | No | Get per-game vote breakdown for a session | | POST | `/api/sessions/{id}/games` | Bearer | Add a game to a session | | POST | `/api/sessions/{id}/chat-import` | Bearer | Import chat log for vote processing | | GET | `/api/sessions/{id}/export` | Bearer | Export session (JSON or TXT) | @@ -369,6 +370,57 @@ curl "http://localhost:5000/api/sessions/5/games" --- +## GET /api/sessions/{id}/votes + +Get per-game vote breakdown for a session. Aggregates votes from the `live_votes` table by game. Results ordered by `net_score` DESC. + +### Authentication + +None. + +### Path Parameters + +| Name | Type | Description | +|------|------|-------------| +| id | integer | Session ID | + +### Response + +**200 OK** + +```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 + } + ] +} +``` + +Returns 200 with an empty `votes` array when the session has no votes. + +### Error Responses + +| Status | Body | When | +|--------|------|------| +| 404 | `{ "error": "Session not found" }` | Invalid session ID | + +### Example + +```bash +curl "http://localhost:5000/api/sessions/5/votes" +``` + +--- + ## POST /api/sessions/{id}/games Add a game to a session. Side effects: increments game `play_count`, sets previous `playing` games to `played` (skipped games stay skipped), triggers `game.added` webhook and WebSocket event, and auto-starts room monitor if `room_code` is provided. diff --git a/docs/api/endpoints/votes.md b/docs/api/endpoints/votes.md index 078bc4c..c9ab1c8 100644 --- a/docs/api/endpoints/votes.md +++ b/docs/api/endpoints/votes.md @@ -6,10 +6,69 @@ Real-time popularity voting. Bots or integrations send votes during live gaming | Method | Path | Auth | Description | |--------|------|------|-------------| +| GET | `/api/votes` | None | Paginated vote history with filtering | | POST | `/api/votes/live` | Bearer | Submit a live vote (up/down) | --- +## GET /api/votes + +Paginated vote history with filtering. Use query parameters to filter by session, game, username, or vote type. + +### Authentication + +None. + +### Query Parameters + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| session_id | int | No | — | Filter by session ID | +| game_id | int | No | — | Filter by game ID | +| username | string | No | — | Filter by voter username | +| vote_type | string | No | — | `"up"` or `"down"` | +| page | int | No | 1 | Page number | +| limit | int | No | 50 | Items per page (max 100) | + +### Response + +**200 OK** + +Results are ordered by `timestamp DESC`. The `vote_type` field is returned as `"up"` or `"down"` (not raw integers). + +```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 + } +} +``` + +### Error Responses + +| Status | Body | When | +|--------|------|------| +| 400 | `{ "error": "..." }` | Invalid `session_id`, `game_id`, or `vote_type` | +| 200 | `{ "votes": [], "pagination": { "page": 1, "limit": 50, "total": 0, "total_pages": 0 } }` | No results match the filters | + +--- + ## POST /api/votes/live Submit a real-time up/down vote for the game currently being played. Automatically finds the active session and matches the vote to the correct game using the provided timestamp and session game intervals. @@ -40,6 +99,7 @@ Bearer token required. Include in header: `Authorization: Bearer `. - Matches the vote timestamp to the game being played at that time (uses interval between consecutive `played_at` timestamps). - Updates game `upvotes`, `downvotes`, and `popularity_score` atomically in a transaction. - **Deduplication:** Rejects votes from the same username within a 1-second window (409 Conflict). +- Broadcasts a `vote.received` WebSocket event to all clients subscribed to the active session. See [WebSocket Protocol](../websocket.md#votereceived) for event payload. ### Response diff --git a/docs/api/guides/voting-and-popularity.md b/docs/api/guides/voting-and-popularity.md index e12cc66..143b13c 100644 --- a/docs/api/guides/voting-and-popularity.md +++ b/docs/api/guides/voting-and-popularity.md @@ -67,6 +67,17 @@ A bot sends individual votes during the stream. Each vote is processed immediate See [Votes live endpoint](../endpoints/votes.md#post-apivoteslive). +**Real-time tracking:** Live votes also broadcast a `vote.received` WebSocket event to all clients subscribed to the active session. This enables stream overlays and bots to react to votes in real-time without polling. See [WebSocket vote.received](../websocket.md#votereceived). + +--- + +## 3b. Querying Vote Data + +Two endpoints expose vote data for reading: + +- **`GET /api/sessions/{id}/votes`** — Per-game vote breakdown for a session. Returns aggregated `upvotes`, `downvotes`, `net_score`, and `total_votes` per game. See [Sessions votes endpoint](../endpoints/sessions.md#get-apisessionsidvotes). +- **`GET /api/votes`** — Paginated global vote history with filtering by `session_id`, `game_id`, `username`, and `vote_type`. Returns individual vote records. See [Votes list endpoint](../endpoints/votes.md#get-apivotes). + --- ## 4. Timestamp Matching Explained diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 67f8f82..30175cb 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -979,6 +979,46 @@ paths: "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } + /api/sessions/{id}/votes: + get: + operationId: getSessionVotes + summary: Get per-game vote breakdown for a session + tags: [Sessions] + parameters: + - name: id + in: path + required: true + schema: { type: integer } + description: Session ID + responses: + "200": + description: Per-game vote aggregates for the session + content: + application/json: + schema: + type: object + required: [session_id, votes] + properties: + session_id: + type: integer + votes: + type: array + items: + type: object + properties: + game_id: { type: integer } + title: { type: string } + pack_name: { type: string } + upvotes: { type: integer } + downvotes: { type: integer } + net_score: { type: integer } + total_votes: { type: integer } + "404": + description: Session not found + content: + application/json: + schema: { $ref: "#/components/schemas/Error" } + /api/sessions/{id}/chat-import: post: operationId: importSessionChat @@ -1399,6 +1439,72 @@ paths: upvotes: { type: integer } downvotes: { type: integer } + /api/votes: + get: + operationId: listVotes + summary: Paginated vote history with filtering + tags: [Votes] + parameters: + - name: session_id + in: query + schema: { type: integer } + description: Filter by session + - name: game_id + in: query + schema: { type: integer } + description: Filter by game + - name: username + in: query + schema: { type: string } + description: Filter by voter + - name: vote_type + in: query + schema: { type: string, enum: [up, down] } + description: Filter by direction + - name: page + in: query + schema: { type: integer, default: 1 } + description: Page number + - name: limit + in: query + schema: { type: integer, default: 50, maximum: 100 } + description: Results per page (max 100) + responses: + "200": + description: Paginated vote records + content: + application/json: + schema: + type: object + required: [votes, pagination] + properties: + votes: + type: array + items: + type: object + properties: + id: { type: integer } + session_id: { type: integer } + game_id: { type: integer } + game_title: { type: string } + pack_name: { type: string } + username: { type: string } + vote_type: { type: string, enum: [up, down] } + timestamp: { type: string, format: date-time } + created_at: { type: string, format: date-time } + pagination: + type: object + properties: + page: { type: integer } + limit: { type: integer } + total: { type: integer } + total_pages: { type: integer } + "400": + description: Invalid filter parameter + content: + application/json: + schema: { $ref: "#/components/schemas/Error" } + /api/votes/live: post: operationId: recordLiveVote diff --git a/docs/api/websocket.md b/docs/api/websocket.md index ce68f76..df8739f 100644 --- a/docs/api/websocket.md +++ b/docs/api/websocket.md @@ -6,6 +6,7 @@ The WebSocket API provides real-time updates for Jackbox gaming sessions. Use it - Receive notifications when sessions start, end, or when games are added - Track player counts as they are updated +- Receive live vote updates (upvotes/downvotes) as viewers vote - Avoid polling REST endpoints for session state changes The WebSocket server runs on the same host and port as the HTTP API. Connect to `/api/sessions/live` to establish a live connection. @@ -129,6 +130,7 @@ Must be authenticated. | `game.added` | Game added to a session (broadcast to subscribers) | | `session.ended` | Session closed (broadcast to subscribers) | | `player-count.updated` | Player count changed (broadcast to subscribers) | +| `vote.received` | Live vote recorded (broadcast to subscribers) | --- @@ -217,6 +219,33 @@ All server-sent events use this envelope: } ``` +### vote.received + +- **Broadcast to:** Clients subscribed to the session +- **Triggered by:** `POST /api/votes/live` (recording a live vote). Only fires for live votes, NOT chat-import. + +**Data:** +```json +{ + "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 + } +} +``` + --- ## 7. Error Handling @@ -373,6 +402,10 @@ ws.onmessage = (event) => { console.log('Player count:', msg.data.playerCount, 'for game', msg.data.gameId); break; + case 'vote.received': + console.log('Vote:', msg.data.vote.type, 'from', msg.data.vote.username, 'for', msg.data.game.title, '- totals:', msg.data.totals); + break; + case 'error': case 'auth_error': console.error('Error:', msg.message);