2026-06-05 17:29:09 +05:30
<!DOCTYPE html>
< html lang = "en" >
< head >
< meta charset = "UTF-8" >
2026-07-19 08:39:50 +05:30
< meta name = "viewport" content = "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" >
2026-06-05 17:29:09 +05:30
< title > Browser Host — Remote Access</ title >
2026-07-02 15:43:02 +05:30
< meta name = "theme-color" content = "#1F3B73" >
< link rel = "icon" href = "/favicon.ico?v=3" sizes = "any" >
< link rel = "icon" type = "image/png" sizes = "32x32" href = "/favicon-32.png?v=3" >
< link rel = "apple-touch-icon" href = "/apple-touch-icon-180.png?v=3" >
2026-06-05 17:29:09 +05:30
< style >
2026-07-19 08:39:50 +05:30
body { font-family : system-ui , sans-serif ; background : #0f172a ; color : #e2e8f0 ; margin : 0 ; padding : calc ( 1.5 rem + env ( safe - area - inset - top , 0 px )) 1.5 rem calc ( 1.5 rem + env ( safe - area - inset - bottom , 0 px )); } /* clear the notch/home-indicator now that the viewport is cover */
2026-06-05 17:29:09 +05:30
. card { max-width : 560 px ; margin : 0 auto ; background : #1e293b ; border-radius : 12 px ; padding : 1.5 rem ; }
h1 { font-size : 1.15 rem ; margin : 0 0 1 rem ; }
input { width : 100 % ; padding : .7 rem ; border-radius : 8 px ; border : 1 px solid #334155 ; background : #0f172a ; color : #e2e8f0 ; margin : .3 rem 0 ; }
button { padding : .7 rem 1.3 rem ; background : #3b82f6 ; color : #fff ; border : none ; border-radius : 8 px ; font-weight : 600 ; cursor : pointer ; }
. status { background : #0f172a ; padding : .7 rem 1 rem ; border-radius : 8 px ; margin : 1 rem 0 ; font-size : .9 rem ; }
. status . on { background : #14532d ; } . status . warn { background : #7c2d12 ; }
. consent { border : 1 px solid #3b82f6 ; border-radius : 10 px ; padding : 1 rem ; margin-top : 1 rem ; }
. grant { background : #22c55e ; color : #052e16 ; } . deny { background : #ef4444 ; margin-left : .5 rem ; }
. muted { color : #94a3b8 ; font-size : .82 rem ; }
# log { font-family : monospace ; font-size : .72 rem ; color : #64748b ; height : 120 px ; overflow-y : auto ; margin-top : 1 rem ; background : #020617 ; padding : .6 rem ; border-radius : 8 px ; }
2026-07-19 08:39:50 +05:30
. indicator { position : fixed ; bottom : 0 ; left : 0 ; right : 0 ; background : #b91c1c ; color : #fff ; text-align : center ; padding : .4 rem calc ( .4 rem + env ( safe - area - inset - right , 0 px )) calc ( .4 rem + env ( safe - area - inset - bottom , 0 px )) calc ( .4 rem + env ( safe - area - inset - left , 0 px )); font-size : .85 rem ; display : none ; }
2026-06-05 17:29:09 +05:30
. indicator . show { display : block ; }
</ style >
2026-06-23 18:47:24 +05:30
< script src = "/icons.js?v=3" ></ script >
2026-06-05 17:29:09 +05:30
</ head >
< body >
< div class = "card" >
2026-06-23 16:15:29 +05:30
< h1 >< span data-ic = "monitor" data-sz = "22" ></ span > Browser Host (no install)</ h1 >
2026-06-05 17:29:09 +05:30
< 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 =>({ '&' : '&' , '<' : '<' , '>' : '>' , '"' : '"' }[ c ]));}
</ script >
2026-06-23 16:15:29 +05:30
< script >( function (){ var s = document . createElement ( 'style' ); s . textContent = '.ic{display:inline-block;vertical-align:middle}' ; document . head . appendChild ( s ); document . querySelectorAll ( '[data-ic]' ). forEach ( function ( e ){ e . insertAdjacentHTML ( 'afterbegin' , window . ic ( e . getAttribute ( 'data-ic' ), + e . getAttribute ( 'data-sz' ) || 16 ));});})();</ script >
2026-06-05 17:29:09 +05:30
</ body >
</ html >