Add 20-second game.status WebSocket heartbeat from active shard monitors containing full game state, and GET /status-live REST endpoint for on-demand polling. Fix missing token destructuring in SessionInfo causing crash. Relax frontend polling from 3s to 60s since WebSocket events now cover real-time updates. Bump version to 0.6.0. Made-with: Cursor
23 KiB
Sessions Endpoints
Sessions represent a gaming night. Only one session can be active at a time. Games are added to the active session as they're played. Sessions track game status, room codes, player counts, and chat logs for voting.
IMPORTANT: In session game sub-routes like /api/sessions/{sessionId}/games/{sessionGameId}/status, the sessionGameId parameter refers to the session_games.id row ID, NOT games.id.
Endpoint Summary
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/sessions |
No | List all sessions with games_played count |
| GET | /api/sessions/active |
No | Get the active session (or null) |
| GET | /api/sessions/{id} |
No | Get a session by ID |
| POST | /api/sessions |
Bearer | Create a new session |
| 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) |
| PATCH | /api/sessions/{sessionId}/games/{sessionGameId}/status |
Bearer | Update session game status |
| DELETE | /api/sessions/{sessionId}/games/{sessionGameId} |
Bearer | Remove game from session |
| PATCH | /api/sessions/{sessionId}/games/{sessionGameId}/room-code |
Bearer | Update room code for session game |
| GET | /api/sessions/{sessionId}/games/{sessionGameId}/status-live |
No | Get live game status from shard monitor |
| POST | /api/sessions/{sessionId}/games/{sessionGameId}/start-player-check |
Bearer | Start room monitor |
| POST | /api/sessions/{sessionId}/games/{sessionGameId}/stop-player-check |
Bearer | Stop room monitor |
| PATCH | /api/sessions/{sessionId}/games/{sessionGameId}/player-count |
Bearer | Update player count for session game |
GET /api/sessions
List all sessions with a games_played count. Ordered by created_at DESC.
Authentication
None.
Response
200 OK
[
{
"id": 5,
"notes": "Friday game night",
"is_active": 1,
"created_at": "2026-03-15T19:00:00.000Z",
"closed_at": null,
"games_played": 3
},
{
"id": 4,
"notes": "Last week's session",
"is_active": 0,
"created_at": "2026-03-08T18:30:00.000Z",
"closed_at": "2026-03-08T23:15:00.000Z",
"games_played": 5
}
]
Example
curl "http://localhost:5000/api/sessions"
GET /api/sessions/active
Get the active session. Returns the session object directly if one is active, or a wrapper with session: null if none.
Authentication
None.
Response
200 OK (active session exists)
{
"id": 5,
"notes": "Friday game night",
"is_active": 1,
"created_at": "2026-03-15T19:00:00.000Z",
"closed_at": null,
"games_played": 3
}
200 OK (no active session)
{
"session": null,
"message": "No active session"
}
Example
curl "http://localhost:5000/api/sessions/active"
GET /api/sessions/{id}
Get a single session by ID.
Authentication
None.
Path Parameters
| Name | Type | Description |
|---|---|---|
| id | integer | Session ID |
Response
200 OK
{
"id": 5,
"notes": "Friday game night",
"is_active": 1,
"created_at": "2026-03-15T19:00:00.000Z",
"closed_at": null,
"games_played": 3
}
Error Responses
| Status | Body | When |
|---|---|---|
| 404 | { "error": "Session not found" } |
Invalid session ID |
Example
curl "http://localhost:5000/api/sessions/5"
POST /api/sessions
Create a new session. Only one active session is allowed at a time. Triggers WebSocket session.started broadcast to all authenticated clients.
Authentication
Bearer token required.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| notes | string | No | Optional notes (e.g., "Friday game night") |
{
"notes": "Friday game night"
}
Response
201 Created
{
"id": 5,
"notes": "Friday game night",
"is_active": 1,
"created_at": "2026-03-15T19:00:00.000Z",
"closed_at": null
}
Error Responses
| Status | Body | When |
|---|---|---|
| 400 | { "error": "An active session already exists. Please close it before creating a new one.", "activeSessionId": 5 } |
An active session already exists |
Example
curl -X POST "http://localhost:5000/api/sessions" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"notes": "Friday game night"}'
POST /api/sessions/{id}/close
Close a session. Auto-sets all games with status playing to played. Optional body updates session notes. Triggers WebSocket session.ended broadcast to session subscribers.
Authentication
Bearer token required.
Path Parameters
| Name | Type | Description |
|---|---|---|
| id | integer | Session ID |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| notes | string | No | Optional notes (updates session notes) |
{
"notes": "Great session!"
}
Response
200 OK
{
"id": 5,
"notes": "Great session!",
"is_active": 0,
"created_at": "2026-03-15T19:00:00.000Z",
"closed_at": "2026-03-15T23:30:00.000Z",
"games_played": 4
}
Error Responses
| Status | Body | When |
|---|---|---|
| 400 | { "error": "Session is already closed" } |
Session was already closed |
| 404 | { "error": "Session not found" } |
Invalid session ID |
Example
curl -X POST "http://localhost:5000/api/sessions/5/close" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"notes": "Great session!"}'
DELETE /api/sessions/{id}
Delete a session. Cannot delete active sessions — close first. Cascades: deletes chat_logs and session_games.
Authentication
Bearer token required.
Path Parameters
| Name | Type | Description |
|---|---|---|
| id | integer | Session ID |
Response
200 OK
{
"message": "Session deleted successfully",
"sessionId": 5
}
Error Responses
| Status | Body | When |
|---|---|---|
| 400 | { "error": "Cannot delete an active session. Please close it first." } |
Session is active |
| 404 | { "error": "Session not found" } |
Invalid session ID |
Example
curl -X DELETE "http://localhost:5000/api/sessions/5" \
-H "Authorization: Bearer $TOKEN"
GET /api/sessions/{id}/games
List all games in a session. Returns SessionGame objects joined with game data. Ordered by played_at ASC.
Authentication
None.
Path Parameters
| Name | Type | Description |
|---|---|---|
| id | integer | Session ID |
Response
200 OK
[
{
"id": 12,
"session_id": 5,
"game_id": 42,
"manually_added": 1,
"status": "played",
"room_code": "ABCD",
"played_at": "2026-03-15T19:15:00.000Z",
"player_count": 6,
"pack_name": "Jackbox Party Pack 7",
"title": "Quiplash 3",
"game_type": "Writing",
"min_players": 3,
"max_players": 8,
"popularity_score": 12,
"upvotes": 15,
"downvotes": 3
},
{
"id": 13,
"session_id": 5,
"game_id": 38,
"manually_added": 0,
"status": "playing",
"room_code": "XY9Z",
"played_at": "2026-03-15T20:00:00.000Z",
"player_count": null,
"pack_name": "Jackbox Party Pack 6",
"title": "Trivia Murder Party 2",
"game_type": "Trivia",
"min_players": 1,
"max_players": 8,
"popularity_score": 8,
"upvotes": 10,
"downvotes": 2
}
]
Example
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
{
"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
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.
Authentication
Bearer token required.
Path Parameters
| Name | Type | Description |
|---|---|---|
| id | integer | Session ID |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| game_id | integer | Yes | Game ID (from games table) |
| manually_added | boolean | No | Whether the game was manually added (default: false) |
| room_code | string | No | 4-character room code; if provided, auto-starts room monitor |
{
"game_id": 42,
"manually_added": true,
"room_code": "ABCD"
}
Response
201 Created
{
"id": 14,
"session_id": 5,
"game_id": 42,
"manually_added": 1,
"status": "playing",
"room_code": "ABCD",
"played_at": "2026-03-15T20:30:00.000Z",
"pack_name": "Jackbox Party Pack 7",
"title": "Quiplash 3",
"game_type": "Writing",
"min_players": 3,
"max_players": 8
}
Error Responses
| Status | Body | When |
|---|---|---|
| 400 | { "error": "game_id is required" } |
Missing game_id |
| 400 | { "error": "Cannot add games to a closed session" } |
Session is closed |
| 404 | { "error": "Session not found" } |
Invalid session ID |
| 404 | { "error": "Game not found" } |
Invalid game_id |
Example
curl -X POST "http://localhost:5000/api/sessions/5/games" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"game_id": 42, "manually_added": true, "room_code": "ABCD"}'
POST /api/sessions/{id}/chat-import
Import chat log and process votes. Matches votes to games by timestamp intervals. "thisgame++" = upvote, "thisgame--" = downvote. Deduplicates by SHA-256 hash of username:message:timestamp.
Authentication
Bearer token required.
Path Parameters
| Name | Type | Description |
|---|---|---|
| id | integer | Session ID |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| chatData | array | Yes | Array of { username, message, timestamp } objects |
{
"chatData": [
{
"username": "viewer1",
"message": "thisgame++",
"timestamp": "2026-03-15T20:30:00Z"
},
{
"username": "viewer2",
"message": "thisgame--",
"timestamp": "2026-03-15T20:31:00Z"
}
]
}
Response
200 OK
{
"message": "Chat log imported and processed successfully",
"messagesImported": 150,
"duplicatesSkipped": 3,
"votesProcessed": 25,
"votesByGame": {
"42": {
"title": "Quiplash 3",
"upvotes": 15,
"downvotes": 2
}
},
"debug": {
"sessionGamesTimeline": [
{
"title": "Quiplash 3",
"played_at": "2026-03-15T20:00:00.000Z",
"played_at_ms": 1742068800000
}
],
"voteMatches": []
}
}
Error Responses
| Status | Body | When |
|---|---|---|
| 400 | { "error": "chatData must be an array" } |
chatData missing or not an array |
| 400 | { "error": "No games played in this session to match votes against" } |
Session has no games |
| 404 | { "error": "Session not found" } |
Invalid session ID |
Example
curl -X POST "http://localhost:5000/api/sessions/5/chat-import" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"chatData":[{"username":"viewer1","message":"thisgame++","timestamp":"2026-03-15T20:30:00Z"}]}'
PATCH /api/sessions/{sessionId}/games/{sessionGameId}/status
Update the status of a session game. Valid values: playing, played, skipped. If setting to playing, auto-sets other playing games to played.
Note: sessionGameId is the session_games.id row ID, NOT games.id.
Authentication
Bearer token required.
Path Parameters
| Name | Type | Description |
|---|---|---|
| sessionId | integer | Session ID |
| sessionGameId | integer | Session game ID (session_games.id) |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| status | string | Yes | "playing", "played", or "skipped" |
{
"status": "played"
}
Response
200 OK
{
"message": "Status updated successfully",
"status": "played"
}
Error Responses
| Status | Body | When |
|---|---|---|
| 400 | { "error": "Invalid status. Must be playing, played, or skipped" } |
Invalid status value |
| 404 | { "error": "Session game not found" } |
Invalid sessionId or sessionGameId |
Example
curl -X PATCH "http://localhost:5000/api/sessions/5/games/14/status" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"status": "played"}'
DELETE /api/sessions/{sessionId}/games/{sessionGameId}
Remove a game from a session. Stops room monitor and player count check.
Note: sessionGameId is the session_games.id row ID, NOT games.id.
Authentication
Bearer token required.
Path Parameters
| Name | Type | Description |
|---|---|---|
| sessionId | integer | Session ID |
| sessionGameId | integer | Session game ID (session_games.id) |
Response
200 OK
{
"message": "Game removed from session successfully"
}
Error Responses
| Status | Body | When |
|---|---|---|
| 404 | { "error": "Session game not found" } |
Invalid sessionId or sessionGameId |
Example
curl -X DELETE "http://localhost:5000/api/sessions/5/games/14" \
-H "Authorization: Bearer $TOKEN"
PATCH /api/sessions/{sessionId}/games/{sessionGameId}/room-code
Update the room code for a session game. Room code must be exactly 4 characters, uppercase A–Z and 0–9 only (regex: ^[A-Z0-9]{4}$).
Note: sessionGameId is the session_games.id row ID, NOT games.id.
Authentication
Bearer token required.
Path Parameters
| Name | Type | Description |
|---|---|---|
| sessionId | integer | Session ID |
| sessionGameId | integer | Session game ID (session_games.id) |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| room_code | string | Yes | 4 uppercase alphanumeric chars (A-Z, 0-9) |
{
"room_code": "XY9Z"
}
Response
200 OK
Returns the updated SessionGame object with joined game data.
{
"id": 14,
"session_id": 5,
"game_id": 42,
"manually_added": 1,
"status": "playing",
"room_code": "XY9Z",
"played_at": "2026-03-15T20:30:00.000Z",
"pack_name": "Jackbox Party Pack 7",
"title": "Quiplash 3",
"game_type": "Writing",
"min_players": 3,
"max_players": 8,
"popularity_score": 12,
"upvotes": 15,
"downvotes": 3
}
Error Responses
| Status | Body | When |
|---|---|---|
| 400 | { "error": "room_code is required" } |
Missing room_code |
| 400 | { "error": "room_code must be exactly 4 alphanumeric characters (A-Z, 0-9)" } |
Invalid format |
| 404 | { "error": "Session game not found" } |
Invalid sessionId or sessionGameId |
Example
curl -X PATCH "http://localhost:5000/api/sessions/5/games/14/room-code" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"room_code": "XY9Z"}'
GET /api/sessions/{id}/export
Export session data as a file download. JSON format includes structured session, games, and chat_logs. TXT format is human-readable plaintext.
Authentication
Bearer token required.
Path Parameters
| Name | Type | Description |
|---|---|---|
| id | integer | Session ID |
Query Parameters
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| format | string | No | txt |
"json" or "txt" |
Response
200 OK
- JSON format: Content-Type
application/json, filenamesession-{id}.json
{
"session": {
"id": 5,
"created_at": "2026-03-15T19:00:00.000Z",
"closed_at": "2026-03-15T23:30:00.000Z",
"is_active": false,
"notes": "Friday game night",
"games_played": 4
},
"games": [
{
"title": "Quiplash 3",
"pack": "Jackbox Party Pack 7",
"players": "3-8",
"type": "Writing",
"played_at": "2026-03-15T19:15:00.000Z",
"manually_added": true,
"status": "played"
}
],
"chat_logs": [
{
"username": "viewer1",
"message": "thisgame++",
"timestamp": "2026-03-15T19:20:00.000Z",
"vote": "thisgame++"
}
]
}
- TXT format: Content-Type
text/plain, filenamesession-{id}.txt— human-readable sections with headers.
Error Responses
| Status | Body | When |
|---|---|---|
| 404 | { "error": "Session not found" } |
Invalid session ID |
Example
# JSON export
curl -o session-5.json "http://localhost:5000/api/sessions/5/export?format=json" \
-H "Authorization: Bearer $TOKEN"
# TXT export (default)
curl -o session-5.txt "http://localhost:5000/api/sessions/5/export" \
-H "Authorization: Bearer $TOKEN"
GET /api/sessions/{sessionId}/games/{sessionGameId}/status-live
Get the live game status from an active shard monitor. If no monitor is running, falls back to data from the database. No authentication required.
The same data is broadcast every 20 seconds via the game.status WebSocket event to subscribed clients.
Note: sessionGameId is the session_games.id row ID, NOT games.id.
Authentication
None required.
Path Parameters
| Name | Type | Description |
|---|---|---|
| sessionId | integer | Session ID |
| sessionGameId | integer | Session game ID (session_games.id) |
Response
200 OK — Live shard data (when monitor is active):
{
"sessionId": 1,
"gameId": 5,
"roomCode": "LSBN",
"appTag": "drawful2international",
"maxPlayers": 8,
"playerCount": 4,
"players": ["Alice", "Bob", "Charlie", "Diana"],
"lobbyState": "CanStart",
"gameState": "Lobby",
"gameStarted": false,
"gameFinished": false,
"monitoring": true
}
200 OK — DB fallback (when no monitor is active):
{
"sessionId": 1,
"gameId": 5,
"roomCode": "LSBN",
"appTag": null,
"maxPlayers": 8,
"playerCount": 4,
"players": [],
"lobbyState": null,
"gameState": null,
"gameStarted": false,
"gameFinished": true,
"monitoring": false,
"title": "Drawful 2",
"packName": "Jackbox Party Pack 8",
"status": "completed"
}
Error Responses
| Status | Body | When |
|---|---|---|
| 404 | { "error": "Session game not found" } |
Invalid sessionId or sessionGameId |
Example
curl "http://localhost:5000/api/sessions/5/games/14/status-live"
POST /api/sessions/{sessionId}/games/{sessionGameId}/start-player-check
Start the room monitor for a session game. The game must have a room code.
Note: sessionGameId is the session_games.id row ID, NOT games.id.
Authentication
Bearer token required.
Path Parameters
| Name | Type | Description |
|---|---|---|
| sessionId | integer | Session ID |
| sessionGameId | integer | Session game ID (session_games.id) |
Response
200 OK
{
"message": "Room monitor started",
"status": "monitoring"
}
Error Responses
| Status | Body | When |
|---|---|---|
| 400 | { "error": "Game does not have a room code" } |
Session game has no room_code |
| 404 | { "error": "Session game not found" } |
Invalid sessionId or sessionGameId |
Example
curl -X POST "http://localhost:5000/api/sessions/5/games/14/start-player-check" \
-H "Authorization: Bearer $TOKEN"
POST /api/sessions/{sessionId}/games/{sessionGameId}/stop-player-check
Stop the room monitor and player count check for a session game.
Note: sessionGameId is the session_games.id row ID, NOT games.id.
Authentication
Bearer token required.
Path Parameters
| Name | Type | Description |
|---|---|---|
| sessionId | integer | Session ID |
| sessionGameId | integer | Session game ID (session_games.id) |
Response
200 OK
{
"message": "Room monitor and player count check stopped",
"status": "stopped"
}
Example
curl -X POST "http://localhost:5000/api/sessions/5/games/14/stop-player-check" \
-H "Authorization: Bearer $TOKEN"
PATCH /api/sessions/{sessionId}/games/{sessionGameId}/player-count
Manually update the player count for a session game. Sets player_count_check_status to completed. Broadcasts WebSocket player-count.updated.
Note: sessionGameId is the session_games.id row ID, NOT games.id.
Authentication
Bearer token required.
Path Parameters
| Name | Type | Description |
|---|---|---|
| sessionId | integer | Session ID |
| sessionGameId | integer | Session game ID (session_games.id) |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| player_count | integer | Yes | Non-negative player count |
{
"player_count": 6
}
Response
200 OK
{
"message": "Player count updated successfully",
"player_count": 6
}
Error Responses
| Status | Body | When |
|---|---|---|
| 400 | { "error": "player_count is required" } |
Missing player_count |
| 400 | { "error": "player_count must be a positive number" } |
Invalid (NaN or negative) |
| 404 | { "error": "Session game not found" } |
Invalid sessionId or sessionGameId |
Example
curl -X PATCH "http://localhost:5000/api/sessions/5/games/14/player-count" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"player_count": 6}'