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 |
|
| POST | `/api/sessions/{id}/close` | Bearer | Close a session |
|
||||||
| DELETE | `/api/sessions/{id}` | Bearer | Delete a closed 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}/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}/games` | Bearer | Add a game to a session |
|
||||||
| POST | `/api/sessions/{id}/chat-import` | Bearer | Import chat log for vote processing |
|
| POST | `/api/sessions/{id}/chat-import` | Bearer | Import chat log for vote processing |
|
||||||
| GET | `/api/sessions/{id}/export` | Bearer | Export session (JSON or TXT) |
|
| 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
|
## 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.
|
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 |
|
| Method | Path | Auth | Description |
|
||||||
|--------|------|------|-------------|
|
|--------|------|------|-------------|
|
||||||
|
| GET | `/api/votes` | None | Paginated vote history with filtering |
|
||||||
| POST | `/api/votes/live` | Bearer | Submit a live vote (up/down) |
|
| 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
|
## 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.
|
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).
|
- 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.
|
- 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).
|
- **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
|
### 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).
|
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
|
## 4. Timestamp Matching Explained
|
||||||
|
|||||||
@@ -979,6 +979,46 @@ paths:
|
|||||||
"401": { $ref: "#/components/responses/Unauthorized" }
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
||||||
"403": { $ref: "#/components/responses/Forbidden" }
|
"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:
|
/api/sessions/{id}/chat-import:
|
||||||
post:
|
post:
|
||||||
operationId: importSessionChat
|
operationId: importSessionChat
|
||||||
@@ -1399,6 +1439,72 @@ paths:
|
|||||||
upvotes: { type: integer }
|
upvotes: { type: integer }
|
||||||
downvotes: { 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:
|
/api/votes/live:
|
||||||
post:
|
post:
|
||||||
operationId: recordLiveVote
|
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
|
- Receive notifications when sessions start, end, or when games are added
|
||||||
- Track player counts as they are updated
|
- Track player counts as they are updated
|
||||||
|
- Receive live vote updates (upvotes/downvotes) as viewers vote
|
||||||
- Avoid polling REST endpoints for session state changes
|
- 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.
|
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) |
|
| `game.added` | Game added to a session (broadcast to subscribers) |
|
||||||
| `session.ended` | Session closed (broadcast to subscribers) |
|
| `session.ended` | Session closed (broadcast to subscribers) |
|
||||||
| `player-count.updated` | Player count changed (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
|
## 7. Error Handling
|
||||||
@@ -373,6 +402,10 @@ ws.onmessage = (event) => {
|
|||||||
console.log('Player count:', msg.data.playerCount, 'for game', msg.data.gameId);
|
console.log('Player count:', msg.data.playerCount, 'for game', msg.data.gameId);
|
||||||
break;
|
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 'error':
|
||||||
case 'auth_error':
|
case 'auth_error':
|
||||||
console.error('Error:', msg.message);
|
console.error('Error:', msg.message);
|
||||||
|
|||||||
Reference in New Issue
Block a user