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:
cottongin
2026-03-15 19:16:23 -04:00
parent e9add95efa
commit 3ed3af06ba
5 changed files with 262 additions and 0 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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);