docs: add vote tracking endpoints to API documentation
Update REST endpoint docs (votes.md, sessions.md), WebSocket protocol (websocket.md), OpenAPI spec, and voting guide with the new GET /api/votes, GET /api/sessions/:id/votes, and vote.received event. Made-with: Cursor
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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 <token>`.
|
||||
- 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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user