Files
BizGaze_Remote/server/public/host.html
T
2026-06-05 17:29:09 +05:30

103 rindas
5.6 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Browser Host — Remote Access</title>
<style>
body { font-family: system-ui, sans-serif; background:#0f172a; color:#e2e8f0; margin:0; padding:1.5rem; }
.card { max-width:560px; margin:0 auto; background:#1e293b; border-radius:12px; padding:1.5rem; }
h1 { font-size:1.15rem; margin:0 0 1rem; }
input { width:100%; padding:.7rem; border-radius:8px; border:1px solid #334155; background:#0f172a; color:#e2e8f0; margin:.3rem 0; }
button { padding:.7rem 1.3rem; background:#3b82f6; color:#fff; border:none; border-radius:8px; font-weight:600; cursor:pointer; }
.status { background:#0f172a; padding:.7rem 1rem; border-radius:8px; margin:1rem 0; font-size:.9rem; }
.status.on { background:#14532d; } .status.warn { background:#7c2d12; }
.consent { border:1px solid #3b82f6; border-radius:10px; padding:1rem; margin-top:1rem; }
.grant { background:#22c55e; color:#052e16; } .deny { background:#ef4444; margin-left:.5rem; }
.muted { color:#94a3b8; font-size:.82rem; }
#log { font-family:monospace; font-size:.72rem; color:#64748b; height:120px; overflow-y:auto; margin-top:1rem; background:#020617; padding:.6rem; border-radius:8px; }
.indicator { position:fixed; bottom:0; left:0; right:0; background:#b91c1c; color:#fff; text-align:center; padding:.4rem; font-size:.85rem; display:none; }
.indicator.show { display:block; }
</style>
</head>
<body>
<div class="card">
<h1>🖥️ Browser Host (no install)</h1>
<p class="muted">Shares this screen with a technician. Paste the enroll token from the console and click Go online.</p>
<input id="token" placeholder="enroll token">
<button id="goBtn">Go online</button>
<div id="status" class="status">Idle.</div>
<div id="consentBox"></div>
<div id="log"></div>
</div>
<div id="indicator" class="indicator">● A technician is viewing this screen</div>
<script>
const statusEl = document.getElementById('status');
const consentBox = document.getElementById('consentBox');
const indicator = document.getElementById('indicator');
const logEl = document.getElementById('log');
const log = (t) => { const d=document.createElement('div'); d.textContent=t; logEl.appendChild(d); logEl.scrollTop=logEl.scrollHeight; };
const setStatus = (t,c='') => { statusEl.textContent=t; statusEl.className='status '+c; };
let ws, pc, localStream, sessionId;
document.getElementById('goBtn').onclick = () => {
const token = document.getElementById('token').value.trim();
if (!token) return setStatus('Enter the enroll token first.', 'warn');
connect(token);
};
function connect(token) {
setStatus('Connecting to server…');
ws = new WebSocket((location.protocol==='https:'?'wss://':'ws://') + location.host + '/ws');
ws.onopen = () => ws.send(JSON.stringify({ type:'agent-hello', enrollToken: token }));
ws.onmessage = (e) => handle(JSON.parse(e.data));
ws.onclose = () => setStatus('Disconnected.', 'warn');
}
async function handle(m) {
switch (m.type) {
case 'agent-registered': setStatus(`Online as "${m.name}". Waiting for a session.`, 'on'); log('registered: '+m.name); break;
case 'session-request': m.unattended ? grant(m.sessionId) : showConsent(m); break;
case 'start-stream': sessionId = 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"><b>${esc(m.technician)}</b> wants to view this screen.
<div style="margin-top:.6rem"><button class="grant" id="g">Allow</button><button class="deny" id="d">Deny</button></div></div>`;
document.getElementById('g').onclick = () => { consentBox.innerHTML=''; grant(m.sessionId); };
document.getElementById('d').onclick = () => { consentBox.innerHTML=''; ws.send(JSON.stringify({type:'consent',sessionId:m.sessionId,granted:false})); };
}
const grant = (sid) => ws.send(JSON.stringify({ type:'consent', sessionId:sid, granted:true }));
async function startStreaming() {
setStatus('Sharing screen…', '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 cancelled/failed.', 'warn'); return; }
pc = new RTCPeerConnection({ iceServers:[{urls:'stun:stun.l.google.com:19302'}] });
localStream.getTracks().forEach(t => pc.addTrack(t, localStream));
pc.ondatachannel = (ev) => { ev.channel.onmessage = (msg) => { let e; try{e=JSON.parse(msg.data)}catch{return} if(e.kind!=='mousemove') log('remote input: '+e.kind+' '+(e.key||'')); }; };
pc.onicecandidate = (ev) => { if (ev.candidate) ws.send(JSON.stringify({type:'ice-candidate',sessionId,candidate:ev.candidate})); };
const offer = await pc.createOffer(); await pc.setLocalDescription(offer);
ws.send(JSON.stringify({ type:'offer', sessionId, sdp: pc.localDescription }));
log('sent offer');
localStream.getVideoTracks()[0].onended = () => teardown();
}
function teardown() {
indicator.classList.remove('show');
if (localStream) { localStream.getTracks().forEach(t=>t.stop()); localStream=null; }
if (pc) { pc.close(); pc=null; }
setStatus('Session ended. Still online, waiting.', 'on');
}
function esc(s){return String(s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));}
</script>
</body>
</html>