136 line
4.8 KiB
JavaScript
136 line
4.8 KiB
JavaScript
// 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])); }
|