diff --git a/desktop/main.js b/desktop/main.js
index 394c635..8193673 100644
--- a/desktop/main.js
+++ b/desktop/main.js
@@ -273,10 +273,12 @@ function createWindow() {
// no-login "Share my screen" option + sign-in. It redirects logged-in users straight to /home.
win.loadURL(SERVER_URL + '/');
- // Open target=_blank / external links in the system browser instead of a new Electron window.
+ // Links: EXTERNAL ones go to the system browser. OUR OWN urls (e.g. a meeting invite link clicked in
+ // chat) must NOT spawn a second app window (#5) — navigate the main window instead.
win.webContents.setWindowOpenHandler(({ url }) => {
if (!url.startsWith(SERVER_URL)) { shell.openExternal(url); return { action: 'deny' }; }
- return { action: 'allow' };
+ try { if (win && !win.isDestroyed()) { if (win.isMinimized()) win.restore(); win.show(); win.focus(); win.loadURL(url); } } catch (_) {}
+ return { action: 'deny' };
});
// Spell-check menu. Two ways in:
diff --git a/desktop/package.json b/desktop/package.json
index 96093b3..441ee8a 100644
--- a/desktop/package.json
+++ b/desktop/package.json
@@ -1,6 +1,6 @@
{
"name": "biz-connect-desktop",
- "version": "0.1.15",
+ "version": "0.1.16",
"description": "Biz Connect technician desktop client — loads the Connect web UI with native screen capture",
"author": {
"name": "BizGaze",
diff --git a/server/public/connect.html b/server/public/connect.html
index 98ee416..23b38f6 100644
--- a/server/public/connect.html
+++ b/server/public/connect.html
@@ -185,7 +185,9 @@ function connectWS(){
case 'session-ready': if(statusEl)statusEl.innerHTML='Allowed — connecting…'; break;
case 'offer': await pc.setRemoteDescription(new RTCSessionDescription(m.sdp));
// Acquire the agent mic once; on renegotiation (e.g. customer unmutes) just answer.
- if(!window.__mic){ try{ const mic=await navigator.mediaDevices.getUserMedia({audio:true}); window.__mic=mic; mic.getAudioTracks().forEach(t=>pc.addTrack(t,mic)); }catch(e){} }
+ // Acquire the agent mic MUTED by default — joining a screen session shouldn't open a hot mic on the
+ // customer without the agent choosing to speak (new #1). The Mic button unmutes it.
+ if(!window.__mic){ try{ const mic=await navigator.mediaDevices.getUserMedia({audio:true}); window.__mic=mic; mic.getAudioTracks().forEach(t=>{ t.enabled=false; pc.addTrack(t,mic); }); try{ setMicBtn(false); }catch(_){} }catch(e){} }
const ans=await pc.createAnswer(); await pc.setLocalDescription(ans);
ws.send(JSON.stringify({type:'answer',sessionId,sdp:pc.localDescription})); break;
case 'ice-candidate': if(m.candidate&&pc) await pc.addIceCandidate(new RTCIceCandidate(m.candidate)); break;
@@ -329,7 +331,7 @@ function buildBar(){
// Floats over the bottom-right corner with tiny icons, so the shared screen fills the whole viewport.
bar.style.cssText='position:fixed;right:16px;bottom:16px;z-index:2147483000;display:flex;flex-direction:row;gap:8px;align-items:center;background:rgba(15,23,42,.72);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);padding:7px 9px;border-radius:14px;box-shadow:0 8px 22px rgba(0,0,0,.35)';
const I=(n)=>(window.ic?window.ic(n,16):'');
- const mic=_btn('micBtn',I('mic'),'Mic','#2563eb');
+ const mic=_btn('micBtn',I('micOff'),'Unmute','#6b7280'); // starts MUTED (new #1)
const ctrl=_btn('ctrlBtn',I('monitor'),'Control OFF — click their screen to take control','#6b7280');
const chat=_btn('chatBtn',I('chat'),'Chat','#334155');
const rec=_btn('recBtn','','Record','#334155');
@@ -340,13 +342,22 @@ function buildBar(){
// Shrink from the default 48px round to a compact 38px so they read as "tiny icons".
[mic,ctrl,chat,rec,end].forEach(b=>{ b.style.width='38px'; b.style.height='38px'; b.style.boxShadow='none'; });
ctrl.onclick=()=>setEngaged(!rcEngaged);
+ makeBarDraggable(bar,'bzc_connectbar_pos'); // new #4: the bar hides part of the shared screen → move it
// A hint over the screen until they take control, so it's obvious how to start driving.
if(!document.getElementById('ctrlHint')){
const h=document.createElement('div'); h.id='ctrlHint';
h.textContent='Click the screen to take control · Esc to release';
document.body.appendChild(h);
}
- mic.onclick=()=>{const m=window.__mic;if(!m)return;const t=m.getAudioTracks()[0];if(!t)return;t.enabled=!t.enabled;mic.title=t.enabled?'Mute':'Unmute';mic.innerHTML=''+I(t.enabled?'mic':'micOff')+'';mic.style.background=t.enabled?'#2563eb':'#6b7280';};
+ mic.onclick=async()=>{
+ // If the mic wasn't acquired yet (e.g. the customer never renegotiated), get it now, then toggle.
+ if(!window.__mic){
+ try{ const s=await navigator.mediaDevices.getUserMedia({audio:true}); window.__mic=s; s.getAudioTracks().forEach(t=>{ t.enabled=false; if(pc) pc.addTrack(t,s); }); }
+ catch(e){ toast('Microphone permission was blocked.'); return; }
+ }
+ const t=window.__mic.getAudioTracks()[0]; if(!t) return;
+ t.enabled=!t.enabled; setMicBtn(t.enabled);
+ };
chat.onclick=toggleChat;
rec.onclick=()=>{ if(mediaRecorder&&mediaRecorder.state==='recording') stopRecording(); else startRecording(); };
end.onclick=()=>{ try{ws.send(JSON.stringify({type:'end-session',sessionId,reason:'agent-ended'}));}catch(_){} };
@@ -420,6 +431,39 @@ video.addEventListener('contextmenu',e=>e.preventDefault());
// in chat, never leaks keystrokes to the remote desktop). Click the screen (or the Control button) to
// take control; click away, press Esc, or leave the window to release it.
function rcTyping(){ const a=document.activeElement; return a && (a.tagName==='INPUT'||a.tagName==='TEXTAREA'); }
+// new #4: drag the floating control bar anywhere — it otherwise sits over part of the shared screen.
+// Drag from the bar background (not the buttons); position is remembered.
+function makeBarDraggable(el,key){
+ if(!el||el._drag) return; el._drag=true;
+ let sx=0,sy=0,ox=0,oy=0,dragging=false;
+ const pt=(e)=>e.touches?e.touches[0]:e;
+ const clamp=()=>{ const w=el.offsetWidth,h=el.offsetHeight;
+ let x=parseFloat(el.style.left||'0'), y=parseFloat(el.style.top||'0');
+ x=Math.max(4,Math.min(x,window.innerWidth-w-4)); y=Math.max(4,Math.min(y,window.innerHeight-h-4));
+ el.style.left=x+'px'; el.style.top=y+'px'; };
+ const pin=(r)=>{ el.style.left=r.left+'px'; el.style.top=r.top+'px'; el.style.right='auto'; el.style.bottom='auto'; };
+ try{ const s=JSON.parse(localStorage.getItem(key)||'null'); if(s&&typeof s.x==='number'){ pin({left:s.x,top:s.y}); clamp(); } }catch(_){}
+ const move=(e)=>{ if(!dragging) return; const p=pt(e); el.style.left=(ox+(p.clientX-sx))+'px'; el.style.top=(oy+(p.clientY-sy))+'px'; clamp(); if(e.cancelable) e.preventDefault(); };
+ const up=()=>{ if(!dragging) return; dragging=false; el.style.opacity='';
+ document.removeEventListener('mousemove',move); document.removeEventListener('mouseup',up);
+ document.removeEventListener('touchmove',move); document.removeEventListener('touchend',up);
+ try{ localStorage.setItem(key, JSON.stringify({x:parseFloat(el.style.left), y:parseFloat(el.style.top)})); }catch(_){} };
+ const down=(e)=>{ if(e.target.closest('button,select,input,a')) return;
+ const r=el.getBoundingClientRect(); pin(r);
+ const p=pt(e); sx=p.clientX; sy=p.clientY; ox=r.left; oy=r.top; dragging=true; el.style.opacity='.92';
+ document.addEventListener('mousemove',move); document.addEventListener('mouseup',up);
+ document.addEventListener('touchmove',move,{passive:false}); document.addEventListener('touchend',up);
+ if(e.cancelable) e.preventDefault(); };
+ el.addEventListener('mousedown',down); el.addEventListener('touchstart',down,{passive:false});
+ el.style.cursor='move'; el.title='Drag to move';
+}
+// Mic button state (kept global so the offer handler can set it once the mic is acquired, muted).
+function setMicBtn(on){
+ const b=document.getElementById('micBtn'); if(!b) return;
+ b.title=on?'Mute':'Unmute';
+ b.innerHTML=''+(window.ic?window.ic(on?'mic':'micOff',16):'')+'';
+ b.style.background=on?'#2563eb':'#6b7280';
+}
let rcEngaged=false;
function setEngaged(on){
const next=!!on; if(next===rcEngaged) return;
diff --git a/server/public/home.html b/server/public/home.html
index 9a8ae05..ac4a049 100644
--- a/server/public/home.html
+++ b/server/public/home.html
@@ -19,8 +19,11 @@