diff --git a/client/public/version.js b/client/public/version.js index 8252c1d..93a1e1e 100644 --- a/client/public/version.js +++ b/client/public/version.js @@ -1,5 +1,5 @@ // Maintainer-controlled web client version. // Format: YYYY.MM.DD Rn (example: 2026.02.20 R2) -window.CHGRID_WEB_VERSION = "2026.02.22 R167"; +window.CHGRID_WEB_VERSION = "2026.02.22 R168"; // Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid. window.CHGRID_TIME_ZONE = "America/Detroit"; diff --git a/client/src/main.ts b/client/src/main.ts index 588e039..21faf6b 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -80,6 +80,7 @@ const MIC_INPUT_GAIN_STEP = 0.05; const HEARTBEAT_INTERVAL_MS = 10_000; const RECONNECT_DELAY_MS = 5_000; const RECONNECT_MAX_ATTEMPTS = 3; +const AUTO_RECONNECT_AFTER_RELOAD_KEY = 'chatGridAutoReconnectAfterReload'; declare global { interface Window { @@ -526,6 +527,19 @@ function handleSignalingStatus(message: string): void { pushChatMessage(message); } +/** Performs cache-busted navigation and marks session for one-time auto-connect. */ +function reloadClientForVersion(version: string): void { + try { + sessionStorage.setItem(AUTO_RECONNECT_AFTER_RELOAD_KEY, '1'); + } catch { + // Ignore sessionStorage failures. + } + const nextUrl = new URL(window.location.href); + nextUrl.searchParams.set('v', version || 'unknown'); + nextUrl.searchParams.set('t', String(Date.now())); + window.location.replace(nextUrl.toString()); +} + /** Appends a chat/system line to the bounded status history buffer. */ function pushChatMessage(message: string): void { messageBuffer.push(message); @@ -1140,6 +1154,10 @@ async function reconnectWithRetry(reason: 'heartbeat' | 'socketClose'): Promise< for (let attempt = 1; attempt <= RECONNECT_MAX_ATTEMPTS; attempt += 1) { await new Promise((resolve) => window.setTimeout(resolve, RECONNECT_DELAY_MS)); await connect(); + const waitStartedAt = Date.now(); + while (!state.running && Date.now() - waitStartedAt < 4_000) { + await new Promise((resolve) => window.setTimeout(resolve, 100)); + } if (state.running) { reconnectInFlight = false; return; @@ -1159,7 +1177,12 @@ function getConnectionFlowDeps(): ConnectFlowDeps { state, dom, sanitizeName, - updateStatus: (message) => pushChatMessage(message), + updateStatus: (message) => { + if (reconnectInFlight && message === 'Disconnected.') { + return; + } + pushChatMessage(message); + }, updateConnectAvailability, settingsSaveNickname: (value) => settings.saveNickname(value), mediaIsConnecting: () => mediaSession.isConnecting(), @@ -1274,7 +1297,7 @@ async function onSignalingMessage(message: IncomingMessage): Promise { reloadScheduledForVersionMismatch = true; pushChatMessage(`Server version ${incomingVersion} detected. Reloading client...`); window.setTimeout(() => { - window.location.reload(); + reloadClientForVersion(incomingVersion); }, 50); return; } @@ -1454,8 +1477,10 @@ function handleNormalModeInput(code: string, shiftKey: boolean): void { state.mode = 'listItems'; const first = state.items.get(state.sortedItemIds[0]); if (first) { + const itemCount = state.sortedItemIds.length; + const itemLabelText = itemCount === 1 ? 'item' : 'items'; updateStatus( - `List: ${itemLabel(first)}, ${distanceDirectionPhrase(state.player.x, state.player.y, first.x, first.y)}, ${first.x}, ${first.y}`, + `${itemCount} ${itemLabelText}. List: ${itemLabel(first)}, ${distanceDirectionPhrase(state.player.x, state.player.y, first.x, first.y)}, ${first.x}, ${first.y}`, ); } audio.sfxUiBlip(); @@ -1566,8 +1591,10 @@ function handleNormalModeInput(code: string, shiftKey: boolean): void { state.mode = 'listUsers'; const first = state.peers.get(state.sortedPeerIds[0]); if (first) { + const userCount = state.sortedPeerIds.length; + const userLabelText = userCount === 1 ? 'user' : 'users'; updateStatus( - `List: ${first.nickname}, ${distanceDirectionPhrase(state.player.x, state.player.y, first.x, first.y)}, ${first.x}, ${first.y}`, + `${userCount} ${userLabelText}. List: ${first.nickname}, ${distanceDirectionPhrase(state.player.x, state.player.y, first.x, first.y)}, ${first.x}, ${first.y}`, ); } audio.sfxUiBlip(); @@ -2171,3 +2198,13 @@ if (storedNickname) { updateConnectAvailability(); updateDeviceSummary(); updateStatus('Welcome to the Chat Grid. Press the Settings button to configure your audio, then Connect to join the grid.'); +try { + if (sessionStorage.getItem(AUTO_RECONNECT_AFTER_RELOAD_KEY) === '1') { + sessionStorage.removeItem(AUTO_RECONNECT_AFTER_RELOAD_KEY); + if (storedNickname) { + void connect(); + } + } +} catch { + // Ignore sessionStorage failures. +} diff --git a/deploy/README.md b/deploy/README.md index e2e822d..54f77ab 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -103,6 +103,7 @@ cd /home/bestmidi/chgrid Notes: - Replace `yourdomain.com` with your real domain. - Script copies `deploy/apache/chgrid-vhost-snippet.conf`, runs `rebuildhttpdconf`, then restarts Apache via WHM restart command. +- Snippet now includes no-cache headers for `/chgrid/` and `/chgrid/index.html` so client updates are not stuck on stale HTML. ## 7) Optional HTTPS relay for HTTP radio streams diff --git a/deploy/apache/chgrid-vhost-snippet.conf b/deploy/apache/chgrid-vhost-snippet.conf index 033e8f5..ba7680e 100644 --- a/deploy/apache/chgrid-vhost-snippet.conf +++ b/deploy/apache/chgrid-vhost-snippet.conf @@ -1,7 +1,15 @@ # Add inside your SSL VirtualHost include for bestmidi.com. # Keep your existing main DocumentRoot unchanged when hosting Chat Grid under /chgrid. # Required modules: proxy, proxy_http, proxy_wstunnel +# Optional but recommended modules for client update freshness: headers, setenvif # Proxy websocket signaling endpoint to local Python service. ProxyPass /ws ws://127.0.0.1:8765 ProxyPassReverse /ws ws://127.0.0.1:8765 + +# Ensure HTML entrypoint is never cached so version updates are picked up quickly. + + Header set Cache-Control "no-store, no-cache, must-revalidate" + Header set Pragma "no-cache" + Header set Expires "0" +