diff --git a/easy.html b/easy.html
index b672463..364fd25 100644
--- a/easy.html
+++ b/easy.html
@@ -146,7 +146,7 @@
}
function connect(){
var ws;
- try{ ws = new WebSocket('ws://' + location.hostname + ':8787') }catch(_){ return }
+ try{ ws = new WebSocket(BLS.relayUrl()) }catch(_){ return }
ws.onopen = function(){
net.ws = ws;
try{ ws.send(JSON.stringify({type:'join', sessionId: net.sid, role:'admin'})) }catch(_){ }
@@ -253,7 +253,7 @@
window.open('patient.html' + (net.sid ? '#' + net.sid : ''), 'bls-patient');
});
document.getElementById('copyLinkBig').addEventListener('click', function(){
- var link = location.origin + '/patient.html#' + net.sid;
+ var link = new URL('patient.html#' + net.sid, location.href).href;
var b = this;
try{ navigator.clipboard.writeText(link) }catch(_){ try{ prompt('Patient link:', link) }catch(__){ } }
b.textContent = '✅ Copied — open it on the patient device';
diff --git a/js/admin.js b/js/admin.js
index 195355f..322993b 100644
--- a/js/admin.js
+++ b/js/admin.js
@@ -439,17 +439,18 @@ if(document.readyState==='loading'){ document.addEventListener('DOMContentLoaded
else if(wsConnected && ws){ ws.send(JSON.stringify(payload)) }
}catch(e){}
}
- function updateJoinLink(){ var sid = (wsSession.value||'').trim(); var base = location.origin + '/patient.html'; if(sid){ joinLink.value = base + '#' + encodeURIComponent(sid) } else { joinLink.value = '' } }
+ function updateJoinLink(){ var sid = (wsSession.value||'').trim(); var base = new URL('patient.html', location.href).href; if(sid){ joinLink.value = base + '#' + encodeURIComponent(sid) } else { joinLink.value = '' } }
function renderOsHelp(){ var ua = navigator.userAgent||''; var isWin = /Windows/i.test(ua); var isMac = /Macintosh|Mac OS/i.test(ua); var start = (isWin? 'start-relay.bat' : isMac? 'start-relay.command' : './start-relay.sh'); var open = (isWin? 'double-click' : isMac? 'double-click' : 'run'); return { setup: start, start: start, open: open } }
(function(){ var el = document.getElementById('osHelp'); if(!el) return; var h = renderOsHelp(); var text = 'Start the server: ' + h.open + ' ' + h.start + ' — nothing to install, first time or any time'; el.innerText = text })();
function copyText(t){ var ok=false; if(navigator.clipboard && navigator.clipboard.writeText){ try{ navigator.clipboard.writeText(t); ok=true }catch(e){} } if(!ok){ var ta=document.createElement('textarea'); ta.value=t; ta.setAttribute('readonly',''); ta.style.position='fixed'; ta.style.bottom='0'; ta.style.left='0'; ta.style.opacity='0.01'; document.body.appendChild(ta); ta.focus(); ta.select(); try{ ok=document.execCommand('copy') }catch(e){} document.body.removeChild(ta) } if(!ok){ try{ alert(t) }catch(_){ } } return ok }
function setRelayStatus(on){ if(relayStatusDot){ relayStatusDot.classList.toggle('on', !!on) } if(relayStatusText){ relayStatusText.textContent = on? 'Running' : 'Not running' } }
(function(){ var btn = document.getElementById('wsConnect'); if(btn){ btn.disabled = true } })();
+ (function(){ if(wsUrl && !(wsUrl.value||'').trim()){ wsUrl.value = BLS.relayUrl() } })();
(function(){
var pollMs = 3000;
function schedule(){ setTimeout(run, pollMs) }
function run(){ checkRelayOnce().then(function(ok){ setRelayStatus(!!ok); var btn = document.getElementById('wsConnect'); if(btn){ btn.disabled = !ok } pollMs = ok ? 3000 : Math.min(Math.round(pollMs*1.5), 30000); schedule() }).catch(function(){ setRelayStatus(false); var btn = document.getElementById('wsConnect'); if(btn){ btn.disabled = true } pollMs = Math.min(Math.round(pollMs*1.5), 30000); schedule() }) }
- async function checkRelayOnce(){ var host = location.hostname || 'localhost'; var url = 'ws://' + host + ':8787'; try{ return await new Promise(function(resolve){ var temp = new WebSocket(url); var done=false; var timer = setTimeout(function(){ if(!done){ resolve(false); if(temp && temp.readyState===1){ try{ temp.close() }catch(e){} } } }, 1500);
+ async function checkRelayOnce(){ var url = BLS.relayUrl(); try{ return await new Promise(function(resolve){ var temp = new WebSocket(url); var done=false; var timer = setTimeout(function(){ if(!done){ resolve(false); if(temp && temp.readyState===1){ try{ temp.close() }catch(e){} } } }, 1500);
temp.onopen = function(){ try{ temp.send(JSON.stringify({type:'ping'})) }catch(e){} };
temp.onmessage = function(ev){ try{ var msg = JSON.parse(ev.data); if(msg && msg.type==='pong'){ done=true; clearTimeout(timer); if(temp && temp.readyState===1){ try{ temp.close() }catch(e){} } resolve(true); return } }catch(e){} resolve(false) };
temp.onerror = function(){ resolve(false) };
@@ -458,7 +459,7 @@ if(document.readyState==='loading'){ document.addEventListener('DOMContentLoaded
run()
})();
if(startRelayBtn){ startRelayBtn.addEventListener('click', function(){ var h = renderOsHelp(); var ok = copyText(h.start); var old = startRelayBtn.textContent; startRelayBtn.textContent = ok? 'Copied!' : 'Copy Start Command'; setTimeout(function(){ startRelayBtn.textContent = old }, 1200) }) }
- if(stopRelayBtn){ stopRelayBtn.addEventListener('click', function(){ var host = location.hostname || 'localhost'; var url = 'ws://' + host + ':8787'; try{ var temp = new WebSocket(url); temp.onopen = function(){ try{ temp.send(JSON.stringify({type:'shutdown'})) }catch(e){} }; setTimeout(function(){ setRelayStatus(false) }, 1500) }catch(e){} }) }
+ if(stopRelayBtn){ stopRelayBtn.addEventListener('click', function(){ var url = BLS.relayUrl(); try{ var temp = new WebSocket(url); temp.onopen = function(){ try{ temp.send(JSON.stringify({type:'shutdown'})) }catch(e){} }; setTimeout(function(){ setRelayStatus(false) }, 1500) }catch(e){} }) }
function wsConnect(){ var url = (wsUrl.value||'').trim(); var sid = (wsSession.value||'').trim(); if(!sid){ sid = randSid(); wsSession.value = sid; updateJoinLink() } var btn = document.getElementById('wsConnect'); if(btn && btn.disabled){ wsStatus.textContent = 'Relay not running'; return } if(ws){ try{ ws.close() }catch(e){} ws=null } if(!url){ wsStatus.textContent = 'Missing URL'; return } try{ ws = new WebSocket(url) }catch(e){ wsStatus.textContent = 'Error'; return } wsSid = sid; wsStatus.textContent = 'Connecting...'; ws.onopen = function(){ wsConnected = true; wsStatus.textContent = 'Connected'; try{ ws.send(JSON.stringify({type:'join', sessionId:sid, role:'admin'})) }catch(e){} publishState() }; ws.onclose = function(){ wsConnected = false; wsStatus.textContent = 'Disconnected' }; ws.onerror = function(){ wsStatus.textContent = 'Error' }; ws.onmessage = function(ev){ try{ var msg = JSON.parse(ev.data); if(msg && msg.type==='joined'){ wsStatus.textContent = 'Connected' } else if(msg && msg.type==='patientViewport'){ var pv = msg.payload||{}; try{ window.PatientViewport = pv }catch(_){ } var r = window.AdminRenderer; if(r){ r.resize(parseInt(pv.canvasW||640,10), parseInt(pv.canvasH||360,10)) } var pvShort = document.getElementById('patientViewportShort'); if(pvShort){ pvShort.textContent = (pv.canvasW||'?')+'×'+(pv.canvasH||'?') } } }catch(e){} } }
function wsDisconnect(){ if(ws){ try{ ws.close() }catch(e){} } ws=null; wsConnected=false; wsStatus.textContent = 'Disconnected' }
function refreshNetMode(){ var m = (netModeSel && netModeSel.value) || 'relay'; if(rtcControls) rtcControls.style.display = (m==='webrtc'? 'flex':'none'); var relayRows = document.querySelectorAll('#osHelp, #wsUrl, #wsSession, #detectIP, #wsConnect, #wsDisconnect'); for(var i=0;i/ws
+ relayUrl: function(){
+ if(!/^https?:$/.test(location.protocol)) return 'ws://localhost:8787';
+ var base = location.pathname.replace(/[^/]*$/, '');
+ if(location.protocol === 'https:' || base !== '/'){
+ return (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + base + 'ws';
+ }
+ return 'ws://' + (location.hostname || 'localhost') + ':8787';
+ },
defaultState: defaultState,
loadState: loadState,
saveState: saveState,
diff --git a/js/patient.js b/js/patient.js
index 11523b3..dba6f50 100644
--- a/js/patient.js
+++ b/js/patient.js
@@ -160,7 +160,7 @@
var qs = new URLSearchParams(location.search);
var hashSid = (location.hash || '').replace('#', '');
var wsSid = qs.get('sid') || hashSid || 'default';
- var wsUrl = qs.get('url') || ('ws://' + (location.hostname || 'localhost') + ':8787');
+ var wsUrl = qs.get('url') || BLS.relayUrl();
var rtcMode = !!qs.get('p2p');
if(!rtcMode && wsUrl){
diff --git a/server.py b/server.py
index 417e984..c2d6bec 100644
--- a/server.py
+++ b/server.py
@@ -213,27 +213,34 @@ class _QuietHTTP(SimpleHTTPRequestHandler):
pass
+def _argval(flag, default):
+ args = sys.argv[1:]
+ return args[args.index(flag) + 1] if flag in args else default
+
+
def main():
os.chdir(os.path.dirname(os.path.abspath(__file__)))
+ bind = _argval('--bind', '0.0.0.0') # e.g. a tailnet IP behind a reverse proxy
+ http_port = int(_argval('--http-port', HTTP_PORT))
socketserver.ThreadingTCPServer.allow_reuse_address = True
ThreadingHTTPServer.allow_reuse_address = True
- httpd = ThreadingHTTPServer(('0.0.0.0', HTTP_PORT), _QuietHTTP)
- wsd = socketserver.ThreadingTCPServer(('0.0.0.0', WS_PORT), WSHandler)
+ httpd = ThreadingHTTPServer((bind, http_port), _QuietHTTP)
+ wsd = socketserver.ThreadingTCPServer((bind, WS_PORT), WSHandler)
httpd.daemon_threads = True
wsd.daemon_threads = True
_SERVERS.extend([httpd, wsd])
- ip = local_ip()
+ ip = bind if bind not in ('0.0.0.0', '') else local_ip()
print('backnforth running:')
- print(' Therapist page: http://%s:%d/' % (ip, HTTP_PORT))
+ print(' Therapist page: http://%s:%d/' % (ip, http_port))
print(' Session relay: ws://%s:%d' % (ip, WS_PORT))
print('Press Ctrl+C to stop.')
threading.Thread(target=wsd.serve_forever, daemon=True).start()
if '--no-browser' not in sys.argv:
try:
- webbrowser.open('http://%s:%d/easy.html' % (ip, HTTP_PORT))
+ webbrowser.open('http://%s:%d/easy.html' % (ip, http_port))
except Exception:
pass
try: