Skip to content

Commit 85e0e7a

Browse files
Markus Holzhäuserclaude
authored andcommitted
fix: WebSocket reconnect resilience + admin wake lock (#646, #647)
#647: Admin page now keeps screen awake during all active game phases, with visibilitychange handler and wake lock on page reload/reconnect. #646: Fix cascade of issues causing mobile players to get locked out: - Reconnect loop now stays on session-based reconnect while cookie exists - Session cookie no longer cleared on transient SESSION_NOT_FOUND - Wake lock activated from join_ack (not just PLAYING phase) - Faster reconnect backoff (500ms for first 3 attempts, then linear) - Server-side add_player() checks actual WS status to resolve race condition where reload + async disconnect = NAME_TAKEN Closes #646, closes #647 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9f42784 commit 85e0e7a

5 files changed

Lines changed: 45 additions & 6 deletions

File tree

custom_components/beatify/game/player_registry.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,16 @@ def add_player(
8888
existing_player.connected = True
8989
_LOGGER.info("Player reconnected: %s", existing_name)
9090
return True, None
91+
# #646: Check if the old WS is actually dead (race condition
92+
# where _handle_disconnect hasn't run yet after browser reload)
93+
if existing_player.ws is None or existing_player.ws.closed:
94+
_LOGGER.info(
95+
"Player %s: stale connected flag, old WS closed — allowing rejoin",
96+
existing_name,
97+
)
98+
existing_player.ws = ws
99+
existing_player.connected = True
100+
return True, None
91101
return False, ERR_NAME_TAKEN
92102

93103
# Check player limit

custom_components/beatify/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@
1212
"documentation": "https://github.com/mholzi/beatify",
1313
"iot_class": "local_push",
1414
"issue_tracker": "https://github.com/mholzi/beatify/issues",
15-
"version": "3.0.4"
15+
"version": "3.0.5"
1616
}

custom_components/beatify/www/js/admin.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ function _releaseWakeLock() {
5555
}
5656
}
5757

58+
// #647: Re-acquire wake lock when admin tab becomes visible during an active game
59+
document.addEventListener('visibilitychange', function() {
60+
if (document.visibilityState === 'visible' && currentGame && currentGame.phase !== 'END') {
61+
_requestWakeLock();
62+
}
63+
});
64+
5865
// Module-level state
5966
let selectedPlaylists = [];
6067
let playlistData = [];
@@ -233,6 +240,7 @@ async function loadStatus() {
233240
// Check for active game and show appropriate view
234241
if (status.active_game && status.active_game.phase === 'LOBBY') {
235242
currentGame = status.active_game;
243+
_requestWakeLock(); // #647: keep screen on when reconnecting to active game
236244
showLobbyView(status.active_game);
237245
// Issue #477: Reconnect admin WS if we have a token
238246
if (!adminWs || adminWs.readyState !== WebSocket.OPEN) {
@@ -2901,6 +2909,13 @@ function handleAdminWsMessage(data) {
29012909
function handleAdminStateUpdate(data) {
29022910
currentGame = data;
29032911

2912+
// #647: Wake lock for all active game phases
2913+
if (['LOBBY', 'PLAYING', 'REVEAL', 'PAUSED'].includes(data.phase)) {
2914+
_requestWakeLock();
2915+
} else {
2916+
_releaseWakeLock();
2917+
}
2918+
29042919
// Hide all phase sections first
29052920
var sections = ['setup-container', 'lobby-section', 'existing-game-section',
29062921
'admin-playing-section', 'admin-reveal-section', 'admin-end-section'];

custom_components/beatify/www/js/admin.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

custom_components/beatify/www/js/player-core.js

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,9 @@ function getStoredLanguage() {
164164
// ============================================
165165

166166
function getReconnectDelay() {
167-
return Math.min(1000 * Math.pow(2, state.reconnectAttempts), MAX_RECONNECT_DELAY_MS);
167+
// #646: First 3 attempts are fast (500ms), then linear ramp to cap
168+
if (state.reconnectAttempts <= 3) return 500;
169+
return Math.min(1000 * (state.reconnectAttempts - 2), MAX_RECONNECT_DELAY_MS);
168170
}
169171

170172
function showConnectionIndicator() {
@@ -339,7 +341,12 @@ function connectWithSession() {
339341

340342
var delay = getReconnectDelay();
341343
console.log('WebSocket closed. Reconnecting in ' + delay + 'ms... (attempt ' + state.reconnectAttempts + ')');
342-
setTimeout(function() { connectWebSocket(state.playerName); }, delay);
344+
// #646: Keep using session reconnect while cookie exists
345+
if (getSessionCookie()) {
346+
setTimeout(connectWithSession, delay);
347+
} else {
348+
setTimeout(function() { connectWebSocket(state.playerName); }, delay);
349+
}
343350
} else if (state.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
344351
state.isReconnecting = false;
345352
hideReconnectingOverlay();
@@ -545,6 +552,8 @@ function handleServerMessage(data) {
545552
clearStoredPlayerName();
546553
}
547554
} else if (data.type === 'join_ack') {
555+
// #646: Request wake lock early — not just during PLAYING
556+
requestWakeLock();
548557
if (data.session_id) {
549558
setSessionCookie(data.session_id);
550559
}
@@ -593,6 +602,11 @@ function handleServerMessage(data) {
593602
return;
594603
}
595604
if (data.code === 'SESSION_NOT_FOUND') {
605+
// #646: Don't clear session cookie during reconnect — may be transient
606+
if (state.isReconnecting) {
607+
console.warn('SESSION_NOT_FOUND during reconnect, will retry with session');
608+
return;
609+
}
596610
clearSessionCookie();
597611
state.intentionalLeave = true;
598612
if (state.ws) {
@@ -935,8 +949,8 @@ if ('serviceWorker' in navigator) {
935949
// reconnect if the socket is dead — without waiting for the onclose backoff timer.
936950
document.addEventListener('visibilitychange', function() {
937951
if (document.visibilityState === 'visible') {
938-
// #622: Re-acquire wake lock when tab becomes visible during an active game
939-
if (state.currentRoundNumber > 0) {
952+
// #646: Re-acquire wake lock when tab becomes visible during any active session
953+
if (state.playerName) {
940954
requestWakeLock();
941955
}
942956
var ws = state.ws;

0 commit comments

Comments
 (0)