From 94b8fac32f8fcd1e63b39067abc96012586054a4 Mon Sep 17 00:00:00 2001 From: sravan Date: Mon, 13 Jul 2026 13:18:01 +0530 Subject: [PATCH] fix: guest pre-join, post-admit lobby, audio device menu, RC keyboard (0.1.15/batch74) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Guest pre-join redesigned: brand backdrop, live camera preview, mic/cam toggles applied on entry, initials avatar, name required, "host may admit you" note. 2. Post-admit bug: the guest stayed on "Waiting for the host…" forever — the lobby screen had replaced the call UI and meeting-joined never re-rendered it. Now it rebuilds the call on admission (_inLobby). 3. Audio devices: dropped the standalone headphones button. The MIC now has a ▾ caret opening one Teams-style menu with Speaker + Microphone sections (radio-selected); speaker uses setSinkId/LiveKit switchActiveDevice, mic switches the live input. Mobile gets a speakerphone toggle that prefers a connected BT/headset when off. 4. Remote-control keyboard: - Injector now maps the PHYSICAL key (KeyboardEvent.code) instead of the character, so Shift+1 types "!" etc. Character mapping was why typing "performed differently". - Keys reach the sharer ONLY while control is ENGAGED (window focused AND you clicked their screen). Minimised/unfocused/chat typing stays local. Esc or clicking away releases; modifiers are released on disengage so nothing sticks. - Explicit control icons: viewer gets a Control ON/OFF button (green when engaged) + an on-screen hint; the SHARER gets a control icon beside mic/chat to allow/stop access at a glance, synced with the consent dialog and banner. Co-Authored-By: Claude Opus 4.8 --- desktop/input/inject.js | 41 ++++++++- desktop/package.json | 2 +- server/public/connect.html | 52 +++++++++-- server/public/home.html | 177 +++++++++++++++++++++++++++++++------ server/public/share.html | 24 ++++- 5 files changed, 255 insertions(+), 41 deletions(-) diff --git a/desktop/input/inject.js b/desktop/input/inject.js index cc94701..b1c627b 100644 --- a/desktop/input/inject.js +++ b/desktop/input/inject.js @@ -18,7 +18,39 @@ try { const available = !!nut; -// Map browser KeyboardEvent.key values to nut-js Key enum names. +// Map the PHYSICAL key (KeyboardEvent.code) to a nut-js Key. This is the correct way to drive a remote +// keyboard: press the same physical key the viewer pressed and let the remote OS apply its own modifier +// state. Mapping by CHARACTER (mapKey below) broke shifted keys — e.g. Shift+1 typed "1" instead of "!" +// and symbols came out wrong ("keyboard performs differently on the sharer's device"). +const CODE_MAP = { + Backspace: 'Backspace', Tab: 'Tab', Enter: 'Enter', NumpadEnter: 'Enter', Escape: 'Escape', Space: 'Space', + ShiftLeft: 'LeftShift', ShiftRight: 'RightShift', + ControlLeft: 'LeftControl', ControlRight: 'RightControl', + AltLeft: 'LeftAlt', AltRight: 'RightAlt', + MetaLeft: 'LeftSuper', MetaRight: 'RightSuper', + CapsLock: 'CapsLock', + PageUp: 'PageUp', PageDown: 'PageDown', End: 'End', Home: 'Home', + ArrowLeft: 'Left', ArrowUp: 'Up', ArrowRight: 'Right', ArrowDown: 'Down', + Insert: 'Insert', Delete: 'Delete', + Minus: 'Minus', Equal: 'Equal', BracketLeft: 'LeftBracket', BracketRight: 'RightBracket', + Backslash: 'Backslash', Semicolon: 'Semicolon', Quote: 'Quote', Backquote: 'Grave', + Comma: 'Comma', Period: 'Period', Slash: 'Slash', + NumpadAdd: 'Add', NumpadSubtract: 'Subtract', NumpadMultiply: 'Multiply', NumpadDivide: 'Divide', NumpadDecimal: 'Decimal', +}; +function mapCode(code) { + if (!nut || !code) return null; + const K = nut.Key; + const named = CODE_MAP[code]; + if (named && K[named] !== undefined) return [K[named]]; + let m; + if ((m = /^Key([A-Z])$/.exec(code)) && K[m[1]] !== undefined) return [K[m[1]]]; + if ((m = /^Digit([0-9])$/.exec(code)) && K['Num' + m[1]] !== undefined) return [K['Num' + m[1]]]; + if ((m = /^Numpad([0-9])$/.exec(code)) && K['NumPad' + m[1]] !== undefined) return [K['NumPad' + m[1]]]; + if ((m = /^(F\d{1,2})$/.exec(code)) && K[m[1]] !== undefined) return [K[m[1]]]; + return null; +} + +// Map browser KeyboardEvent.key values to nut-js Key enum names. (Fallback when there's no usable code.) function mapKey(key, code) { if (!nut) return null; const K = nut.Key; @@ -82,14 +114,15 @@ async function inject(evt) { if (evt.dx) await (evt.dx > 0 ? nut.mouse.scrollRight(Math.abs(evt.dx)) : nut.mouse.scrollLeft(Math.abs(evt.dx))); break; case 'keydown': { - const m = mapKey(evt.key, evt.code); + // Prefer the PHYSICAL key so the remote OS applies its own shift/altgr state (correct symbols). + const m = mapCode(evt.code) || mapKey(evt.key, evt.code); if (!m) break; - if (m.type) { await nut.keyboard.type(m.type); break; } + if (m.type) { await nut.keyboard.type(m.type); break; } // last-resort: type the literal character await nut.keyboard.pressKey(...m); m.forEach((k) => pressed.add(k)); break; } case 'keyup': { - const m = mapKey(evt.key, evt.code); + const m = mapCode(evt.code) || mapKey(evt.key, evt.code); if (!m || m.type) break; await nut.keyboard.releaseKey(...m); m.forEach((k) => pressed.delete(k)); break; diff --git a/desktop/package.json b/desktop/package.json index f04de2b..96093b3 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,6 +1,6 @@ { "name": "biz-connect-desktop", - "version": "0.1.14", + "version": "0.1.15", "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 a69a3d4..afbb5f3 100644 --- a/server/public/connect.html +++ b/server/public/connect.html @@ -48,6 +48,9 @@ FULL viewport underneath it. */ body.has-bar #video{width:100vw;height:100vh;} body.has-bar{background:#0b1220;} + /* Control engaged: a green inset ring makes it obvious your keyboard now drives THEIR machine. */ + #video.engaged{box-shadow:inset 0 0 0 3px #16a34a;} + #ctrlHint{position:fixed;left:50%;bottom:18px;transform:translateX(-50%);z-index:2147483000;background:rgba(15,23,42,.78);color:#fff;font-family:'Segoe UI',system-ui,sans-serif;font-size:.78rem;padding:.4rem .8rem;border-radius:999px;pointer-events:none;} .profile{position:relative} .profile .pbtn{display:flex;align-items:center;gap:.4rem;background:rgba(255,255,255,.14);color:#fff;border:1px solid #46598c;border-radius:10px;padding:.45rem .85rem;font-weight:600;font-size:.88rem;cursor:pointer} .profile .pbtn:hover{background:rgba(255,255,255,.24)} @@ -327,14 +330,22 @@ function buildBar(){ 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 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'); const end=_btn('endBtn2',I('callEnd'),'End','#dc2626'); - bar.appendChild(mic);bar.appendChild(chat);bar.appendChild(rec);bar.appendChild(end); + bar.appendChild(mic);bar.appendChild(ctrl);bar.appendChild(chat);bar.appendChild(rec);bar.appendChild(end); document.body.appendChild(bar); document.body.classList.add('has-bar'); // Shrink from the default 48px round to a compact 38px so they read as "tiny icons". - [mic,chat,rec,end].forEach(b=>{ b.style.width='38px'; b.style.height='38px'; b.style.boxShadow='none'; }); + [mic,ctrl,chat,rec,end].forEach(b=>{ b.style.width='38px'; b.style.height='38px'; b.style.boxShadow='none'; }); + ctrl.onclick=()=>setEngaged(!rcEngaged); + // 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';}; chat.onclick=toggleChat; rec.onclick=()=>{ if(mediaRecorder&&mediaRecorder.state==='recording') stopRecording(); else startRecording(); }; @@ -403,12 +414,39 @@ video.addEventListener('mouseup',e=>send({kind:'mouseup',button:e.button,...rel( video.addEventListener('dblclick',e=>send({kind:'dblclick',...rel(e)})); video.addEventListener('wheel',e=>{e.preventDefault();send({kind:'scroll',dx:e.deltaX,dy:e.deltaY});},{passive:false}); video.addEventListener('contextmenu',e=>e.preventDefault()); -// Keyboard: capture at the DOCUMENT level while a session is live so keys work regardless of which -// element has focus (the