first commit
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
// 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])); }
|
||||
Reference in New Issue
Block a user