From a0017f20361d4c8d265800080dd8980fa8c0bfea Mon Sep 17 00:00:00 2001 From: sravan Date: Tue, 14 Jul 2026 13:38:55 +0530 Subject: [PATCH] feat: detect new web builds + hard-refresh escape hatch (0.1.17/batch78) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of "the fix works on mobile but not on desktop/web": nothing was wrong with the code — the desktop app now CLOSES TO TRAY, so it can run for weeks on the page it loaded on day one and never re-fetch after a deploy. There was also no way to force it off a stale page (no menu bar → no reload accelerator). - Server: GET /api/build returns home.html's __BUILD marker. - Client: polls it (boot, on focus/visibility, every 10 min); when the server's build differs from the running one, shows a branded "A new version is available — Refresh" banner. Settings gains an always-available "Refresh app" with the current build shown. - hardReloadApp(): in the browser it unregisters service workers + clears CacheStorage then reloads cache-busted; in the shell it calls the native hard reload. - Desktop: hard-reload IPC (clears the session HTTP cache + reloadIgnoringCache), wired to Ctrl+R (reload), Ctrl+Shift+R / F5 (hard reload), and a "Refresh app (get latest)" tray item. Previously there was literally no way to clear the cache from the app. Co-Authored-By: Claude Opus 4.8 --- desktop/main.js | 25 +++++++++++++++++++++++++ desktop/package.json | 2 +- desktop/preload.js | 3 +++ server/public/home.html | 37 ++++++++++++++++++++++++++++++++++++- server/routes.js | 11 +++++++++++ 5 files changed, 76 insertions(+), 2 deletions(-) diff --git a/desktop/main.js b/desktop/main.js index 8193673..4565b43 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -83,6 +83,17 @@ function avatarToTempPng(src) { }); } +// ---- Hard refresh ------------------------------------------------------------------------------- +// The app closes to TRAY, so it can run for weeks on the page it first loaded and never see a new web +// deploy. This clears the shell's HTTP cache and reloads ignoring cache, so a stale UI is always +// recoverable. Reachable from: the in-app "Refresh" banner / Settings, Ctrl+R (reload), +// Ctrl+Shift+R or F5 (hard reload), and the tray menu. +async function hardReloadWin() { + try { await session.fromPartition('persist:bizconnect').clearCache(); } catch (_) {} + try { if (win && !win.isDestroyed()) win.webContents.reloadIgnoringCache(); } catch (_) {} +} +ipcMain.handle('hard-reload', async () => { await hardReloadWin(); return true; }); + // ---- Remote control: OS input injection for a screen the local user is SHARING ---- // The renderer (share flow) forwards a viewer's mouse/keyboard events here for injection. Injection is // HARD-GATED behind an explicit consent flag (rcArmed): nothing is injected until the local user clicks @@ -207,6 +218,7 @@ function createTray() { const showApp = () => { if (!win) return createWindow(); if (win.isMinimized()) win.restore(); win.show(); win.focus(); }; tray.setContextMenu(Menu.buildFromTemplate([ { label: 'Open Biz Connect', click: showApp }, + { label: 'Refresh app (get latest)', click: () => { showApp(); hardReloadWin(); } }, { type: 'separator' }, { label: 'Quit', click: () => { isQuitting = true; app.quit(); } }, ])); @@ -257,6 +269,19 @@ function createWindow() { win.once('ready-to-show', reveal); setTimeout(reveal, 12000); + // The menu bar is hidden, so the usual reload accelerators don't exist — wire them by hand. Without + // these there was literally no way to force the app off a stale page. + win.webContents.on('before-input-event', (e, input) => { + if (input.type !== 'keyDown') return; + const k = String(input.key || '').toLowerCase(); + const mod = input.control || input.meta; + if ((mod && k === 'r') || k === 'f5') { + e.preventDefault(); + if (input.shift || k === 'f5') hardReloadWin(); // hard: clear cache + reload + else { try { win.webContents.reload(); } catch (_) {} } // plain reload + } + }); + // Close = hide to tray (keep running for notifications). First time, tell the user where it went. let toldTray = false; win.on('close', (e) => { diff --git a/desktop/package.json b/desktop/package.json index 441ee8a..2e60a5e 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,6 +1,6 @@ { "name": "biz-connect-desktop", - "version": "0.1.16", + "version": "0.1.17", "description": "Biz Connect technician desktop client — loads the Connect web UI with native screen capture", "author": { "name": "BizGaze", diff --git a/desktop/preload.js b/desktop/preload.js index 6c9dcd9..7a0c486 100644 --- a/desktop/preload.js +++ b/desktop/preload.js @@ -34,6 +34,9 @@ contextBridge.exposeInMainWorld('bizConnectNative', Object.freeze({ rcAvailable: () => { try { return !!ipcRenderer.sendSync('rc-available'); } catch (_) { return false; } }, rcArm: (on) => { try { ipcRenderer.send('rc-arm', !!on); } catch (_) {} }, rcInput: (evt) => { try { ipcRenderer.send('rc-input', evt); } catch (_) {} }, + // Force the newest web build: clears the shell's HTTP cache and reloads ignoring cache. The app closes + // to tray and can run for weeks, so without this it would keep serving the page it first loaded. + hardReload: () => { try { return ipcRenderer.invoke('hard-reload'); } catch (_) { return Promise.resolve(false); } }, // Manual "Check for updates" from Settings. Resolves {status:'available'|'current'|'dev'|'error', version?}. // On 'available' the shell downloads in the background and prompts to restart when ready. checkForUpdates: () => ipcRenderer.invoke('check-updates'), diff --git a/server/public/home.html b/server/public/home.html index 3bffc64..e6b2917 100644 --- a/server/public/home.html +++ b/server/public/home.html @@ -1002,7 +1002,7 @@ - @@ -1225,10 +1225,13 @@ function openSettings(){ +sw('setDm','Direct message notifications', notifOn('dm')) +'' +(window.bizConnectNative?(''):'') + // Always-available escape hatch: force the newest web build (clears cache / service worker). + +'' +'
These preferences are saved on this device.
'; document.body.appendChild(ov); ov.onclick=e=>{ if(e.target===ov) ov.remove(); }; ov.querySelector('#setClose').onclick=()=>ov.remove(); + { const rb=ov.querySelector('#setRefresh'); if(rb) rb.onclick=()=>{ ov.remove(); hardReloadApp(); }; } const updBtn=ov.querySelector('#setUpd'); // #12: desktop version + manual update check if(updBtn) updBtn.onclick=async()=>{ if(_pendingUpdate&&_pendingUpdate.phase==='ready'){ try{ window.bizConnectNative.restartToUpdate&&window.bizConnectNative.restartToUpdate(); }catch(_){} return; } // update already downloaded → restart @@ -2775,6 +2778,38 @@ function wireUpdateBanner(){ else if(d.phase==='current'||d.phase==='error'){ if(el){ el.classList.remove('show'); setTimeout(()=>{ if(el&&!el.classList.contains('show')) el.remove(); }, 400); } } }); } +// ---- New-web-build detection ---------------------------------------------------------------- +// The desktop app closes to TRAY, so it can stay open for weeks and keep running the page it loaded on +// day one — a deploy would never reach it (that's why fixes "worked on mobile but not on desktop"). +// Poll the server's build marker; when it changes, offer a Refresh. Also exposed in Settings. +let _newBuild=null; +async function checkWebBuild(){ + try{ + const r=await fetch('/api/build',{cache:'no-store'}); if(!r.ok) return; + const d=await r.json(); + if(d && d.build && window.__BUILD && d.build!==window.__BUILD && d.build!==_newBuild){ _newBuild=d.build; showRefreshBanner(); } + }catch(_){} +} +function showRefreshBanner(){ + if(document.getElementById('refreshBanner')) return; + const el=document.createElement('div'); el.id='refreshBanner'; el.className='upd-banner show ready'; + el.innerHTML=ic('download',15)+' A new version of Biz Connect is available '; + document.body.appendChild(el); + el.querySelector('#rbGo').onclick=()=>hardReloadApp(); +} +// Hard refresh: on desktop this clears the shell's HTTP cache and reloads ignoring cache. In a browser we +// also drop any service-worker + CacheStorage entries, then reload — so there's no stale-code dead end. +async function hardReloadApp(){ + if(meetState==='call' && !(await bzConfirm('You are in a call. Refreshing will leave it.', {title:'Refresh now?', okText:'Refresh'}))) return; + try{ const n=window.bizConnectNative; if(n && n.hardReload){ n.hardReload(); return; } }catch(_){} + try{ if('serviceWorker' in navigator){ const rs=await navigator.serviceWorker.getRegistrations(); await Promise.all(rs.map(r=>r.unregister().catch(()=>{}))); } }catch(_){} + try{ if(window.caches && caches.keys){ const ks=await caches.keys(); await Promise.all(ks.map(k=>caches.delete(k).catch(()=>{}))); } }catch(_){} + try{ location.replace(location.pathname+'?_r='+Date.now()); }catch(_){ location.reload(); } +} +setInterval(checkWebBuild, 10*60*1000); // every 10 min +window.addEventListener('focus', checkWebBuild); // and whenever the user comes back to the app +document.addEventListener('visibilitychange', ()=>{ if(!document.hidden) checkWebBuild(); }); +setTimeout(checkWebBuild, 4000); // shortly after boot // Small 'i' badge on the profile button when an update is pending; clicking opens Settings. function applyUpdateBadge(){ document.querySelectorAll('.upd-dot').forEach(x=>x.remove()); diff --git a/server/routes.js b/server/routes.js index 4517fa4..e650975 100644 --- a/server/routes.js +++ b/server/routes.js @@ -917,6 +917,17 @@ route('GET', '/api/meetings/config', (req, res) => { json(res, 200, { sfu: LIVEKIT_ENABLED, url: LIVEKIT_ENABLED ? LIVEKIT_URL : '' }); }); +// The web build currently on the server (home.html's __BUILD marker). Long-running clients poll this and +// offer a Refresh when it changes. This matters because the desktop app now CLOSES TO TRAY — it can run +// for weeks without ever reloading the page, so it would silently keep serving stale code after a deploy. +let APP_BUILD = ''; +try { + const h = fs.readFileSync(path.join(require('./config').PUBLIC_DIR, 'home.html'), 'utf8'); + const m = /__BUILD='([^']+)'/.exec(h); + if (m) APP_BUILD = m[1]; +} catch (_) {} +route('GET', '/api/build', (req, res) => json(res, 200, { build: APP_BUILD })); + // Mint a LiveKit join token for the signed-in user + a specific room (the 6-digit meeting code). // The room-membership/host authorization already happens over the meeting WebSocket; this only // hands the client a media-plane credential scoped to that room and its own identity.