// Agent renderer: connects to the signaling server, handles consent, // captures the screen, and streams it over WebRTC to the technician's viewer. // Remote input events arrive on the data channel and are handed to the main // process (via the preload bridge) for OS-level injection. const statusEl = document.getElementById('status'); const consentBox = document.getElementById('consentBox'); const indicator = document.getElementById('indicator'); const logEl = document.getElementById('log'); let cfg = null; let ws = null; let pc = null; let localStream = null; let currentSessionId = null; const log = (t) => { const d = document.createElement('div'); d.textContent = t; logEl.appendChild(d); logEl.scrollTop = logEl.scrollHeight; }; const setStatus = (t, cls = '') => { statusEl.textContent = t; statusEl.className = 'status ' + cls; }; window.agent.onConfig((c) => { cfg = c; if (!cfg.enrollToken) { setStatus('No enroll token. Set AGENT_ENROLL_TOKEN and restart.', 'warn'); return; } connect(); }); function wsUrl() { const u = new URL(cfg.serverUrl); const proto = u.protocol === 'https:' ? 'wss:' : 'ws:'; return `${proto}//${u.host}/ws`; } function connect() { setStatus('Connecting to server…'); ws = new WebSocket(wsUrl()); ws.onopen = () => { ws.send(JSON.stringify({ type: 'agent-hello', enrollToken: cfg.enrollToken })); }; ws.onmessage = (e) => handle(JSON.parse(e.data)); ws.onclose = () => { setStatus('Disconnected. Reconnecting in 3s…', 'warn'); setTimeout(connect, 3000); }; } async function handle(m) { switch (m.type) { case 'agent-registered': setStatus(`Online as "${m.name}". Waiting for sessions.`, 'on'); log(`registered: ${m.name}`); break; case 'session-request': if (m.unattended) { log(`unattended session ${m.sessionId} — auto-granting`); grant(m.sessionId); } else showConsent(m); break; case 'start-stream': currentSessionId = m.sessionId; await startStreaming(); break; case 'answer': if (pc) await pc.setRemoteDescription(new RTCSessionDescription(m.sdp)); break; case 'ice-candidate': if (m.candidate && pc) await pc.addIceCandidate(new RTCIceCandidate(m.candidate)); break; case 'session-ended': teardown(); break; case 'error': setStatus('Server: ' + m.message, 'warn'); break; } } function showConsent(m) { consentBox.innerHTML = `
`; document.getElementById('grantBtn').onclick = () => { consentBox.innerHTML = ''; grant(m.sessionId); }; document.getElementById('denyBtn').onclick = () => { consentBox.innerHTML = ''; deny(m.sessionId); }; } const grant = (sessionId) => ws.send(JSON.stringify({ type: 'consent', sessionId, granted: true })); const deny = (sessionId) => ws.send(JSON.stringify({ type: 'consent', sessionId, granted: false })); async function startStreaming() { setStatus('Sharing screen with technician…', 'on'); indicator.classList.add('show'); try { localStream = await navigator.mediaDevices.getDisplayMedia({ video: { frameRate: { ideal: 30 } }, audio: false }); } catch (err) { log('getDisplayMedia failed: ' + err.message); setStatus('Screen capture failed.', 'warn'); return; } pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }); localStream.getTracks().forEach((t) => pc.addTrack(t, localStream)); // Viewer creates the input data channel; we receive it here. pc.ondatachannel = (ev) => { const ch = ev.channel; ch.onmessage = (msg) => { let evt; try { evt = JSON.parse(msg.data); } catch { return; } window.agent.injectInput(evt); // -> main process -> OS injection }; }; pc.onicecandidate = (ev) => { if (ev.candidate) ws.send(JSON.stringify({ type: 'ice-candidate', sessionId: currentSessionId, candidate: ev.candidate })); }; const offer = await pc.createOffer(); await pc.setLocalDescription(offer); ws.send(JSON.stringify({ type: 'offer', sessionId: currentSessionId, sdp: pc.localDescription })); log('sent offer for session ' + currentSessionId); } function teardown() { indicator.classList.remove('show'); window.agent.sessionEnded(); if (localStream) { localStream.getTracks().forEach((t) => t.stop()); localStream = null; } if (pc) { pc.close(); pc = null; } currentSessionId = null; setStatus('Session ended. Waiting for sessions.', 'on'); } function escapeHtml(s) { return String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); }