From 899770ed02cfce4665b841826917425ef59999c2 Mon Sep 17 00:00:00 2001 From: sravan Date: Wed, 1 Jul 2026 21:28:53 +0530 Subject: [PATCH] feat: Windows download link + self-served update feed + install tracking Downloads/updates: - config: DOWNLOADS_DIR (override to a mounted volume in prod). - static.js: serves /downloads/* (installer, latest.yml, .blockmap) with range support for resumable + differential auto-updates; /download/windows redirects to the current .exe (stable link). Landing page gets a "Download the Windows desktop app" button (hidden in-app). Install tracking (who installed the app): - db app_installs + repos.appInstalls (upsert by install_id, fills in the user on sign-in). - POST /api/v1/telemetry/install (records install + user once authenticated); GET /api/v1/admin/installs (admin: list installs with user/version/os/last-seen). - desktop main.js: stable per-install id in userData, exposed via preload (bizConnectNative.installId/version/os); home.html reportInstall() posts it after login. - e2e: +2 checks (telemetry recorded, admin sees it). 119/119. build batch20. Co-Authored-By: Claude Opus 4.8 --- .gitignore | 1 + desktop/main.js | 19 +++++++++++++++++++ desktop/preload.js | 8 +++++++- server/config.js | 5 +++++ server/db.js | 19 +++++++++++++++++++ server/public/home.html | 10 +++++++++- server/public/index.html | 3 +++ server/repos.js | 18 +++++++++++++++++- server/routes.js | 18 ++++++++++++++++++ server/static.js | 40 +++++++++++++++++++++++++++++++++++++++- server/test/e2e.js | 6 ++++++ 11 files changed, 143 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 0fae3f3..fca2b26 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ mobile/ios/ server/recordings/ server/transcripts/ server/uploads/ +server/downloads/ # OS files .DS_Store diff --git a/desktop/main.js b/desktop/main.js index b9518f3..942f86e 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -10,6 +10,25 @@ // Server origin is configurable so the same build works against prod or a dev server. const { app, BrowserWindow, session, desktopCapturer, shell, Menu, ipcMain, nativeImage } = require('electron'); const path = require('path'); +const fs = require('fs'); +const os = require('os'); +const crypto = require('crypto'); + +// A stable per-install id (persisted in userData), so the server can count installs and +// associate them with the user who signs in. Created once, then reused across launches. +function getInstallId() { + try { + const p = path.join(app.getPath('userData'), 'install-id'); + if (fs.existsSync(p)) { const v = fs.readFileSync(p, 'utf8').trim(); if (v) return v; } + const id = crypto.randomUUID(); + fs.writeFileSync(p, id); + return id; + } catch (_) { return 'unknown'; } +} +// The renderer (web app) reads this synchronously to report telemetry after login. +ipcMain.on('get-install-info', (e) => { + e.returnValue = { installId: getInstallId(), appVersion: app.getVersion(), os: process.platform + ' ' + os.release() }; +}); // Auto-update: only NATIVE shell changes (this .exe) need this — all web/UI changes arrive // live from the server. Checks the self-hosted feed (publish config in package.json → // https://remote.bizgaze.com/downloads/latest.yml), downloads in the background, and installs diff --git a/desktop/preload.js b/desktop/preload.js index 4aaa106..6d13f59 100644 --- a/desktop/preload.js +++ b/desktop/preload.js @@ -3,10 +3,16 @@ // or prefer native push). No Node APIs are exposed to page JS. const { contextBridge, ipcRenderer } = require('electron'); +// Pull the stable install id + version/os from main (synchronous, one-time at preload). +let info = { installId: '', appVersion: '', os: '' }; +try { info = ipcRenderer.sendSync('get-install-info') || info; } catch (_) {} + contextBridge.exposeInMainWorld('__NATIVE__', 'desktop'); contextBridge.exposeInMainWorld('bizConnectNative', Object.freeze({ platform: 'desktop', - version: process.env.npm_package_version || '0.1.0', + version: info.appVersion || '0.1.0', + installId: info.installId || '', + os: info.os || '', // Bring the app window to the foreground (e.g. when a notification is clicked) — the web // Notification's window.focus() can't raise an Electron window; the main process must. focusApp: () => ipcRenderer.send('focus-window'), diff --git a/server/config.js b/server/config.js index 07aafbf..25a25d2 100644 --- a/server/config.js +++ b/server/config.js @@ -6,9 +6,13 @@ const PUBLIC_DIR = path.join(__dirname, 'public'); const REC_DIR = path.join(__dirname, 'recordings'); const TRANS_DIR = path.join(__dirname, 'transcripts'); const UPLOADS_DIR = path.join(__dirname, 'uploads'); +// Desktop installers + auto-update feed (latest.yml). Override with DOWNLOADS_DIR to point at a +// mounted volume in production; IT drops the electron-builder dist/ output here. +const DOWNLOADS_DIR = process.env.DOWNLOADS_DIR || path.join(__dirname, 'downloads'); try { fs.mkdirSync(REC_DIR, { recursive: true }); } catch (e) {} try { fs.mkdirSync(TRANS_DIR, { recursive: true }); } catch (e) {} try { fs.mkdirSync(UPLOADS_DIR, { recursive: true }); } catch (e) {} +try { fs.mkdirSync(DOWNLOADS_DIR, { recursive: true }); } catch (e) {} module.exports = { PORT: process.env.PORT || 8090, @@ -17,6 +21,7 @@ module.exports = { REC_DIR, TRANS_DIR, UPLOADS_DIR, + DOWNLOADS_DIR, SESSION_TTL: 1000 * 60 * 60 * 24, // 24h access-token / cookie lifetime REFRESH_TTL: 1000 * 60 * 60 * 24 * 90, // 90d refresh-token lifetime (native clients) }; diff --git a/server/db.js b/server/db.js index c44999e..7104bba 100644 --- a/server/db.js +++ b/server/db.js @@ -321,6 +321,25 @@ CREATE TABLE IF NOT EXISTS device_tokens ( CREATE INDEX IF NOT EXISTS idx_devtok_user ON device_tokens(user_id); `); +// App installs (desktop/mobile clients): one row per install, associated with the user once +// they sign in. Lets admins see who installed the app, which version, and when it was last used. +db.exec(` +CREATE TABLE IF NOT EXISTS app_installs ( + id TEXT PRIMARY KEY, + install_id TEXT NOT NULL UNIQUE, + user_id TEXT, + user_email TEXT, + tenant_id TEXT, + platform TEXT, + app_version TEXT, + os TEXT, + first_seen INTEGER NOT NULL, + last_seen INTEGER NOT NULL +); +CREATE INDEX IF NOT EXISTS idx_installs_tenant ON app_installs(tenant_id); +CREATE INDEX IF NOT EXISTS idx_installs_user ON app_installs(user_id); +`); + // Favourite conversations (per user). target = 'dm:' or 'group:'. db.exec(` CREATE TABLE IF NOT EXISTS favorites ( diff --git a/server/public/home.html b/server/public/home.html index fc963e7..c7ac4ee 100644 --- a/server/public/home.html +++ b/server/public/home.html @@ -731,7 +731,7 @@ - +
Loading…
@@ -1831,6 +1831,13 @@ function urlB64ToUint8(base64){ const pad='='.repeat((4-base64.length%4)%4); con // FCM/APNs (see server/push.js). No bundler needed — plugins live on Capacitor.Plugins. function capPlugin(name){ const C=window.Capacitor; return (C && C.isNativePlatform && C.isNativePlatform() && C.Plugins && C.Plugins[name]) ? C.Plugins[name] : null; } function nativePlatform(){ const C=window.Capacitor; if(C && C.getPlatform){ const p=C.getPlatform(); if(p==='ios'||p==='android') return p; } if(window.__NATIVE__==='desktop') return 'desktop'; return ''; } +// Record this install against the signed-in user (desktop shell exposes a stable installId). +// Fires post-login; the request carries the session so admins can see who installed the app. +function reportInstall(){ + try{ const n=window.bizConnectNative; if(!n||!n.installId) return; + postJSON('/api/v1/telemetry/install',{ installId:n.installId, platform:n.platform||'desktop', appVersion:n.version||'', os:n.os||'' }).catch(()=>{}); + }catch(_){} +} let _nativeTok=null; async function setupNativePush(){ const PN=capPlugin('PushNotifications'); const plat=nativePlatform(); @@ -2756,6 +2763,7 @@ async function doRegister(){ loadNotifs(); wireBell(); wireProfile(); placeHdrRight(); window.addEventListener('resize', placeHdrRight); // mobile: bell+profile live in the chat-list header (no top bar) connectChatWs(); + reportInstall(); // desktop/mobile shell: record this install against the signed-in user setupPush(); // register the notification service worker + subscribe to Web Push (if granted) { const cl=document.getElementById('chatlist'); if(cl) enablePullRefresh(cl, loadSidebar); } // pull-to-refresh the chat list setTimeout(maybeNotifPrompt, 1500); // gentle "enable notifications" prompt if still undecided (key for iOS PWA) diff --git a/server/public/index.html b/server/public/index.html index b08204c..9cdc2c0 100644 --- a/server/public/index.html +++ b/server/public/index.html @@ -67,6 +67,7 @@
BizGaze team member?
B Log in with BizGaze + Download the Windows desktop app
© BizGaze · Remote Support
@@ -76,6 +77,8 @@ function profileHTML(name){return '