Compare commits

...

2 Commits

Author SHA1 Message Date
cottongin
8ba32e128c docs: comprehensive API documentation from source code
Replace existing docs with fresh documentation built entirely from source
code analysis. OpenAPI 3.1 spec as source of truth, plus human-readable
Markdown with curl examples, response samples, and workflow guides.

- OpenAPI 3.1 spec covering all 42 endpoints (validated against source)
- 7 endpoint reference docs (auth, games, sessions, picker, stats, votes, webhooks)
- WebSocket protocol documentation (auth, subscriptions, 4 event types)
- 4 guide documents (getting started, session lifecycle, voting, webhooks)
- API README with overview, auth docs, and quick reference table
- Old docs archived to docs/archive/

Made-with: Cursor
2026-03-15 16:44:53 -04:00
cottongin
505c335d20 Decouple room monitoring from player count, fix Jackbox API fetch
Extracts checkRoomStatus into shared jackbox-api.js with proper
User-Agent header (bare fetch was silently rejected by Jackbox API)
and always-on error logging (previously gated behind DEBUG flag).

Splits room-start detection (room-monitor.js) from audience-based
player counting (player-count-checker.js) to eliminate circular
dependency and allow immediate game.started detection. Room monitor
now polls immediately instead of waiting 10 seconds for first check.

Made-with: Cursor
2026-03-08 18:25:52 -04:00
29 changed files with 6767 additions and 210 deletions

View File

@@ -4,7 +4,8 @@ const { authenticateToken } = require('../middleware/auth');
const db = require('../database');
const { triggerWebhook } = require('../utils/webhooks');
const { getWebSocketManager } = require('../utils/websocket-manager');
const { startPlayerCountCheck, stopPlayerCountCheck } = require('../utils/player-count-checker');
const { stopPlayerCountCheck } = require('../utils/player-count-checker');
const { startRoomMonitor, stopRoomMonitor } = require('../utils/room-monitor');
const router = express.Router();
@@ -356,13 +357,12 @@ router.post('/:id/games', authenticateToken, (req, res) => {
console.error('Error triggering notifications:', error);
}
// Automatically start player count check if room code was provided
// Automatically start room monitoring if room code was provided
if (room_code) {
try {
startPlayerCountCheck(req.params.id, result.lastInsertRowid, room_code, game.max_players);
startRoomMonitor(req.params.id, result.lastInsertRowid, room_code, game.max_players);
} catch (error) {
console.error('Error starting player count check:', error);
// Don't fail the request if player count check fails
console.error('Error starting room monitor:', error);
}
}
@@ -580,12 +580,13 @@ router.patch('/:sessionId/games/:gameId/status', authenticateToken, (req, res) =
return res.status(404).json({ error: 'Session game not found' });
}
// Stop player count check if game is no longer playing
// Stop room monitor and player count check if game is no longer playing
if (status !== 'playing') {
try {
stopRoomMonitor(sessionId, gameId);
stopPlayerCountCheck(sessionId, gameId);
} catch (error) {
console.error('Error stopping player count check:', error);
console.error('Error stopping room monitor/player count check:', error);
}
}
@@ -600,11 +601,12 @@ router.delete('/:sessionId/games/:gameId', authenticateToken, (req, res) => {
try {
const { sessionId, gameId } = req.params;
// Stop player count check before deleting
// Stop room monitor and player count check before deleting
try {
stopRoomMonitor(sessionId, gameId);
stopPlayerCountCheck(sessionId, gameId);
} catch (error) {
console.error('Error stopping player count check:', error);
console.error('Error stopping room monitor/player count check:', error);
}
const result = db.prepare(`
@@ -826,12 +828,12 @@ router.post('/:sessionId/games/:gameId/start-player-check', authenticateToken, (
return res.status(400).json({ error: 'Game does not have a room code' });
}
// Start the check
startPlayerCountCheck(sessionId, gameId, game.room_code, game.max_players);
// Start room monitoring (will hand off to player count check when game starts)
startRoomMonitor(sessionId, gameId, game.room_code, game.max_players);
res.json({
message: 'Player count check started',
status: 'waiting'
message: 'Room monitor started',
status: 'monitoring'
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -843,11 +845,12 @@ router.post('/:sessionId/games/:gameId/stop-player-check', authenticateToken, (r
try {
const { sessionId, gameId } = req.params;
// Stop the check
// Stop both room monitor and player count check
stopRoomMonitor(sessionId, gameId);
stopPlayerCountCheck(sessionId, gameId);
res.json({
message: 'Player count check stopped',
message: 'Room monitor and player count check stopped',
status: 'stopped'
});
} catch (error) {

View File

@@ -0,0 +1,42 @@
const JACKBOX_API_BASE = 'https://ecast.jackboxgames.com/api/v2';
const DEFAULT_HEADERS = {
'User-Agent': 'Mozilla/5.0 (compatible; GamePicker/1.0)'
};
/**
* Check room status via the Jackbox ecast REST API.
* Shared by room-monitor (polling for lock) and player-count-checker (room existence).
*/
async function checkRoomStatus(roomCode) {
try {
const response = await fetch(`${JACKBOX_API_BASE}/rooms/${roomCode}`, {
headers: DEFAULT_HEADERS
});
if (!response.ok) {
console.log(`[Jackbox API] Room ${roomCode}: HTTP ${response.status}`);
return { exists: false };
}
const data = await response.json();
const roomData = data.body || data;
if (process.env.DEBUG === 'true') {
console.log('[Jackbox API] Room data:', JSON.stringify(roomData, null, 2));
}
return {
exists: true,
locked: roomData.locked || false,
full: roomData.full || false,
maxPlayers: roomData.maxPlayers || 8,
minPlayers: roomData.minPlayers || 0
};
} catch (e) {
console.error(`[Jackbox API] Error checking room ${roomCode}:`, e.message);
return { exists: false };
}
}
module.exports = { checkRoomStatus };

View File

@@ -1,40 +1,11 @@
const puppeteer = require('puppeteer');
const db = require('../database');
const { getWebSocketManager } = require('./websocket-manager');
const { checkRoomStatus } = require('./jackbox-api');
// Store active check jobs
const activeChecks = new Map();
/**
* Check room status via Jackbox API
*/
async function checkRoomStatus(roomCode) {
try {
const response = await fetch(`https://ecast.jackboxgames.com/api/v2/rooms/${roomCode}`);
if (response.ok) {
const data = await response.json();
const roomData = data.body || data;
if (process.env.DEBUG) {
console.log('[API] Room data:', JSON.stringify(roomData, null, 2));
}
return {
exists: true,
locked: roomData.locked || false,
full: roomData.full || false,
maxPlayers: roomData.maxPlayers || 8,
minPlayers: roomData.minPlayers || 0
};
}
return { exists: false };
} catch (e) {
if (process.env.DEBUG) {
console.error('[API] Error checking room:', e.message);
}
return { exists: false };
}
}
/**
* Watch a game from start to finish as audience member
* Collects analytics throughout the entire game lifecycle
@@ -67,7 +38,7 @@ async function watchGameAsAudience(sessionId, gameId, roomCode, maxPlayers) {
let bestPlayerCount = null;
let startPlayerCount = null; // Authoritative count from 'start' action
let gameEnded = false;
let audienceJoined = false; // Track whether we've confirmed audience join
let audienceJoined = false;
let frameCount = 0;
// Enable CDP and listen for WebSocket frames BEFORE navigating
@@ -98,7 +69,6 @@ async function watchGameAsAudience(sessionId, gameId, roomCode, maxPlayers) {
audienceJoined = true;
console.log(`[Audience] Successfully joined room ${roomCode} as audience`);
// Broadcast audience.joined event via WebSocket
const wsManager = getWebSocketManager();
if (wsManager) {
wsManager.broadcastEvent('audience.joined', {
@@ -121,7 +91,6 @@ async function watchGameAsAudience(sessionId, gameId, roomCode, maxPlayers) {
if (process.env.DEBUG) {
console.log(`[Frame ${frameCount}] 🎉 GAME ENDED - Final count: ${finalCount} players`);
// Verify it matches start count if we had one
if (startPlayerCount !== null && startPlayerCount !== finalCount) {
console.log(`[Frame ${frameCount}] ⚠️ WARNING: Start count (${startPlayerCount}) != Final count (${finalCount})`);
} else if (startPlayerCount !== null) {
@@ -130,7 +99,6 @@ async function watchGameAsAudience(sessionId, gameId, roomCode, maxPlayers) {
}
bestPlayerCount = finalCount;
gameEnded = true;
// Update immediately with final count
updatePlayerCount(sessionId, gameId, finalCount, 'completed');
return;
}
@@ -138,7 +106,6 @@ async function watchGameAsAudience(sessionId, gameId, roomCode, maxPlayers) {
// Extract player counts from analytics (game in progress)
if (roomVal.analytics && Array.isArray(roomVal.analytics)) {
for (const analytic of roomVal.analytics) {
// Check for 'start' action - this is authoritative
if (analytic.action === 'start' && analytic.value && typeof analytic.value === 'number') {
if (startPlayerCount === null) {
startPlayerCount = analytic.value;
@@ -146,25 +113,20 @@ async function watchGameAsAudience(sessionId, gameId, roomCode, maxPlayers) {
if (process.env.DEBUG) {
console.log(`[Frame ${frameCount}] 🎯 Found 'start' action: ${analytic.value} players (authoritative)`);
}
// Update UI with authoritative start count
updatePlayerCount(sessionId, gameId, startPlayerCount, 'checking');
}
continue; // Skip to next analytic
continue;
}
// If we already have start count, we don't need to keep counting
if (startPlayerCount !== null) {
continue;
}
// Otherwise, look for any numeric value that could be a player count
if (analytic.value && typeof analytic.value === 'number' && analytic.value > 0 && analytic.value <= 100) {
seenPlayerCounts.add(analytic.value);
// Clamp to maxPlayers to avoid cumulative stats inflating count
const clampedValue = Math.min(analytic.value, maxPlayers);
// Update best guess (highest count seen so far, clamped to maxPlayers)
if (bestPlayerCount === null || clampedValue > bestPlayerCount) {
bestPlayerCount = clampedValue;
if (process.env.DEBUG) {
@@ -174,7 +136,6 @@ async function watchGameAsAudience(sessionId, gameId, roomCode, maxPlayers) {
console.log(`[Frame ${frameCount}] 📊 Found player count ${analytic.value} in action '${analytic.action}' (best so far)`);
}
}
// Update UI with current best guess
updatePlayerCount(sessionId, gameId, bestPlayerCount, 'checking');
}
}
@@ -237,9 +198,7 @@ async function watchGameAsAudience(sessionId, gameId, roomCode, maxPlayers) {
if (process.env.DEBUG) console.log('[Audience] 👀 Watching game... (will monitor until game ends)');
// Keep watching until game ends or we're told to stop
// Check every 5 seconds if we should still be watching
const checkInterval = setInterval(async () => {
// Check if we should stop
const game = db.prepare(`
SELECT status, player_count_check_status
FROM session_games
@@ -256,14 +215,12 @@ async function watchGameAsAudience(sessionId, gameId, roomCode, maxPlayers) {
return;
}
// Check if game ended
if (gameEnded) {
clearInterval(checkInterval);
if (browser) await browser.close();
return;
}
// Check if room still exists
const roomStatus = await checkRoomStatus(roomCode);
if (!roomStatus.exists) {
if (process.env.DEBUG) {
@@ -281,7 +238,6 @@ async function watchGameAsAudience(sessionId, gameId, roomCode, maxPlayers) {
}
}, 5000);
// Store the interval so we can clean it up
const check = activeChecks.get(checkKey);
if (check) {
check.watchInterval = checkInterval;
@@ -293,7 +249,6 @@ async function watchGameAsAudience(sessionId, gameId, roomCode, maxPlayers) {
if (browser) {
await browser.close();
}
// If we had a best guess, use it; otherwise fail
if (bestPlayerCount !== null) {
updatePlayerCount(sessionId, gameId, bestPlayerCount, 'completed');
} else {
@@ -303,27 +258,7 @@ async function watchGameAsAudience(sessionId, gameId, roomCode, maxPlayers) {
}
/**
* Broadcast game.started event when room becomes locked
*/
function broadcastGameStarted(sessionId, gameId, roomCode, maxPlayers) {
try {
const wsManager = getWebSocketManager();
if (wsManager) {
wsManager.broadcastEvent('game.started', {
sessionId,
gameId,
roomCode,
maxPlayers
}, parseInt(sessionId));
}
console.log(`[Player Count] Broadcasted game.started for room ${roomCode} (max: ${maxPlayers})`);
} catch (error) {
console.error('[Player Count] Failed to broadcast game.started:', error.message);
}
}
/**
* Update player count in database
* Update player count in database and broadcast via WebSocket
*/
function updatePlayerCount(sessionId, gameId, playerCount, status) {
try {
@@ -333,7 +268,6 @@ function updatePlayerCount(sessionId, gameId, playerCount, status) {
WHERE session_id = ? AND id = ?
`).run(playerCount, status, sessionId, gameId);
// Broadcast via WebSocket
const wsManager = getWebSocketManager();
if (wsManager) {
wsManager.broadcastEvent('player-count.updated', {
@@ -351,25 +285,18 @@ function updatePlayerCount(sessionId, gameId, playerCount, status) {
}
/**
* Start checking player count for a game
* Strategy:
* 1. Wait 10 seconds for initial room setup
* 2. Poll every 10 seconds until game is locked (started)
* 3. Broadcast game.started event when locked detected
* 4. Join audience and watch entire game
* 5. Update UI as we learn more
* 6. Finalize when game ends
* Start player count checking for a game.
* Called by room-monitor once the game is confirmed started (room locked).
* Goes straight to joining the audience — no polling needed.
*/
async function startPlayerCountCheck(sessionId, gameId, roomCode, maxPlayers = 8) {
const checkKey = `${sessionId}-${gameId}`;
// If already checking, don't start again
if (activeChecks.has(checkKey)) {
console.log(`[Player Count] Already checking ${checkKey}`);
return;
}
// Check if already completed (but allow retrying failed checks)
const game = db.prepare(`
SELECT player_count_check_status
FROM session_games
@@ -381,122 +308,27 @@ async function startPlayerCountCheck(sessionId, gameId, roomCode, maxPlayers = 8
return;
}
// If retrying a failed check, reset the status
if (game && game.player_count_check_status === 'failed') {
console.log(`[Player Count] Retrying failed check for ${checkKey}`);
}
console.log(`[Player Count] Starting check for game ${gameId} with room code ${roomCode}`);
console.log(`[Player Count] Starting audience watch for game ${gameId} (room ${roomCode}, max ${maxPlayers})`);
// Update status to waiting
db.prepare(`
UPDATE session_games
SET player_count_check_status = 'waiting'
SET player_count_check_status = 'checking'
WHERE session_id = ? AND id = ?
`).run(sessionId, gameId);
// Function to check if game is ready (locked)
const waitForGameStart = async () => {
const roomStatus = await checkRoomStatus(roomCode);
if (!roomStatus.exists) {
console.log(`[Player Count] Room ${roomCode} does not exist`);
updatePlayerCount(sessionId, gameId, null, 'failed');
stopPlayerCountCheck(sessionId, gameId);
return false;
}
// If locked, game has started - ready to watch
if (roomStatus.locked) {
console.log(`[Player Count] Room is LOCKED - game in progress, starting watch`);
return { ready: true, maxPlayers: roomStatus.maxPlayers };
}
// Log if full but not yet started
if (roomStatus.full) {
console.log(`[Player Count] Room is FULL but not locked yet - waiting for game start`);
} else {
console.log(`[Player Count] Room not ready yet (lobby still open)`);
}
return null; // Not ready, keep polling
};
// Wait 10 seconds before first check
const initialTimeout = setTimeout(async () => {
try {
// Update status to checking
db.prepare(`
UPDATE session_games
SET player_count_check_status = 'checking'
WHERE session_id = ? AND id = ?
`).run(sessionId, gameId);
console.log(`[Player Count] Initial check after 10s for ${checkKey}`);
const result = await waitForGameStart();
if (result && result.ready === true) {
// Game is locked, broadcast game.started and start watching
const realMaxPlayers = result.maxPlayers;
broadcastGameStarted(sessionId, gameId, roomCode, realMaxPlayers);
console.log(`[Player Count] Using real maxPlayers from Jackbox: ${realMaxPlayers} (database had: ${maxPlayers})`);
await watchGameAsAudience(sessionId, gameId, roomCode, realMaxPlayers);
} else if (result === null) {
// Not ready yet, poll every 10 seconds
const checkInterval = setInterval(async () => {
// Check if we should stop
const game = db.prepare(`
SELECT status, player_count_check_status
FROM session_games
WHERE session_id = ? AND id = ?
`).get(sessionId, gameId);
if (!game || game.status === 'skipped' || game.status === 'played' || game.player_count_check_status === 'stopped' || game.player_count_check_status === 'completed') {
console.log(`[Player Count] Stopping check for ${checkKey} - game status changed`);
stopPlayerCountCheck(sessionId, gameId);
return;
}
const result = await waitForGameStart();
if (result && result.ready === true) {
// Game is now locked, stop interval, broadcast game.started, and start watching
clearInterval(checkInterval);
const check = activeChecks.get(checkKey);
if (check) check.interval = null;
const realMaxPlayers = result.maxPlayers;
broadcastGameStarted(sessionId, gameId, roomCode, realMaxPlayers);
console.log(`[Player Count] Using real maxPlayers from Jackbox: ${realMaxPlayers} (database had: ${maxPlayers})`);
await watchGameAsAudience(sessionId, gameId, roomCode, realMaxPlayers);
} else if (result === false) {
// Check failed or completed, stop
clearInterval(checkInterval);
stopPlayerCountCheck(sessionId, gameId);
}
}, 10000); // Poll every 10 seconds
// Store the interval
const check = activeChecks.get(checkKey);
if (check) check.interval = checkInterval;
}
// If ready === false, check already stopped/completed
} catch (error) {
console.error(`[Player Count] Error starting check for ${checkKey}:`, error.message);
updatePlayerCount(sessionId, gameId, null, 'failed');
stopPlayerCountCheck(sessionId, gameId);
}
}, 10000); // Wait 10 seconds before first check
// Store the check references
activeChecks.set(checkKey, {
sessionId,
gameId,
roomCode,
initialTimeout,
interval: null,
watchInterval: null,
browser: null
});
await watchGameAsAudience(sessionId, gameId, roomCode, maxPlayers);
}
/**
@@ -507,12 +339,6 @@ async function stopPlayerCountCheck(sessionId, gameId) {
const check = activeChecks.get(checkKey);
if (check) {
if (check.initialTimeout) {
clearTimeout(check.initialTimeout);
}
if (check.interval) {
clearInterval(check.interval);
}
if (check.watchInterval) {
clearInterval(check.watchInterval);
}
@@ -525,7 +351,6 @@ async function stopPlayerCountCheck(sessionId, gameId) {
}
activeChecks.delete(checkKey);
// Update status to stopped if not already completed or failed
const game = db.prepare(`
SELECT player_count_check_status
FROM session_games
@@ -548,13 +373,7 @@ async function stopPlayerCountCheck(sessionId, gameId) {
* Clean up all active checks (for graceful shutdown)
*/
async function cleanupAllChecks() {
for (const [checkKey, check] of activeChecks.entries()) {
if (check.initialTimeout) {
clearTimeout(check.initialTimeout);
}
if (check.interval) {
clearInterval(check.interval);
}
for (const [, check] of activeChecks.entries()) {
if (check.watchInterval) {
clearInterval(check.watchInterval);
}

View File

@@ -0,0 +1,135 @@
const db = require('../database');
const { getWebSocketManager } = require('./websocket-manager');
const { checkRoomStatus } = require('./jackbox-api');
const POLL_INTERVAL_MS = 10000;
// Active room monitors, keyed by "{sessionId}-{gameId}"
const activeMonitors = new Map();
/**
* Broadcast game.started event when room becomes locked
*/
function broadcastGameStarted(sessionId, gameId, roomCode, maxPlayers) {
try {
const wsManager = getWebSocketManager();
if (wsManager) {
wsManager.broadcastEvent('game.started', {
sessionId,
gameId,
roomCode,
maxPlayers
}, parseInt(sessionId));
}
console.log(`[Room Monitor] Broadcasted game.started for room ${roomCode} (max: ${maxPlayers})`);
} catch (error) {
console.error('[Room Monitor] Failed to broadcast game.started:', error.message);
}
}
/**
* Start monitoring a Jackbox room for game start (locked state).
*
* Polls the Jackbox REST API every 10 seconds. When the room becomes
* locked, broadcasts a game.started WebSocket event and then hands off
* to the player-count-checker to join as audience.
*/
async function startRoomMonitor(sessionId, gameId, roomCode, maxPlayers = 8) {
const monitorKey = `${sessionId}-${gameId}`;
if (activeMonitors.has(monitorKey)) {
console.log(`[Room Monitor] Already monitoring ${monitorKey}`);
return;
}
console.log(`[Room Monitor] Starting monitor for room ${roomCode} (${monitorKey})`);
const onGameStarted = (realMaxPlayers) => {
broadcastGameStarted(sessionId, gameId, roomCode, realMaxPlayers);
// Lazy require breaks the circular dependency with player-count-checker
const { startPlayerCountCheck } = require('./player-count-checker');
console.log(`[Room Monitor] Room ${roomCode} locked — handing off to player count checker`);
startPlayerCountCheck(sessionId, gameId, roomCode, realMaxPlayers);
};
const pollRoom = async () => {
const game = db.prepare(`
SELECT status FROM session_games
WHERE session_id = ? AND id = ?
`).get(sessionId, gameId);
if (!game || game.status === 'skipped' || game.status === 'played') {
console.log(`[Room Monitor] Stopping — game status changed for ${monitorKey}`);
stopRoomMonitor(sessionId, gameId);
return;
}
const roomStatus = await checkRoomStatus(roomCode);
if (!roomStatus.exists) {
console.log(`[Room Monitor] Room ${roomCode} does not exist — stopping`);
stopRoomMonitor(sessionId, gameId);
return;
}
if (roomStatus.locked) {
stopRoomMonitor(sessionId, gameId);
onGameStarted(roomStatus.maxPlayers);
return;
}
if (roomStatus.full) {
console.log(`[Room Monitor] Room ${roomCode} is full but not locked yet — waiting`);
} else {
console.log(`[Room Monitor] Room ${roomCode} lobby still open — waiting`);
}
};
// Poll immediately, then every POLL_INTERVAL_MS
activeMonitors.set(monitorKey, {
sessionId,
gameId,
roomCode,
interval: null
});
await pollRoom();
// If the monitor was already stopped (room locked or gone on first check), bail
if (!activeMonitors.has(monitorKey)) return;
const interval = setInterval(() => pollRoom(), POLL_INTERVAL_MS);
const monitor = activeMonitors.get(monitorKey);
if (monitor) monitor.interval = interval;
}
/**
* Stop monitoring a room
*/
function stopRoomMonitor(sessionId, gameId) {
const monitorKey = `${sessionId}-${gameId}`;
const monitor = activeMonitors.get(monitorKey);
if (monitor) {
if (monitor.interval) clearInterval(monitor.interval);
activeMonitors.delete(monitorKey);
console.log(`[Room Monitor] Stopped monitor for ${monitorKey}`);
}
}
/**
* Clean up all active monitors (for graceful shutdown)
*/
function cleanupAllMonitors() {
for (const [, monitor] of activeMonitors.entries()) {
if (monitor.interval) clearInterval(monitor.interval);
}
activeMonitors.clear();
console.log('[Room Monitor] Cleaned up all active monitors');
}
module.exports = {
startRoomMonitor,
stopRoomMonitor,
cleanupAllMonitors
};

176
docs/api/README.md Normal file
View File

@@ -0,0 +1,176 @@
# Jackbox Game Picker API
## Overview
The API manages Jackbox Party Pack games, runs gaming sessions, tracks popularity via voting, picks games with weighted random selection, and notifies external systems via webhooks and WebSocket.
## Base URL
| Environment | Base URL | Notes |
|-------------|----------|-------|
| Local development | `http://localhost:5000` | Backend direct |
| Docker Compose | `http://localhost:3000/api` | Via Vite/Nginx proxy |
All REST endpoints are prefixed with `/api/` except `/health`.
## Authentication
1. **Login**: POST to `/api/auth/login` with JSON body:
```json
{ "key": "<admin-key>" }
```
Returns a JWT token.
2. **Authorization**: Include the token in requests:
```
Authorization: Bearer <token>
```
3. **Expiry**: Tokens expire in 24 hours.
### Public Endpoints (no auth required)
- `GET /api/games`
- `GET /api/games/packs`
- `GET /api/games/meta/packs`
- `GET /api/games/{id}`
- `GET /api/sessions`
- `GET /api/sessions/active`
- `GET /api/sessions/{id}`
- `GET /api/sessions/{id}/games`
- `GET /api/stats`
- `POST /api/pick`
- `GET /health`
All write and admin operations require authentication.
## Request/Response Format
- Request and response bodies use JSON. Set `Content-Type: application/json`.
- **Exceptions**:
- `GET /api/games/export/csv` returns `text/csv`
- `GET /api/sessions/{id}/export` returns `text/plain` or `application/json` depending on `format` query param
## Error Handling
All errors return:
```json
{ "error": "description" }
```
| Status | Meaning |
|--------|---------|
| 400 | Bad request / validation failure |
| 401 | No token provided |
| 403 | Invalid or expired token |
| 404 | Not found |
| 409 | Conflict (e.g. duplicate vote) |
| 500 | Server error |
Global error handler may include additional detail:
```json
{ "error": "Something went wrong!", "message": "<details>" }
```
## Boolean Fields
SQLite stores booleans as integers (0/1). In request bodies, pass JavaScript booleans (`true`/`false`); the API converts them. In responses, expect `0`/`1` for game and session fields. **Exception**: `Webhook.enabled` returns a JavaScript boolean.
## Pagination
No pagination. All list endpoints return full result sets.
## Quick Reference
### Health
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/health` | No | Health check |
### Auth
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/api/auth/login` | No | Authenticate with admin key |
| POST | `/api/auth/verify` | Yes | Verify JWT token |
### Games
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/api/games` | No | List games with optional filters |
| POST | `/api/games` | Yes | Create a new game |
| GET | `/api/games/packs` | No | List all packs |
| GET | `/api/games/meta/packs` | No | List pack metadata |
| GET | `/api/games/export/csv` | Yes | Export games as CSV |
| POST | `/api/games/import/csv` | Yes | Import games from CSV |
| PATCH | `/api/games/packs/{name}/favor` | Yes | Update pack favor bias |
| PATCH | `/api/games/packs/{name}/toggle` | Yes | Enable or disable a pack |
| GET | `/api/games/{id}` | No | Get a game by ID |
| PUT | `/api/games/{id}` | Yes | Update a game |
| DELETE | `/api/games/{id}` | Yes | Delete a game |
| PATCH | `/api/games/{id}/toggle` | Yes | Toggle game enabled status |
| PATCH | `/api/games/{id}/favor` | Yes | Update game favor bias |
### Sessions
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/api/sessions` | No | List all sessions |
| POST | `/api/sessions` | Yes | Create a new session |
| GET | `/api/sessions/active` | No | Get the active session |
| GET | `/api/sessions/{id}` | No | Get a session by ID |
| DELETE | `/api/sessions/{id}` | Yes | Delete a session |
| POST | `/api/sessions/{id}/close` | Yes | Close a session |
| GET | `/api/sessions/{id}/games` | No | List games in a session |
| POST | `/api/sessions/{id}/games` | Yes | Add a game to a session |
| POST | `/api/sessions/{id}/chat-import` | Yes | Import chat log for vote processing |
| GET | `/api/sessions/{id}/export` | Yes | Export session |
| PATCH | `/api/sessions/{sessionId}/games/{sessionGameId}/status` | Yes | Update session game status |
| DELETE | `/api/sessions/{sessionId}/games/{sessionGameId}` | Yes | Remove game from session |
| PATCH | `/api/sessions/{sessionId}/games/{sessionGameId}/room-code` | Yes | Update room code for session game |
| POST | `/api/sessions/{sessionId}/games/{sessionGameId}/start-player-check` | Yes | Start room monitor for player count |
| POST | `/api/sessions/{sessionId}/games/{sessionGameId}/stop-player-check` | Yes | Stop room monitor |
| PATCH | `/api/sessions/{sessionId}/games/{sessionGameId}/player-count` | Yes | Update player count for session game |
### Picker
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/api/pick` | No | Pick a random game with optional filters |
### Stats
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/api/stats` | No | Get aggregate statistics |
### Votes
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/api/votes/live` | Yes | Record a live vote (up/down) |
### Webhooks
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/api/webhooks` | Yes | List all webhooks |
| POST | `/api/webhooks` | Yes | Create a webhook |
| GET | `/api/webhooks/{id}` | Yes | Get a webhook by ID |
| PATCH | `/api/webhooks/{id}` | Yes | Update a webhook |
| DELETE | `/api/webhooks/{id}` | Yes | Delete a webhook |
| POST | `/api/webhooks/test/{id}` | Yes | Send test webhook |
| GET | `/api/webhooks/{id}/logs` | Yes | List webhook delivery logs |
## Documentation Links
- [OpenAPI Spec](openapi.yaml)
- **Endpoint docs**: [Auth](endpoints/auth.md), [Games](endpoints/games.md), [Sessions](endpoints/sessions.md), [Picker](endpoints/picker.md), [Stats](endpoints/stats.md), [Votes](endpoints/votes.md), [Webhooks](endpoints/webhooks.md)
- [WebSocket Protocol](websocket.md)
- **Guides**: [Getting Started](guides/getting-started.md), [Session Lifecycle](guides/session-lifecycle.md), [Voting & Popularity](guides/voting-and-popularity.md), [Webhooks & Events](guides/webhooks-and-events.md)

135
docs/api/endpoints/auth.md Normal file
View File

@@ -0,0 +1,135 @@
# Auth Endpoints
Simple admin-key authentication. Single role (admin). No user management. Obtain a JWT from `POST /api/auth/login` and use it as a Bearer token for protected endpoints.
## Endpoint Summary
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/api/auth/login` | No | Exchange admin key for JWT |
| POST | `/api/auth/verify` | Bearer | Validate token and return user info |
---
## POST /api/auth/login
Exchange an admin key for a JWT. Use the returned token in the `Authorization: Bearer <token>` header for protected routes. Tokens expire after 24 hours.
### Authentication
None. This endpoint is public.
### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| key | string | Yes | Admin key (configured via `ADMIN_KEY` env) |
```json
{
"key": "your-admin-key"
}
```
### Response
**200 OK**
```json
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"message": "Authentication successful",
"expiresIn": "24h"
}
```
| Field | Description |
|-------|-------------|
| token | JWT to use in `Authorization: Bearer <token>` |
| message | Success message |
| expiresIn | Token lifetime |
### Error Responses
| Status | Body | When |
|--------|------|------|
| 400 | `{ "error": "Admin key is required" }` | `key` field missing |
| 401 | `{ "error": "Invalid admin key" }` | Wrong key |
### Example
```bash
curl -X POST http://localhost:5000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"key": "your-admin-key"}'
```
**Sample response:**
```json
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYWRtaW4iLCJ0aW1lc3RhbXAiOjE3MTAwMDAwMDAwLCJpYXQiOjE3MTAwMDAwMDB9.abc123",
"message": "Authentication successful",
"expiresIn": "24h"
}
```
---
## POST /api/auth/verify
Verify that the provided Bearer token is valid and return the decoded user payload.
### Authentication
Bearer token required. Include in header: `Authorization: Bearer <token>`.
### Parameters
None.
### Response
**200 OK**
```json
{
"valid": true,
"user": {
"role": "admin",
"timestamp": 1710000000000
}
}
```
| Field | Description |
|-------|-------------|
| valid | Always `true` when token is valid |
| user.role | User role (always `"admin"`) |
| user.timestamp | Unix ms when token was issued |
### Error Responses
| Status | Body | When |
|--------|------|------|
| 401 | `{ "error": "Access token required" }` | No `Authorization` header or Bearer token |
| 403 | `{ "error": "Invalid or expired token" }` | Bad or expired token |
### Example
```bash
curl -X POST http://localhost:5000/api/auth/verify \
-H "Authorization: Bearer $TOKEN"
```
**Sample response:**
```json
{
"valid": true,
"user": {
"role": "admin",
"timestamp": 1710000000000
}
}
```

627
docs/api/endpoints/games.md Normal file
View File

@@ -0,0 +1,627 @@
# Games Endpoints
Manage the Jackbox game catalog. Games belong to packs (e.g., "Jackbox Party Pack 7"). Each game has player limits, type, audience support, family-friendliness, and favor bias for weighted selection. Packs can also have favor/disfavor bias to influence the picker.
## Endpoint Summary
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/api/games` | No | List games with optional filters |
| GET | `/api/games/packs` | No | List all packs |
| GET | `/api/games/meta/packs` | No | Pack metadata (counts, plays) |
| GET | `/api/games/export/csv` | Bearer | Export games as CSV |
| PATCH | `/api/games/packs/{name}/favor` | Bearer | Set pack favor bias |
| GET | `/api/games/{id}` | No | Get single game |
| POST | `/api/games` | Bearer | Create game |
| PUT | `/api/games/{id}` | Bearer | Update game |
| DELETE | `/api/games/{id}` | Bearer | Delete game |
| PATCH | `/api/games/{id}/toggle` | Bearer | Toggle game enabled status |
| PATCH | `/api/games/packs/{name}/toggle` | Bearer | Enable/disable all games in pack |
| POST | `/api/games/import/csv` | Bearer | Import games from CSV |
| PATCH | `/api/games/{id}/favor` | Bearer | Set game favor bias |
---
## GET /api/games
List all games with optional query filters. Results are ordered by `pack_name`, then `title`.
### Authentication
None.
### Parameters
| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| enabled | query | string | No | `"true"` or `"false"` to filter by enabled status |
| playerCount | query | integer | No | Filter games where `min_players ≤ count ≤ max_players` |
| drawing | query | string | No | `"only"` = `game_type='Drawing'`, `"exclude"` = exclude Drawing |
| length | query | string | No | `"short"` (≤15 min or NULL), `"medium"` (1625 min), `"long"` (>25 min) |
| familyFriendly | query | string | No | `"true"` or `"false"` |
| pack | query | string | No | Exact `pack_name` match |
### Response
**200 OK**
```json
[
{
"id": 1,
"pack_name": "Jackbox Party Pack 7",
"title": "Quiplash 3",
"min_players": 3,
"max_players": 8,
"length_minutes": 15,
"has_audience": 1,
"family_friendly": 0,
"game_type": "Writing",
"secondary_type": null,
"play_count": 0,
"popularity_score": 0,
"enabled": 1,
"favor_bias": 0,
"created_at": "2024-01-15T12:00:00.000Z"
}
]
```
### Error Responses
| Status | Body | When |
|--------|------|------|
| 500 | `{ "error": "..." }` | Server error |
### Example
```bash
curl "http://localhost:5000/api/games?playerCount=6&pack=Jackbox%20Party%20Pack%207"
```
---
## GET /api/games/packs
List all packs with their favor bias.
### Authentication
None.
### Response
**200 OK**
```json
[
{
"id": 1,
"name": "Jackbox Party Pack 7",
"favor_bias": 0,
"created_at": "2024-01-15T12:00:00.000Z"
}
]
```
---
## GET /api/games/meta/packs
Return pack metadata: total game count, enabled count, and total plays per pack.
### Authentication
None.
### Response
**200 OK**
```json
[
{
"name": "Jackbox Party Pack 7",
"total_count": 5,
"enabled_count": 5,
"total_plays": 42
}
]
```
---
## GET /api/games/export/csv
Export the full game catalog as a CSV file download.
### Authentication
Bearer token required.
### Response
**200 OK**
- Content-Type: `text/csv`
- Content-Disposition: `attachment; filename="jackbox-games.csv"`
- Columns: Pack Name, Title, Min Players, Max Players, Length (minutes), Audience, Family Friendly, Game Type, Secondary Type
### Example
```bash
curl -o jackbox-games.csv "http://localhost:5000/api/games/export/csv" \
-H "Authorization: Bearer $TOKEN"
```
---
## PATCH /api/games/packs/{name}/favor
Set favor bias for a pack. Affects weighted random selection in the picker.
### Authentication
Bearer token required.
### Path Parameters
| Name | Type | Description |
|------|------|-------------|
| name | string | Pack name (exact match, URL-encode if spaces) |
### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| favor_bias | integer | Yes | `1` = favor, `-1` = disfavor, `0` = neutral |
```json
{
"favor_bias": 1
}
```
### Response
**200 OK**
```json
{
"message": "Pack favor bias updated successfully",
"favor_bias": 1
}
```
### Error Responses
| Status | Body | When |
|--------|------|------|
| 400 | `{ "error": "favor_bias must be 1 (favor), -1 (disfavor), or 0 (neutral)" }` | Invalid value |
### Example
```bash
curl -X PATCH "http://localhost:5000/api/games/packs/Jackbox%20Party%20Pack%207/favor" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"favor_bias": 1}'
```
---
## GET /api/games/{id}
Get a single game by ID.
### Authentication
None.
### Path Parameters
| Name | Type | Description |
|------|------|-------------|
| id | integer | Game ID |
### Response
**200 OK**
```json
{
"id": 1,
"pack_name": "Jackbox Party Pack 7",
"title": "Quiplash 3",
"min_players": 3,
"max_players": 8,
"length_minutes": 15,
"has_audience": 1,
"family_friendly": 0,
"game_type": "Writing",
"secondary_type": null,
"play_count": 0,
"popularity_score": 0,
"enabled": 1,
"favor_bias": 0,
"created_at": "2024-01-15T12:00:00.000Z"
}
```
### Error Responses
| Status | Body | When |
|--------|------|------|
| 404 | `{ "error": "Game not found" }` | Invalid ID |
### Example
```bash
curl "http://localhost:5000/api/games/1"
```
---
## POST /api/games
Create a new game. Pack is created automatically if it does not exist.
### Authentication
Bearer token required.
### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| pack_name | string | Yes | Pack name (e.g., "Jackbox Party Pack 7") |
| title | string | Yes | Game title |
| min_players | integer | Yes | Minimum players |
| max_players | integer | Yes | Maximum players |
| length_minutes | integer | No | Approx. play length |
| has_audience | boolean | No | Audience mode supported |
| family_friendly | boolean | No | Family-friendly rating |
| game_type | string | No | Primary type (e.g., "Writing", "Drawing") |
| secondary_type | string | No | Secondary type |
```json
{
"pack_name": "Jackbox Party Pack 7",
"title": "Quiplash 3",
"min_players": 3,
"max_players": 8,
"length_minutes": 15,
"has_audience": true,
"family_friendly": false,
"game_type": "Writing",
"secondary_type": null
}
```
### Response
**201 Created**
Returns the created game object.
### Error Responses
| Status | Body | When |
|--------|------|------|
| 400 | `{ "error": "Missing required fields" }` | Missing pack_name, title, min_players, or max_players |
### Example
```bash
curl -X POST "http://localhost:5000/api/games" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"pack_name": "Jackbox Party Pack 7",
"title": "Quiplash 3",
"min_players": 3,
"max_players": 8,
"length_minutes": 15,
"has_audience": true,
"family_friendly": false,
"game_type": "Writing",
"secondary_type": null
}'
```
---
## PUT /api/games/{id}
Update a game. All fields are optional; uses COALESCE (only provided fields are updated).
### Authentication
Bearer token required.
### Path Parameters
| Name | Type | Description |
|------|------|-------------|
| id | integer | Game ID |
### Request Body
All fields optional. Include only the fields to update.
```json
{
"pack_name": "Jackbox Party Pack 7",
"title": "Quiplash 3",
"min_players": 3,
"max_players": 8,
"length_minutes": 20,
"has_audience": true,
"family_friendly": false,
"game_type": "Writing",
"secondary_type": null,
"enabled": true
}
```
### Response
**200 OK**
Returns the updated game object.
### Error Responses
| Status | Body | When |
|--------|------|------|
| 404 | `{ "error": "Game not found" }` | Invalid ID |
### Example
```bash
curl -X PUT "http://localhost:5000/api/games/1" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"length_minutes": 20, "enabled": true}'
```
---
## DELETE /api/games/{id}
Delete a game permanently.
### Authentication
Bearer token required.
### Path Parameters
| Name | Type | Description |
|------|------|-------------|
| id | integer | Game ID |
### Response
**200 OK**
```json
{
"message": "Game deleted successfully"
}
```
### Error Responses
| Status | Body | When |
|--------|------|------|
| 404 | `{ "error": "Game not found" }` | Invalid ID |
### Example
```bash
curl -X DELETE "http://localhost:5000/api/games/1" \
-H "Authorization: Bearer $TOKEN"
```
---
## PATCH /api/games/{id}/toggle
Toggle the game's `enabled` field (0↔1). Use to quickly enable/disable a game without a full PUT.
### Authentication
Bearer token required.
### Path Parameters
| Name | Type | Description |
|------|------|-------------|
| id | integer | Game ID |
### Response
**200 OK**
Returns the updated game object with the flipped `enabled` value.
### Error Responses
| Status | Body | When |
|--------|------|------|
| 404 | `{ "error": "Game not found" }` | Invalid ID |
### Example
```bash
curl -X PATCH "http://localhost:5000/api/games/1/toggle" \
-H "Authorization: Bearer $TOKEN"
```
---
## PATCH /api/games/packs/{name}/toggle
Enable or disable all games in a pack at once.
### Authentication
Bearer token required.
### Path Parameters
| Name | Type | Description |
|------|------|-------------|
| name | string | Pack name (URL-encode if spaces) |
### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| enabled | boolean | Yes | `true` to enable, `false` to disable |
```json
{
"enabled": true
}
```
### Response
**200 OK**
```json
{
"message": "Pack enabled successfully",
"gamesAffected": 12
}
```
Message varies: "Pack enabled successfully" or "Pack disabled successfully" based on the `enabled` value.
### Error Responses
| Status | Body | When |
|--------|------|------|
| 400 | `{ "error": "enabled status required" }` | Missing `enabled` field |
### Example
```bash
curl -X PATCH "http://localhost:5000/api/games/packs/Jackbox%20Party%20Pack%207/toggle" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"enabled": true}'
```
---
## POST /api/games/import/csv
Import games from CSV data. Default mode is `append`. Use `"replace"` to delete all existing games before importing.
### Authentication
Bearer token required.
### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| csvData | string | Yes | Raw CSV content (header + rows) |
| mode | string | No | `"append"` (default) or `"replace"` |
**CSV columns:** Game Pack, Game Title, Min. Players, Max. Players, Length, Audience, Family Friendly?, Game Type, Secondary Type
```json
{
"csvData": "Game Pack,Game Title,Min. Players,Max. Players,Length,Audience,Family Friendly?,Game Type,Secondary Type\nJackbox Party Pack 7,Quiplash 3,3,8,15,Yes,No,Writing,\nJackbox Party Pack 7,The Devils and the Details,3,7,25,Yes,No,Strategy,",
"mode": "append"
}
```
### Response
**200 OK**
```json
{
"message": "Successfully imported 5 games",
"count": 5,
"mode": "append"
}
```
### Error Responses
| Status | Body | When |
|--------|------|------|
| 400 | `{ "error": "CSV data required" }` | Missing `csvData` |
### Example
```bash
curl -X POST "http://localhost:5000/api/games/import/csv" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"csvData": "Game Pack,Game Title,Min. Players,Max. Players,Length,Audience,Family Friendly?,Game Type,Secondary Type\nJackbox Party Pack 7,Quiplash 3,3,8,15,Yes,No,Writing,\nJackbox Party Pack 7,The Devils and the Details,3,7,25,Yes,No,Strategy,",
"mode": "append"
}'
```
---
## PATCH /api/games/{id}/favor
Set favor bias for a single game. Affects weighted random selection in the picker.
### Authentication
Bearer token required.
### Path Parameters
| Name | Type | Description |
|------|------|-------------|
| id | integer | Game ID |
### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| favor_bias | integer | Yes | `1` = favor, `-1` = disfavor, `0` = neutral |
```json
{
"favor_bias": -1
}
```
### Response
**200 OK**
```json
{
"message": "Favor bias updated successfully",
"favor_bias": -1
}
```
### Error Responses
| Status | Body | When |
|--------|------|------|
| 400 | `{ "error": "favor_bias must be 1 (favor), -1 (disfavor), or 0 (neutral)" }` | Invalid value |
| 404 | `{ "error": "Game not found" }` | Invalid ID |
### Example
```bash
curl -X PATCH "http://localhost:5000/api/games/1/favor" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"favor_bias": -1}'
```

View File

@@ -0,0 +1,120 @@
# Picker Endpoints
Weighted random game selection. Picks from enabled games matching your filters, with favor bias affecting probability. Avoids recently played games within a session.
## Endpoint Summary
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/api/pick` | No | Pick a random game with filters and repeat avoidance |
---
## POST /api/pick
Select a game using weighted random selection. Filters to only enabled games, applies favor/disfavor bias to influence probability, and optionally excludes recently played games when a session is provided.
### Authentication
None.
### Request Body
All fields optional. Provide only the filters you want to apply.
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| playerCount | integer | No | Filter games where `min_players ≤ count ≤ max_players` |
| drawing | string | No | `"only"` = Drawing type only, `"exclude"` = exclude Drawing type |
| length | string | No | `"short"` (≤15 min or NULL), `"medium"` (1625 min), `"long"` (>25 min) |
| familyFriendly | boolean | No | Filter by family-friendly rating |
| sessionId | integer | No | Session ID for repeat avoidance |
| excludePlayed | boolean | No | When `true`, exclude ALL games played in session. Default: exclude last 2 only |
```json
{
"playerCount": 6,
"drawing": "exclude",
"length": "short",
"familyFriendly": true,
"sessionId": 3,
"excludePlayed": false
}
```
### Filters
- **Enabled games only:** Only games with `enabled = 1` are considered.
- **playerCount:** Filters games where `min_players ≤ playerCount ≤ max_players`.
- **drawing:** `"only"` = games with `game_type = 'Drawing'`; `"exclude"` = games that are not Drawing type.
- **length:** `"short"` = ≤15 min (includes NULL); `"medium"` = 1625 min; `"long"` = >25 min.
- **familyFriendly:** `true` or `false` filters by `family_friendly`.
### Weighted Selection
- **Game favor_bias:** `1` = 3× weight, `0` = 1× weight, `-1` = 0.2× weight.
- **Pack favor_bias:** `1` = 2× weight, `0` = 1× weight, `-1` = 0.3× weight.
- Game and pack biases multiply together.
### Repeat Avoidance (with sessionId)
- **Default (`excludePlayed: false`):** Excludes the last 2 played games in the session.
- **With `excludePlayed: true`:** Excludes ALL games played in the session.
### Response
**200 OK**
```json
{
"game": {
"id": 42,
"pack_name": "Jackbox Party Pack 7",
"title": "Quiplash 3",
"min_players": 3,
"max_players": 8,
"length_minutes": 15,
"has_audience": 1,
"family_friendly": 0,
"game_type": "Writing",
"secondary_type": null,
"play_count": 0,
"popularity_score": 0,
"enabled": 1,
"favor_bias": 0,
"created_at": "2024-01-15T12:00:00.000Z"
},
"poolSize": 15,
"totalEnabled": 17
}
```
| Field | Description |
|-------|-------------|
| game | Full game object for the selected game |
| poolSize | Number of games in the eligible pool after filters |
| totalEnabled | Approximate total enabled games (includes excluded when sessionId provided) |
### Error Responses
| Status | Body | When |
|--------|------|------|
| 404 | `{ "error": "No games match the current filters", "suggestion": "Try adjusting your filters or enabling more games" }` | No games match the filters |
| 404 | `{ "error": "All eligible games have been played in this session", "suggestion": "Enable more games or adjust your filters", "recentlyPlayed": [1, 5, 12] }` | All eligible games already played in session (when `excludePlayed: true`) |
| 404 | `{ "error": "All eligible games have been played recently", "suggestion": "Enable more games or adjust your filters", "recentlyPlayed": [1, 5] }` | Last 2 games are the only matches (when `excludePlayed: false`) |
| 500 | `{ "error": "..." }` | Server error |
### Example
```bash
curl -X POST "http://localhost:5000/api/pick" \
-H "Content-Type: application/json" \
-d '{
"playerCount": 6,
"drawing": "exclude",
"length": "short",
"familyFriendly": true,
"sessionId": 3,
"excludePlayed": false
}'
```

View File

@@ -0,0 +1,919 @@
# 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 |
| 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 |
| 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**
```json
[
{
"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
```bash
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)
```json
{
"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)
```json
{
"session": null,
"message": "No active session"
}
```
### Example
```bash
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**
```json
{
"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
```bash
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") |
```json
{
"notes": "Friday game night"
}
```
### Response
**201 Created**
```json
{
"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
```bash
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) |
```json
{
"notes": "Great session!"
}
```
### Response
**200 OK**
```json
{
"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
```bash
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**
```json
{
"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
```bash
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**
```json
[
{
"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
```bash
curl "http://localhost:5000/api/sessions/5/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.
### 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 |
```json
{
"game_id": 42,
"manually_added": true,
"room_code": "ABCD"
}
```
### Response
**201 Created**
```json
{
"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
```bash
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 |
```json
{
"chatData": [
{
"username": "viewer1",
"message": "thisgame++",
"timestamp": "2026-03-15T20:30:00Z"
},
{
"username": "viewer2",
"message": "thisgame--",
"timestamp": "2026-03-15T20:31:00Z"
}
]
}
```
### Response
**200 OK**
```json
{
"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
```bash
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"` |
```json
{
"status": "played"
}
```
### Response
**200 OK**
```json
{
"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
```bash
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**
```json
{
"message": "Game removed from session successfully"
}
```
### Error Responses
| Status | Body | When |
|--------|------|------|
| 404 | `{ "error": "Session game not found" }` | Invalid sessionId or sessionGameId |
### Example
```bash
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 AZ and 09 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) |
```json
{
"room_code": "XY9Z"
}
```
### Response
**200 OK**
Returns the updated SessionGame object with joined game data.
```json
{
"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
```bash
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`, filename `session-{id}.json`
```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`, filename `session-{id}.txt` — human-readable sections with headers.
### Error Responses
| Status | Body | When |
|--------|------|------|
| 404 | `{ "error": "Session not found" }` | Invalid session ID |
### Example
```bash
# 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"
```
---
## 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**
```json
{
"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
```bash
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**
```json
{
"message": "Room monitor and player count check stopped",
"status": "stopped"
}
```
### Example
```bash
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 |
```json
{
"player_count": 6
}
```
### Response
**200 OK**
```json
{
"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
```bash
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}'
```

View File

@@ -0,0 +1,79 @@
# Stats Endpoints
Aggregate statistics about the game library, sessions, and popularity.
## Endpoint Summary
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/api/stats` | No | Get aggregate statistics |
---
## GET /api/stats
Return aggregate statistics: game counts, pack count, session counts, total games played, most-played games, and top-rated games.
### Authentication
None.
### Response
**200 OK**
```json
{
"games": { "count": 89 },
"gamesEnabled": { "count": 75 },
"packs": { "count": 9 },
"sessions": { "count": 12 },
"activeSessions": { "count": 1 },
"totalGamesPlayed": { "count": 156 },
"mostPlayedGames": [
{
"id": 42,
"title": "Quiplash 3",
"pack_name": "Jackbox Party Pack 7",
"play_count": 15,
"popularity_score": 8,
"upvotes": 10,
"downvotes": 2
}
],
"topRatedGames": [
{
"id": 42,
"title": "Quiplash 3",
"pack_name": "Jackbox Party Pack 7",
"play_count": 15,
"popularity_score": 8,
"upvotes": 10,
"downvotes": 2
}
]
}
```
| Field | Description |
|-------|-------------|
| games.count | Total number of games in the library |
| gamesEnabled.count | Number of enabled games |
| packs.count | Number of distinct packs |
| sessions.count | Total sessions (all time) |
| activeSessions.count | Sessions with `is_active = 1` |
| totalGamesPlayed.count | Total game plays across all sessions |
| mostPlayedGames | Top 10 games by `play_count` DESC (only games with `play_count` > 0) |
| topRatedGames | Top 10 games by `popularity_score` DESC (only games with `popularity_score` > 0) |
### Error Responses
| Status | Body | When |
|--------|------|------|
| 500 | `{ "error": "..." }` | Server error |
### Example
```bash
curl "http://localhost:5000/api/stats"
```

View File

@@ -0,0 +1,92 @@
# Votes Endpoints
Real-time popularity voting. Bots or integrations send votes during live gaming sessions. Votes are matched to the currently-playing game using timestamp intervals.
## Endpoint Summary
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/api/votes/live` | Bearer | Submit a live vote (up/down) |
---
## 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.
### Authentication
Bearer token required. Include in header: `Authorization: Bearer <token>`.
### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| username | string | Yes | Identifier for the voter (used for deduplication) |
| vote | string | Yes | `"up"` or `"down"` |
| timestamp | string | Yes | ISO 8601 timestamp when the vote occurred |
```json
{
"username": "viewer123",
"vote": "up",
"timestamp": "2026-03-15T20:30:00Z"
}
```
### Behavior
- Finds the active session (single session with `is_active = 1`).
- 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).
### Response
**200 OK**
```json
{
"success": true,
"message": "Vote recorded successfully",
"session": { "id": 3, "games_played": 5 },
"game": {
"id": 42,
"title": "Quiplash 3",
"upvotes": 11,
"downvotes": 2,
"popularity_score": 9
},
"vote": {
"username": "viewer123",
"type": "up",
"timestamp": "2026-03-15T20:30:00Z"
}
}
```
### Error Responses
| Status | Body | When |
|--------|------|------|
| 400 | `{ "error": "Missing required fields: username, vote, timestamp" }` | Missing required fields |
| 400 | `{ "error": "vote must be either \"up\" or \"down\"" }` | Invalid vote value |
| 400 | `{ "error": "Invalid timestamp format. Use ISO 8601 format (e.g., 2025-11-01T20:30:00Z)" }` | Invalid timestamp |
| 404 | `{ "error": "No active session found" }` | No session with `is_active = 1` |
| 404 | `{ "error": "No games have been played in the active session yet" }` | Active session has no games |
| 404 | `{ "error": "Vote timestamp does not match any game in the active session", "debug": { "voteTimestamp": "2026-03-15T20:30:00Z", "sessionGames": [{ "title": "Quiplash 3", "played_at": "..." }] } }` | Timestamp outside any game interval |
| 409 | `{ "error": "Duplicate vote detected (within 1 second of previous vote)", "message": "Please wait at least 1 second between votes", "timeSinceLastVote": 0.5 }` | Same username voted within 1 second |
| 500 | `{ "error": "..." }` | Server error |
### Example
```bash
curl -X POST "http://localhost:5000/api/votes/live" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"username": "viewer123",
"vote": "up",
"timestamp": "2026-03-15T20:30:00Z"
}'
```

View File

@@ -0,0 +1,382 @@
# Webhooks Endpoints
HTTP callback endpoints for external integrations. Register webhook URLs to receive notifications about events like game additions. All endpoints require authentication.
## Endpoint Summary
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/api/webhooks` | Bearer | List all webhooks |
| GET | `/api/webhooks/{id}` | Bearer | Get single webhook |
| POST | `/api/webhooks` | Bearer | Create webhook |
| PATCH | `/api/webhooks/{id}` | Bearer | Update webhook |
| DELETE | `/api/webhooks/{id}` | Bearer | Delete webhook |
| POST | `/api/webhooks/test/{id}` | Bearer | Send test event |
| GET | `/api/webhooks/{id}/logs` | Bearer | Get webhook delivery logs |
---
## GET /api/webhooks
List all registered webhooks. `secret` is not included in responses. `events` is returned as a parsed array. `enabled` is returned as a boolean.
### Authentication
Bearer token required.
### Response
**200 OK**
```json
[
{
"id": 1,
"name": "Discord Bot",
"url": "https://example.com/webhook",
"events": ["game.added"],
"enabled": true,
"created_at": "2024-01-15T12:00:00.000Z"
}
]
```
Note: `secret` is never returned.
### Error Responses
| Status | Body | When |
|--------|------|------|
| 401 | `{ "error": "Access token required" }` | No Bearer token |
| 403 | `{ "error": "Invalid or expired token" }` | Bad or expired token |
| 500 | `{ "error": "..." }` | Server error |
### Example
```bash
curl "http://localhost:5000/api/webhooks" \
-H "Authorization: Bearer $TOKEN"
```
---
## GET /api/webhooks/{id}
Get a single webhook by ID.
### Authentication
Bearer token required.
### Path Parameters
| Name | Type | Description |
|------|------|-------------|
| id | integer | Webhook ID |
### Response
**200 OK**
```json
{
"id": 1,
"name": "Discord Bot",
"url": "https://example.com/webhook",
"events": ["game.added"],
"enabled": true,
"created_at": "2024-01-15T12:00:00.000Z"
}
```
### Error Responses
| Status | Body | When |
|--------|------|------|
| 404 | `{ "error": "Webhook not found" }` | Invalid ID |
### Example
```bash
curl "http://localhost:5000/api/webhooks/1" \
-H "Authorization: Bearer $TOKEN"
```
---
## POST /api/webhooks
Create a new webhook.
### Authentication
Bearer token required.
### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| name | string | Yes | Display name for the webhook |
| url | string | Yes | Callback URL (must be valid) |
| secret | string | Yes | Secret for signing payloads |
| events | array | Yes | Event types to subscribe to (e.g., `["game.added"]`) |
```json
{
"name": "Discord Bot",
"url": "https://example.com/webhook",
"secret": "mysecret123",
"events": ["game.added"]
}
```
### Response
**201 Created**
```json
{
"id": 5,
"name": "Discord Bot",
"url": "https://example.com/webhook",
"events": ["game.added"],
"enabled": true,
"created_at": "2024-01-15T12:00:00.000Z",
"message": "Webhook created successfully"
}
```
### Error Responses
| Status | Body | When |
|--------|------|------|
| 400 | `{ "error": "Missing required fields: name, url, secret, events" }` | Missing fields |
| 400 | `{ "error": "events must be an array" }` | `events` is not an array |
| 400 | `{ "error": "Invalid URL format" }` | URL validation failed |
### Example
```bash
curl -X POST "http://localhost:5000/api/webhooks" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Discord Bot",
"url": "https://example.com/webhook",
"secret": "mysecret123",
"events": ["game.added"]
}'
```
---
## PATCH /api/webhooks/{id}
Update an existing webhook. At least one field must be provided.
### Authentication
Bearer token required.
### Path Parameters
| Name | Type | Description |
|------|------|-------------|
| id | integer | Webhook ID |
### Request Body
All fields optional. Include only the fields to update.
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| name | string | No | Display name |
| url | string | No | Callback URL (must be valid) |
| secret | string | No | New secret |
| events | array | No | Event types (must be array) |
| enabled | boolean | No | Enable or disable the webhook |
```json
{
"name": "Discord Bot Updated",
"url": "https://example.com/webhook-v2",
"enabled": true
}
```
### Response
**200 OK**
```json
{
"id": 5,
"name": "Discord Bot Updated",
"url": "https://example.com/webhook-v2",
"events": ["game.added"],
"enabled": true,
"created_at": "2024-01-15T12:00:00.000Z",
"message": "Webhook updated successfully"
}
```
### Error Responses
| Status | Body | When |
|--------|------|------|
| 400 | `{ "error": "No fields to update" }` | No fields in body |
| 400 | `{ "error": "Invalid URL format" }` | Invalid URL |
| 400 | `{ "error": "events must be an array" }` | `events` not an array |
| 404 | `{ "error": "Webhook not found" }` | Invalid ID |
### Example
```bash
curl -X PATCH "http://localhost:5000/api/webhooks/5" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Discord Bot Updated", "enabled": true}'
```
---
## DELETE /api/webhooks/{id}
Delete a webhook. Cascades to `webhook_logs` (logs are deleted).
### Authentication
Bearer token required.
### Path Parameters
| Name | Type | Description |
|------|------|-------------|
| id | integer | Webhook ID |
### Response
**200 OK**
```json
{
"message": "Webhook deleted successfully",
"webhookId": 5
}
```
### Error Responses
| Status | Body | When |
|--------|------|------|
| 404 | `{ "error": "Webhook not found" }` | Invalid ID |
### Example
```bash
curl -X DELETE "http://localhost:5000/api/webhooks/5" \
-H "Authorization: Bearer $TOKEN"
```
---
## POST /api/webhooks/test/{id}
Send a test `game.added` event with dummy data to the webhook URL. Delivery runs asynchronously; check `webhook_logs` for status.
### Authentication
Bearer token required.
### Path Parameters
| Name | Type | Description |
|------|------|-------------|
| id | integer | Webhook ID |
### Response
**200 OK**
```json
{
"message": "Test webhook sent",
"note": "Check webhook_logs table for delivery status"
}
```
### Error Responses
| Status | Body | When |
|--------|------|------|
| 404 | `{ "error": "Webhook not found" }` | Invalid ID |
### Example
```bash
curl -X POST "http://localhost:5000/api/webhooks/test/5" \
-H "Authorization: Bearer $TOKEN"
```
---
## GET /api/webhooks/{id}/logs
Get delivery logs for a webhook. Payload is parsed from JSON string to object.
### Authentication
Bearer token required.
### Path Parameters
| Name | Type | Description |
|------|------|-------------|
| id | integer | Webhook ID |
### Query Parameters
| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| limit | query | integer | No | Max number of logs (default: 50) |
### Response
**200 OK**
```json
[
{
"id": 1,
"webhook_id": 5,
"event_type": "game.added",
"payload": {
"session": { "id": 3, "is_active": true, "games_played": 2 },
"game": {
"id": 42,
"title": "Quiplash 3",
"pack_name": "Jackbox Party Pack 7",
"min_players": 3,
"max_players": 8,
"manually_added": false
}
},
"response_status": 200,
"error_message": null,
"created_at": "2024-01-15T12:00:00.000Z"
}
]
```
### Error Responses
| Status | Body | When |
|--------|------|------|
| 500 | `{ "error": "..." }` | Server error |
### Example
```bash
curl "http://localhost:5000/api/webhooks/5/logs?limit=20" \
-H "Authorization: Bearer $TOKEN"
```

View File

@@ -0,0 +1,316 @@
# Getting Started
A narrative walkthrough of the minimum viable integration path. Use this guide to go from zero to a completed game night session using the Jackbox Game Picker API.
**Prerequisites:** API running locally (`http://localhost:5000`), admin key set via `ADMIN_KEY` environment variable.
---
## 1. Health Check
Verify the API is running before anything else. The health endpoint requires no authentication.
**Why:** Quick sanity check. If this fails, nothing else will work.
```bash
curl http://localhost:5000/health
```
**Sample response (200 OK):**
```json
{
"status": "ok",
"message": "Jackbox Game Picker API is running"
}
```
---
## 2. Authenticate
Exchange your admin key for a JWT. You'll use this token for all write operations (creating sessions, adding games, closing sessions).
**Why:** Creating sessions, adding games to them, and closing sessions require authentication. The picker and game listings are public, but session management is not.
See [Auth endpoints](../endpoints/auth.md) for full details.
```bash
TOKEN=$(curl -s -X POST http://localhost:5000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"key": "your-admin-key"}' | jq -r '.token')
```
Or capture the full response:
```bash
curl -X POST http://localhost:5000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"key": "your-admin-key"}'
```
**Sample response (200 OK):**
```json
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYWRtaW4iLCJ0aW1lc3RhbXAiOjE3MTAwMDAwMDAwLCJpYXQiOjE3MTAwMDAwMDB9.abc123",
"message": "Authentication successful",
"expiresIn": "24h"
}
```
Store `token` in `$TOKEN` for the remaining steps. Tokens expire after 24 hours.
---
## 3. Browse Games
List available games. Use query parameters to narrow the catalog—for example, `playerCount` filters to games that support that many players.
**Why:** Know what's in the catalog before you pick. Filtering by player count ensures you only see games you can actually play.
See [Games endpoints](../endpoints/games.md) for all filters.
```bash
curl "http://localhost:5000/api/games?playerCount=6"
```
**Sample response (200 OK):**
```json
[
{
"id": 1,
"pack_name": "Jackbox Party Pack 7",
"title": "Quiplash 3",
"min_players": 3,
"max_players": 8,
"length_minutes": 15,
"has_audience": 1,
"family_friendly": 0,
"game_type": "Writing",
"secondary_type": null,
"play_count": 0,
"popularity_score": 0,
"enabled": 1,
"favor_bias": 0,
"created_at": "2024-01-15T12:00:00.000Z"
},
{
"id": 2,
"pack_name": "Jackbox Party Pack 7",
"title": "The Devils and the Details",
"min_players": 3,
"max_players": 7,
"length_minutes": 25,
"has_audience": 1,
"family_friendly": 0,
"game_type": "Strategy",
"secondary_type": null,
"play_count": 0,
"popularity_score": 0,
"enabled": 1,
"favor_bias": 0,
"created_at": "2024-01-15T12:00:00.000Z"
}
]
```
---
## 4. Pick a Game
Get a weighted random game based on your filters. The picker considers favor/disfavor bias and can avoid recently played games when a session is provided.
**Why:** Instead of manually choosing, let the API pick a game that fits your player count, length, and other preferences. Use the same filters you used to browse.
See [Picker endpoint](../endpoints/picker.md) for all options.
```bash
curl -X POST http://localhost:5000/api/pick \
-H "Content-Type: application/json" \
-d '{"playerCount": 6}'
```
**Sample response (200 OK):**
```json
{
"game": {
"id": 1,
"pack_name": "Jackbox Party Pack 7",
"title": "Quiplash 3",
"min_players": 3,
"max_players": 8,
"length_minutes": 15,
"has_audience": 1,
"family_friendly": 0,
"game_type": "Writing",
"secondary_type": null,
"play_count": 0,
"popularity_score": 0,
"enabled": 1,
"favor_bias": 0,
"created_at": "2024-01-15T12:00:00.000Z"
},
"poolSize": 12,
"totalEnabled": 17
}
```
Save the `game.id` (e.g. `1`) — you'll use it when adding the game to the session.
---
## 5. Start a Session
Create a new gaming session. Only one session can be active at a time. Use notes to label the night (e.g., "Friday game night").
**Why:** Sessions track which games you played, when, and support voting and room monitoring. Starting a session marks the beginning of your game night.
See [Sessions endpoints](../endpoints/sessions.md) for full details.
```bash
curl -X POST http://localhost:5000/api/sessions \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"notes": "Friday game night"}'
```
**Sample response (201 Created):**
```json
{
"id": 5,
"notes": "Friday game night",
"is_active": 1,
"created_at": "2026-03-15T19:00:00.000Z",
"closed_at": null
}
```
Save the `id` (e.g. `5`) — you'll use it to add games and close the session.
---
## 6. Add the Picked Game
Add the game you picked (step 4) to the session you created (step 5). You can optionally pass a room code once the game is running.
**Why:** Adding a game to the session records that you played it, increments play counts, and enables voting and room monitoring. Use `game_id` from the pick response.
```bash
curl -X POST "http://localhost:5000/api/sessions/5/games" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"game_id": 1, "room_code": "ABCD"}'
```
Replace `5` with your session ID and `1` with the `game.id` from the pick response.
**Sample response (201 Created):**
```json
{
"id": 14,
"session_id": 5,
"game_id": 1,
"manually_added": 0,
"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
}
```
---
## 7. Close the Session
When the game night is over, close the session. Any games still marked `playing` are automatically marked `played`.
**Why:** Closing the session finalizes it, frees the "active session" slot for the next night, and triggers any end-of-session webhooks or WebSocket events.
```bash
curl -X POST "http://localhost:5000/api/sessions/5/close" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"notes": "Great session!"}'
```
Replace `5` with your session ID.
**Sample response (200 OK):**
```json
{
"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": 1
}
```
---
## Quick Reference
| Step | Endpoint | Auth |
|------|----------|------|
| 1 | `GET /health` | No |
| 2 | `POST /api/auth/login` | No |
| 3 | `GET /api/games?playerCount=6` | No |
| 4 | `POST /api/pick` | No |
| 5 | `POST /api/sessions` | Bearer |
| 6 | `POST /api/sessions/{id}/games` | Bearer |
| 7 | `POST /api/sessions/{id}/close` | Bearer |
---
## Full Copy-Paste Flow
```bash
# 1. Health check
curl http://localhost:5000/health
# 2. Get token (replace with your actual admin key)
TOKEN=$(curl -s -X POST http://localhost:5000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"key": "your-admin-key"}' | jq -r '.token')
# 3. Browse games for 6 players
curl "http://localhost:5000/api/games?playerCount=6"
# 4. Pick a game for 6 players
PICK=$(curl -s -X POST http://localhost:5000/api/pick \
-H "Content-Type: application/json" \
-d '{"playerCount": 6}')
GAME_ID=$(echo $PICK | jq -r '.game.id')
# 5. Start session
SESSION=$(curl -s -X POST http://localhost:5000/api/sessions \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"notes": "Friday game night"}')
SESSION_ID=$(echo $SESSION | jq -r '.id')
# 6. Add picked game to session
curl -X POST "http://localhost:5000/api/sessions/$SESSION_ID/games" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"game_id\": $GAME_ID, \"room_code\": \"ABCD\"}"
# 7. Close session when done
curl -X POST "http://localhost:5000/api/sessions/$SESSION_ID/close" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"notes": "Great session!"}'
```
This assumes `jq` is installed for JSON parsing. Without it, extract IDs manually from the JSON responses.

View File

@@ -0,0 +1,287 @@
# Session Lifecycle Guide
This guide walks through the full lifecycle of a Jackbox gaming session—from creation through closing and deletion—with narrative explanations, behavior notes, and curl examples.
**Base URL:** `http://localhost:5000`
**Authentication:** All write operations require a Bearer token. Set `TOKEN` in your shell and use `-H "Authorization: Bearer $TOKEN"` in curl examples.
---
## 1. Creating a Session
Only **one active session** can exist at a time. If an active session already exists, you must close it before creating a new one.
Notes are optional; they help you remember what a session was for (e.g., "Friday game night", "Birthday party").
Creating a session triggers a **`session.started`** WebSocket event broadcast to all authenticated clients. See [Real-time updates via WebSocket](#9-real-time-updates-via-websocket) for details.
**Endpoint:** [POST /api/sessions](../endpoints/sessions.md#post-apisessions)
```bash
# Create a session with notes
curl -X POST "http://localhost:5000/api/sessions" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"notes": "Friday game night"}'
# Create a session without notes (body can be empty)
curl -X POST "http://localhost:5000/api/sessions" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
**Response (201 Created):**
```json
{
"id": 5,
"notes": "Friday game night",
"is_active": 1,
"created_at": "2026-03-15T19:00:00.000Z",
"closed_at": null
}
```
If an active session already exists, you receive `400` with a message like `"An active session already exists. Please close it before creating a new one."` and an `activeSessionId` in the response.
---
## 2. Adding Games
You can add games in two ways: via the **picker** (weighted random selection) or **manually** by specifying a game ID.
### Via the Picker
First, use [POST /api/pick](../endpoints/picker.md#post-apipick) to select a game with filters and repeat avoidance. Then add that game to the session.
```bash
# 1. Pick a game (optionally filter by player count, session for repeat avoidance)
GAME=$(curl -s -X POST "http://localhost:5000/api/pick" \
-H "Content-Type: application/json" \
-d '{"playerCount": 6, "sessionId": 5}' | jq -r '.game.id')
# 2. Add the picked game to the session
curl -X POST "http://localhost:5000/api/sessions/5/games" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"game_id\": $GAME, \"manually_added\": false}"
```
### Manual Addition
Add a game directly by its `game_id` (from the games catalog):
```bash
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"}'
```
**Endpoint:** [POST /api/sessions/{id}/games](../endpoints/sessions.md#post-apisessionsidgames)
### Side Effects of Adding a Game
When you add a game to an active session, several things happen automatically:
1. **Previous `playing` games** are auto-transitioned to **`played`**. At most one game is `playing` at a time.
2. The game's **`play_count`** is incremented in the catalog.
3. The **`game.added`** webhook is fired (if you have webhooks configured) and a **`game.added`** WebSocket event is broadcast to session subscribers.
4. If you provide a **`room_code`**, the room monitor is **auto-started** for player count tracking.
Newly added games start with status **`playing`**.
---
## 3. Tracking Game Status
Each game in a session has a status: **`playing`**, **`played`**, or **`skipped`**.
| Status | Meaning |
|----------|-------------------------------------------|
| `playing`| Currently being played (at most one at a time) |
| `played` | Finished playing |
| `skipped`| Skipped (e.g., technical issues); stays skipped |
**Behavior:** When you change a game's status to **`playing`**, any other games with status `playing` are automatically set to **`played`**. Skipped games are never auto-transitioned; they remain `skipped`.
**Endpoint:** [PATCH /api/sessions/{sessionId}/games/{sessionGameId}/status](../endpoints/sessions.md#patch-apisessionssessionidgamessessiongameidstatus)
**Important:** In session game sub-routes, `sessionGameId` refers to **`session_games.id`** (the row in the `session_games` table), **not** `games.id`. When listing session games with `GET /api/sessions/{id}/games`, the `id` field in each object is the `session_games.id`.
```bash
# Mark a game as played (sessionGameId 14, not game_id)
curl -X PATCH "http://localhost:5000/api/sessions/5/games/14/status" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"status": "played"}'
# Mark a game as playing (others playing → played)
curl -X PATCH "http://localhost:5000/api/sessions/5/games/14/status" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"status": "playing"}'
# Mark a game as skipped
curl -X PATCH "http://localhost:5000/api/sessions/5/games/14/status" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"status": "skipped"}'
```
---
## 4. Room Codes
Room codes are 4-character strings used by Jackbox games for lobby entry. Valid format: exactly 4 characters, uppercase letters (AZ) and digits (09) only. Example: `ABCD`, `XY9Z`.
A room code enables **room monitoring** for player count. You can set or update it when adding a game or via a dedicated PATCH endpoint.
**Endpoint:** [PATCH /api/sessions/{sessionId}/games/{sessionGameId}/room-code](../endpoints/sessions.md#patch-apisessionssessionidgamessessiongameidroom-code)
```bash
# Set room code when adding a game
curl -X POST "http://localhost:5000/api/sessions/5/games" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"game_id": 42, "room_code": "ABCD"}'
# Update room code later
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"}'
```
---
## 5. Player Count Monitoring
For games with a room code, you can track how many players join. The room monitor polls the Jackbox lobby to detect player count changes.
- **Start monitoring:** [POST /api/sessions/{sessionId}/games/{sessionGameId}/start-player-check](../endpoints/sessions.md#post-apisessionssessionidgamessessiongameidstart-player-check)
- **Stop monitoring:** [POST /api/sessions/{sessionId}/games/{sessionGameId}/stop-player-check](../endpoints/sessions.md#post-apisessionssessionidgamessessiongameidstop-player-check)
- **Manual update:** [PATCH /api/sessions/{sessionId}/games/{sessionGameId}/player-count](../endpoints/sessions.md#patch-apisessionssessionidgamessessiongameidplayer-count)
When the player count changes (via room monitor or manual update), a **`player-count.updated`** WebSocket event is broadcast to session subscribers.
```bash
# Start room monitor (game must have a room code)
curl -X POST "http://localhost:5000/api/sessions/5/games/14/start-player-check" \
-H "Authorization: Bearer $TOKEN"
# Manually set player count
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}'
# Stop monitoring
curl -X POST "http://localhost:5000/api/sessions/5/games/14/stop-player-check" \
-H "Authorization: Bearer $TOKEN"
```
---
## 6. Closing Sessions
Closing a session marks it as inactive. The API:
1. Auto-finalizes all games with status **`playing`** to **`played`**
2. Sets `closed_at` and `is_active = 0`
3. Triggers a **`session.ended`** WebSocket broadcast to session subscribers
You can add or update session notes in the close request body.
**Endpoint:** [POST /api/sessions/{id}/close](../endpoints/sessions.md#post-apisessionsidclose)
```bash
curl -X POST "http://localhost:5000/api/sessions/5/close" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"notes": "Great session!"}'
```
**Response (200 OK):**
```json
{
"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
}
```
You cannot add games to a closed session.
---
## 7. Exporting Session Data
Export a session in two formats: **JSON** (structured) or **TXT** (human-readable).
**Endpoint:** [GET /api/sessions/{id}/export](../endpoints/sessions.md#get-apisessionsidexport)
- **JSON** (`?format=json`): Includes `session`, `games`, and `chat_logs` as structured data. Useful for archival or integrations.
- **TXT** (default): Human-readable plaintext with headers and sections.
```bash
# Export as JSON
curl -o session-5.json "http://localhost:5000/api/sessions/5/export?format=json" \
-H "Authorization: Bearer $TOKEN"
# Export as TXT (default)
curl -o session-5.txt "http://localhost:5000/api/sessions/5/export" \
-H "Authorization: Bearer $TOKEN"
```
---
## 8. Deleting Sessions
Sessions must be **closed** before deletion. Active sessions cannot be deleted.
Deletion **cascades** to related data:
- `session_games` rows are deleted
- `chat_logs` rows are deleted
**Endpoint:** [DELETE /api/sessions/{id}](../endpoints/sessions.md#delete-apisessionsid)
```bash
curl -X DELETE "http://localhost:5000/api/sessions/5" \
-H "Authorization: Bearer $TOKEN"
```
**Response (200 OK):**
```json
{
"message": "Session deleted successfully",
"sessionId": 5
}
```
---
## 9. Real-time Updates via WebSocket
The API provides real-time updates over WebSocket for session events: `session.started`, `game.added`, `session.ended`, and `player-count.updated`. Connect to `/api/sessions/live`, authenticate with your JWT, and subscribe to session IDs to receive these events without polling.
For connection setup, message types, and event payloads, see [WebSocket Protocol](../websocket.md).
---
## Quick Reference: sessionGameId vs game_id
| Context | ID meaning | Example |
|---------|------------|---------|
| `POST /api/sessions/{id}/games` body | `game_id` = catalog `games.id` | `{"game_id": 42}` |
| `GET /api/sessions/{id}/games` response `id` | `session_games.id` | Use `14` in sub-routes |
| `PATCH .../games/{sessionGameId}/status` | `sessionGameId` = `session_games.id` | `/sessions/5/games/14/status` |
When in doubt: session game sub-routes use **`session_games.id`**, not `games.id`.

View File

@@ -0,0 +1,207 @@
# Voting and Popularity
A narrative guide to how the Jackbox Game Picker handles community voting and game popularity. This system lets viewers and stream chat influence which games rise to the top—without directly controlling the random picker.
---
## 1. How Popularity Works
Every game has a **popularity score** stored in the database:
```
popularity_score = upvotes - downvotes
```
The score is computed from `upvotes` and `downvotes` and persisted per game. As votes accumulate across sessions, the score reflects community sentiment over time.
**Important:** Popularity is used for **rankings** (e.g., "top rated games" in stats) but **does not directly affect picker weights**. The random picker uses favor bias, not popularity, when selecting games.
---
## 2. Favor Bias vs Popularity
Two separate systems govern how games are treated:
| Aspect | **Favor Bias** | **Popularity** |
|--------|----------------|----------------|
| Who controls it | Admin (via API) | Community (via votes) |
| Values | `-1` (disfavor), `0` (neutral), `1` (favor) | `upvotes - downvotes` (unbounded) |
| Affects picker? | Yes — directly changes weights | No |
| Purpose | Manual curation; push/penalize specific games | Community sentiment; rankings |
**Favor bias** affects picker probability directly. Setting `favor_bias` to `1` on a game boosts its weight; `-1` reduces it. See [Games favor endpoint](../endpoints/games.md#patch-apigamesidfavor) and [Picker weighted selection](../endpoints/picker.md#weighted-selection).
**Popularity** is driven entirely by viewer votes. It surfaces in stats (e.g., `topRatedGames`) and session game lists, but the picker does not read it. These systems are independent.
---
## 3. Two Voting Mechanisms
The API supports two ways to record votes: batch chat import (after the fact) and live votes (real-time from bots).
### Chat Import (Batch, After-the-Fact)
Collect Twitch or YouTube chat logs containing `thisgame++` (upvote) and `thisgame--` (downvote), then submit them in bulk.
**Flow:**
1. Export chat logs with `username`, `message`, and `timestamp` for each message.
2. Filter or pass messages; the API parses `thisgame++` and `thisgame--` from the `message` field.
3. POST to `POST /api/sessions/{id}/chat-import` with a `chatData` array of `{ username, message, timestamp }`.
4. The API matches each votes timestamp to the game that was playing at that time (using `played_at` intervals).
5. Votes are deduplicated by SHA-256 hash of `username:message:timestamp`.
6. Response includes `votesByGame` breakdown and `debug` info (e.g., session timeline, vote matches).
See [Sessions chat-import endpoint](../endpoints/sessions.md#post-apisessionsidchat-import).
### Live Votes (Real-Time, from Bots)
A bot sends individual votes during the stream. Each vote is processed immediately.
**Flow:**
1. Bot detects `thisgame++` or `thisgame--` (or equivalent) in chat.
2. Bot sends `POST /api/votes/live` with `{ username, vote, timestamp }`.
3. `vote` must be `"up"` or `"down"`.
4. `timestamp` must be ISO 8601 (e.g., `2026-03-15T20:30:00Z`).
5. The API finds the active session and matches the vote timestamp to the game playing at that time.
6. **Deduplication:** Votes from the same username within 1 second are rejected with `409 Conflict`.
See [Votes live endpoint](../endpoints/votes.md#post-apivoteslive).
---
## 4. Timestamp Matching Explained
Games in a session have a `played_at` timestamp. A votes timestamp determines which game it belongs to.
**Rule:** A vote belongs to the game whose `played_at` is the **most recent one before** the vote timestamp.
Example session timeline:
- Game A: `played_at` 20:00
- Game B: `played_at` 20:15
- Game C: `played_at` 20:30
- Vote at 20:10 → Game A (last `played_at` before 20:10)
- Vote at 20:20 → Game B
- Vote at 20:45 → Game C (last game in session; captures all votes after it started)
The **last game** in the session captures all votes that occur after its `played_at`.
---
## 5. How Stats Reflect Popularity
`GET /api/stats` returns aggregate statistics, including:
- **mostPlayedGames** — top 10 by `play_count` (games with `play_count` > 0).
- **topRatedGames** — top 10 by `popularity_score` (games with `popularity_score` > 0).
Both are limited to the top 10 and exclude games with score/count ≤ 0. See [Stats endpoint](../endpoints/stats.md).
---
## 6. Example Requests
### Chat Import
Import a batch of chat messages for session `5`:
```bash
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"
},
{
"username": "viewer2",
"message": "thisgame--",
"timestamp": "2026-03-15T20:31:00Z"
},
{
"username": "viewer3",
"message": "thisgame++",
"timestamp": "2026-03-15T20:32:00Z"
}
]
}'
```
**Sample response (200 OK):**
```json
{
"message": "Chat log imported and processed successfully",
"messagesImported": 3,
"duplicatesSkipped": 0,
"votesProcessed": 3,
"votesByGame": {
"42": {
"title": "Quiplash 3",
"upvotes": 2,
"downvotes": 1
}
},
"debug": {
"sessionGamesTimeline": [
{
"title": "Quiplash 3",
"played_at": "2026-03-15T20:00:00.000Z",
"played_at_ms": 1742068800000
}
],
"voteMatches": []
}
}
```
### Live Vote
Submit a single live vote (requires active session):
```bash
curl -X POST "http://localhost:5000/api/votes/live" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"username": "viewer123",
"vote": "up",
"timestamp": "2026-03-15T20:30:00Z"
}'
```
**Sample response (200 OK):**
```json
{
"success": true,
"message": "Vote recorded successfully",
"session": { "id": 3, "games_played": 5 },
"game": {
"id": 42,
"title": "Quiplash 3",
"upvotes": 11,
"downvotes": 2,
"popularity_score": 9
},
"vote": {
"username": "viewer123",
"type": "up",
"timestamp": "2026-03-15T20:30:00Z"
}
}
```
---
## Related Documentation
- [Sessions endpoints](../endpoints/sessions.md) — chat import, session games, `played_at`
- [Votes endpoints](../endpoints/votes.md) — live votes, deduplication, errors
- [Stats endpoints](../endpoints/stats.md) — `mostPlayedGames`, `topRatedGames`
- [Picker endpoints](../endpoints/picker.md) — weighted selection, favor bias (no popularity)
- [Games endpoints](../endpoints/games.md) — favor bias per game and pack

View File

@@ -0,0 +1,216 @@
# Webhooks and Events
A narrative guide to the Jackbox Game Picker event notification system: webhooks (HTTP callbacks) and WebSocket (persistent real-time connections). Both deliver event data about session and game activity.
---
## 1. Two Notification Systems
The API offers two complementary ways to receive event notifications:
| System | Model | Best for |
|--------|-------|----------|
| **Webhooks** | HTTP POST callbacks to your URL | Server-to-server, external integrations |
| **WebSocket** | Persistent bidirectional connection | Real-time UIs, dashboards, live tools |
Both systems emit the same kinds of events (e.g. `game.added`) but differ in how they deliver them.
---
## 2. When to Use Which
### Use Webhooks when:
- **Server-to-server** — Discord bots, Slack, logging pipelines, external APIs
- **Stateless** — Your endpoint receives a POST, processes it, and returns. No long-lived connection
- **Behind firewalls** — Your server can receive HTTP but may not hold open WebSocket connections
- **Async delivery** — Youre fine with HTTP round-trip latency and want delivery logged and auditable
### Use WebSocket when:
- **Real-time UI** — Dashboards, admin panels, live session viewers
- **Instant updates** — You need push-style notifications with minimal latency
- **Persistent connection** — Your app keeps a live connection and subscribes to specific sessions
- **Best-effort is fine** — WebSocket is push-only; theres no built-in delivery log for events
---
## 3. Webhook Setup
Webhooks are registered via the REST API. See [Webhooks endpoints](../endpoints/webhooks.md) for full CRUD details.
### Create a Webhook
`POST /api/webhooks` with:
- `name` — Display name (e.g. `"Discord Bot"`)
- `url` — Callback URL (must be a valid HTTP/HTTPS URL)
- `secret` — Shared secret for signing payloads (HMAC-SHA256)
- `events` — Array of event types that trigger this webhook (e.g. `["game.added"]`)
**Example:**
```bash
curl -X POST "http://localhost:5000/api/webhooks" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Discord Bot",
"url": "https://my-server.com/webhooks/jackbox",
"secret": "mysecret123",
"events": ["game.added"]
}'
```
The `events` array defines which events fire this webhook. Currently, the codebase triggers webhooks for **`game.added`** when a game is added to a session. The `triggerWebhook` function in `backend/utils/webhooks.js` is invoked from `sessions.js` on that event.
### Update, Enable/Disable, Delete
- **Update:** `PATCH /api/webhooks/{id}` — Change `name`, `url`, `secret`, `events`, or `enabled`
- **Disable:** `PATCH /api/webhooks/{id}` with `"enabled": false` — Stops delivery without deleting config
- **Delete:** `DELETE /api/webhooks/{id}` — Removes webhook and its logs
---
## 4. Webhook Delivery
### How it works
When an event occurs (e.g. a game is added), the server:
1. Finds all enabled webhooks subscribed to that event
2. Sends an async HTTP POST to each webhook URL
3. Logs each delivery attempt in `webhook_logs` (status, error, payload)
### Payload format
Each POST body is JSON:
```json
{
"event": "game.added",
"timestamp": "2026-03-15T20:30:00.000Z",
"data": {
"session": { "id": 3, "is_active": true, "games_played": 2 },
"game": {
"id": 42,
"title": "Quiplash 3",
"pack_name": "Jackbox Party Pack 7",
"min_players": 3,
"max_players": 8,
"manually_added": false
}
}
}
```
Headers include:
- `Content-Type: application/json`
- `X-Webhook-Event: game.added`
- `X-Webhook-Signature: sha256=<hmac>` — Use your `secret` to verify the payload
### View delivery logs
`GET /api/webhooks/{id}/logs` returns recent delivery attempts (status, error message, payload).
### Test a webhook
`POST /api/webhooks/test/{id}` sends a dummy `game.added` event to the webhook URL. Delivery runs asynchronously; check logs for status.
---
## 5. WebSocket Events
The WebSocket server runs at `/api/sessions/live` on the same host and port as the HTTP API. See [WebSocket protocol](../websocket.md) for connection, authentication, and subscription details.
### Event types and audience
| Event | Broadcast to | Triggered by |
|-------|--------------|--------------|
| `session.started` | All authenticated clients | `POST /api/sessions` |
| `game.added` | Session subscribers | `POST /api/sessions/{id}/games` |
| `session.ended` | Session subscribers | `POST /api/sessions/{id}/close` |
| `player-count.updated` | Session subscribers | `PATCH /api/sessions/{sessionId}/games/{sessionGameId}/player-count` |
`session.started` goes to every authenticated client. The others go only to clients that have subscribed to the relevant session via `{ "type": "subscribe", "sessionId": 3 }`.
### Envelope format
All events use this envelope:
```json
{
"type": "<event-type>",
"timestamp": "2026-03-15T20:30:00.000Z",
"data": { ... }
}
```
`data` contains event-specific fields (session, game, player count, etc.) as described in [WebSocket protocol](../websocket.md).
---
## 6. Comparison
| Feature | Webhooks | WebSocket |
|---------|----------|-----------|
| **Connection** | Stateless HTTP | Persistent |
| **Auth** | Secret in config | JWT per connection |
| **Events** | `game.added` | `session.started`, `game.added`, `session.ended`, `player-count.updated` |
| **Latency** | Higher (HTTP round trip) | Lower (push) |
| **Reliability** | Logged, auditable | Best-effort |
---
## 7. Example: Discord Bot
Use a webhook to post game additions to a Discord channel. Youll need:
1. A webhook created in the Game Picker API pointing to your server
2. A small server that receives the webhook and forwards to Discords Incoming Webhook
**Webhook receiver (Node.js):**
```javascript
const crypto = require('crypto');
app.post('/webhooks/jackbox', express.json(), (req, res) => {
const signature = req.headers['x-webhook-signature'];
const payload = JSON.stringify(req.body);
// Verify HMAC-SHA256 using your webhook secret
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(payload)
.digest('hex');
if (signature !== expected) {
return res.status(401).send('Invalid signature');
}
if (req.body.event === 'game.added') {
const { session, game } = req.body.data;
const discordPayload = {
content: `🎮 **${game.title}** added to session #${session.id} (${game.min_players}-${game.max_players} players)`
};
fetch(process.env.DISCORD_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(discordPayload)
}).catch(err => console.error('Discord post failed:', err));
}
res.status(200).send('OK');
});
```
Register the Game Picker webhook with your servers URL (e.g. `https://my-bot.example.com/webhooks/jackbox`), set `events` to `["game.added"]`, and use the same `secret` in your servers `WEBHOOK_SECRET`.
---
## Cross-references
- **[Webhooks endpoints](../endpoints/webhooks.md)** — Full CRUD, request/response schemas, errors
- **[WebSocket protocol](../websocket.md)** — Connection, auth, subscriptions, event payloads

1693
docs/api/openapi.yaml Normal file

File diff suppressed because it is too large Load Diff

399
docs/api/websocket.md Normal file
View File

@@ -0,0 +1,399 @@
# WebSocket Protocol
## 1. Overview
The WebSocket API provides real-time updates for Jackbox gaming sessions. Use it to:
- Receive notifications when sessions start, end, or when games are added
- Track player counts as they are updated
- 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.
---
## 2. Connection Setup
**URL:** `ws://host:port/api/sessions/live`
- Use `ws://` for HTTP and `wss://` for HTTPS
- No query parameters are required
- Connection can be established without authentication (auth happens via a message after connect)
**JavaScript example:**
```javascript
const host = 'localhost';
const port = 5000;
const protocol = 'ws';
const ws = new WebSocket(`${protocol}://${host}:${port}/api/sessions/live`);
ws.onopen = () => {
console.log('Connected');
};
```
---
## 3. Authentication
Authentication is required for subscribing to sessions and for receiving most events. Send your JWT token in an `auth` message after connecting.
**Send (client → server):**
```json
{ "type": "auth", "token": "<jwt>" }
```
**Success response:**
```json
{ "type": "auth_success", "message": "Authenticated successfully" }
```
**Failure responses:**
```json
{ "type": "auth_error", "message": "Invalid or expired token" }
```
```json
{ "type": "auth_error", "message": "Token required" }
```
**JavaScript example:**
```javascript
// After opening the connection...
ws.send(JSON.stringify({
type: 'auth',
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
}));
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'auth_success') {
console.log('Authenticated');
} else if (msg.type === 'auth_error') {
console.error('Auth failed:', msg.message);
}
};
```
Obtain a JWT by calling `POST /api/auth/login` with your admin key.
---
## 4. Message Types — Client to Server
| Type | Required Fields | Description |
|-------------|-----------------|--------------------------------------|
| `auth` | `token` | Authenticate with a JWT |
| `subscribe` | `sessionId` | Subscribe to a session's events |
| `unsubscribe`| `sessionId` | Unsubscribe from a session |
| `ping` | — | Heartbeat; server responds with `pong` |
### auth
```json
{ "type": "auth", "token": "<jwt>" }
```
### subscribe
Must be authenticated. You can subscribe to multiple sessions.
```json
{ "type": "subscribe", "sessionId": 3 }
```
### unsubscribe
Must be authenticated.
```json
{ "type": "unsubscribe", "sessionId": 3 }
```
### ping
```json
{ "type": "ping" }
```
---
## 5. Message Types — Server to Client
| Type | Description |
|---------------|------------------------------------------|
| `auth_success`| Authentication succeeded |
| `auth_error` | Authentication failed |
| `subscribed` | Successfully subscribed to a session |
| `unsubscribed`| Successfully unsubscribed from a session |
| `pong` | Response to client `ping` |
| `error` | General error (e.g., not authenticated) |
| `session.started` | New session created (broadcast to all authenticated clients) |
| `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) |
---
## 6. Event Reference
All server-sent events use this envelope:
```json
{
"type": "<event-type>",
"timestamp": "2026-03-15T20:30:00.000Z",
"data": { ... }
}
```
### session.started
- **Broadcast to:** All authenticated clients (not session-specific)
- **Triggered by:** `POST /api/sessions` (creating a new session)
**Data:**
```json
{
"session": {
"id": 3,
"is_active": 1,
"created_at": "2026-03-15T20:00:00",
"notes": "Friday game night"
}
}
```
### game.added
- **Broadcast to:** Clients subscribed to the session
- **Triggered by:** `POST /api/sessions/{id}/games` (adding a game)
**Data:**
```json
{
"session": {
"id": 3,
"is_active": true,
"games_played": 5
},
"game": {
"id": 42,
"title": "Quiplash 3",
"pack_name": "Jackbox Party Pack 7",
"min_players": 3,
"max_players": 8,
"manually_added": false,
"room_code": "ABCD"
}
}
```
### session.ended
- **Broadcast to:** Clients subscribed to the session
- **Triggered by:** `POST /api/sessions/{id}/close` (closing a session)
**Data:**
```json
{
"session": {
"id": 3,
"is_active": 0,
"games_played": 8
}
}
```
### player-count.updated
- **Broadcast to:** Clients subscribed to the session
- **Triggered by:** `PATCH /api/sessions/{sessionId}/games/{sessionGameId}/player-count`
**Data:**
```json
{
"sessionId": "3",
"gameId": "7",
"playerCount": 6,
"status": "completed"
}
```
---
## 7. Error Handling
| Type | Message | When |
|--------------|----------------------------------------|-----------------------------------------|
| `error` | `Not authenticated` | subscribe/unsubscribe without auth |
| `error` | `Session ID required` | subscribe without `sessionId` |
| `error` | `Unknown message type: foo` | Unknown `type` in client message |
| `error` | `Invalid message format` | Unparseable or non-JSON message |
| `auth_error` | `Token required` | auth without token |
| `auth_error` | `Invalid or expired token` | auth with invalid/expired JWT |
---
## 8. Heartbeat and Timeout
- **Client → Server:** Send `{ "type": "ping" }` periodically
- **Server → Client:** Responds with `{ "type": "pong" }`
- **Timeout:** If no ping is received for **60 seconds**, the server terminates the connection
- **Server check:** The server checks for stale connections every **30 seconds**
Implement a heartbeat on the client to keep the connection alive:
```javascript
let pingInterval;
function startHeartbeat() {
pingInterval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30000); // every 30 seconds
}
ws.onopen = () => {
startHeartbeat();
};
ws.onclose = () => {
clearInterval(pingInterval);
};
```
---
## 9. Reconnection
The server does **not** maintain state across disconnects. After reconnecting:
1. **Re-authenticate** with an `auth` message
2. **Re-subscribe** to any sessions you were tracking
Implement exponential backoff for reconnection attempts:
```javascript
let reconnectAttempts = 0;
const maxReconnectAttempts = 10;
const baseDelay = 1000;
function connect() {
const ws = new WebSocket('ws://localhost:5000/api/sessions/live');
ws.onopen = () => {
reconnectAttempts = 0;
ws.send(JSON.stringify({ type: 'auth', token: jwt }));
// After auth_success, re-subscribe to sessions...
};
ws.onclose = () => {
if (reconnectAttempts < maxReconnectAttempts) {
const delay = Math.min(baseDelay * Math.pow(2, reconnectAttempts), 60000);
reconnectAttempts++;
setTimeout(connect, delay);
}
};
}
connect();
```
---
## 10. Complete Example
Full session lifecycle from connect to disconnect:
```javascript
const JWT = 'your-jwt-token';
const WS_URL = 'ws://localhost:5000/api/sessions/live';
const ws = new WebSocket(WS_URL);
let pingInterval;
let subscribedSessions = new Set();
function send(msg) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(msg));
}
}
ws.onopen = () => {
console.log('Connected');
send({ type: 'auth', token: JWT });
pingInterval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
send({ type: 'ping' });
}
}, 30000);
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
switch (msg.type) {
case 'auth_success':
console.log('Authenticated');
send({ type: 'subscribe', sessionId: 3 });
break;
case 'auth_error':
console.error('Auth failed:', msg.message);
break;
case 'subscribed':
subscribedSessions.add(msg.sessionId);
console.log('Subscribed to session', msg.sessionId);
break;
case 'unsubscribed':
subscribedSessions.delete(msg.sessionId);
console.log('Unsubscribed from session', msg.sessionId);
break;
case 'pong':
// Heartbeat acknowledged
break;
case 'session.started':
console.log('New session:', msg.data.session);
break;
case 'game.added':
console.log('Game added:', msg.data.game.title, 'to session', msg.data.session.id);
break;
case 'session.ended':
console.log('Session ended:', msg.data.session.id);
subscribedSessions.delete(msg.data.session.id);
break;
case 'player-count.updated':
console.log('Player count:', msg.data.playerCount, 'for game', msg.data.gameId);
break;
case 'error':
case 'auth_error':
console.error('Error:', msg.message);
break;
default:
console.log('Unknown message:', msg);
}
};
ws.onerror = (err) => console.error('WebSocket error:', err);
ws.onclose = () => {
clearInterval(pingInterval);
console.log('Disconnected');
};
// Later: unsubscribe and close
function disconnect() {
subscribedSessions.forEach((sessionId) => {
send({ type: 'unsubscribe', sessionId });
});
ws.close();
}
```

View File

@@ -693,12 +693,24 @@ curl -X GET "http://localhost:5000/api/webhooks/1/logs" \
- `game.added` - Triggered when a game is added to an active session. Sent to clients subscribed to that session. Includes `room_code`.
- `session.started` - Triggered when a new session is created. Broadcast to **all** authenticated clients (no subscription required).
- `session.ended` - Triggered when a session is closed. Sent to clients subscribed to that session.
- `game.started` - Triggered when the Jackbox room becomes locked (gameplay has begun). Detected by polling the Jackbox REST API every 10 seconds. Sent to clients subscribed to that session. Includes `roomCode` and `maxPlayers`.
- `audience.joined` - Triggered when the app successfully joins a Jackbox room as an audience member. Sent to clients subscribed to that session. This confirms the room code is valid and the game is being monitored.
- `game.started` - Triggered when the Jackbox room becomes locked (gameplay has begun). Detected by polling the Jackbox REST API every 10 seconds via the room monitor. Sent to clients subscribed to that session. Includes `roomCode` and `maxPlayers`.
- `audience.joined` - Triggered when the app successfully joins a Jackbox room as an audience member for player count tracking. Sent to clients subscribed to that session.
- `player-count.updated` - Triggered when the player count for a game is updated. Sent to clients subscribed to that session.
> **Tip:** To receive `session.started` events, your bot only needs to authenticate — no subscription is needed. Once you receive a `session.started` event, subscribe to the new session ID to receive `game.added` and `session.ended` events for it.
### Event Lifecycle (for a game with room code)
When a game is added with a room code, events fire in this order:
1. **`game.added`** — Game added to the session (immediate).
2. **`game.started`** — Jackbox room becomes locked, gameplay has begun. Detected by a room monitor that polls the Jackbox REST API every 10 seconds. This is independent of the player count system.
3. **`audience.joined`** — The player count bot successfully joined the Jackbox room as an audience member (seconds after `game.started`).
4. **`player-count.updated`** (status: `checking`) — Player count data received from the game's WebSocket traffic (ongoing).
5. **`player-count.updated`** (status: `completed`) — Game ended, final player count confirmed.
Room monitoring and player counting are separate systems. The room monitor (`room-monitor.js`) handles steps 1-2 and then hands off to the player count checker (`player-count-checker.js`) for steps 3-5.
More events may be added in the future (e.g., `vote.recorded`).
---

View File

@@ -0,0 +1,130 @@
# API Documentation Design
**Date:** 2026-03-15
**Status:** Approved
## Goal
Create comprehensive, accurate API documentation for the Jackbox Game Picker by reading the source code directly — not relying on existing docs which may be stale or incorrect. The documentation serves both internal maintainers and external integrators (bot developers, extension authors, etc.).
## Scope
- All 41 REST/HTTP endpoints across 7 route groups (Auth, Games, Sessions, Picker, Stats, Votes, Webhooks) plus the health check
- WebSocket protocol at `/api/sessions/live` (auth, subscriptions, event broadcasting)
- Does NOT cover: Chrome extension internals, deployment/Docker setup, frontend
## Approach
**OpenAPI-first with generated Markdown** (Approach A from brainstorming).
- `openapi.yaml` (OpenAPI 3.1) is the single source of truth for REST endpoints
- Human-readable Markdown endpoint docs are derived from the spec and enriched with guide-style prose, curl examples, and workflow explanations
- WebSocket protocol documented separately in Markdown (outside OpenAPI's scope)
- Existing `docs/` files archived to `docs/archive/`
## File Structure
```
docs/
├── archive/ # Old docs preserved here
│ ├── API_QUICK_REFERENCE.md
│ ├── BOT_INTEGRATION.md
│ ├── SESSION_END_QUICK_START.md
│ ├── SESSION_END_WEBSOCKET.md
│ ├── SESSION_START_WEBSOCKET.md
│ ├── WEBSOCKET_FLOW_DIAGRAM.md
│ ├── WEBSOCKET_SUBSCRIPTION_GUIDE.md
│ ├── WEBSOCKET_TESTING.md
│ └── todos.md
├── api/
│ ├── openapi.yaml # OpenAPI 3.1 spec (source of truth)
│ ├── README.md # API overview, auth, base URL, error conventions
│ ├── endpoints/
│ │ ├── auth.md
│ │ ├── games.md
│ │ ├── sessions.md
│ │ ├── picker.md
│ │ ├── stats.md
│ │ ├── votes.md
│ │ └── webhooks.md
│ ├── websocket.md # WebSocket protocol documentation
│ └── guides/
│ ├── getting-started.md # Quick start: auth, pick a game, run a session
│ ├── session-lifecycle.md # Sessions end-to-end
│ ├── voting-and-popularity.md # Chat import, live votes, popularity scoring
│ └── webhooks-and-events.md # Webhooks + WS event system
└── plans/
```
## OpenAPI Spec Design
### Info & Servers
- Title: "Jackbox Game Picker API"
- Servers: local dev (`http://localhost:5000`), Docker proxy (`http://localhost:3000/api`)
### Security
- `bearerAuth` scheme (JWT via `Authorization: Bearer <token>`)
- Applied per-operation; public endpoints explicitly marked
### Tags
Auth, Games, Sessions, Picker, Stats, Votes, Webhooks
### Schemas (components)
- `Game`, `Session`, `SessionGame`, `Pack`, `PackMeta`
- `Webhook`, `WebhookLog`
- `ChatMessage`, `LiveVote`
- `Error` (reusable error response)
- Enums: `status` (playing/played/skipped), `vote_type` (up/down), `favor_bias` (-1/0/1), `drawing` (only/exclude), `length` (short/medium/long)
### Per-operation
Each path operation includes: `operationId`, `summary`, `description`, `parameters`, `requestBody`, `responses` (success + all documented error codes)
## Markdown Endpoint Template
Each file in `docs/api/endpoints/` follows:
1. **Header:** Resource overview, what it represents, common use cases
2. **Summary table:** Method | Path | Auth | Description
3. **Per-endpoint sections:**
- Description and when to use it
- Authentication requirement
- Parameters table (Name | In | Type | Required | Description)
- Request body (JSON schema with field descriptions)
- Success response (JSON example with annotations)
- Error responses table (Status | Body | When)
- curl example + sample response
## WebSocket Documentation Structure
`docs/api/websocket.md` covers:
- Connection URL and setup
- Authentication flow (send `auth` message with JWT)
- Client-to-server message types: `auth`, `subscribe`, `unsubscribe`, `ping`
- Server-to-client message types: `auth_success`, `subscribed`, `unsubscribed`, `pong`, `session.started`, `game.added`, `session.ended`, `player-count.updated`, `error`, `auth_error`
- Subscription model (per-session)
- Event payloads with full JSON examples
- Heartbeat/timeout (60s) and reconnection guidance
- Complete session lifecycle example
## Guide Documents
Each guide uses narrative prose connecting endpoints into workflows:
- **getting-started.md:** Authenticate, browse games, pick a game, start a session — minimum viable integration path
- **session-lifecycle.md:** Create session → add games → track status → room codes → player counts → close session
- **voting-and-popularity.md:** How `popularity_score`, `upvotes`, `downvotes` work; chat import flow; live vote endpoint; how voting affects the picker
- **webhooks-and-events.md:** Create/manage webhooks, event types, delivery logs, relationship between webhook events and WebSocket events
## Maintenance Strategy
- `openapi.yaml` is the source of truth for REST endpoints
- When endpoints change: update spec first, then update Markdown
- WebSocket and guide docs are maintained manually
- No build-time generation tooling — Markdown committed directly
## Validation Plan
After writing, cross-reference:
1. Every route file in `backend/routes/` against the spec — no endpoints missed
2. Request/response shapes against database schema (`backend/database.js`) and route handlers
3. Auth requirements against middleware usage in each route

View File

@@ -0,0 +1,768 @@
# API Documentation Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Create comprehensive, accurate API documentation from source code — OpenAPI 3.1 spec as source of truth, plus human-readable Markdown with examples and guide-style prose.
**Architecture:** OpenAPI YAML spec covers all 41 REST endpoints. Separate Markdown files per route group with curl examples and response samples. WebSocket protocol documented in dedicated Markdown. Guide files connect endpoints into workflows.
**Tech Stack:** OpenAPI 3.1 YAML, Markdown, curl for examples
---
### Task 1: Archive existing docs and create directory structure
**Files:**
- Move: `docs/*.md``docs/archive/`
- Create directories: `docs/api/`, `docs/api/endpoints/`, `docs/api/guides/`
**Step 1: Create the archive directory**
```bash
mkdir -p docs/archive docs/api/endpoints docs/api/guides
```
**Step 2: Move existing docs to archive**
```bash
mv docs/API_QUICK_REFERENCE.md docs/archive/
mv docs/BOT_INTEGRATION.md docs/archive/
mv docs/SESSION_END_QUICK_START.md docs/archive/
mv docs/SESSION_END_WEBSOCKET.md docs/archive/
mv docs/SESSION_START_WEBSOCKET.md docs/archive/
mv docs/WEBSOCKET_FLOW_DIAGRAM.md docs/archive/
mv docs/WEBSOCKET_SUBSCRIPTION_GUIDE.md docs/archive/
mv docs/WEBSOCKET_TESTING.md docs/archive/
mv docs/todos.md docs/archive/
```
**Step 3: Verify structure**
```bash
ls -R docs/
```
Expected: `archive/` with old files, `api/endpoints/` and `api/guides/` empty, `plans/` with design docs.
**Step 4: Commit**
```bash
git add docs/
git commit -m "docs: archive old documentation, create new docs structure"
```
---
### Task 2: Write OpenAPI spec — info, servers, security, and component schemas
**Files:**
- Create: `docs/api/openapi.yaml`
**Step 1: Write the OpenAPI header, servers, security schemes, and all reusable component schemas**
Write `docs/api/openapi.yaml` with:
```yaml
openapi: 3.1.0
info:
title: Jackbox Game Picker API
description: API for managing Jackbox Party Pack games, sessions, voting, and integrations.
version: "1.0"
license:
name: MIT
servers:
- url: http://localhost:5000
description: Local development (backend direct)
- url: http://localhost:3000/api
description: Docker Compose (via Vite/Nginx proxy)
security: []
tags:
- name: Auth
description: Authentication endpoints
- name: Games
description: Game management and filtering
- name: Sessions
description: Session lifecycle and game tracking
- name: Picker
description: Weighted random game selection
- name: Stats
description: Aggregate statistics
- name: Votes
description: Real-time popularity voting
- name: Webhooks
description: Webhook management for external integrations
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: >
JWT token obtained from POST /api/auth/login.
Pass as `Authorization: Bearer <token>`. Tokens expire after 24 hours.
schemas:
Error:
type: object
properties:
error:
type: string
required:
- error
Game:
type: object
properties:
id:
type: integer
pack_name:
type: string
title:
type: string
min_players:
type: integer
max_players:
type: integer
length_minutes:
type: integer
nullable: true
has_audience:
type: integer
enum: [0, 1]
family_friendly:
type: integer
enum: [0, 1]
game_type:
type: string
nullable: true
secondary_type:
type: string
nullable: true
play_count:
type: integer
popularity_score:
type: integer
upvotes:
type: integer
downvotes:
type: integer
enabled:
type: integer
enum: [0, 1]
favor_bias:
type: integer
enum: [-1, 0, 1]
created_at:
type: string
format: date-time
Session:
type: object
properties:
id:
type: integer
created_at:
type: string
format: date-time
closed_at:
type: string
format: date-time
nullable: true
is_active:
type: integer
enum: [0, 1]
notes:
type: string
nullable: true
games_played:
type: integer
SessionGame:
type: object
properties:
id:
type: integer
session_id:
type: integer
game_id:
type: integer
played_at:
type: string
format: date-time
manually_added:
type: integer
enum: [0, 1]
status:
type: string
enum: [playing, played, skipped]
room_code:
type: string
nullable: true
player_count:
type: integer
nullable: true
player_count_check_status:
type: string
nullable: true
pack_name:
type: string
title:
type: string
game_type:
type: string
nullable: true
min_players:
type: integer
max_players:
type: integer
popularity_score:
type: integer
upvotes:
type: integer
downvotes:
type: integer
Pack:
type: object
properties:
id:
type: integer
name:
type: string
favor_bias:
type: integer
enum: [-1, 0, 1]
created_at:
type: string
format: date-time
PackMeta:
type: object
properties:
name:
type: string
total_count:
type: integer
enabled_count:
type: integer
total_plays:
type: integer
Webhook:
type: object
properties:
id:
type: integer
name:
type: string
url:
type: string
format: uri
events:
type: array
items:
type: string
enabled:
type: boolean
created_at:
type: string
format: date-time
WebhookLog:
type: object
properties:
id:
type: integer
webhook_id:
type: integer
event_type:
type: string
payload:
type: object
response_status:
type: integer
nullable: true
error_message:
type: string
nullable: true
created_at:
type: string
format: date-time
```
**Step 2: Validate YAML syntax**
```bash
node -e "const fs=require('fs'); const y=require('yaml'); y.parse(fs.readFileSync('docs/api/openapi.yaml','utf8')); console.log('Valid YAML')"
```
If `yaml` module not available, use: `npx -y yaml-cli docs/api/openapi.yaml` or manually verify structure.
**Step 3: Commit**
```bash
git add docs/api/openapi.yaml
git commit -m "docs: add OpenAPI spec with schemas and security definitions"
```
---
### Task 3: Write OpenAPI paths — Auth and Games endpoints
**Files:**
- Modify: `docs/api/openapi.yaml`
**Step 1: Add `paths` section with Auth endpoints**
Source: `backend/routes/auth.js` — 2 endpoints.
Add paths for:
- `POST /api/auth/login` — Body: `{ key }`, responses: 200 (token+message+expiresIn), 400 (missing key), 401 (invalid key)
- `POST /api/auth/verify` — Security: bearerAuth, responses: 200 (`{ valid, user: { role, timestamp } }`)
**Step 2: Add Games endpoints**
Source: `backend/routes/games.js` — 13 endpoints.
Add paths for:
- `GET /api/games` — Query params: `enabled`, `minPlayers`, `maxPlayers`, `playerCount`, `drawing` (only/exclude), `length` (short/medium/long), `familyFriendly`, `pack`. Response: array of Game.
- `GET /api/games/packs` — Response: array of Pack.
- `GET /api/games/meta/packs` — Response: array of PackMeta.
- `GET /api/games/export/csv` — Security: bearerAuth. Response: CSV file (text/csv).
- `PATCH /api/games/packs/{name}/favor` — Security: bearerAuth. Body: `{ favor_bias }` (-1/0/1). Response: `{ message, favor_bias }`. Error 400 for invalid value.
- `GET /api/games/{id}` — Response: Game or 404.
- `POST /api/games` — Security: bearerAuth. Body: `{ pack_name, title, min_players, max_players, length_minutes?, has_audience?, family_friendly?, game_type?, secondary_type? }`. Response 201: Game. Error 400: missing fields.
- `PUT /api/games/{id}` — Security: bearerAuth. Body: same fields (all optional). Response: Game or 404.
- `DELETE /api/games/{id}` — Security: bearerAuth. Response: `{ message }` or 404.
- `PATCH /api/games/{id}/toggle` — Security: bearerAuth. Response: Game (with toggled enabled) or 404.
- `PATCH /api/games/packs/{name}/toggle` — Security: bearerAuth. Body: `{ enabled }`. Response: `{ message, gamesAffected }`. Error 400: missing enabled.
- `POST /api/games/import/csv` — Security: bearerAuth. Body: `{ csvData, mode }` (mode: "append" or "replace"). Response: `{ message, count, mode }`. Error 400: missing csvData. CSV columns: Game Pack, Game Title, Min. Players, Max. Players, Length, Audience, Family Friendly?, Game Type, Secondary Type.
- `PATCH /api/games/{id}/favor` — Security: bearerAuth. Body: `{ favor_bias }` (-1/0/1). Response: `{ message, favor_bias }`. Error 400/404.
**Step 3: Validate YAML syntax**
Same validation command as Task 2 Step 2.
**Step 4: Commit**
```bash
git add docs/api/openapi.yaml
git commit -m "docs: add Auth and Games endpoint paths to OpenAPI spec"
```
---
### Task 4: Write OpenAPI paths — Sessions, Picker, Stats, Votes, Webhooks
**Files:**
- Modify: `docs/api/openapi.yaml`
**Step 1: Add Sessions endpoints (15 endpoints)**
Source: `backend/routes/sessions.js`
Add paths for:
- `GET /api/sessions` — Response: array of Session (with games_played count).
- `GET /api/sessions/active` — Response: Session object or `{ session: null, message }`.
- `GET /api/sessions/{id}` — Response: Session or 404.
- `POST /api/sessions` — Security: bearerAuth. Body: `{ notes? }`. Response 201: Session. Error 400: active session already exists (`{ error, activeSessionId }`). Triggers WebSocket `session.started` broadcast.
- `POST /api/sessions/{id}/close` — Security: bearerAuth. Body: `{ notes? }`. Response: closed Session (with games_played). Error 404/400 (already closed). Sets all 'playing' games to 'played'. Triggers WebSocket `session.ended`.
- `DELETE /api/sessions/{id}` — Security: bearerAuth. Response: `{ message, sessionId }`. Error 404/400 (cannot delete active). Cascades: deletes chat_logs and session_games.
- `GET /api/sessions/{id}/games` — Response: array of SessionGame (joined with game data).
- `POST /api/sessions/{id}/games` — Security: bearerAuth. Body: `{ game_id, manually_added?, room_code? }`. Response 201: SessionGame. Error 400 (closed session, missing game_id), 404 (session/game not found). Side effects: increments play_count, sets previous 'playing' games to 'played', triggers `game.added` webhook + WebSocket, auto-starts room monitor if room_code provided.
- `POST /api/sessions/{id}/chat-import` — Security: bearerAuth. Body: `{ chatData: [{ username, message, timestamp }] }`. Response: `{ message, messagesImported, duplicatesSkipped, votesProcessed, votesByGame, debug }`. Vote patterns: "thisgame++" = upvote, "thisgame--" = downvote. Matches votes to games by timestamp intervals.
- `PATCH /api/sessions/{sessionId}/games/{gameId}/status` — Security: bearerAuth. Body: `{ status }` (playing/played/skipped). Response: `{ message, status }`. Error 400/404. If setting to 'playing', auto-sets other playing games to 'played'.
- `DELETE /api/sessions/{sessionId}/games/{gameId}` — Security: bearerAuth. Response: `{ message }`. Error 404. Stops room monitor/player count check.
- `PATCH /api/sessions/{sessionId}/games/{gameId}/room-code` — Security: bearerAuth. Body: `{ room_code }` (exactly 4 chars, A-Z0-9). Response: SessionGame. Error 400 (invalid format)/404.
- `GET /api/sessions/{id}/export` — Security: bearerAuth. Query: `format` ("json" or "txt", default "txt"). Response: file download (application/json or text/plain).
- `POST /api/sessions/{sessionId}/games/{gameId}/start-player-check` — Security: bearerAuth. Response: `{ message, status: "monitoring" }`. Error 400 (no room code)/404.
- `POST /api/sessions/{sessionId}/games/{gameId}/stop-player-check` — Security: bearerAuth. Response: `{ message, status: "stopped" }`.
- `PATCH /api/sessions/{sessionId}/games/{gameId}/player-count` — Security: bearerAuth. Body: `{ player_count }` (non-negative integer). Response: `{ message, player_count }`. Error 400/404. Triggers WebSocket `player-count.updated`.
**Step 2: Add Picker endpoint**
Source: `backend/routes/picker.js` — mounted at `/api` (not `/api/picker`).
- `POST /api/pick` — No auth. Body: `{ playerCount?, drawing?, length?, familyFriendly?, sessionId?, excludePlayed? }`. Response 200: `{ game: Game, poolSize, totalEnabled }`. Response 404: `{ error, suggestion, recentlyPlayed? }`. Bias: game favor_bias 1=3x, -1=0.2x; pack favor_bias 1=2x, -1=0.3x. Repeat avoidance: excludes last 2 played games by default, or all played if excludePlayed=true.
**Step 3: Add Stats endpoint**
Source: `backend/routes/stats.js`
- `GET /api/stats` — No auth. Response: `{ games: { count }, gamesEnabled: { count }, packs: { count }, sessions: { count }, activeSessions: { count }, totalGamesPlayed: { count }, mostPlayedGames: [...], topRatedGames: [...] }`. The game arrays include: id, title, pack_name, play_count, popularity_score, upvotes, downvotes. Limited to top 10.
**Step 4: Add Votes endpoint**
Source: `backend/routes/votes.js`
- `POST /api/votes/live` — Security: bearerAuth. Body: `{ username, vote, timestamp }` where vote is "up" or "down", timestamp is ISO 8601. Response 200: `{ success, message, session: { id, games_played }, game: { id, title, upvotes, downvotes, popularity_score }, vote: { username, type, timestamp } }`. Error 400 (missing fields, invalid vote/timestamp), 404 (no active session, no games, vote doesn't match a game), 409 (duplicate within 1 second).
**Step 5: Add Webhooks endpoints (7 endpoints)**
Source: `backend/routes/webhooks.js`
- `GET /api/webhooks` — Security: bearerAuth. Response: array of Webhook (events parsed from JSON, enabled as boolean).
- `GET /api/webhooks/{id}` — Security: bearerAuth. Response: Webhook or 404.
- `POST /api/webhooks` — Security: bearerAuth. Body: `{ name, url, secret, events }` where events is string array. Response 201: Webhook + `{ message }`. Error 400: missing fields, invalid URL, events not array.
- `PATCH /api/webhooks/{id}` — Security: bearerAuth. Body: `{ name?, url?, secret?, events?, enabled? }`. Response: Webhook + `{ message }`. Error 400 (no fields, invalid URL, events not array)/404.
- `DELETE /api/webhooks/{id}` — Security: bearerAuth. Response: `{ message, webhookId }`. Error 404.
- `POST /api/webhooks/test/{id}` — Security: bearerAuth. Sends test `game.added` payload. Response: `{ message, note }`. Error 404.
- `GET /api/webhooks/{id}/logs` — Security: bearerAuth. Query: `limit` (default 50). Response: array of WebhookLog (payload parsed from JSON).
**Step 6: Add Health endpoint**
- `GET /health` — No auth. Response: `{ status: "ok", message: "Jackbox Game Picker API is running" }`.
**Step 7: Validate YAML syntax**
Same validation as before.
**Step 8: Commit**
```bash
git add docs/api/openapi.yaml
git commit -m "docs: complete all OpenAPI endpoint paths"
```
---
### Task 5: Write API README
**Files:**
- Create: `docs/api/README.md`
**Step 1: Write the API overview document**
Content should include:
- **Overview:** What the API does (manage Jackbox games, run sessions, track popularity, pick games with weighted randomness)
- **Base URL:** `http://localhost:5000` (direct) or `http://localhost:3000/api` (Docker proxy). All REST endpoints prefixed with `/api/` except `/health`.
- **Authentication:** POST to `/api/auth/login` with admin key → receive JWT. Include as `Authorization: Bearer <token>`. Tokens expire in 24 hours. Public endpoints (GET games, GET sessions, GET stats, POST pick, GET health) don't require auth. All write operations require auth.
- **Request/Response format:** JSON request/response. `Content-Type: application/json`. Exceptions: CSV export returns `text/csv`, session export can return `text/plain`.
- **Error handling:** All errors return `{ "error": "message" }`. HTTP status codes: 400 (bad request/validation), 401 (no token), 403 (invalid/expired token), 404 (not found), 409 (conflict/duplicate), 500 (server error).
- **Boolean fields:** SQLite stores booleans as integers (0/1). In request bodies, pass JS booleans; the API converts. In responses, expect 0/1 except for Webhook.enabled which returns a JS boolean.
- **Pagination:** No pagination — all list endpoints return full result sets.
- **Quick reference table:** All 41 endpoints in a single table: Method | Path | Auth | Description
- **Links:** to endpoint docs, WebSocket docs, and guides
**Step 2: Commit**
```bash
git add docs/api/README.md
git commit -m "docs: add API README with overview, auth, and quick reference"
```
---
### Task 6: Write endpoint docs — Auth and Games
**Files:**
- Create: `docs/api/endpoints/auth.md`
- Create: `docs/api/endpoints/games.md`
**Step 1: Write auth.md**
Cover the 2 auth endpoints with the template from the design doc. Include:
- Overview: simple admin-key authentication. One role (admin). No user management.
- Endpoint table
- For each endpoint: description, auth, parameters, request body, response, errors, curl example
- curl examples must use realistic sample data
**Step 2: Write games.md**
Cover all 13 games endpoints with the template. Important details to include from source code:
- GET /api/games filter behavior: `drawing=only` matches `game_type='Drawing'`, `drawing=exclude` excludes Drawing. `length=short` is ≤15min (including NULL), `medium` is 16-25min, `long` is >25min. Results ordered by pack_name, title.
- POST /api/games requires: pack_name, title, min_players, max_players. Optional: length_minutes, has_audience, family_friendly, game_type, secondary_type.
- PUT /api/games/:id uses COALESCE for most fields (only updates what's provided), but length_minutes, game_type, secondary_type accept explicit null.
- CSV import expects columns: Game Pack, Game Title, Min. Players, Max. Players, Length, Audience, Family Friendly?, Game Type, Secondary Type. Mode "replace" deletes ALL existing games first.
- Favor bias: 1=favor, -1=disfavor, 0=neutral. Applies to picker weighting.
**Step 3: Commit**
```bash
git add docs/api/endpoints/auth.md docs/api/endpoints/games.md
git commit -m "docs: add Auth and Games endpoint documentation"
```
---
### Task 7: Write endpoint docs — Sessions
**Files:**
- Create: `docs/api/endpoints/sessions.md`
**Step 1: Write sessions.md**
Cover all 15 sessions endpoints. Important details from source code:
- Only one active session at a time. Creating a session when one exists returns 400 with activeSessionId.
- Closing a session auto-sets all 'playing' games to 'played'.
- Cannot delete active sessions — must close first. Delete cascades chat_logs and session_games.
- Adding a game to session: auto-sets previous 'playing' games to 'played' (skipped games stay skipped), increments game play_count, triggers `game.added` webhook + WebSocket, auto-starts room monitor if room_code provided.
- Chat import: matches vote timestamps to games using interval logic (vote belongs to game whose played_at is most recent before vote timestamp). Deduplicates by SHA-256 hash of `username:message:timestamp`.
- Status update to 'playing' auto-sets other 'playing' games to 'played'.
- Room code: exactly 4 chars, uppercase A-Z and 0-9 only. Regex: `/^[A-Z0-9]{4}$/`.
- Export formats: JSON (structured with session+games+chat_logs) and TXT (human-readable plaintext).
- Player count: body is `{ player_count }`, must be non-negative integer. Sets `player_count_check_status` to 'completed'. Broadcasts `player-count.updated` via WebSocket.
- The `gameId` parameter in session game sub-routes refers to `session_games.id`, NOT `games.id`.
**Step 2: Commit**
```bash
git add docs/api/endpoints/sessions.md
git commit -m "docs: add Sessions endpoint documentation"
```
---
### Task 8: Write endpoint docs — Picker, Stats, Votes, Webhooks
**Files:**
- Create: `docs/api/endpoints/picker.md`
- Create: `docs/api/endpoints/stats.md`
- Create: `docs/api/endpoints/votes.md`
- Create: `docs/api/endpoints/webhooks.md`
**Step 1: Write picker.md**
Key details from source code:
- Filters enabled games only (enabled=1)
- Weighted random: game favor_bias 1 = 3x weight, -1 = 0.2x weight. Pack favor_bias 1 = 2x weight, -1 = 0.3x weight. Biases multiply.
- Repeat avoidance: with sessionId, excludes last 2 games by default. With excludePlayed=true, excludes ALL games played in session.
- 404 when no games match filters (with suggestion), or when all eligible games have been played.
**Step 2: Write stats.md**
Key details: single endpoint, no auth, returns aggregate counts and top-10 lists (most played, top rated). mostPlayedGames sorted by play_count DESC, topRatedGames sorted by popularity_score DESC, both only include games with > 0 in respective metric.
**Step 3: Write votes.md**
Key details:
- Requires auth. Body: `{ username, vote, timestamp }`.
- `vote` must be "up" or "down". `timestamp` must be valid ISO 8601.
- Automatically matches vote to the correct game in the active session using timestamp interval logic.
- Deduplication: rejects votes from same username within 1 second (409).
- Updates game upvotes/downvotes/popularity_score atomically in a transaction.
**Step 4: Write webhooks.md**
Key details:
- All endpoints require auth.
- Events stored as JSON string in DB, returned as parsed array.
- `enabled` stored as 0/1 in DB, returned as JS boolean.
- Secret is never returned in GET responses (excluded from SELECT).
- Test sends a `game.added` event with dummy data.
- Logs include parsed payload, limited by `limit` query param (default 50).
**Step 5: Commit**
```bash
git add docs/api/endpoints/picker.md docs/api/endpoints/stats.md docs/api/endpoints/votes.md docs/api/endpoints/webhooks.md
git commit -m "docs: add Picker, Stats, Votes, and Webhooks endpoint documentation"
```
---
### Task 9: Write WebSocket documentation
**Files:**
- Create: `docs/api/websocket.md`
**Step 1: Write websocket.md**
Cover the full WebSocket protocol from `backend/utils/websocket-manager.js`:
- **Connection:** `ws://host:port/api/sessions/live`. No query params needed. Connection established without auth.
- **Authentication:** Send `{ "type": "auth", "token": "<jwt>" }`. Server responds with `{ "type": "auth_success", "message": "Authenticated successfully" }` or `{ "type": "auth_error", "message": "..." }`. Auth required for subscribe/unsubscribe.
- **Subscription model:** Subscribe to a specific session's events with `{ "type": "subscribe", "sessionId": <number> }`. Response: `{ "type": "subscribed", "sessionId": <number>, "message": "..." }`. Can subscribe to multiple sessions. Unsubscribe with `{ "type": "unsubscribe", "sessionId": <number> }`.
- **Heartbeat:** Client sends `{ "type": "ping" }`, server responds `{ "type": "pong" }`. Timeout: 60 seconds since last ping — server terminates connection. Heartbeat check runs every 30 seconds.
- **Events (server → client):**
- `session.started` — broadcast to ALL authenticated clients (not session-specific). Data: `{ session: { id, is_active, created_at, notes } }`. Triggered when `POST /api/sessions` creates a new session.
- `game.added` — broadcast to session subscribers. Data: `{ session: { id, is_active, games_played }, game: { id, title, pack_name, min_players, max_players, manually_added, room_code } }`. Triggered when `POST /api/sessions/:id/games` adds a game.
- `session.ended` — broadcast to session subscribers. Data: `{ session: { id, is_active, games_played } }`. Triggered when `POST /api/sessions/:id/close` closes session.
- `player-count.updated` — broadcast to session subscribers. Data: `{ sessionId, gameId, playerCount, status }`. Triggered when player count is updated.
- **Event envelope:** `{ "type": "<event-type>", "timestamp": "<ISO 8601>", "data": { ... } }`
- **Error messages:** `{ "type": "error", "message": "..." }` for general errors, `{ "type": "auth_error", "message": "..." }` for auth failures.
- **Connection lifecycle example:** Connect → auth → subscribe → receive events → ping/pong loop → unsubscribe → close.
- **Reconnection:** Server doesn't maintain state across disconnects. Client must re-authenticate and re-subscribe after reconnecting.
**Step 2: Commit**
```bash
git add docs/api/websocket.md
git commit -m "docs: add WebSocket protocol documentation"
```
---
### Task 10: Write guide — Getting Started
**Files:**
- Create: `docs/api/guides/getting-started.md`
**Step 1: Write getting-started.md**
Narrative guide walking through the minimum viable integration:
1. Health check — verify API is running
2. Authenticate — get a JWT token
3. Browse games — list all games, filter by player count
4. Pick a game — use the picker with filters
5. Start a session — create session, add the picked game
6. Close the session
Each step includes a curl example and expected response. Cross-references the endpoint docs for full details.
**Step 2: Commit**
```bash
git add docs/api/guides/getting-started.md
git commit -m "docs: add Getting Started guide"
```
---
### Task 11: Write guide — Session Lifecycle
**Files:**
- Create: `docs/api/guides/session-lifecycle.md`
**Step 1: Write session-lifecycle.md**
Narrative guide covering:
1. Creating a session (one active at a time constraint)
2. Adding games (via picker or manual), understanding auto-status-transitions
3. Tracking game status (playing → played/skipped)
4. Room codes and player count monitoring
5. Closing sessions (auto-finalizes playing games)
6. Exporting session data (JSON and TXT formats)
7. Deleting old sessions (must close first, cascades)
8. WebSocket integration for real-time updates
**Step 2: Commit**
```bash
git add docs/api/guides/session-lifecycle.md
git commit -m "docs: add Session Lifecycle guide"
```
---
### Task 12: Write guide — Voting and Popularity
**Files:**
- Create: `docs/api/guides/voting-and-popularity.md`
**Step 1: Write voting-and-popularity.md**
Narrative guide covering:
1. How popularity works: `popularity_score = upvotes - downvotes`
2. Two voting mechanisms: chat import (batch, after-the-fact) and live votes (real-time, from bots)
3. Chat import flow: collect chat logs with `thisgame++`/`thisgame--` patterns, POST to chat-import, timestamp-matching algorithm explained
4. Live vote flow: bot sends votes in real-time via POST /api/votes/live, same timestamp-matching logic, deduplication within 1 second
5. How voting affects the picker: popularity_score doesn't directly affect picker weights (favor_bias does), but topRatedGames in stats uses it
6. Favor bias vs popularity: favor_bias is admin-controlled weighting for the picker; popularity is community-driven sentiment tracking
**Step 2: Commit**
```bash
git add docs/api/guides/voting-and-popularity.md
git commit -m "docs: add Voting and Popularity guide"
```
---
### Task 13: Write guide — Webhooks and Events
**Files:**
- Create: `docs/api/guides/webhooks-and-events.md`
**Step 1: Write webhooks-and-events.md**
Narrative guide covering:
1. Two notification systems: Webhooks (HTTP callbacks) and WebSocket (persistent connections)
2. When to use which: Webhooks for server-to-server integrations (bots, Discord); WebSocket for real-time UI or tools that maintain persistent connections
3. Webhook setup: create webhook, specify events, provide URL and secret
4. Webhook events: `game.added` (currently the only webhook event triggered in code — verify this). Payload shape.
5. Webhook delivery: async, logged in webhook_logs. Test with POST /api/webhooks/test/:id.
6. WebSocket events: `session.started`, `game.added`, `session.ended`, `player-count.updated`. Which are broadcast to all clients vs session subscribers.
7. Event payload reference (linking to websocket.md)
**Step 2: Commit**
```bash
git add docs/api/guides/webhooks-and-events.md
git commit -m "docs: add Webhooks and Events guide"
```
---
### Task 14: Validate documentation against source code
**Files:**
- Read: all `backend/routes/*.js` files
- Read: `docs/api/openapi.yaml`
- Read: all `docs/api/endpoints/*.md` files
**Step 1: Cross-reference every route handler against the OpenAPI spec**
For each file in `backend/routes/`:
- Count endpoints in source code
- Count corresponding paths in `openapi.yaml`
- Verify HTTP methods match
- Verify path patterns match (including parameter names)
- Verify auth requirements match (`authenticateToken` usage)
**Step 2: Verify request/response shapes**
For each endpoint:
- Compare request body fields to what the route handler destructures from `req.body`
- Compare response shapes to what the route handler actually sends via `res.json()`
- Check error status codes and messages match
**Step 3: Fix any discrepancies found**
Edit the OpenAPI spec and/or Markdown files to match the source code.
**Step 4: Commit any fixes**
```bash
git add docs/
git commit -m "docs: fix discrepancies found during validation"
```
---
### Task 15: Final review and summary commit
**Step 1: Verify all files exist**
```bash
ls -la docs/api/
ls -la docs/api/endpoints/
ls -la docs/api/guides/
ls -la docs/archive/
```
Expected:
- `docs/api/`: openapi.yaml, README.md, websocket.md
- `docs/api/endpoints/`: auth.md, games.md, sessions.md, picker.md, stats.md, votes.md, webhooks.md
- `docs/api/guides/`: getting-started.md, session-lifecycle.md, voting-and-popularity.md, webhooks-and-events.md
- `docs/archive/`: 9 old doc files
**Step 2: Verify total endpoint count in OpenAPI spec**
```bash
grep -c "operationId:" docs/api/openapi.yaml
```
Expected: 42 (41 API endpoints + 1 health check)
**Step 3: Final commit if any files were missed**
```bash
git add docs/
git status
git commit -m "docs: complete API documentation with OpenAPI spec, endpoint docs, WebSocket, and guides"
```