Compare commits
2 Commits
4747aa9632
...
8ba32e128c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ba32e128c
|
||
|
|
505c335d20
|
@@ -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) {
|
||||
|
||||
42
backend/utils/jackbox-api.js
Normal file
42
backend/utils/jackbox-api.js
Normal 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 };
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
135
backend/utils/room-monitor.js
Normal file
135
backend/utils/room-monitor.js
Normal 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
176
docs/api/README.md
Normal 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
135
docs/api/endpoints/auth.md
Normal 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
627
docs/api/endpoints/games.md
Normal 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"` (16–25 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}'
|
||||
```
|
||||
120
docs/api/endpoints/picker.md
Normal file
120
docs/api/endpoints/picker.md
Normal 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"` (16–25 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"` = 16–25 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
|
||||
}'
|
||||
```
|
||||
919
docs/api/endpoints/sessions.md
Normal file
919
docs/api/endpoints/sessions.md
Normal 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 A–Z and 0–9 only (regex: `^[A-Z0-9]{4}$`).
|
||||
|
||||
**Note:** `sessionGameId` is the `session_games.id` row ID, NOT `games.id`.
|
||||
|
||||
### Authentication
|
||||
|
||||
Bearer token required.
|
||||
|
||||
### Path Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| sessionId | integer | Session ID |
|
||||
| sessionGameId | integer | Session game ID (`session_games.id`) |
|
||||
|
||||
### Request Body
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| room_code | string | Yes | 4 uppercase alphanumeric chars (A-Z, 0-9) |
|
||||
|
||||
```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}'
|
||||
```
|
||||
79
docs/api/endpoints/stats.md
Normal file
79
docs/api/endpoints/stats.md
Normal 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"
|
||||
```
|
||||
92
docs/api/endpoints/votes.md
Normal file
92
docs/api/endpoints/votes.md
Normal 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"
|
||||
}'
|
||||
```
|
||||
382
docs/api/endpoints/webhooks.md
Normal file
382
docs/api/endpoints/webhooks.md
Normal 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"
|
||||
```
|
||||
316
docs/api/guides/getting-started.md
Normal file
316
docs/api/guides/getting-started.md
Normal 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.
|
||||
287
docs/api/guides/session-lifecycle.md
Normal file
287
docs/api/guides/session-lifecycle.md
Normal 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 (A–Z) and digits (0–9) 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`.
|
||||
207
docs/api/guides/voting-and-popularity.md
Normal file
207
docs/api/guides/voting-and-popularity.md
Normal 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 vote’s 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 vote’s 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
|
||||
216
docs/api/guides/webhooks-and-events.md
Normal file
216
docs/api/guides/webhooks-and-events.md
Normal 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** — You’re 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; there’s 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. You’ll 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 Discord’s 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 server’s URL (e.g. `https://my-bot.example.com/webhooks/jackbox`), set `events` to `["game.added"]`, and use the same `secret` in your server’s `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
1693
docs/api/openapi.yaml
Normal file
File diff suppressed because it is too large
Load Diff
399
docs/api/websocket.md
Normal file
399
docs/api/websocket.md
Normal 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();
|
||||
}
|
||||
```
|
||||
@@ -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`).
|
||||
|
||||
---
|
||||
130
docs/plans/2026-03-15-api-documentation-design.md
Normal file
130
docs/plans/2026-03-15-api-documentation-design.md
Normal 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
|
||||
768
docs/plans/2026-03-15-api-documentation-implementation.md
Normal file
768
docs/plans/2026-03-15-api-documentation-implementation.md
Normal 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"
|
||||
```
|
||||
Reference in New Issue
Block a user