Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
/**
* Self-contained local dashboard for the machine-adapter, served by the hook server on
* 127.0.0.1:<port>. Vanilla HTML/CSS/JS (no framework, no build) — polls GET /api/status and drives
* the loopback management API (approve pairing, unpair, stop). Scoped to LOCAL-only concerns (adapter
* health, machine fingerprint, local pairings); it never renders chat/transcripts — that's the cloud
* web (WEB_URL/machine/<agentId>).
*
* CSRF: mutating calls send the `x-adapter-local: 1` header, which a cross-origin page cannot set on a
* simple request (a custom header forces a CORS preflight the server never allows) — so only this
* same-origin page (and the CLI) can trigger actions.
*/
export const LOCAL_WEB_HTML = /* html */ `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>harness</title>
<style>
:root { color-scheme: dark; --bg:#0e1116; --card:#161b22; --line:#232a34; --fg:#e6edf3; --mut:#8b949e; --faint:#6e7681; --grn:#3fb950; --red:#e5534b; --yel:#d29922; --acc:#4c8bf5; --mono: ui-monospace, SFMono-Regular, Menlo, monospace; }
* { box-sizing: border-box; }
body { margin:0; background:var(--bg); color:var(--fg); font:14px/1.5 -apple-system, system-ui, sans-serif; }
a { color:var(--acc); text-decoration:none; } a:hover { text-decoration:underline; }
.wrap { max-width:960px; margin:0 auto; padding:20px 16px 48px; }
.bar { display:flex; flex-wrap:wrap; align-items:center; gap:8px 16px; background:var(--card); border:1px solid var(--line); border-radius:12px; padding:14px 16px; }
.brand { font-weight:600; }
.dot { width:9px; height:9px; border-radius:50%; display:inline-block; vertical-align:middle; }
.on { background:var(--grn); } .off { background:var(--red); } .idle { background:var(--faint); }
.mut { color:var(--mut); } .mono { font-family:var(--mono); }
.fp { font-family:var(--mono); letter-spacing:.04em; }
.pill { display:inline-block; border:1px solid var(--line); border-radius:999px; padding:1px 7px; font-size:11px; color:var(--mut); margin-right:6px; text-transform:uppercase; }
.spacer { flex:1; }
.btn { background:none; border:1px solid var(--line); color:var(--fg); border-radius:8px; padding:5px 12px; font-size:12.5px; cursor:pointer; }
.btn:hover { border-color:var(--acc); }
.btn.danger:hover { border-color:var(--red); color:var(--red); }
.btn.primary { background:var(--acc); border-color:var(--acc); color:#fff; font-weight:600; }
.btn.primary:hover { filter:brightness(1.08); }
.btn:disabled { opacity:.6; cursor:default; }
/* approve modal */
.modal { position:fixed; inset:0; background:rgba(0,0,0,.55); display:flex; align-items:center; justify-content:center; z-index:50; padding:16px; }
.modalCard { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:22px 22px 18px; max-width:440px; width:100%; box-shadow:0 12px 40px rgba(0,0,0,.5); }
.mTitle { font-size:17px; font-weight:600; }
.mSub { color:var(--mut); font-size:13px; margin-top:8px; line-height:1.5; }
.mCodeLabel { font-size:11px; letter-spacing:.06em; text-transform:uppercase; color:var(--faint); margin-top:16px; }
.mCode { font-family:var(--mono); font-size:30px; letter-spacing:.22em; font-weight:700; margin-top:4px; }
.mNote { font-size:12px; color:var(--mut); margin-top:6px; }
.mInput { font-family:var(--mono); background:var(--bg); border:1px solid var(--line); color:var(--fg); border-radius:8px; padding:8px 12px; letter-spacing:.2em; text-transform:uppercase; width:150px; margin-top:6px; }
.mFp { font-size:12px; color:var(--mut); margin-top:16px; }
.mBtns { display:flex; justify-content:flex-end; gap:10px; margin-top:20px; }
.grid { display:grid; grid-template-columns:1fr 1fr; gap:16px; margin-top:16px; }
@media (max-width:720px){ .grid { grid-template-columns:1fr; } }
.card { background:var(--card); border:1px solid var(--line); border-radius:12px; padding:14px 16px; }
.card h2 { margin:0 0 10px; font-size:13px; font-weight:600; color:var(--mut); text-transform:uppercase; letter-spacing:.05em; }
.row { display:flex; align-items:center; gap:10px; padding:8px 0; border-top:1px solid var(--line); }
.row:first-of-type { border-top:0; }
.row .main { flex:1; min-width:0; }
.row .name { font-weight:500; }
.row .sub { color:var(--faint); font-size:12px; font-family:var(--mono); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.empty { color:var(--faint); padding:8px 0; }
.pending { margin-top:16px; background:#20160a; border:1px solid #4a3a12; border-radius:12px; padding:14px 16px; }
.pending input { font-family:var(--mono); background:var(--bg); border:1px solid var(--line); color:var(--fg); border-radius:8px; padding:6px 10px; letter-spacing:.2em; text-transform:uppercase; width:120px; }
.about { margin-top:16px; }
.statusGrid { display:grid; grid-template-columns:1fr 1fr; gap:8px 18px; }
@media (max-width:520px){ .statusGrid { grid-template-columns:1fr; } }
.metric { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:7px 0; border-top:1px solid var(--line); }
.metric:first-child, .metric:nth-child(2) { border-top:0; }
@media (max-width:520px){ .metric:nth-child(2) { border-top:1px solid var(--line); } }
.metric .k { color:var(--mut); }
.metric .v { font-weight:600; white-space:nowrap; }
.actions { display:flex; flex-wrap:wrap; gap:8px; margin-top:12px; }
.adv { margin-top:12px; border-top:1px solid var(--line); padding-top:10px; }
.adv summary { cursor:pointer; color:var(--mut); font-size:12.5px; }
.adv .kv { display:flex; gap:8px; padding:3px 0; font-size:12.5px; }
.adv .k { color:var(--faint); width:110px; flex:none; }
.adv .v { font-family:var(--mono); overflow:hidden; text-overflow:ellipsis; }
.toast { position:fixed; bottom:16px; left:50%; transform:translateX(-50%); background:var(--card); border:1px solid var(--line); border-radius:10px; padding:10px 16px; opacity:0; transition:opacity .2s; }
.toast.show { opacity:1; }
pre.logs { max-height:220px; overflow:auto; background:var(--bg); border:1px solid var(--line); border-radius:8px; padding:10px; font-size:11.5px; color:var(--mut); margin:0; }
</style>
</head>
<body>
<div class="wrap">
<div class="bar">
<span class="brand">harness</span>
<span id="conn"><span class="dot idle"></span> <span class="mut">connecting…</span></span>
<span id="device"><span class="dot idle"></span> <span class="mut">device —</span></span>
<span class="mut">machine <span id="agent" class="mono">—</span></span>
<span class="mut" id="agents">no agents</span>
<span class="mut">up <span id="uptime">—</span></span>
<span class="spacer"></span>
<button class="btn danger" onclick="stopAdapter()">Stop</button>
</div>
<div class="bar" style="margin-top:10px;">
<span class="mut">machine fingerprint</span> <span id="fp" class="fp">—</span>
<span class="spacer"></span>
<a id="weblink" href="#" target="_blank" rel="noreferrer">open in web ↗</a>
</div>
<div id="pending"></div>
<div class="grid">
<div class="card">
<h2>Sessions (tmux claude)</h2>
<div id="sessions"><div class="empty">loading…</div></div>
</div>
<div class="card">
<h2>Paired browsers <button class="btn danger" style="float:right;font-size:11px;padding:2px 8px" onclick="unpairAll()">Unpair all</button></h2>
<div id="pairs"><div class="empty">loading…</div></div>
</div>
</div>
<div class="card about">
<h2>Machine</h2>
<div id="about"></div>
<details id="logsBox" style="margin-top:10px"><summary class="mut" style="cursor:pointer">Logs</summary>
<pre class="logs" id="logs">—</pre>
</details>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
const H = { 'x-adapter-local': '1' };
let pendKey = null; // identity of the pending shown — so polling doesn't clobber the modal/input
let dismissedKey = null; // pending fully handled (approved) — don't rebuild until it changes
let modalClosed = false; // user hit Close on the approve modal → behave like no ?code (normal UI) this session
let lastStatus = null; // last /api/status snapshot (for closeModal to re-render inline immediately)
// Code from the "Open dashboard & approve" link on the web (?code=…). It travels via the URL the user
// clicks in their own browser (loopback), never through the backend — so CPace security is preserved.
const params = new URLSearchParams(location.search);
const urlCode = (params.get('code') || '').toUpperCase();
const setupMode = params.get('setup') === 'browser';
let setupModalOpen = false;
let setupInFlight = false;
let setupAutoAttempted = false;
function esc(s){ return String(s==null?'':s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
function toast(m){ const t=document.getElementById('toast'); t.textContent=m; t.classList.add('show'); clearTimeout(t._h); t._h=setTimeout(()=>t.classList.remove('show'),1800); }
function fmtDur(s){ s=Math.max(0,s|0); const d=s/86400|0,h=s%86400/3600|0,m=s%3600/60|0; return d?d+'d '+h+'h':h?h+'h '+m+'m':m?m+'m':s+'s'; }
function fmtWhen(ts){ try{ return new Date(ts).toISOString().slice(0,16).replace('T',' ') }catch{ return '' } }
async function post(path, body){
const r = await fetch(path, { method:'POST', headers:{...H, ...(body?{'content-type':'application/json'}:{})}, body: body?JSON.stringify(body):undefined });
const j = await r.json().catch(()=>({})); return { ok:r.ok, status:r.status, j };
}
// Strip ?code=… from the address bar (no reload) once the pairing is handled — so a refresh doesn't
// re-open the modal with a stale/consumed code, and the code doesn't linger in history.
function clearUrlCode(){ try { history.replaceState(null, '', location.pathname); } catch(e) {} }
function clearSetupUrl(){ try { history.replaceState(null, '', location.pathname); } catch(e) {} }
function showBrowserSetup(html){
document.getElementById('pending').innerHTML = '<div class="pending">'+html+'</div>';
}
function buildSetupApproveModal(s){
const machineUrl = s && s.webUrl && s.agentId ? (s.webUrl.replace(/\\/$/,'')+'/machine/'+s.agentId) : 'Machine';
document.getElementById('pending').innerHTML =
'<div class="modal"><div class="modalCard">'
+ '<div class="mTitle">Set up browser encryption?</div>'
+ '<div class="mSub">This creates a reusable 7-day setup link. Anyone with the link can pair a browser until it expires, so keep it private.</div>'
+ '<div class="mCodeLabel">machine</div><div class="mFp"><span class="fp">'+esc((s&&s.fingerprint)||'—')+'</span></div>'
+ '<div class="mCodeLabel">destination</div><div class="mFp">'+esc(machineUrl)+'</div>'
+ '<div class="mBtns"><button class="btn" onclick="closeSetupModal()">Cancel</button><button id="setupApproveBtn" class="btn primary" onclick="approveBrowserSetup()">Approve</button></div>'
+ '</div></div>';
}
function startBrowserSetup(){
if (setupInFlight) return;
setupModalOpen = true;
buildSetupApproveModal(lastStatus || {});
}
function closeSetupModal(){
setupModalOpen = false;
if (setupMode) {
history.back();
setTimeout(() => {
clearSetupUrl();
document.getElementById('pending').innerHTML = '';
if (lastStatus) render(lastStatus);
}, 250);
return;
}
clearSetupUrl();
document.getElementById('pending').innerHTML = '';
if (lastStatus) render(lastStatus);
}
async function approveBrowserSetup(){
if (setupInFlight) return;
setupInFlight = true;
setupModalOpen = false;
const btn = document.getElementById('setupApproveBtn');
if (btn) { btn.disabled = true; btn.textContent = 'Approving…'; }
showBrowserSetup('<b>Generating browser setup link…</b><div class="mut" style="margin-top:6px">You will be returned to Machine automatically.</div>');
const { ok, j } = await post('/api/e2ee/setup-link');
if (ok && typeof j.url === 'string' && j.url) {
location.replace(j.url);
return;
}
setupInFlight = false;
clearSetupUrl();
showBrowserSetup(
'<b>Could not generate setup link</b>'
+ '<div class="mut" style="margin-top:6px">'+esc(j.error || 'Try running harness browser-link in the terminal.')+'</div>'
+ '<div style="margin-top:10px"><button class="btn" onclick="startBrowserSetup()">Try again</button></div>'
);
}
async function doApprove(){
const el = document.getElementById('code');
const code = (urlCode || (el ? el.value.trim().toUpperCase() : '')).trim();
if(!code){ toast('Enter the pairing code'); return; }
clearUrlCode();
const btn = document.getElementById('approveBtn'); if(btn){ btn.disabled = true; btn.textContent = 'Approving…'; }
const { ok, j } = await post('/api/pair', { code });
if (ok){ toast('✓ Paired '+(j.fingerprint||'')); dismissedKey = pendKey; document.getElementById('pending').innerHTML = ''; }
else { toast('✗ '+(j.error||'failed')); if(btn){ btn.disabled = false; btn.textContent = 'Approve'; } }
refresh();
}
// Close = don't approve now → drop back to the normal page (same as opening without ?code): show the
// subtle inline banner instead of the blocking modal, for the rest of this session.
function closeModal(){ modalClosed = true; clearUrlCode(); pendKey = null; if (lastStatus) render(lastStatus); else document.getElementById('pending').innerHTML = ''; }
function pendingRoleLabel(s){ return s && s.pending && s.pending.role === 'device' ? 'device' : 'browser'; }
// Inline (non-blocking) banner when the dashboard is opened directly and a client happens to be waiting.
function buildInlinePending(s){
const role = pendingRoleLabel(s);
document.getElementById('pending').innerHTML =
'<div class="pending"><b>🔒 A '+esc(role)+' is waiting to pair</b> — <span class="mut">'+esc(s.pending.label)+'</span>'
+ '<div style="margin-top:8px">Enter the code shown on that '+esc(role)+': '
+ '<input id="code" maxlength="8" placeholder="ABC123" onkeydown="if(event.key===\\'Enter\\')doApprove()"> '
+ '<button class="btn" onclick="doApprove()">Approve</button></div></div>';
}
function buildApproveModal(s){
const p = s.pending;
const role = pendingRoleLabel(s);
const codeBlock = urlCode
? '<div class="mCodeLabel">verification code</div><div class="mCode">'+esc(urlCode)+'</div><div class="mNote">Make sure this matches the code on your '+esc(role)+' screen.</div>'
: '<div class="mCodeLabel">enter the code shown on your '+esc(role)+'</div><input id="code" class="mInput" maxlength="8" placeholder="ABC123" onkeydown="if(event.key===\\'Enter\\')doApprove()">';
document.getElementById('pending').innerHTML =
'<div class="modal"><div class="modalCard">'
+ '<div class="mTitle">🔒 Approve this '+esc(role)+'?</div>'
+ '<div class="mSub">A '+esc(role)+' (<b>'+esc(p.label)+'</b>) is asking to pair for end-to-end encryption. Approve only if you started this flow.</div>'
+ codeBlock
+ '<div class="mFp">this computer · <span class="fp">'+esc(s.fingerprint||'')+'</span></div>'
+ '<div class="mBtns"><button class="btn" onclick="closeModal()">Close</button><button id="approveBtn" class="btn primary" onclick="doApprove()">Approve</button></div>'
+ '</div></div>';
const el = document.getElementById('code'); if (el) el.focus();
}
async function unpair(id){ const { ok, j } = await post('/api/revoke', { id }); toast(ok?('✓ Unpaired '+(j.fingerprint||'')):('✗ '+(j.error||'failed'))); refresh(); }
async function unpairAll(){ if(!confirm('Unpair every browser?')) return; const { j } = await post('/api/revoke-all'); toast('✓ Unpaired '+(j.count||0)); refresh(); }
async function stopAdapter(){ if(!confirm('Stop the adapter?')) return; await post('/api/stop'); toast('Stopping…'); }
function isDevicePair(p){
return !!p && (p.role === 'device' || String(p.label || '').toLowerCase() === 'device');
}
function render(s){
lastStatus = s;
const c = document.getElementById('conn');
c.innerHTML = '<span class="dot '+(s.connected?'on':'off')+'"></span> <span class="mut">'+(s.connected?'cloud connected':'cloud reconnecting…')+'</span>';
const d = document.getElementById('device');
const deviceOnline = !!s.deviceE2eeConnected || !!((s.pairs||[]).find(p => isDevicePair(p) && p.online));
const deviceTransport = !!s.deviceTransportConnected;
d.innerHTML = '<span class="dot '+(deviceOnline?'on':deviceTransport?'idle':'off')+'"></span> <span class="mut">device '+(deviceOnline?'connected':deviceTransport?'pairing':'offline')+'</span>';
document.getElementById('agent').textContent = (s.agentId||'').slice(0,8) || '—';
const nAgents = (s.sessions||[]).length;
document.getElementById('agents').textContent = s.discoveryReady === false
? 'discovering terminal agents…'
: nAgents === 0 ? 'no agents' : nAgents + (nAgents === 1 ? ' agent' : ' agents');
document.getElementById('uptime').textContent = fmtDur(s.uptimeSec);
document.getElementById('fp').textContent = s.fingerprint || '—';
const wl = document.getElementById('weblink');
wl.href = s.webUrl ? (s.webUrl.replace(/\\/$/,'')+'/machine/'+s.agentId) : '#';
// sessions
const se = document.getElementById('sessions');
se.innerHTML = s.discoveryReady === false
? '<div class="empty">Discovering terminal agents…</div>'
: (s.sessions&&s.sessions.length) ? s.sessions.map(x =>
'<div class="row"><div class="main"><div class="name">'+esc(x.name)+'</div><div class="sub">'+esc(x.cwd||'')+' · '+esc((x.terminal&&x.terminal.primary)||x.tmuxPane||'')+'</div></div><div class="mut" style="font-size:12px">'+fmtWhen(x.updatedAt)+'</div></div>'
).join('') : '<div class="empty">'+(s.discoveryError ? esc('Terminal discovery unavailable: '+s.discoveryError) : 'No terminal agents.')+'</div>';
// pairs
const pe = document.getElementById('pairs');
pe.innerHTML = (s.pairs&&s.pairs.length) ? s.pairs.map(p => {
const role = isDevicePair(p) ? 'device' : (p.role || 'web');
return '<div class="row"><span class="dot '+(p.online?'on':'idle')+'"></span><div class="main"><div class="name fp">'+esc(p.fingerprint)+'</div><div class="sub"><span class="pill">'+esc(role)+'</span>'+esc(p.label)+' · paired '+fmtWhen(p.pairedAt)+'</div></div><button class="btn danger" onclick="unpair(\\''+esc(p.fingerprint)+'\\')">Unpair</button></div>';
}).join('') : '<div class="empty">No clients paired. Use Set up browser or run <span class="mono">harness browser-link</span>.</div>';
// pending — rebuild ONLY when it actually changes (new/rotated pairing), else leave the code input
// and whatever the user typed/pasted intact (polling every 2.5s must not wipe the field).
// Pending pairing → a MODAL popup. Built once per pending (guarded by pendKey) so polling every 2.5s
// doesn't clobber it. If the web link supplied ?code=…, it's pre-filled and the user just clicks
// Approve — nothing to type. Otherwise a code input is shown as a fallback.
const pd = document.getElementById('pending');
if (setupModalOpen) {
buildSetupApproveModal(s);
} else if (!setupInFlight) {
if (s.pending) {
const key = s.pending.expiresAt + '|' + s.pending.label;
if (key !== pendKey) {
pendKey = key;
// A blocking modal ONLY when arrived via the web "Approve" link (?code=…) and not yet closed;
// opening directly (or after Close) shows a subtle inline banner instead (normal UI, no overlay).
if (dismissedKey !== key) { if (urlCode && !modalClosed) buildApproveModal(s); else buildInlinePending(s); }
}
} else if (pendKey !== null) { pd.innerHTML = ''; pendKey = null; dismissedKey = null; }
}
// machine summary — keep the default view human-readable; tuck raw paths/ports into Advanced.
const cfg = s.config||{};
const sessionCount = (s.sessions&&s.sessions.length) || 0;
const pairCount = (s.pairs&&s.pairs.length) || 0;
const machineUrl = s.webUrl ? (s.webUrl.replace(/\\/$/,'')+'/machine/'+s.agentId) : '#';
document.getElementById('about').innerHTML =
'<div class="statusGrid">'
+ metric('Machine cloud', s.connected ? 'Connected' : 'Reconnecting')
+ metric('Claude sessions', sessionCount ? ('Watching '+sessionCount) : 'Waiting')
+ metric('Browsers', pairCount ? (pairCount+' paired') : 'None paired')
+ metric('Device', deviceOnline ? 'Connected' : deviceTransport ? 'Pairing' : 'Offline')
+ '</div>'
+ '<div class="actions"><a class="btn" href="'+esc(machineUrl)+'" target="_blank" rel="noreferrer">Open machine</a><button class="btn" onclick="startBrowserSetup()">Set up browser</button><a class="btn" href="#pairs">Clients</a><button class="btn" onclick="document.getElementById(\\'logsBox\\').open=true;document.getElementById(\\'logsBox\\').scrollIntoView({behavior:\\'smooth\\',block:\\'nearest\\'});">View logs</button></div>'
+ '<details class="adv"><summary>Advanced</summary>'
+ kv('Backend URL', s.backendUrl)
+ kv('Watched folder', cfg.watching)
+ kv('CLI data', cfg.dataDir)
+ kv('Local control', cfg.port)
+ '</details>';
}
function metric(k,v){ return '<div class="metric"><span class="k">'+esc(k)+'</span><span class="v">'+esc(v||'')+'</span></div>'; }
function kv(k,v){ return '<div class="kv"><span class="k">'+esc(k)+'</span><span class="v">'+esc(v||'')+'</span></div>'; }
async function refresh(){
try {
const s = await (await fetch('/api/status')).json();
render(s);
if (setupMode && !setupAutoAttempted) { setupAutoAttempted = true; startBrowserSetup(); }
fetch('/api/logs').then(r=>r.ok?r.text():'').then(t=>{ if(t) document.getElementById('logs').textContent = t; }).catch(()=>{});
} catch { document.getElementById('conn').innerHTML = '<span class="dot off"></span> <span class="mut">cloud unreachable</span>'; }
}
refresh(); setInterval(refresh, 2500);
</script>
</body>
</html>`
29c21cf07This file is pinned to the selected release. Download the original to open it in your design tool.