backnforth/index.html
type-two 4a46417cba Safety, privacy, session security, bug fixes, zero-install server
- First-run safety notice (photosensitivity, clinical use, privacy); glow
  rate capped at 2.5Hz below the photosensitive trigger band
- Patient data: initials/code hint, Export All / Wipe All buttons
- Relay sessions get crypto-random room ids instead of guessable room-1
- Fix: Pan Sync off now actually stops pink-noise panning
- Fix: slider drags no longer audibly restart the noise source
- Fix: switching pink->discrete preset no longer leaves noise running
- Fix: Speed Boost no longer compounds into stored speed across reloads
- Fix: patient/preset names HTML-escaped in lists
- Fix: patientViewport now relays over LAN (old server dropped it)
- server.py rewritten stdlib-only: static site + WS relay in one process,
  no pip/venv; setup.sh/bat deleted, start scripts are now 3 lines
- MIT LICENSE added; README rewritten with safety-first quick start

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 14:38:05 +10:00

671 lines
30 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bilateral Stimulation Simulator - Admin</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='6' fill='%231f6feb'/%3E%3C/svg%3E">
<link rel="stylesheet" href="css/admin.css">
</head>
<body>
<div id="safetyNotice" style="display:none; position:fixed; inset:0; z-index:9999; background:rgba(0,0,0,0.8); overflow:auto;">
<div style="max-width:560px; margin:8vh auto; background:#1c2128; color:#e6edf3; border-radius:12px; padding:28px; font-family:system-ui,-apple-system,sans-serif; line-height:1.55;">
<h2 style="margin-top:0">⚠️ Read before first use</h2>
<ul style="padding-left:20px">
<li style="margin-bottom:10px"><strong>Photosensitivity:</strong> moving and pulsing visuals can trigger seizures in people with photosensitive epilepsy. Ask about seizure history before use, keep Glow Pulse rate low, and stop immediately if the viewer feels unwell.</li>
<li style="margin-bottom:10px"><strong>Clinical use:</strong> bilateral stimulation can surface intense memories and emotions. This tool assists a trained clinician — it is not therapy by itself and not for unsupervised trauma processing.</li>
<li style="margin-bottom:10px"><strong>Privacy:</strong> patient records and notes are stored unencrypted in this browser. Use initials or a code instead of full names, and treat this computer as part of the clinical record.</li>
</ul>
<button id="safetyAck" style="width:100%; padding:12px; border:none; border-radius:8px; background:#1f6feb; color:#fff; font-size:15px; cursor:pointer">I understand</button>
</div>
</div>
<script>
(function(){
var seen = false; try{ seen = localStorage.getItem('bls_safety_ack')==='1' }catch(_){ }
if(seen) return;
var n = document.getElementById('safetyNotice');
n.style.display = 'block';
document.getElementById('safetyAck').addEventListener('click', function(){
try{ localStorage.setItem('bls_safety_ack','1') }catch(_){ }
n.style.display = 'none';
});
})();
</script>
<header>
<div class="header-content">
<h1>Bilateral Stimulation Simulator</h1>
<div class="simple-toggle" style="margin:0 auto; display:flex; align-items:center; gap:8px;">
<label class="control-label" style="color:inherit;">Simple View</label>
<label class="toggle">
<input type="checkbox" id="simpleViewEnable">
<span class="toggle-track"><span class="toggle-thumb"></span></span>
</label>
</div>
<div class="header-actions">
<button id="openPatient" class="btn-secondary">
<span class="btn-icon">👁️</span>
Open Patient View
</button>
</div>
</div>
</header>
<div id="lockBanner" class="lock-banner">
<span class="lock-icon">🔒</span>
LOCKED • Hold U to unlock
</div>
<main>
<!-- Left Column: Controls -->
<aside class="sidebar">
<section class="panel collapsible" id="visualPanel">
<h2 role="button" tabindex="0">
<span class="panel-icon">🎨</span>
Visual Settings
<span class="panel-actions">
<label class="toggle" title="Toggle Dark/Light">
<input type="checkbox" id="themeToggle">
<span class="toggle-track"><span class="toggle-thumb"></span></span>
</label>
</span>
</h2>
<div class="control-group">
<label class="control-label">Shape</label>
<div class="button-group" id="shapeControls">
<button data-shape="circle" class="active">⚪ Circle</button>
<button data-shape="square">⬛ Square</button>
<button data-shape="triangle">🔺 Triangle</button>
</div>
</div>
<div class="control-group">
<label class="control-label">Shape Color</label>
<div class="color-swatches" id="shapeSwatches">
<button class="swatch swatch-green" data-color="#00ff00" title="Green"></button>
<button class="swatch swatch-blue" data-color="#0066ff" title="Blue"></button>
<button class="swatch swatch-yellow" data-color="#ffd400" title="Yellow"></button>
<button class="swatch swatch-red" data-color="#ff2a2a" title="Red"></button>
<button class="swatch swatch-white" data-color="#ffffff" title="White"></button>
<button class="swatch swatch-custom" id="shapeCustom" title="Custom Color"></button>
</div>
</div>
<div class="control-group">
<label class="control-label">Background</label>
<div class="color-swatches" id="bgSwatches">
<button class="swatch swatch-black" data-color="#000000" title="Black"></button>
<button class="swatch swatch-blue" data-color="#0066ff" title="Blue"></button>
<button class="swatch swatch-yellow" data-color="#ffd400" title="Yellow"></button>
<button class="swatch swatch-red" data-color="#ff2a2a" title="Red"></button>
<button class="swatch swatch-white" data-color="#ffffff" title="White"></button>
<button class="swatch swatch-custom" id="bgCustom" title="Custom Color"></button>
</div>
</div>
<div class="control-group">
<label class="control-label">Movement Direction</label>
<div class="button-group" id="directionControls">
<button data-direction="lr" class="active">HORIZ ↔️</button>
<button data-direction="ud">VERT ↕️</button>
<button data-direction="diag_down">DIAG L ↘️</button>
<button data-direction="diag_up">DIAG R ↗️</button>
</div>
</div>
<div class="control-group">
<label class="control-label">Display Width</label>
<div class="button-group" id="widthControls">
<button data-width="full" class="active">Full</button>
<button data-width="reduced">Reduced</button>
</div>
</div>
<div class="control-group">
<label class="control-label">Size: <span id="sizeLabel">28 px</span></label>
<input type="range" id="size" min="10" max="100" step="0.5" value="28" class="slider">
</div>
<div class="control-group">
<label class="control-label">Vertical Position: <span id="yPercentLabel">50%</span></label>
<div class="slider-with-reset">
<input type="range" id="yPercent" min="0" max="100" step="1" value="50" class="slider">
<button id="yReset" class="btn-small">Reset</button>
</div>
<div class="control-row" style="margin-top:8px;">
<div class="control-group">
<label class="control-label" style="margin:0;">Vertical Oscillation</label>
<label class="toggle">
<input type="checkbox" id="yOscillateEnable">
<span class="toggle-track"><span class="toggle-thumb"></span></span>
</label>
</div>
<div class="control-group">
<label class="control-label">Gradual Speed Ramp</label>
<label class="toggle">
<input type="checkbox" id="rampEnable">
<span class="toggle-track"><span class="toggle-thumb"></span></span>
</label>
</div>
</div>
<div class="control-group">
<label class="control-label">Ramp Duration: <span id="rampLabel">10 s</span></label>
<input type="range" id="rampSeconds" min="1" max="60" step="1" value="10" class="slider">
</div>
</div>
</section>
<section class="panel collapsible" id="networkPanel">
<h2 role="button" tabindex="0">
<span class="panel-icon"></span>
Effects
</h2>
<div class="control-group">
<label class="control-label">Glow Pulse Effect</label>
<label class="toggle">
<input type="checkbox" id="glowEnable">
<span class="toggle-track"><span class="toggle-thumb"></span></span>
</label>
</div>
<div class="control-group control-indent">
<label class="control-label">Intensity: <span id="glowIntensityLabel">100%</span></label>
<input type="range" id="glowIntensity" min="0" max="200" step="5" value="100" class="slider">
</div>
<div class="control-group control-indent">
<label class="control-label">Rate: <span id="glowRateLabel">0.5 Hz</span></label>
<!-- max 2.5 Hz: photosensitive-epilepsy trigger band starts around 3 Hz -->
<input type="range" id="glowRate" min="0.1" max="2.5" step="0.1" value="0.5" class="slider">
</div>
<div class="control-group">
<label class="control-label">Wiggle Effect</label>
<label class="toggle">
<input type="checkbox" id="wiggleEnable">
<span class="toggle-track"><span class="toggle-thumb"></span></span>
</label>
</div>
<div class="control-group control-indent">
<label class="control-label">Amount: <span id="wiggleLabel">0 px</span></label>
<input type="range" id="wiggleAmplitude" min="0" max="10" step="1" value="0" class="slider">
</div>
<div class="control-group">
<label class="control-label">Movement Style</label>
<div class="button-group" id="easingControls">
<button data-easing="linear" class="active">Linear</button>
<button data-easing="ease">Smooth</button>
</div>
</div>
<div class="control-group">
<label class="control-label">Edge Pause: <span id="edgePauseLabel">0 ms</span></label>
<input type="range" id="edgePause" min="0" max="150" step="10" value="0" class="slider">
</div>
</section>
<section class="panel panel-recording collapsible" id="recordingPanel">
<h2 role="button" tabindex="0">
<span class="panel-icon">🔊</span>
Audio Settings
</h2>
<div class="control-group">
<div class="button-group" id="audioModeButtons">
<button data-mode="programmatic" class="active">Program</button>
<button id="loadAudioBtn">Load Audio</button>
</div>
</div>
<div class="control-group" style="display:flex; align-items:center; justify-content:space-between; gap:8px;">
<div>
<label class="control-label">Enable Audio</label>
<label class="toggle">
<input type="checkbox" id="audioEnable">
<span class="toggle-track"><span class="toggle-thumb"></span></span>
</label>
</div>
<button id="testSound" class="btn-small">Test Sound</button>
</div>
<div id="programmaticControls" class="audio-controls">
<div class="control-group">
<div class="button-group" id="soundToneToggle">
<button data-mode="sound" class="active">Sound</button>
<button data-mode="tone">Tone</button>
</div>
</div>
<div id="soundPresetControls" class="control-group">
<label class="control-label">Sound Type</label>
<select id="soundPreset" class="select-input">
<option value="pink">Pink Noise</option>
<option value="hybrid">Hybrid (noise + edge cues)</option>
<option value="click">Click</option>
<option value="ping">Ping</option>
<option value="woodblock">Woodblock</option>
<option value="bell">Bell</option>
<option value="bass">Bass</option>
<option value="kick">Kick</option>
<option value="snare">Snare</option>
<option value="hihat">Hi-Hat</option>
<option value="sweep">Sweep</option>
<option value="zap">Zap</option>
<option value="bubble">Bubble</option>
</select>
</div>
<div id="tonePresetControls" class="control-group">
<label class="control-label">Tone Preset</label>
<select id="tonePreset" class="select-input">
<option value="warm_low">Warm Low — 180 Hz @ 100 ms</option>
<option value="soft_pulse">Soft Pulse — 220 Hz @ 90 ms</option>
<option value="neutral_pulse">Neutral Pulse — 280 Hz @ 90 ms</option>
<option value="bright_pulse">Bright Pulse — 330 Hz @ 80 ms</option>
<option value="soft_tick">Soft Tick — 440 Hz @ 70 ms</option>
<option value="medium_tick">Medium Tick — 520 Hz @ 60 ms</option>
<option value="clear_click">Clear Click — 660 Hz @ 40 ms</option>
<option value="bright_ping">Bright Ping — 880 Hz @ 50 ms</option>
<option value="light_ping">Light Ping — 1000 Hz @ 50 ms</option>
<option value="sharp_cue">Sharp Cue — 1200 Hz @ 30 ms</option>
<option value="accent">Accent — 1500 Hz @ 30 ms</option>
<option value="spark">Spark — 1800 Hz @ 30 ms</option>
<option value="crisp">Crisp — 2000 Hz @ 30 ms</option>
</select>
</div>
<div id="toneFreqRow" class="control-group">
<label class="control-label">Frequency (Hz)</label>
<input type="number" id="toneFreq" min="100" max="2000" step="10" value="440" class="number-input">
</div>
<div id="toneDurRow" class="control-group">
<label class="control-label">Duration: <span id="toneDurationLabel">70 ms</span></label>
<input type="range" id="toneDuration" min="30" max="150" step="5" value="70" class="slider">
</div>
<div id="adsrField" class="adsr-controls">
<details class="adsr-details">
<summary>Advanced: ADSR Envelope</summary>
<div class="adsr-grid">
<div class="adsr-control">
<label>Attack: <span id="adsrAttackLabel">8 ms</span></label>
<input type="range" id="adsrAttack" min="5" max="20" step="1" value="8" class="slider">
</div>
<div class="adsr-control">
<label>Decay: <span id="adsrDecayLabel">80 ms</span></label>
<input type="range" id="adsrDecay" min="40" max="120" step="5" value="80" class="slider">
</div>
<div class="adsr-control">
<label>Sustain: <span id="adsrSustainLabel">0.30</span></label>
<input type="range" id="adsrSustain" min="0" max="1" step="0.05" value="0.3" class="slider">
</div>
<div class="adsr-control">
<label>Release: <span id="adsrReleaseLabel">10 ms</span></label>
<input type="range" id="adsrRelease" min="5" max="50" step="1" value="10" class="slider">
</div>
</div>
</details>
</div>
<div class="control-group">
<label class="control-label">Pan synced to movement</label>
<label class="toggle">
<input type="checkbox" id="syncPan" checked>
<span class="toggle-track"><span class="toggle-thumb"></span></span>
</label>
</div>
<div class="control-group">
<label class="control-label">Pan Rate: <span id="panRateLabel">0.5 Hz</span></label>
<input type="range" id="panRate" min="0.1" max="2" step="0.1" value="0.5" class="slider">
</div>
<div class="control-group">
<label class="control-label">Cue Rate: <span id="cueRateLabel">1.2 Hz</span></label>
<input type="range" id="cueRate" min="0.1" max="3" step="0.1" value="1.2" class="slider">
</div>
<div class="control-group">
<label class="control-label">Volume: <span id="volumeLabel">60%</span></label>
<input type="range" id="volume" min="0" max="100" step="5" value="60" class="slider">
</div>
</div>
</section>
<section class="panel collapsible" id="presetsPanel">
<h2 role="button" tabindex="0">PRESETS</h2>
<div class="preset-controls">
<div class="control-row">
<input type="text" id="presetName" placeholder="Preset name..." class="text-input flex-1">
<button id="savePreset" class="btn-secondary">Save</button>
</div>
<div class="control-row">
<select id="presetList" class="select-input flex-1"></select>
<button id="applyPreset" class="btn-secondary">Apply</button>
<button id="deletePreset" class="btn-danger-outline">Delete</button>
</div>
<div class="control-row">
<button id="exportPresets" class="btn-secondary">Export JSON</button>
<button id="importPresets" class="btn-secondary">Import JSON</button>
</div>
</div>
</section>
<section id="controllersPanel" class="panel collapsible">
<h2 role="button" tabindex="0">CONTROL</h2>
<div class="controller-content">
<div class="status-display">
<span id="ctlStatusDot" class="status-dot"></span>
<span id="ctlStatusText" class="status-value">No device</span>
</div>
<div class="control-group">
<label class="control-label">MIDI Input</label>
<select id="midiSelect" class="select-input"></select>
</div>
<div class="mapping-section">
<h3>Map Actions</h3>
<div class="mapping-buttons">
<button data-map-action="start" class="btn-map">Start Session</button>
<button data-map-action="stop" class="btn-map">End Session</button>
<button data-map-action="speed_up" class="btn-map">Speed +</button>
<button data-map-action="speed_down" class="btn-map">Speed </button>
<button data-map-action="volume_up" class="btn-map">Volume +</button>
<button data-map-action="volume_down" class="btn-map">Volume </button>
<button data-map-action="audio_toggle" class="btn-map">Audio Toggle</button>
<button data-map-action="glow_toggle" class="btn-map">Glow Toggle</button>
<button data-map-action="wiggle_toggle" class="btn-map">Wiggle Toggle</button>
<button data-map-action="lock_toggle" class="btn-map">Lock Toggle</button>
<button data-map-action="record_start" class="btn-map">Record Start</button>
<button data-map-action="record_stop" class="btn-map">Record Stop</button>
<button data-map-action="replay" class="btn-map">Replay</button>
<button data-map-action="shape_next" class="btn-map">Cycle Shape</button>
<button data-map-action="direction_next" class="btn-map">Cycle Direction</button>
<button data-map-action="easing_next" class="btn-map">Cycle Smoothness</button>
<button data-map-action="program_next" class="btn-map">Cycle Audio Preset</button>
</div>
</div>
<div class="mapping-section">
<h3>Current Mappings</h3>
<div id="ctlMappings" class="mappings-list"></div>
</div>
<div class="mapping-section">
<h3>Hardware Controller</h3>
<div class="control-row">
<button id="connectHID" class="btn-secondary">Connect HID Device</button>
<span id="deviceStatus" class="status-value">Not connected</span>
</div>
<div class="control-group">
<label class="control-label">Sensitivity: <span id="knobSensLabel">x5</span></label>
<input type="range" id="knobSens" min="1" max="50" step="1" value="5" class="slider">
</div>
<div class="control-group" style="display:flex; align-items:center; justify-content:space-between; gap:8px;">
<div>
<label class="control-label">Enable Lock Hotkey (L to lock, U to unlock)</label>
<label class="toggle">
<input type="checkbox" id="lockHotkeyEnable" checked>
<span class="toggle-track"><span class="toggle-thumb"></span></span>
</label>
</div>
</div>
</div>
</div>
</section>
</aside>
<!-- Right Column: Session & Preview -->
<div class="main-content">
<section class="panel panel-session collapsible">
<h2 role="button" tabindex="0">
<span class="panel-icon"></span>
Session Control
</h2>
<div class="preview-container">
<canvas id="previewCanvas" class="preview-canvas" width="640" height="360"></canvas>
</div>
<div class="session-actions" style="margin-top:10px;">
<button id="startSession" class="btn-primary btn-large">
<span class="btn-icon">▶️</span>
Start Session
</button>
<button id="endSession" class="btn-danger btn-large">
<span class="btn-icon">⏹️</span>
End Session
</button>
</div>
<div class="session-controls">
<div class="control-row">
<div class="control-group">
<label class="control-label">Speed: <span id="speedLabel">250 px/s</span></label>
<div class="speed-control">
<button id="speedDown" class="btn-adjust"></button>
<input type="range" id="speed" min="50" max="1800" step="1" value="250" class="slider slider-wide">
<button id="speedUp" class="btn-adjust">+</button>
</div>
</div>
</div>
<div class="timer-section">
<div id="patientViewportShort" class="status-value" style="margin-bottom:8px; font-size:12px; opacity:0.8;"></div>
<div class="timer-display">
<div class="timer-box">
<label>Duration (minutes)</label>
<input type="number" id="timerMinutes" min="0" value="10" class="timer-input">
</div>
<div class="timer-box">
<label>Remaining</label>
<div id="timerDisplay" class="timer-value">10:00</div>
</div>
<div class="timer-box">
<label>Elapsed</label>
<div id="elapsedText" class="timer-value">00:00</div>
</div>
</div>
<div class="status-bar">
<span class="status-label">Status:</span>
<span id="statusText" class="status-value">Idle</span>
</div>
</div>
</div>
</section>
<section class="panel panel-recording collapsible">
<h2 role="button" tabindex="0">
<span class="panel-icon"><EFBFBD></span>
Recording & Playback
</h2>
<div class="recording-controls">
<div class="control-row">
<button id="recStart" class="btn-record">
<span class="record-dot"></span>
Record
</button>
<button id="recStop" class="btn-secondary">Stop & Save</button>
</div>
<div class="control-row">
<select id="recList" class="select-input flex-1"></select>
<button id="recPlay" class="btn-secondary">Replay</button>
<button id="recExport" class="btn-secondary">Export</button>
<button id="recImport" class="btn-secondary">Import</button>
</div>
<div class="control-row">
<button id="recDelete" class="btn-danger-outline">Delete</button>
<button id="recClear" class="btn-danger-outline">Clear All</button>
</div>
</div>
</section>
<section class="panel collapsible">
<h2 role="button" tabindex="0">
<span class="panel-icon">👤</span>
Patient Management
</h2>
<div class="patient-controls">
<div class="control-row">
<input type="text" id="patientName" placeholder="Initials or code (not full name)..." class="text-input flex-1">
<button id="savePatient" class="btn-secondary">Save</button>
</div>
<div class="control-label" style="opacity:0.75; font-size:12px">
🔒 Stored unencrypted in this browser — use initials or a code, never a full name.
</div>
<div class="control-row">
<select id="patientList" class="select-input flex-1"></select>
<button id="loadPatient" class="btn-secondary">Load</button>
<button id="deletePatient" class="btn-danger-outline">Delete</button>
</div>
<div class="patient-stats">
<div class="stat-item">
<span class="stat-label">Total Sessions:</span>
<span id="sessionCount" class="stat-value">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Last Session:</span>
<span id="lastSessionInfo" class="stat-value"></span>
</div>
</div>
<div class="control-group">
<label class="control-label">Session History</label>
<div id="sessionHistory" class="session-history"></div>
<button id="clearSessions" class="btn-danger-outline btn-small">Clear All Sessions</button>
</div>
<div class="control-group">
<label class="control-label">Session Notes</label>
<div class="control-row">
<select id="sessionSelect" class="select-input flex-1"></select>
<button id="loadSession" class="btn-secondary btn-small">Load</button>
<button id="saveSessionNotes" class="btn-secondary btn-small">Save Notes</button>
<button id="deleteSession" class="btn-danger-outline btn-small">Delete Session</button>
</div>
<textarea id="sessionNotes" class="textarea-input" placeholder="Enter session notes..."></textarea>
</div>
<div class="control-row">
<button id="exportAllData" class="btn-secondary btn-small">Export All Patient Data</button>
<button id="wipeAllData" class="btn-danger-outline btn-small">Wipe All Patient Data</button>
</div>
</div>
</section>
<section class="panel collapsible" id="networkPanelSection">
<h2 id="networkHeading" role="button" tabindex="0">
<span class="panel-icon">🌐</span>
Network Connection
</h2>
<div class="network-controls">
<div class="control-group">
<label class="control-label">Connection Mode</label>
<select id="netMode" class="select-input">
<option value="relay">Relay (WebSocket)</option>
<option value="webrtc">WebRTC (P2P)</option>
</select>
</div>
<div id="webrtcControls" class="webrtc-section">
<div class="control-group">
<button id="rtcCreateOffer" class="btn-secondary">Create Offer</button>
<input type="text" id="rtcOffer" placeholder="Offer (base64)" class="text-input flex-1">
</div>
<div class="qr-container">
<img id="rtcOfferQR" alt="Offer QR Code" class="qr-code">
</div>
<div class="control-group">
<button id="rtcAcceptAnswer" class="btn-secondary">Accept Answer</button>
<input type="text" id="rtcAnswer" placeholder="Answer (base64)" class="text-input flex-1">
</div>
<div class="status-display">
<span class="status-label">WebRTC Status:</span>
<span id="rtcStatus" class="status-value">Idle</span>
</div>
</div>
<div class="relay-section">
<div class="status-display">
<span id="relayStatusDot" class="status-dot"></span>
<span id="relayStatusText" class="status-value">Not running</span>
</div>
<div class="control-row">
<button id="startRelayBtn" class="btn-secondary">Copy Start Command</button>
<button id="stopRelayBtn" class="btn-danger-outline">Stop Server</button>
</div>
<div class="control-group">
<label class="control-label">Connection Details</label>
<div class="connection-inputs">
<input type="text" id="wsUrl" placeholder="ws://192.168.x.x:8787" class="text-input">
<input type="text" id="wsSession" placeholder="auto (random room id)" class="text-input">
</div>
</div>
<div class="control-row">
<button id="detectIP" class="btn-secondary">Detect Local IP</button>
<button id="wsConnect" class="btn-secondary">Connect</button>
<button id="wsDisconnect" class="btn-danger-outline">Disconnect</button>
</div>
<div class="status-display">
<span class="status-label">Status:</span>
<span id="wsStatus" class="status-value">Disconnected</span>
</div>
<div class="control-group">
<label class="control-label">Patient Join Link</label>
<div class="control-row">
<input type="text" id="joinLink" readonly class="text-input flex-1">
<button id="copyJoinLink" class="btn-secondary">Copy</button>
</div>
</div>
<div id="osHelp" class="help-text"></div>
</div>
</div>
</section>
</div>
</main>
<script src="js/bls.js"></script>
<script src="js/audio.js"></script>
<script src="js/webrtc.js"></script>
<script src="js/recorder.js"></script>
<script src="js/recording-store.js"></script>
<script src="js/controllers.js"></script>
<script src="js/admin.js"></script>
</body>
</html>