Files
BizGaze_Remote/server/public/viewer.html
T
Sravan dc1915bb43 feat: live presence, delivery-tick sync, brand rollout, call fixes, desktop 0.1.3
Live presence (fixes stale in-call/status until refresh — impossible in apps):
- server broadcasts a user's status over the chat socket on connect/disconnect, call
  join/leave, and status change (chat.js broadcastPresence; signaling + routes hooks).
- client onPresence() updates the contact dot + open-chat header live.

Chat delivery ticks (#6): chat-list row now mirrors the thread (delivered→double grey,
read→blue) via a new 'with' field on the delivered relay + onChatRead/onChatDelivered.

Call fixes: no bogus 'host handed over' when a 1:1 call ends (leaveMeeting forced);
branded call-connecting + chat-thread loaders; header subtitle tracks live call state.

Notifications: web notify + sw.js use sender/group DP + brand icon (not old wordmark);
desktop shell drops Web Push so only the single native toast fires (#5).

Brand: master icon/splash/loaders wired everywhere (PWA/favicon/apple-touch/.ico),
branded login (blue + gold CTA), branded toasts (BZToast) on all pages, Electron splash.

Desktop: dev auto-targets localhost (packaged→prod); version 0.1.3 with new multi-size
icon; dropped unused node-notifier; removed home-mockup.html.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 15:43:02 +05:30

115 lines
5.3 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Remote Session</title>
<meta name="theme-color" content="#1F3B73">
<link rel="icon" href="/favicon.ico?v=3" sizes="any">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png?v=3">
<link rel="apple-touch-icon" href="/apple-touch-icon-180.png?v=3">
<style>
body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; margin: 0; }
header { background: #1e293b; padding: 0.6rem 1rem; display: flex; justify-content: space-between; align-items: center; }
#status { font-size: 0.9rem; color: #94a3b8; }
#video { width: 100vw; height: calc(100vh - 48px); background: #020617; object-fit: contain; cursor: crosshair; display: block; outline: none; }
button { padding: 0.5rem 1rem; background: #334155; color: #fff; border: none; border-radius: 6px; cursor: pointer; }
a { color: #3b82f6; }
</style>
<script src="/icons.js?v=3"></script>
</head>
<body>
<header>
<div id="status"><img src="/loaders/loader-orbit-dark.svg" width="18" height="18" style="vertical-align:-4px;margin-right:6px" alt="">Connecting…</div>
<div>
<a href="/"><span data-ic="arrowLeft" data-sz="16"></span> Console</a>
<button id="endBtn">End session</button>
</div>
</header>
<video id="video" autoplay playsinline muted tabindex="0"></video>
<script>
const params = new URLSearchParams(location.search);
const machineId = params.get('machine');
const machineName = params.get('name') || 'remote PC';
const statusEl = document.getElementById('status');
const video = document.getElementById('video');
let pc, inputChannel, sessionId;
const ws = new WebSocket((location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws');
const setStatus = (t) => (statusEl.textContent = t);
ws.onopen = () => {
setStatus(`Requesting access to ${machineName}…`);
ws.send(JSON.stringify({ type: 'viewer-connect', machineId }));
};
ws.onmessage = async (e) => {
const m = JSON.parse(e.data);
switch (m.type) {
case 'session-pending':
sessionId = m.sessionId;
setStatus(`Waiting for ${machineName} to grant consent…`);
break;
case 'session-denied':
setStatus('Consent denied by the remote user.');
break;
case 'session-ready':
setStatus('Consent granted. Establishing connection…');
setupPeer();
break;
case 'offer':
await pc.setRemoteDescription(new RTCSessionDescription(m.sdp));
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;
case 'session-ended':
setStatus('Session ended.');
video.srcObject = null;
break;
case 'error':
setStatus('Error: ' + m.message);
break;
}
};
function setupPeer() {
pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] });
// The agent (offerer) creates the 'input' channel; we receive it here and send input on it.
pc.ondatachannel = (ev) => { if (ev.channel.label === 'input') inputChannel = ev.channel; };
pc.ontrack = (ev) => {
video.srcObject = ev.streams[0];
setStatus(`Connected to ${machineName} — controlling. Click the screen to send input.`);
video.focus();
};
pc.onicecandidate = (ev) => {
if (ev.candidate) ws.send(JSON.stringify({ type: 'ice-candidate', sessionId, candidate: ev.candidate }));
};
}
// ---- input capture (normalized coords) ----
const send = (o) => { if (inputChannel && inputChannel.readyState === 'open') inputChannel.send(JSON.stringify(o)); };
const rel = (e) => { const r = video.getBoundingClientRect(); return { x: (e.clientX - r.left) / r.width, y: (e.clientY - r.top) / r.height }; };
let lastMove = 0;
video.addEventListener('mousemove', (e) => { const t = performance.now(); if (t - lastMove < 30) return; lastMove = t; send({ kind: 'mousemove', ...rel(e) }); });
video.addEventListener('mousedown', (e) => { video.focus(); send({ kind: 'mousedown', button: e.button, ...rel(e) }); });
video.addEventListener('mouseup', (e) => send({ kind: 'mouseup', button: e.button, ...rel(e) }));
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());
video.addEventListener('keydown', (e) => { e.preventDefault(); send({ kind: 'keydown', key: e.key, code: e.code, ctrl: e.ctrlKey, alt: e.altKey, shift: e.shiftKey, meta: e.metaKey }); });
video.addEventListener('keyup', (e) => { e.preventDefault(); send({ kind: 'keyup', key: e.key, code: e.code }); });
document.getElementById('endBtn').onclick = () => {
ws.send(JSON.stringify({ type: 'end-session', sessionId }));
setTimeout(() => (location.href = '/'), 300);
};
</script>
<script>(function(){var s=document.createElement('style');s.textContent='.ic{display:inline-block;vertical-align:middle}';document.head.appendChild(s);document.querySelectorAll('[data-ic]').forEach(function(e){e.insertAdjacentHTML('afterbegin',window.ic(e.getAttribute('data-ic'),+e.getAttribute('data-sz')||16));});})();</script>
</body>
</html>