diff --git a/brand-assets/ADD-BRAND-ASSETS.md b/brand-assets/ADD-BRAND-ASSETS.md
new file mode 100644
index 0000000..37b4fa3
--- /dev/null
+++ b/brand-assets/ADD-BRAND-ASSETS.md
@@ -0,0 +1,122 @@
+# TASK FOR CLAUDE (VS Code): Apply the Biz Connect brand — icons, splash, loaders, toasts
+
+Read this whole file, then execute it. Work CONSERVATIVELY — only ADD / restyle, do NOT
+refactor unrelated code. The app is live in production. Find the right spots in the CURRENT
+code yourself (don't assume old names). Summarise all changes and STOP for my review before
+committing anything.
+
+Repo root: C:\BizGaze_Support\remote-access-app
+Master brand assets are already placed in: brand-assets\
+ - app-icon-1024.png (1024 full-bleed app icon master)
+ - splash-2732-dark.png (2732 splash, brand blue — primary)
+ - splash-2732-light.png (2732 splash, white — optional light theme)
+ - loader-ring.svg (default spinner, light backgrounds)
+ - loader-orbit.svg (branded spinner, light backgrounds)
+ - loader-orbit-dark.svg (branded spinner, dark/overlay backgrounds)
+Brand colours: blue #1F3B73, blue-dark #16294F, yellow #FFC708, gold #E0AC00.
+
+Note: brand-assets/ is only the SOURCE. Put the GENERATED / used copies where each target
+needs them — web assets under server/public/ (served over HTTP); native icons/splash into the
+platform build folders (Electron / Android / iOS) as applicable.
+
+## 1. App icons (from brand-assets/app-icon-1024.png)
+Generate and place:
+- Windows .ico (multi-size 16/32/48/256) — fixes the tiny/blurry taskbar icon.
+- Favicon: server/public/favicon.ico + a 32px PNG; add to each page
:
+
+
+- PWA icons 192, 512, and a maskable 512; reference them in the web manifest, and set the
+ manifest "background_color" and "theme_color" to "#1F3B73".
+- Native mobile icons (Android mipmap set + adaptive, iOS AppIcon set) into their folders.
+
+## 2. Splash (from brand-assets/splash-2732-dark.png)
+- Web/PWA loading + Electron splash: brand-blue background #1F3B73 with the logo centred
+ (use the 2732 image, or reuse the app's splash.html if present).
+- Native launch screens: use the 2732 image (Android 12 SplashScreen background #1F3B73 + the
+ app icon; iOS LaunchScreen background #1F3B73 + centred logo). Keep native launch STATIC.
+- Use splash-2732-light.png only if a light theme is supported.
+
+## 3. Loaders (from brand-assets/loader-*.svg)
+Copy the three SVGs into server/public/ (e.g. server/public/loaders/). Wire them:
+- Everyday "Loading…" and in-app spinners -> loader-ring.svg
+- The hero "Connecting…" moment (session connect) -> loader-orbit.svg (light) / loader-orbit-dark.svg (on dark)
+Show one centered in the middle of the screen/panel while loading; hide when done. Always pair
+show with hide on BOTH the success and error paths so it can never get stuck.
+
+## 4. Branded notification toasts
+Create server/public/bizconnect-toast.css with EXACTLY:
+
+```css
+/* Biz Connect — branded notification toast. BZToast.success('…') / .error / .message / .info */
+.bzt-wrap{position:fixed;top:16px;right:16px;z-index:2147483600;display:flex;flex-direction:column;gap:10px;max-width:min(380px,92vw)}
+@supports(top:env(safe-area-inset-top)){.bzt-wrap{top:calc(16px + env(safe-area-inset-top));right:calc(16px + env(safe-area-inset-right))}}
+.bzt{display:flex;align-items:flex-start;gap:12px;background:#fff;color:#1f2430;border-radius:14px;padding:12px 14px;
+ box-shadow:0 12px 30px rgba(16,26,53,.20);border-left:5px solid #1F3B73;
+ transform:translateX(120%);opacity:0;transition:transform .3s cubic-bezier(.2,.7,.2,1),opacity .3s}
+.bzt.bzt-in{transform:translateX(0);opacity:1}
+.bzt.success{border-left-color:#16a34a}.bzt.error{border-left-color:#b91c1c}
+.bzt-badge{flex:none;width:34px;height:34px;border-radius:50%;display:grid;place-items:center;background:#1F3B73}
+.bzt.success .bzt-badge{background:#16a34a}.bzt.error .bzt-badge{background:#b91c1c}
+.bzt-badge svg{width:20px;height:20px}
+.bzt-body{flex:1;min-width:0;padding-top:1px}
+.bzt-title{font:700 13.5px/1.3 'Segoe UI',system-ui,sans-serif;color:#1F3B73;margin:0 0 1px}
+.bzt.success .bzt-title{color:#15803d}.bzt.error .bzt-title{color:#b91c1c}
+.bzt-msg{font:500 13px/1.4 'Segoe UI',system-ui,sans-serif;color:#3a4152;overflow-wrap:anywhere}
+.bzt-x{flex:none;background:none;border:none;color:#9aa3b2;cursor:pointer;font-size:18px;line-height:1;padding:2px 4px}
+.bzt-x:hover{color:#1f2430}
+@media(prefers-reduced-motion:reduce){.bzt{transition:opacity .2s}}
+```
+
+Create server/public/bizconnect-toast.js with EXACTLY:
+
+```js
+/* Biz Connect toast API. Requires bizconnect-toast.css.
+ BZToast.success('Saved'); BZToast.error('Connection lost'); BZToast.message('Hi', {title:'Ravi'}); */
+window.BZToast=(function(){
+ var wrap=null;
+ var ICON={
+ message:'',
+ info:'',
+ success:'',
+ error:''
+ };
+ function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return{'&':'&','<':'<','>':'>','"':'"'}[c];});}
+ function ensure(){ if(wrap) return wrap; wrap=document.createElement('div'); wrap.className='bzt-wrap'; document.body.appendChild(wrap); return wrap; }
+ function show(message,opts){
+ opts=opts||{}; var type=opts.type||'message'; var w=ensure();
+ var t=document.createElement('div'); t.className='bzt '+type;
+ t.innerHTML='
'+(ICON[type]||ICON.message)+'
'
+ +(opts.title?'
'+esc(opts.title)+'
':'')
+ +'
'+esc(message)+'
';
+ w.appendChild(t); requestAnimationFrame(function(){ t.classList.add('bzt-in'); });
+ var dur=(opts.duration==null?4000:opts.duration), timer;
+ function close(){ t.classList.remove('bzt-in'); setTimeout(function(){ if(t.parentNode) t.parentNode.removeChild(t); },320); clearTimeout(timer); }
+ t.querySelector('.bzt-x').onclick=close; if(dur>0) timer=setTimeout(close,dur); return close;
+ }
+ return { show:show,
+ message:function(m,o){o=o||{};o.type='message';return show(m,o);},
+ success:function(m,o){o=o||{};o.type='success';return show(m,o);},
+ error:function(m,o){o=o||{};o.type='error';return show(m,o);},
+ info:function(m,o){o=o||{};o.type='info';return show(m,o);} };
+})();
+```
+
+Then, on every page that shows notifications (connect, share, home, dashboard, console),
+include both files near the top BEFORE the page's own inline
+(Load the JS via src only — never inline.)
+
+Now REPLACE the existing toast / notification pop-ups with the branded API (keep the same
+triggers and text, just route them through BZToast so they look on-brand):
+- Incoming chat message -> BZToast.message(text, {title: senderName})
+- A success (e.g. recording/transcript saved, agent added) -> BZToast.success('…')
+- An error (connection lost, upload failed, invalid code) -> BZToast.error('…')
+- Neutral info -> BZToast.info('…')
+Remove the old inline toast markup/styles it replaces. Toasts appear top-right and auto-dismiss.
+
+## 5. Verify (do not commit until I review)
+- node --check any JS you extract/touch; confirm every page still loads.
+- Confirm: taskbar/favicon icon is crisp, splash shows on launch, a loader appears centered
+ during connect/report, and notifications now use the branded toast.
+- List exactly which files changed, then STOP for my review.
diff --git a/desktop/PACKAGING.md b/desktop/PACKAGING.md
index 97fd9ed..7926051 100644
--- a/desktop/PACKAGING.md
+++ b/desktop/PACKAGING.md
@@ -22,8 +22,9 @@ Output in `desktop/dist/`:
- `latest.yml` — the update manifest electron-updater reads
- `*.blockmap` — enables delta downloads
-The release build points at production (`SERVER_URL` defaults to `https://remote.bizgaze.com`
-in `main.js`).
+A PACKAGED build points at production (`main.js` defaults `SERVER_URL` to
+`https://remote.bizgaze.com` when `app.isPackaged`); running from source in dev defaults to
+`http://localhost:8090`. `SERVER_URL` overrides either.
## Publish a release (self-hosted feed)
1. Bump `version` in `desktop/package.json` (semver — electron-updater compares this).
diff --git a/desktop/README.md b/desktop/README.md
index 5ee3f2d..f2326dc 100644
--- a/desktop/README.md
+++ b/desktop/README.md
@@ -3,11 +3,15 @@
Electron shell that loads the live Connect web UI and adds native screen capture. See the
overall plan in [../CLIENTS.md](../CLIENTS.md).
-## Run (dev)
+## Run (dev = local testing)
```bash
npm install
-SERVER_URL=http://localhost:8090 npm start # default is https://remote.bizgaze.com
+npm start # dev auto-targets http://localhost:8090 (your local server)
+SERVER_URL=https://remote.bizgaze.com npm start # …or point dev at production to compare
```
+`npm start` IS the local desktop test — no separate "local" installer needed. In dev (unpackaged)
+the shell defaults to the local server; a PACKAGED installer defaults to production. `SERVER_URL`
+overrides either. (Bash/Git-Bash syntax above; in PowerShell: `$env:SERVER_URL='…'; npm start`.)
## Build installers
```bash
diff --git a/desktop/main.js b/desktop/main.js
index fe04ad9..30154cc 100644
--- a/desktop/main.js
+++ b/desktop/main.js
@@ -97,9 +97,26 @@ ipcMain.on('set-unread', (_e, { count, dataUrl } = {}) => {
} catch (_) {}
});
-const SERVER_URL = (process.env.SERVER_URL || 'https://remote.bizgaze.com').replace(/\/+$/, '');
+// Server origin: a PACKAGED build (the installer) points at production; running from source in dev
+// (`npm start`, unpackaged) defaults to the local server so you can test the shell against localhost
+// with no flags or separate "local" build. SERVER_URL always overrides (e.g. point dev at prod).
+const SERVER_URL = (process.env.SERVER_URL || (app.isPackaged ? 'https://remote.bizgaze.com' : 'http://localhost:8090')).replace(/\/+$/, '');
let win;
+let splash;
+
+// A tiny brand-blue splash (splash.html) shown while the web UI loads, so launch feels instant
+// and on-brand instead of a blank window. Closed as soon as the main window is ready to show.
+function createSplash() {
+ splash = new BrowserWindow({
+ width: 440, height: 440, frame: false, resizable: false, center: true,
+ backgroundColor: '#1F3B73', skipTaskbar: true, alwaysOnTop: true, show: true,
+ webPreferences: { contextIsolation: true, nodeIntegration: false },
+ });
+ splash.loadFile(path.join(__dirname, 'splash.html'));
+ splash.on('closed', () => { splash = null; });
+}
+function closeSplash() { if (splash) { try { splash.close(); } catch (_) {} splash = null; } }
function createWindow() {
win = new BrowserWindow({
@@ -108,7 +125,8 @@ function createWindow() {
minWidth: 880,
minHeight: 600,
title: 'Biz Connect',
- backgroundColor: '#0f1830',
+ backgroundColor: '#1F3B73',
+ show: false, // reveal only once the page is ready — the splash covers the gap
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
@@ -118,6 +136,12 @@ function createWindow() {
},
});
+ // Reveal the main window when its first paint is ready, and retire the splash. A fallback
+ // timer guarantees we never get stuck on the splash if the load stalls.
+ const reveal = () => { closeSplash(); if (win && !win.isVisible()) { win.show(); win.focus(); } };
+ win.once('ready-to-show', reveal);
+ setTimeout(reveal, 12000);
+
// Open the landing page (same entry as the website): the "before login" screen with the
// no-login "Share my screen" option + sign-in. It redirects logged-in users straight to /home.
win.loadURL(SERVER_URL + '/');
@@ -158,6 +182,7 @@ function configureSession() {
app.whenReady().then(() => {
configureSession();
+ createSplash();
createWindow();
Menu.setApplicationMenu(null); // hide the default menu bar; the web UI is the chrome
app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); });
diff --git a/desktop/package.json b/desktop/package.json
index 334b9c8..d38af5d 100644
--- a/desktop/package.json
+++ b/desktop/package.json
@@ -1,6 +1,6 @@
{
"name": "biz-connect-desktop",
- "version": "0.1.2",
+ "version": "0.1.3",
"description": "Biz Connect technician desktop client — loads the Connect web UI with native screen capture",
"author": {
"name": "BizGaze",
@@ -12,8 +12,7 @@
"dist": "electron-builder"
},
"dependencies": {
- "electron-updater": "^6.3.9",
- "node-notifier": "^10.0.1"
+ "electron-updater": "^6.3.9"
},
"devDependencies": {
"electron": "^31.0.0",
@@ -26,9 +25,6 @@
"buildResources": "build",
"output": "dist"
},
- "asarUnpack": [
- "**/node_modules/node-notifier/**"
- ],
"publish": [
{
"provider": "generic",
diff --git a/desktop/splash.html b/desktop/splash.html
new file mode 100644
index 0000000..a395857
--- /dev/null
+++ b/desktop/splash.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
Biz Connect
+
Starting…
+
+
diff --git a/server/chat.js b/server/chat.js
index 9009057..a6dfa81 100644
--- a/server/chat.js
+++ b/server/chat.js
@@ -1,7 +1,8 @@
// Chat presence + real-time delivery. A logged-in user opens a WebSocket and sends
// `chat-hello`; signaling.js registers the socket here. Messages are persisted over HTTP
// (routes.js) and pushed live to the recipient's sockets via pushToUser().
-const { chatClients } = require('./presence');
+const { chatClients, meetingRooms } = require('./presence');
+let _repos = null; const repos = () => (_repos || (_repos = require('./repos'))); // lazy: avoid a require cycle
function register(userId, ws) {
if (!chatClients.has(userId)) chatClients.set(userId, new Set());
@@ -28,4 +29,27 @@ function pushToUser(userId, obj) {
for (const ws of s) { if (ws.readyState === 1) { try { ws.send(data); } catch (_) {} } }
}
-module.exports = { register, unregister, isOnline, pushToUser };
+// --- Live presence -------------------------------------------------------------------------
+// A user's effective status (online / in-a-call / away / …) changes with no HTTP round-trip:
+// they connect or disconnect a socket, or join/leave a call. Without pushing that change, OTHER
+// users only see it after a full page reload — impossible in the desktop/mobile apps. So whenever
+// it changes we broadcast the user's fresh status to everyone else's sockets, and the client
+// updates that contact's dot/subtitle in place.
+function isInCall(userId) {
+ for (const [, peers] of meetingRooms) { for (const [, p] of peers) { if (p.ws && p.ws._meetingUserId === userId) return true; } }
+ return false;
+}
+function effectiveStatus(userId) {
+ if (isInCall(userId)) return 'incall'; // derived (overrides the stored status)
+ try { const u = repos().users.byId(userId); return (u && u.status) || 'active'; } catch (_) { return 'active'; }
+}
+function broadcastPresence(userId) {
+ if (!userId) return;
+ const payload = JSON.stringify({ type: 'presence', userId, online: isOnline(userId), status: effectiveStatus(userId) });
+ for (const [uid, set] of chatClients) {
+ if (uid === userId) continue; // no need to tell someone about their own status
+ for (const ws of set) { if (ws.readyState === 1) { try { ws.send(payload); } catch (_) {} } }
+ }
+}
+
+module.exports = { register, unregister, isOnline, pushToUser, broadcastPresence };
diff --git a/server/public/apple-touch-icon.png b/server/public/apple-touch-icon-180.png
similarity index 100%
rename from server/public/apple-touch-icon.png
rename to server/public/apple-touch-icon-180.png
diff --git a/server/public/bizconnect-toast.css b/server/public/bizconnect-toast.css
new file mode 100644
index 0000000..6d7c6b9
--- /dev/null
+++ b/server/public/bizconnect-toast.css
@@ -0,0 +1,18 @@
+/* Biz Connect — branded notification toast. BZToast.success('…') / .error / .message / .info */
+.bzt-wrap{position:fixed;top:16px;right:16px;z-index:2147483600;display:flex;flex-direction:column;gap:10px;max-width:min(380px,92vw)}
+@supports(top:env(safe-area-inset-top)){.bzt-wrap{top:calc(16px + env(safe-area-inset-top));right:calc(16px + env(safe-area-inset-right))}}
+.bzt{display:flex;align-items:flex-start;gap:12px;background:#fff;color:#1f2430;border-radius:14px;padding:12px 14px;
+ box-shadow:0 12px 30px rgba(16,26,53,.20);border-left:5px solid #1F3B73;
+ transform:translateX(120%);opacity:0;transition:transform .3s cubic-bezier(.2,.7,.2,1),opacity .3s}
+.bzt.bzt-in{transform:translateX(0);opacity:1}
+.bzt.success{border-left-color:#16a34a}.bzt.error{border-left-color:#b91c1c}
+.bzt-badge{flex:none;width:34px;height:34px;border-radius:50%;display:grid;place-items:center;background:#1F3B73}
+.bzt.success .bzt-badge{background:#16a34a}.bzt.error .bzt-badge{background:#b91c1c}
+.bzt-badge svg{width:20px;height:20px}
+.bzt-body{flex:1;min-width:0;padding-top:1px}
+.bzt-title{font:700 13.5px/1.3 'Segoe UI',system-ui,sans-serif;color:#1F3B73;margin:0 0 1px}
+.bzt.success .bzt-title{color:#15803d}.bzt.error .bzt-title{color:#b91c1c}
+.bzt-msg{font:500 13px/1.4 'Segoe UI',system-ui,sans-serif;color:#3a4152;overflow-wrap:anywhere}
+.bzt-x{flex:none;background:none;border:none;color:#9aa3b2;cursor:pointer;font-size:18px;line-height:1;padding:2px 4px}
+.bzt-x:hover{color:#1f2430}
+@media(prefers-reduced-motion:reduce){.bzt{transition:opacity .2s}}
diff --git a/server/public/bizconnect-toast.js b/server/public/bizconnect-toast.js
new file mode 100644
index 0000000..01be92e
--- /dev/null
+++ b/server/public/bizconnect-toast.js
@@ -0,0 +1,29 @@
+/* Biz Connect toast API. Requires bizconnect-toast.css.
+ BZToast.success('Saved'); BZToast.error('Connection lost'); BZToast.message('Hi', {title:'Ravi'}); */
+window.BZToast=(function(){
+ var wrap=null;
+ var ICON={
+ message:'',
+ info:'',
+ success:'',
+ error:''
+ };
+ function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return{'&':'&','<':'<','>':'>','"':'"'}[c];});}
+ function ensure(){ if(wrap) return wrap; wrap=document.createElement('div'); wrap.className='bzt-wrap'; document.body.appendChild(wrap); return wrap; }
+ function show(message,opts){
+ opts=opts||{}; var type=opts.type||'message'; var w=ensure();
+ var t=document.createElement('div'); t.className='bzt '+type;
+ t.innerHTML='
Soon you'll be able to host multi-party video meetings with your BizGaze team and customers — right here, no install needed. We're putting on the finishing touches.
-
-
In the meantime, use Share Screen or Connect Screen to start a session.
-
-
-
-
-
-
-
-
-
-
Share your screen
-
Let a teammate or customer see your screen instantly. You'll get a 6-digit code to share — they enter it to connect. No download, works right in the browser.