Add stale data banner when game state hasn't updated in 20s
Subtle top banner with dimmed page (not a blocking overlay like the full disconnect state). Shows elapsed time since last update. Separate from the hard disconnect which triggers after 3 fetch failures. Debug panel: added 'Stale' button to test.
This commit is contained in:
parent
34a4f47402
commit
dd4787615d
1 changed files with 68 additions and 0 deletions
|
|
@ -140,6 +140,19 @@ h1 { color: #fff; font-size: 1.5em; text-align: center; }
|
||||||
.disconnected-overlay .retry { color: #555; font-size: 0.8em; margin-top: 15px; }
|
.disconnected-overlay .retry { color: #555; font-size: 0.8em; margin-top: 15px; }
|
||||||
body.greyed-out .container { filter: grayscale(80%) brightness(0.5); pointer-events: none; }
|
body.greyed-out .container { filter: grayscale(80%) brightness(0.5); pointer-events: none; }
|
||||||
|
|
||||||
|
/* ===== STALE STATE ===== */
|
||||||
|
.stale-banner {
|
||||||
|
position: fixed; top: 0; left: 0; right: 0; z-index: 400;
|
||||||
|
background: rgba(30, 30, 50, 0.92); border-bottom: 1px solid #444;
|
||||||
|
padding: 8px 20px; text-align: center; font-size: 0.85em; color: #aaa;
|
||||||
|
display: none; backdrop-filter: blur(2px);
|
||||||
|
animation: stale-fade-in 0.3s ease-out;
|
||||||
|
}
|
||||||
|
@keyframes stale-fade-in { from { opacity: 0; transform: translateY(-100%); } to { opacity: 1; transform: translateY(0); } }
|
||||||
|
.stale-banner .icon { margin-right: 6px; }
|
||||||
|
.stale-banner .age { color: #f39c12; font-weight: 500; }
|
||||||
|
body.stale .container { filter: brightness(0.8) saturate(0.7); }
|
||||||
|
|
||||||
/* ===== GAME OVER ===== */
|
/* ===== GAME OVER ===== */
|
||||||
.game-over-banner {
|
.game-over-banner {
|
||||||
position: fixed; top: 0; left: 0; right: 0;
|
position: fixed; top: 0; left: 0; right: 0;
|
||||||
|
|
@ -223,6 +236,12 @@ body.game-over .container { padding-top: 100px; }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Stale data banner -->
|
||||||
|
<div class="stale-banner" id="stale-banner">
|
||||||
|
<span class="icon">⏸️</span>
|
||||||
|
Game data hasn't updated in <span class="age" id="stale-age">--</span> — the bridge may have stopped
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Confetti canvas -->
|
<!-- Confetti canvas -->
|
||||||
<canvas id="confetti-canvas"></canvas>
|
<canvas id="confetti-canvas"></canvas>
|
||||||
|
|
||||||
|
|
@ -238,6 +257,7 @@ body.game-over .container { padding-top: 100px; }
|
||||||
<button onclick="debugState('zero')">Zero State (no game)</button>
|
<button onclick="debugState('zero')">Zero State (no game)</button>
|
||||||
<button onclick="debugState('playing')">Playing (demo)</button>
|
<button onclick="debugState('playing')">Playing (demo)</button>
|
||||||
<button onclick="debugState('gameover')">Game Over (confetti)</button>
|
<button onclick="debugState('gameover')">Game Over (confetti)</button>
|
||||||
|
<button onclick="debugState('stale')">Stale (no updates)</button>
|
||||||
<button onclick="debugState('disconnected')">Disconnected</button>
|
<button onclick="debugState('disconnected')">Disconnected</button>
|
||||||
<button onclick="debugState('reset')">Reset (live mode)</button>
|
<button onclick="debugState('reset')">Reset (live mode)</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -267,6 +287,8 @@ let retryCountdown = 0;
|
||||||
let retryTimer = null;
|
let retryTimer = null;
|
||||||
let gameOverShown = false; // only trigger confetti once per game
|
let gameOverShown = false; // only trigger confetti once per game
|
||||||
let confettiRunning = false;
|
let confettiRunning = false;
|
||||||
|
let lastStateTimestamp = null; // ISO string from state.lastUpdated
|
||||||
|
const STALE_THRESHOLD_MS = 20000;
|
||||||
|
|
||||||
function showView(view) {
|
function showView(view) {
|
||||||
currentView = view;
|
currentView = view;
|
||||||
|
|
@ -553,6 +575,37 @@ function isGameOver(state) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== STALE DETECTION =====
|
||||||
|
function checkStale() {
|
||||||
|
if (!lastStateTimestamp || currentView === 'zero' || currentView === 'gameover') {
|
||||||
|
hideStale();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const age = Date.now() - new Date(lastStateTimestamp).getTime();
|
||||||
|
if (age > STALE_THRESHOLD_MS) {
|
||||||
|
showStale(age);
|
||||||
|
} else {
|
||||||
|
hideStale();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showStale(ageMs) {
|
||||||
|
const secs = Math.floor(ageMs / 1000);
|
||||||
|
let ageText;
|
||||||
|
if (secs < 60) ageText = `${secs}s`;
|
||||||
|
else if (secs < 3600) ageText = `${Math.floor(secs / 60)}m ${secs % 60}s`;
|
||||||
|
else ageText = `${Math.floor(secs / 3600)}h ${Math.floor((secs % 3600) / 60)}m`;
|
||||||
|
|
||||||
|
document.getElementById('stale-age').textContent = ageText;
|
||||||
|
document.getElementById('stale-banner').style.display = 'block';
|
||||||
|
document.body.classList.add('stale');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideStale() {
|
||||||
|
document.getElementById('stale-banner').style.display = 'none';
|
||||||
|
document.body.classList.remove('stale');
|
||||||
|
}
|
||||||
|
|
||||||
// ===== CONNECTION MONITORING =====
|
// ===== CONNECTION MONITORING =====
|
||||||
function showDisconnected() {
|
function showDisconnected() {
|
||||||
if (currentView === 'disconnected') return;
|
if (currentView === 'disconnected') return;
|
||||||
|
|
@ -592,6 +645,10 @@ async function update() {
|
||||||
if (consecutiveFailures >= 3) hideDisconnected();
|
if (consecutiveFailures >= 3) hideDisconnected();
|
||||||
consecutiveFailures = 0;
|
consecutiveFailures = 0;
|
||||||
|
|
||||||
|
// Track last update time for stale detection
|
||||||
|
if (state.lastUpdated) lastStateTimestamp = state.lastUpdated;
|
||||||
|
checkStale();
|
||||||
|
|
||||||
// Determine what to show
|
// Determine what to show
|
||||||
if (!state.players || state.players.length === 0) {
|
if (!state.players || state.players.length === 0) {
|
||||||
// No players = no game
|
// No players = no game
|
||||||
|
|
@ -633,6 +690,7 @@ async function update() {
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
consecutiveFailures++;
|
consecutiveFailures++;
|
||||||
|
checkStale(); // still update stale timer on failures
|
||||||
if (consecutiveFailures >= 3) {
|
if (consecutiveFailures >= 3) {
|
||||||
showDisconnected();
|
showDisconnected();
|
||||||
}
|
}
|
||||||
|
|
@ -719,7 +777,9 @@ function debugState(mode) {
|
||||||
if (mode === 'reset') {
|
if (mode === 'reset') {
|
||||||
debugMode = null;
|
debugMode = null;
|
||||||
lastUpdate = '';
|
lastUpdate = '';
|
||||||
|
lastStateTimestamp = null;
|
||||||
hideDisconnected();
|
hideDisconnected();
|
||||||
|
hideStale();
|
||||||
update();
|
update();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -741,6 +801,14 @@ function debugState(mode) {
|
||||||
document.getElementById('winner-name').textContent = `🏆 ${winner} WINS!`;
|
document.getElementById('winner-name').textContent = `🏆 ${winner} WINS!`;
|
||||||
startConfetti();
|
startConfetti();
|
||||||
document.getElementById('status').textContent = 'Debug: Game Over';
|
document.getElementById('status').textContent = 'Debug: Game Over';
|
||||||
|
} else if (mode === 'stale') {
|
||||||
|
const st = demoPlayingState();
|
||||||
|
st.lastUpdated = new Date(Date.now() - 120000).toISOString(); // 2 min ago
|
||||||
|
lastStateTimestamp = st.lastUpdated;
|
||||||
|
showView('playing');
|
||||||
|
renderGame(st);
|
||||||
|
checkStale();
|
||||||
|
document.getElementById('status').textContent = 'Debug: Stale data';
|
||||||
} else if (mode === 'disconnected') {
|
} else if (mode === 'disconnected') {
|
||||||
// Show game underneath the overlay
|
// Show game underneath the overlay
|
||||||
const st = demoPlayingState();
|
const st = demoPlayingState();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue