| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135 |
- // 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 = `
- <div class="consent">
- <h2>Allow remote support?</h2>
- <p class="muted"><b>${escapeHtml(m.technician)}</b> is requesting to view and control this PC.</p>
- <button class="grant" id="grantBtn">Allow</button>
- <button class="deny" id="denyBtn">Deny</button>
- </div>`;
- 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])); }
|