Files
jackboxpartypack-gamepicker/backend/routes/sessions.js
cottongin 171303a6f9 fix: enforce single playing game and clean up stale shard monitors
Mark games as 'played' when shard detects game.ended or room_closed.
Stop old shard monitors before demoting previous playing games on new
game add or status change. Sync frontend playingGame state with the
games list on every refresh to prevent stale UI. Use terminate() for
probe connections to prevent shard connection leaks.

Made-with: Cursor
2026-03-20 23:34:22 -04:00

1005 lines
29 KiB
JavaScript

const express = require('express');
const crypto = require('crypto');
const { authenticateToken } = require('../middleware/auth');
const db = require('../database');
const { triggerWebhook } = require('../utils/webhooks');
const { getWebSocketManager } = require('../utils/websocket-manager');
const { startMonitor, stopMonitor, getMonitorSnapshot } = require('../utils/ecast-shard-client');
const router = express.Router();
// Helper function to create a hash of a message
function createMessageHash(username, message, timestamp) {
return crypto
.createHash('sha256')
.update(`${username}:${message}:${timestamp}`)
.digest('hex');
}
// Get all sessions
router.get('/', (req, res) => {
try {
const sessions = db.prepare(`
SELECT
s.*,
COUNT(sg.id) as games_played
FROM sessions s
LEFT JOIN session_games sg ON s.id = sg.session_id
GROUP BY s.id
ORDER BY s.created_at DESC
`).all();
res.json(sessions);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Get active session
router.get('/active', (req, res) => {
try {
const session = db.prepare(`
SELECT
s.*,
COUNT(sg.id) as games_played
FROM sessions s
LEFT JOIN session_games sg ON s.id = sg.session_id
WHERE s.is_active = 1
GROUP BY s.id
LIMIT 1
`).get();
// Return null instead of 404 when no active session
if (!session) {
return res.json({ session: null, message: 'No active session' });
}
res.json(session);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Get single session by ID
router.get('/:id', (req, res) => {
try {
const session = db.prepare(`
SELECT
s.*,
COUNT(sg.id) as games_played
FROM sessions s
LEFT JOIN session_games sg ON s.id = sg.session_id
WHERE s.id = ?
GROUP BY s.id
`).get(req.params.id);
if (!session) {
return res.status(404).json({ error: 'Session not found' });
}
res.json(session);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Create new session (admin only)
router.post('/', authenticateToken, (req, res) => {
try {
const { notes } = req.body;
// Check if there's already an active session
const activeSession = db.prepare('SELECT id FROM sessions WHERE is_active = 1').get();
if (activeSession) {
return res.status(400).json({
error: 'An active session already exists. Please close it before creating a new one.',
activeSessionId: activeSession.id
});
}
const stmt = db.prepare(`
INSERT INTO sessions (notes, is_active)
VALUES (?, 1)
`);
const result = stmt.run(notes || null);
const newSession = db.prepare('SELECT * FROM sessions WHERE id = ?').get(result.lastInsertRowid);
// Broadcast session.started event via WebSocket to all authenticated clients
try {
const wsManager = getWebSocketManager();
if (wsManager) {
const eventData = {
session: {
id: newSession.id,
is_active: 1,
created_at: newSession.created_at,
notes: newSession.notes
}
};
wsManager.broadcastToAll('session.started', eventData);
console.log(`[Sessions] Broadcasted session.started event for session ${newSession.id} to all clients`);
}
} catch (error) {
// Log error but don't fail the request
console.error('Error broadcasting session.started event:', error);
}
res.status(201).json(newSession);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Close/finalize session (admin only)
router.post('/:id/close', authenticateToken, (req, res) => {
try {
const { notes } = req.body;
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(req.params.id);
if (!session) {
return res.status(404).json({ error: 'Session not found' });
}
if (session.is_active === 0) {
return res.status(400).json({ error: 'Session is already closed' });
}
// Set all 'playing' games to 'played' before closing
db.prepare(`
UPDATE session_games
SET status = 'played'
WHERE session_id = ? AND status = 'playing'
`).run(req.params.id);
const stmt = db.prepare(`
UPDATE sessions
SET is_active = 0, closed_at = CURRENT_TIMESTAMP, notes = COALESCE(?, notes)
WHERE id = ?
`);
stmt.run(notes || null, req.params.id);
// Get updated session with games count
const closedSession = db.prepare(`
SELECT
s.*,
COUNT(sg.id) as games_played
FROM sessions s
LEFT JOIN session_games sg ON s.id = sg.session_id
WHERE s.id = ?
GROUP BY s.id
`).get(req.params.id);
// Broadcast session.ended event via WebSocket
try {
const wsManager = getWebSocketManager();
if (wsManager) {
const eventData = {
session: {
id: closedSession.id,
is_active: 0,
games_played: closedSession.games_played
}
};
wsManager.broadcastEvent('session.ended', eventData, parseInt(req.params.id));
console.log(`[Sessions] Broadcasted session.ended event for session ${req.params.id}`);
}
} catch (error) {
// Log error but don't fail the request
console.error('Error broadcasting session.ended event:', error);
}
res.json(closedSession);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Delete session (admin only)
router.delete('/:id', authenticateToken, (req, res) => {
try {
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(req.params.id);
if (!session) {
return res.status(404).json({ error: 'Session not found' });
}
// Prevent deletion of active sessions
if (session.is_active === 1) {
return res.status(400).json({ error: 'Cannot delete an active session. Please close it first.' });
}
// Delete related data first (cascade)
db.prepare('DELETE FROM chat_logs WHERE session_id = ?').run(req.params.id);
db.prepare('DELETE FROM session_games WHERE session_id = ?').run(req.params.id);
// Delete the session
db.prepare('DELETE FROM sessions WHERE id = ?').run(req.params.id);
res.json({ message: 'Session deleted successfully', sessionId: parseInt(req.params.id) });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Get games played in a session
router.get('/:id/games', (req, res) => {
try {
const games = db.prepare(`
SELECT
sg.*,
g.pack_name,
g.title,
g.game_type,
g.min_players,
g.max_players,
g.popularity_score,
g.upvotes,
g.downvotes
FROM session_games sg
JOIN games g ON sg.game_id = g.id
WHERE sg.session_id = ?
ORDER BY sg.played_at ASC
`).all(req.params.id);
res.json(games);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Get vote breakdown for a session
router.get('/:id/votes', (req, res) => {
try {
const session = db.prepare('SELECT id FROM sessions WHERE id = ?').get(req.params.id);
if (!session) {
return res.status(404).json({ error: 'Session not found' });
}
const votes = db.prepare(`
SELECT
lv.game_id,
g.title,
g.pack_name,
SUM(CASE WHEN lv.vote_type = 1 THEN 1 ELSE 0 END) AS upvotes,
SUM(CASE WHEN lv.vote_type = -1 THEN 1 ELSE 0 END) AS downvotes,
SUM(lv.vote_type) AS net_score,
COUNT(*) AS total_votes
FROM live_votes lv
JOIN games g ON lv.game_id = g.id
WHERE lv.session_id = ?
GROUP BY lv.game_id
ORDER BY net_score DESC
`).all(req.params.id);
res.json({
session_id: parseInt(req.params.id),
votes,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Add game to session (admin only)
router.post('/:id/games', authenticateToken, (req, res) => {
try {
const { game_id, manually_added, room_code } = req.body;
if (!game_id) {
return res.status(400).json({ error: 'game_id is required' });
}
// Verify session exists and is active
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(req.params.id);
if (!session) {
return res.status(404).json({ error: 'Session not found' });
}
if (session.is_active === 0) {
return res.status(400).json({ error: 'Cannot add games to a closed session' });
}
// Verify game exists
const game = db.prepare('SELECT * FROM games WHERE id = ?').get(game_id);
if (!game) {
return res.status(404).json({ error: 'Game not found' });
}
// Stop monitors for currently-playing games before demoting them
const previouslyPlaying = db.prepare(
'SELECT id FROM session_games WHERE session_id = ? AND status = ?'
).all(req.params.id, 'playing');
for (const prev of previouslyPlaying) {
try { stopMonitor(req.params.id, prev.id); } catch (_) {}
}
// Set all current 'playing' games to 'played' (except skipped ones)
db.prepare(`
UPDATE session_games
SET status = CASE
WHEN status = 'skipped' THEN 'skipped'
ELSE 'played'
END
WHERE session_id = ? AND status = 'playing'
`).run(req.params.id);
// Add game to session with 'playing' status
const stmt = db.prepare(`
INSERT INTO session_games (session_id, game_id, manually_added, status, room_code)
VALUES (?, ?, ?, 'playing', ?)
`);
const result = stmt.run(req.params.id, game_id, manually_added ? 1 : 0, room_code || null);
// Increment play count for the game
db.prepare('UPDATE games SET play_count = play_count + 1 WHERE id = ?').run(game_id);
const sessionGame = db.prepare(`
SELECT
sg.*,
g.pack_name,
g.title,
g.game_type,
g.min_players,
g.max_players
FROM session_games sg
JOIN games g ON sg.game_id = g.id
WHERE sg.id = ?
`).get(result.lastInsertRowid);
// Trigger webhook and WebSocket for game.added event
try {
const sessionStats = db.prepare(`
SELECT
s.*,
COUNT(sg.id) as games_played
FROM sessions s
LEFT JOIN session_games sg ON s.id = sg.session_id
WHERE s.id = ?
GROUP BY s.id
`).get(req.params.id);
const eventData = {
session: {
id: sessionStats.id,
is_active: sessionStats.is_active === 1,
games_played: sessionStats.games_played
},
game: {
id: game.id,
title: game.title,
pack_name: game.pack_name,
min_players: game.min_players,
max_players: game.max_players,
manually_added: manually_added || false,
room_code: room_code || null
}
};
// Trigger webhook (for backwards compatibility)
triggerWebhook('game.added', eventData);
// Broadcast via WebSocket (new preferred method)
const wsManager = getWebSocketManager();
if (wsManager) {
wsManager.broadcastEvent('game.added', eventData, parseInt(req.params.id));
}
} catch (error) {
// Log error but don't fail the request
console.error('Error triggering notifications:', error);
}
// Automatically start room monitoring if room code was provided
if (room_code) {
try {
startMonitor(req.params.id, result.lastInsertRowid, room_code, game.max_players);
} catch (error) {
console.error('Error starting room monitor:', error);
}
}
res.status(201).json(sessionGame);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Import chat log and process popularity (admin only)
router.post('/:id/chat-import', authenticateToken, (req, res) => {
try {
const { chatData } = req.body;
if (!chatData || !Array.isArray(chatData)) {
return res.status(400).json({ error: 'chatData must be an array' });
}
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(req.params.id);
if (!session) {
return res.status(404).json({ error: 'Session not found' });
}
// Get all games played in this session with timestamps
const sessionGames = db.prepare(`
SELECT sg.game_id, sg.played_at, g.title, g.upvotes, g.downvotes
FROM session_games sg
JOIN games g ON sg.game_id = g.id
WHERE sg.session_id = ?
ORDER BY sg.played_at ASC
`).all(req.params.id);
if (sessionGames.length === 0) {
return res.status(400).json({ error: 'No games played in this session to match votes against' });
}
let votesProcessed = 0;
let duplicatesSkipped = 0;
const votesByGame = {};
const sessionId = parseInt(req.params.id);
const voteMatches = []; // Debug: track which votes matched to which games
const insertChatLog = db.prepare(`
INSERT INTO chat_logs (session_id, chatter_name, message, timestamp, parsed_vote, message_hash)
VALUES (?, ?, ?, ?, ?, ?)
`);
const checkDuplicate = db.prepare(`
SELECT id FROM chat_logs WHERE message_hash = ? LIMIT 1
`);
const updateUpvote = db.prepare(`
UPDATE games SET upvotes = upvotes + 1, popularity_score = popularity_score + 1 WHERE id = ?
`);
const updateDownvote = db.prepare(`
UPDATE games SET downvotes = downvotes + 1, popularity_score = popularity_score - 1 WHERE id = ?
`);
const processVotes = db.transaction((messages) => {
for (const msg of messages) {
const { username, message, timestamp } = msg;
if (!username || !message || !timestamp) {
continue;
}
// Create hash for this message
const messageHash = createMessageHash(username, message, timestamp);
// Check if we've already imported this exact message
const existing = checkDuplicate.get(messageHash);
if (existing) {
duplicatesSkipped++;
continue; // Skip this duplicate message
}
// Check for vote patterns
let vote = null;
if (message.includes('thisgame++')) {
vote = 'thisgame++';
} else if (message.includes('thisgame--')) {
vote = 'thisgame--';
}
// Insert into chat logs with hash
insertChatLog.run(
sessionId,
username,
message,
timestamp,
vote,
messageHash
);
if (vote) {
// Find which game was being played at this timestamp
const messageTime = new Date(timestamp).getTime();
let matchedGame = null;
for (let i = 0; i < sessionGames.length; i++) {
const currentGame = sessionGames[i];
const nextGame = sessionGames[i + 1];
const currentGameTime = new Date(currentGame.played_at).getTime();
if (nextGame) {
const nextGameTime = new Date(nextGame.played_at).getTime();
if (messageTime >= currentGameTime && messageTime < nextGameTime) {
matchedGame = currentGame;
break;
}
} else {
// Last game in session
if (messageTime >= currentGameTime) {
matchedGame = currentGame;
break;
}
}
}
if (matchedGame) {
const isUpvote = vote === 'thisgame++';
// Debug: track this vote match
voteMatches.push({
username: username,
vote: vote,
timestamp: timestamp,
timestamp_ms: messageTime,
matched_game: matchedGame.title,
game_played_at: matchedGame.played_at,
game_played_at_ms: new Date(matchedGame.played_at).getTime()
});
if (isUpvote) {
updateUpvote.run(matchedGame.game_id);
} else {
updateDownvote.run(matchedGame.game_id);
}
if (!votesByGame[matchedGame.game_id]) {
votesByGame[matchedGame.game_id] = {
title: matchedGame.title,
upvotes: 0,
downvotes: 0
};
}
if (isUpvote) {
votesByGame[matchedGame.game_id].upvotes++;
} else {
votesByGame[matchedGame.game_id].downvotes++;
}
votesProcessed++;
}
}
}
});
processVotes(chatData);
res.json({
message: 'Chat log imported and processed successfully',
messagesImported: chatData.length,
duplicatesSkipped,
votesProcessed,
votesByGame,
debug: {
sessionGamesTimeline: sessionGames.map(sg => ({
title: sg.title,
played_at: sg.played_at,
played_at_ms: new Date(sg.played_at).getTime()
})),
voteMatches: voteMatches
}
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Update session game status (admin only)
router.patch('/:sessionId/games/:gameId/status', authenticateToken, (req, res) => {
try {
const { status } = req.body;
const { sessionId, gameId } = req.params;
if (!status || !['playing', 'played', 'skipped'].includes(status)) {
return res.status(400).json({ error: 'Invalid status. Must be playing, played, or skipped' });
}
// If setting to 'playing', first stop monitors and demote other playing games
if (status === 'playing') {
const previouslyPlaying = db.prepare(
'SELECT id FROM session_games WHERE session_id = ? AND status = ?'
).all(sessionId, 'playing');
for (const prev of previouslyPlaying) {
if (String(prev.id) !== String(gameId)) {
try { stopMonitor(sessionId, prev.id); } catch (_) {}
}
}
db.prepare(`
UPDATE session_games
SET status = CASE
WHEN status = 'skipped' THEN 'skipped'
ELSE 'played'
END
WHERE session_id = ? AND status = 'playing'
`).run(sessionId);
}
// Update the specific game
const result = db.prepare(`
UPDATE session_games
SET status = ?
WHERE session_id = ? AND id = ?
`).run(status, sessionId, gameId);
if (result.changes === 0) {
return res.status(404).json({ error: 'Session game not found' });
}
// Stop room monitor and player count check if game is no longer playing
if (status !== 'playing') {
try {
stopMonitor(sessionId, gameId);
} catch (error) {
console.error('Error stopping room monitor/player count check:', error);
}
}
res.json({ message: 'Status updated successfully', status });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Delete session game (admin only)
router.delete('/:sessionId/games/:gameId', authenticateToken, (req, res) => {
try {
const { sessionId, gameId } = req.params;
// Stop room monitor and player count check before deleting
try {
stopMonitor(sessionId, gameId);
} catch (error) {
console.error('Error stopping room monitor/player count check:', error);
}
const result = db.prepare(`
DELETE FROM session_games
WHERE session_id = ? AND id = ?
`).run(sessionId, gameId);
if (result.changes === 0) {
return res.status(404).json({ error: 'Session game not found' });
}
res.json({ message: 'Game removed from session successfully' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Update room code for a session game (admin only)
router.patch('/:sessionId/games/:gameId/room-code', authenticateToken, (req, res) => {
try {
const { sessionId, gameId } = req.params;
const { room_code } = req.body;
if (!room_code) {
return res.status(400).json({ error: 'room_code is required' });
}
// Validate room code format: 4 characters, A-Z and 0-9 only
const roomCodeRegex = /^[A-Z0-9]{4}$/;
if (!roomCodeRegex.test(room_code)) {
return res.status(400).json({ error: 'room_code must be exactly 4 alphanumeric characters (A-Z, 0-9)' });
}
// Update the room code
const result = db.prepare(`
UPDATE session_games
SET room_code = ?
WHERE session_id = ? AND id = ?
`).run(room_code, sessionId, gameId);
if (result.changes === 0) {
return res.status(404).json({ error: 'Session game not found' });
}
// Return updated game data
const updatedGame = db.prepare(`
SELECT
sg.*,
g.pack_name,
g.title,
g.game_type,
g.min_players,
g.max_players,
g.popularity_score,
g.upvotes,
g.downvotes
FROM session_games sg
JOIN games g ON sg.game_id = g.id
WHERE sg.session_id = ? AND sg.id = ?
`).get(sessionId, gameId);
res.json(updatedGame);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Export session data (plaintext and JSON)
router.get('/:id/export', authenticateToken, (req, res) => {
try {
const { format } = req.query; // 'json' or 'txt'
const sessionId = req.params.id;
// Get session info
const session = db.prepare(`
SELECT
s.*,
COUNT(sg.id) as games_played
FROM sessions s
LEFT JOIN session_games sg ON s.id = sg.session_id
WHERE s.id = ?
GROUP BY s.id
`).get(sessionId);
if (!session) {
return res.status(404).json({ error: 'Session not found' });
}
// Get games for this session
const games = db.prepare(`
SELECT
sg.*,
g.title,
g.pack_name,
g.min_players,
g.max_players,
g.game_type
FROM session_games sg
JOIN games g ON sg.game_id = g.id
WHERE sg.session_id = ?
ORDER BY sg.played_at ASC
`).all(sessionId);
// Get chat logs if any
const chatLogs = db.prepare(`
SELECT * FROM chat_logs
WHERE session_id = ?
ORDER BY timestamp ASC
`).all(sessionId);
if (format === 'json') {
// JSON Export
const exportData = {
session: {
id: session.id,
created_at: session.created_at,
closed_at: session.closed_at,
is_active: session.is_active === 1,
notes: session.notes,
games_played: session.games_played
},
games: games.map(game => ({
title: game.title,
pack: game.pack_name,
players: `${game.min_players}-${game.max_players}`,
type: game.game_type,
played_at: game.played_at,
manually_added: game.manually_added === 1,
status: game.status
})),
chat_logs: chatLogs.map(log => ({
username: log.chatter_name,
message: log.message,
timestamp: log.timestamp,
vote: log.parsed_vote
}))
};
res.setHeader('Content-Type', 'application/json');
res.setHeader('Content-Disposition', `attachment; filename="session-${sessionId}.json"`);
res.send(JSON.stringify(exportData, null, 2));
} else {
// Plain Text Export
let text = `JACKBOX GAME PICKER - SESSION EXPORT\n`;
text += `${'='.repeat(50)}\n\n`;
text += `Session ID: ${session.id}\n`;
text += `Created: ${session.created_at}\n`;
if (session.closed_at) {
text += `Closed: ${session.closed_at}\n`;
}
text += `Status: ${session.is_active ? 'Active' : 'Ended'}\n`;
if (session.notes) {
text += `Notes: ${session.notes}\n`;
}
text += `\nGames Played: ${session.games_played}\n`;
text += `\n${'='.repeat(50)}\n\n`;
if (games.length > 0) {
text += `GAMES:\n`;
text += `${'-'.repeat(50)}\n`;
games.forEach((game, index) => {
text += `\n${index + 1}. ${game.title}\n`;
text += ` Pack: ${game.pack_name}\n`;
text += ` Players: ${game.min_players}-${game.max_players}\n`;
if (game.game_type) {
text += ` Type: ${game.game_type}\n`;
}
text += ` Played: ${game.played_at}\n`;
text += ` Status: ${game.status}\n`;
if (game.manually_added === 1) {
text += ` (Manually Added)\n`;
}
});
text += `\n${'-'.repeat(50)}\n`;
}
if (chatLogs.length > 0) {
text += `\nCHAT LOGS:\n`;
text += `${'-'.repeat(50)}\n`;
chatLogs.forEach(log => {
text += `\n[${log.timestamp}] ${log.chatter_name}:\n`;
text += ` ${log.message}\n`;
if (log.parsed_vote) {
text += ` Vote: ${log.parsed_vote}\n`;
}
});
text += `\n${'-'.repeat(50)}\n`;
}
text += `\nEnd of Session Export\n`;
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Disposition', `attachment; filename="session-${sessionId}.txt"`);
res.send(text);
}
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Get live game status from shard monitor or DB fallback
router.get('/:sessionId/games/:gameId/status-live', (req, res) => {
try {
const { sessionId, gameId } = req.params;
const snapshot = getMonitorSnapshot(sessionId, gameId);
if (snapshot) {
return res.json(snapshot);
}
const game = db.prepare(`
SELECT
sg.room_code,
sg.player_count,
sg.player_count_check_status,
g.title,
g.pack_name,
g.max_players
FROM session_games sg
JOIN games g ON sg.game_id = g.id
WHERE sg.session_id = ? AND sg.id = ?
`).get(sessionId, gameId);
if (!game) {
return res.status(404).json({ error: 'Session game not found' });
}
res.json({
sessionId: parseInt(sessionId, 10),
gameId: parseInt(gameId, 10),
roomCode: game.room_code,
appTag: null,
maxPlayers: game.max_players,
playerCount: game.player_count,
players: [],
lobbyState: null,
gameState: null,
gameStarted: false,
gameFinished: game.player_count_check_status === 'completed',
monitoring: false,
title: game.title,
packName: game.pack_name,
status: game.player_count_check_status,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Start player count check for a session game (admin only)
router.post('/:sessionId/games/:gameId/start-player-check', authenticateToken, (req, res) => {
try {
const { sessionId, gameId } = req.params;
// Get the game to verify it exists and has a room code
const game = db.prepare(`
SELECT sg.*, g.max_players
FROM session_games sg
JOIN games g ON sg.game_id = g.id
WHERE sg.session_id = ? AND sg.id = ?
`).get(sessionId, gameId);
if (!game) {
return res.status(404).json({ error: 'Session game not found' });
}
if (!game.room_code) {
return res.status(400).json({ error: 'Game does not have a room code' });
}
// Start room monitoring (will hand off to player count check when game starts)
startMonitor(sessionId, gameId, game.room_code, game.max_players);
res.json({
message: 'Room monitor started',
status: 'monitoring'
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Stop player count check for a session game (admin only)
router.post('/:sessionId/games/:gameId/stop-player-check', authenticateToken, (req, res) => {
try {
const { sessionId, gameId } = req.params;
// Stop both room monitor and player count check
stopMonitor(sessionId, gameId);
res.json({
message: 'Room monitor and player count check stopped',
status: 'stopped'
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Manually update player count for a session game (admin only)
router.patch('/:sessionId/games/:gameId/player-count', authenticateToken, (req, res) => {
try {
const { sessionId, gameId } = req.params;
const { player_count } = req.body;
if (player_count === undefined || player_count === null) {
return res.status(400).json({ error: 'player_count is required' });
}
const count = parseInt(player_count);
if (isNaN(count) || count < 0) {
return res.status(400).json({ error: 'player_count must be a positive number' });
}
// Update the player count
const result = db.prepare(`
UPDATE session_games
SET player_count = ?, player_count_check_status = 'completed'
WHERE session_id = ? AND id = ?
`).run(count, sessionId, gameId);
if (result.changes === 0) {
return res.status(404).json({ error: 'Session game not found' });
}
// Broadcast via WebSocket
const wsManager = getWebSocketManager();
if (wsManager) {
wsManager.broadcastEvent('player-count.updated', {
sessionId,
gameId,
playerCount: count,
status: 'completed'
}, parseInt(sessionId));
}
res.json({
message: 'Player count updated successfully',
player_count: count
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;