diff --git a/backend/routes/sessions.js b/backend/routes/sessions.js index 9952cd3..a4732d2 100644 --- a/backend/routes/sessions.js +++ b/backend/routes/sessions.js @@ -314,6 +314,14 @@ router.post('/:id/games', authenticateToken, (req, res) => { 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 @@ -590,8 +598,17 @@ router.patch('/:sessionId/games/:gameId/status', authenticateToken, (req, res) = return res.status(400).json({ error: 'Invalid status. Must be playing, played, or skipped' }); } - // If setting to 'playing', first set all other games in session to 'played' or keep as '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 diff --git a/backend/utils/ecast-shard-client.js b/backend/utils/ecast-shard-client.js index e3c68ab..6a0a3a2 100644 --- a/backend/utils/ecast-shard-client.js +++ b/backend/utils/ecast-shard-client.js @@ -126,7 +126,16 @@ class EcastShardClient { return new Promise((resolve) => { const url = `wss://${this.host}/api/v2/rooms/${this.roomCode}/play?role=shard&name=GamePickerProbe&format=json`; let resolved = false; - const done = () => { if (!resolved) { resolved = true; resolve(); } }; + let welcomed = false; + const done = (probe) => { + if (!resolved) { + resolved = true; + if (probe) { + try { probe.removeAllListeners(); probe.terminate(); } catch (_) {} + } + resolve(); + } + }; try { const probe = new WebSocket(url, ['ecast-v0'], { @@ -134,15 +143,14 @@ class EcastShardClient { handshakeTimeout: 8000, }); - const timeout = setTimeout(() => { - try { probe.close(); } catch (_) {} - done(); - }, 10000); + const timeout = setTimeout(() => done(probe), 10000); probe.on('message', (data) => { + if (welcomed) return; try { const msg = JSON.parse(data.toString()); if (msg.opcode === 'client/welcome') { + welcomed = true; const { playerCount, playerNames } = EcastShardClient.parsePlayersFromHere(msg.result.here); if (playerCount > this.playerCount || playerNames.length !== this.playerNames.length) { this.playerCount = playerCount; @@ -165,14 +173,13 @@ class EcastShardClient { } } catch (_) {} clearTimeout(timeout); - try { probe.close(); } catch (_) {} - done(); + done(probe); }); - probe.on('error', () => { clearTimeout(timeout); done(); }); - probe.on('close', () => { clearTimeout(timeout); done(); }); + probe.on('error', () => { clearTimeout(timeout); done(probe); }); + probe.on('close', () => { clearTimeout(timeout); done(null); }); } catch (_) { - done(); + done(null); } }); } @@ -532,11 +539,17 @@ function broadcastAndPersist(sessionId, gameId) { } if (['room.connected', 'lobby.player-joined', 'game.started', 'game.ended'].includes(eventType)) { - const status = eventType === 'game.ended' ? 'completed' : 'monitoring'; + const checkStatus = eventType === 'game.ended' ? 'completed' : 'monitoring'; try { - db.prepare( - 'UPDATE session_games SET player_count = ?, player_count_check_status = ? WHERE session_id = ? AND id = ?' - ).run(eventData.playerCount ?? null, status, sessionId, gameId); + if (eventType === 'game.ended') { + db.prepare( + 'UPDATE session_games SET player_count = ?, player_count_check_status = ?, status = ? WHERE session_id = ? AND id = ?' + ).run(eventData.playerCount ?? null, checkStatus, 'played', sessionId, gameId); + } else { + db.prepare( + 'UPDATE session_games SET player_count = ?, player_count_check_status = ? WHERE session_id = ? AND id = ?' + ).run(eventData.playerCount ?? null, checkStatus, sessionId, gameId); + } } catch (e) { console.error('[Shard Monitor] DB update failed:', e.message); } @@ -544,15 +557,22 @@ function broadcastAndPersist(sessionId, gameId) { if (eventType === 'room.disconnected') { const reason = eventData.reason; - const status = + const checkStatus = reason === 'room_closed' ? 'completed' : reason === 'manually_stopped' ? 'stopped' : 'failed'; try { const game = db - .prepare('SELECT player_count_check_status FROM session_games WHERE session_id = ? AND id = ?') + .prepare('SELECT player_count_check_status, status FROM session_games WHERE session_id = ? AND id = ?') .get(sessionId, gameId); if (game && game.player_count_check_status !== 'completed') { db.prepare('UPDATE session_games SET player_count_check_status = ? WHERE session_id = ? AND id = ?').run( - status, + checkStatus, + sessionId, + gameId + ); + } + if (game && reason === 'room_closed' && game.status === 'playing') { + db.prepare('UPDATE session_games SET status = ? WHERE session_id = ? AND id = ?').run( + 'played', sessionId, gameId ); diff --git a/frontend/src/pages/Picker.jsx b/frontend/src/pages/Picker.jsx index 8f6414a..5b82647 100644 --- a/frontend/src/pages/Picker.jsx +++ b/frontend/src/pages/Picker.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { useAuth } from '../context/AuthContext'; import api from '../api/axios'; @@ -952,17 +952,33 @@ function SessionInfo({ sessionId, onGamesUpdate, playingGame, setPlayingGame }) const [editingPlayerCount, setEditingPlayerCount] = useState(null); const [newPlayerCount, setNewPlayerCount] = useState(''); + const playingGameRef = useRef(playingGame); + playingGameRef.current = playingGame; + const loadGames = useCallback(async () => { try { const response = await api.get(`/sessions/${sessionId}/games`); - // Reverse chronological order (most recent first) - setGames(response.data.reverse()); + const freshGames = response.data; + setGames(freshGames.reverse()); + + const currentPlaying = freshGames.find(g => g.status === 'playing'); + const prev = playingGameRef.current; + if (currentPlaying) { + if (!prev || prev.id !== currentPlaying.id || prev.player_count !== currentPlaying.player_count) { + setPlayingGame(currentPlaying); + } + } else if (prev) { + const still = freshGames.find(g => g.id === prev.id); + if (!still || still.status !== 'playing') { + setPlayingGame(null); + } + } } catch (err) { console.error('Failed to load session games'); } finally { setLoading(false); } - }, [sessionId]); + }, [sessionId, setPlayingGame]); useEffect(() => { loadGames();