← back to Harness Viewer
harness cockpit: engines + live lanes + tickets + gate queue + ledger
b3648139a359e6268b832adb4dd18c0ca2352433 · 2026-09-22 12:00:17 -0700 · Steve
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D2KFGQK7p7ahEzug7HBMZF
Files touched
A .gitignoreA index.htmlA server.js
Diff
commit b3648139a359e6268b832adb4dd18c0ca2352433
Author: Steve <steve@designerwallcoverings.com>
Date: Tue Sep 22 12:00:17 2026 -0700
harness cockpit: engines + live lanes + tickets + gate queue + ledger
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D2KFGQK7p7ahEzug7HBMZF
---
.gitignore | 4 ++
index.html | 170 ++++++++++++++++++++++++++++++++++++++++++++++++
server.js | 216 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 390 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1bcdbdc
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+node_modules/
+.env*
+*.log
+.DS_Store
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..925829d
--- /dev/null
+++ b/index.html
@@ -0,0 +1,170 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Harness Cockpit</title>
+<style>
+ :root{--bg:#0f1117;--panel:#171a22;--panel2:#1d212b;--line:#272c38;--fg:#e6e9ef;--dim:#8a92a6;--accent:#5aa9ff;--green:#3fb950;--red:#f85149;--yellow:#d29922;--blue:#58a6ff;--purple:#bc8cff;}
+ *{box-sizing:border-box}
+ body{margin:0;background:var(--bg);color:var(--fg);font:13px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,monospace}
+ a{color:var(--accent);text-decoration:none}
+ header{position:sticky;top:0;z-index:10;background:linear-gradient(180deg,#12141b,#0f1117);border-bottom:1px solid var(--line);padding:10px 16px;display:flex;flex-wrap:wrap;align-items:center;gap:14px}
+ header h1{font-size:15px;margin:0;font-weight:600;letter-spacing:.3px}
+ .counts{display:flex;flex-wrap:wrap;gap:8px;align-items:center;font-size:12px}
+ .pill{background:var(--panel2);border:1px solid var(--line);border-radius:12px;padding:3px 10px;color:var(--dim)}
+ .pill b{color:var(--fg)}
+ .grow{flex:1}
+ .updated{color:var(--dim);font-size:11px}
+ .chiprow{display:flex;flex-wrap:wrap;gap:8px;padding:12px 16px 4px}
+ .chip{display:flex;flex-direction:column;gap:2px;background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:8px 12px;min-width:150px}
+ .chip .top{display:flex;align-items:center;gap:7px;font-weight:600}
+ .dot{width:9px;height:9px;border-radius:50%;flex:none}
+ .dot.up{background:var(--green);box-shadow:0 0 6px var(--green)}
+ .dot.down{background:var(--red);box-shadow:0 0 6px var(--red)}
+ .chip .ep{color:var(--dim);font-size:11px;word-break:break-all}
+ .chip .pt{color:var(--dim);font-size:10px}
+ .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(360px,1fr));gap:14px;padding:14px 16px 40px}
+ .panel{background:var(--panel);border:1px solid var(--line);border-radius:10px;overflow:hidden;display:flex;flex-direction:column;max-height:560px}
+ .panel h2{margin:0;font-size:12px;text-transform:uppercase;letter-spacing:.6px;color:var(--dim);padding:10px 14px;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:8px;background:var(--panel2)}
+ .panel h2 .n{color:var(--fg);background:#0f1117;border:1px solid var(--line);border-radius:10px;padding:1px 8px;font-size:11px}
+ .panel .body{overflow:auto;padding:4px 0}
+ .row{padding:7px 14px;border-bottom:1px solid #1e222c}
+ .row:last-child{border-bottom:none}
+ .row .l1{display:flex;align-items:center;gap:8px;justify-content:space-between}
+ .row .name{font-weight:600;color:var(--fg)}
+ .row .meta{color:var(--dim);font-size:11px;white-space:nowrap}
+ .row .txt{color:#c3c9d6;font-size:12px;margin-top:2px;word-break:break-word}
+ .badge{font-size:10px;font-weight:700;border-radius:5px;padding:1px 7px;letter-spacing:.4px}
+ .b-DOING{background:#153a5b;color:#79c0ff}
+ .b-BLOCKED{background:#5b2a15;color:#ffa657}
+ .b-DONE{background:#183a1f;color:#56d364}
+ .ages{display:flex;gap:6px;margin-left:auto}
+ .age{font-size:10px;border-radius:10px;padding:1px 8px;border:1px solid var(--line);color:var(--dim)}
+ .age.hot{color:#ffa657;border-color:#5b2a15}
+ .memo-link{cursor:pointer}
+ .memo-link:hover{color:var(--accent)}
+ .undo{color:#7d8598;font-size:11px;margin-top:2px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;word-break:break-all}
+ button{background:var(--panel2);color:var(--fg);border:1px solid var(--line);border-radius:6px;padding:4px 10px;font-size:11px;cursor:pointer}
+ button:hover{border-color:var(--accent)}
+ pre{white-space:pre-wrap;word-break:break-word;margin:0;padding:10px 14px;font:11px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;color:#c3c9d6}
+ .modal{position:fixed;inset:0;background:rgba(0,0,0,.6);display:none;align-items:center;justify-content:center;z-index:50;padding:20px}
+ .modal.show{display:flex}
+ .modal .box{background:var(--panel);border:1px solid var(--line);border-radius:10px;max-width:820px;width:100%;max-height:80vh;display:flex;flex-direction:column}
+ .modal .box h3{margin:0;padding:12px 16px;border-bottom:1px solid var(--line);font-size:13px;display:flex;justify-content:space-between;align-items:center}
+ .modal .box .c{overflow:auto}
+ .empty{color:var(--dim);padding:14px;font-size:12px}
+ .spin{color:var(--dim)}
+</style>
+</head>
+<body>
+<header>
+ <h1>🛰 Harness Cockpit</h1>
+ <div class="counts" id="counts"><span class="spin">loading…</span></div>
+ <div class="grow"></div>
+ <div class="updated" id="updated"></div>
+</header>
+
+<div class="chiprow" id="chips"></div>
+
+<div class="grid">
+ <section class="panel"><h2>Live Lanes <span class="n" id="n-lanes">0</span><span class="grow" style="flex:1"></span></h2><div class="body" id="p-lanes"></div></section>
+ <section class="panel"><h2>Tickets <span class="n" id="n-tickets">0</span></h2><div class="body" id="p-tickets"></div></section>
+ <section class="panel"><h2>Gate Queue <span class="n" id="n-gate">0</span></h2><div class="body" id="p-gate"></div></section>
+ <section class="panel"><h2>Reversible Ledger <span class="n" id="n-ledger">0</span></h2><div class="body" id="p-ledger"></div></section>
+ <section class="panel" style="grid-column:1/-1">
+ <h2>Overnight Audit <span class="grow" style="flex:1"></span><button id="run-audit">Run audit</button></h2>
+ <div class="body"><pre id="p-audit" class="empty">On-demand — click "Run audit" to run audit-overnight-closes.mjs (proof-join of closes backed by evidence vs not). Not part of auto-refresh.</pre></div>
+ </section>
+</div>
+
+<div class="modal" id="modal"><div class="box"><h3><span id="m-title">memo</span><button onclick="closeModal()">close</button></h3><div class="c"><pre id="m-body"></pre></div></div></div>
+
+<script>
+const $ = (id) => document.getElementById(id);
+const esc = (s) => String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
+function ago(iso){ if(!iso) return ''; const d=Date.now()-Date.parse(iso); if(isNaN(d)) return ''; const m=Math.floor(d/60000); if(m<1) return 'just now'; if(m<60) return m+'m ago'; const h=Math.floor(m/60); if(h<24) return h+'h '+(m%60)+'m ago'; return Math.floor(h/24)+'d ago'; }
+
+async function load(){
+ let s;
+ try{ const r=await fetch('/api/state'); s=await r.json(); }
+ catch(e){ $('counts').innerHTML='<span class="pill">fetch failed — retrying</span>'; return; }
+ render(s);
+}
+
+function render(s){
+ // header counts
+ $('counts').innerHTML =
+ pill('engines','<b>'+s.enginesUp+'</b>/'+s.enginesTotal+' up') +
+ pill('active lanes','<b>'+(s.lanes?s.lanes.length:0)+'</b>') +
+ pill('tickets DOING','<b>'+(s.doing||0)+'</b>') +
+ pill('gated','<b>'+(s.gate?s.gate.total:0)+'</b>');
+ $('updated').textContent = 'updated '+ago(s.ts);
+
+ // engine chips
+ $('chips').innerHTML = (s.engines||[]).map(e=>
+ '<div class="chip"><div class="top"><span class="dot '+(e.up?'up':'down')+'"></span>'+esc(e.label)+'</div>'+
+ '<div class="ep">'+esc(e.endpoint)+'</div><div class="pt">probed '+ago(e.ts)+'</div></div>').join('');
+
+ // lanes
+ $('n-lanes').textContent = (s.lanes||[]).length;
+ $('p-lanes').innerHTML = (s.lanes&&s.lanes.length) ? s.lanes.map(l=>
+ '<div class="row"><div class="l1"><span class="name">'+esc(l.agent)+'</span>'+
+ '<span class="meta">'+l.actions+' act · '+ago(l.ts)+'</span></div>'+
+ '<div class="txt">'+esc(l.lastText)+'</div></div>').join('')
+ : '<div class="empty">No agent activity in the last 30 minutes.</div>';
+
+ // tickets
+ $('n-tickets').textContent = (s.tickets||[]).length;
+ $('p-tickets').innerHTML = (s.tickets&&s.tickets.length) ? s.tickets.map(t=>
+ '<div class="row"><div class="l1"><span class="name">'+esc(t.id)+' <span class="badge b-'+esc(t.state)+'">'+esc(t.state)+'</span></span>'+
+ '<span class="meta">'+esc(t.agent||'—')+' · '+ago(t.ts)+'</span></div>'+
+ '<div class="txt">'+esc(t.lastText)+'</div></div>').join('')
+ : '<div class="empty">No recent ticket activity.</div>';
+
+ // gate queue
+ const g=s.gate||{total:0,newest:[]};
+ $('n-gate').textContent = g.total;
+ let ghdr='<div class="row"><div class="l1"><span class="name">'+g.total+' pending memos</span>'+
+ '<span class="ages"><span class="age'+(g.p3?' hot':'')+'">>3h: '+g.p3+'</span>'+
+ '<span class="age'+(g.p24?' hot':'')+'">>24h: '+g.p24+'</span>'+
+ '<span class="age'+(g.p7d?' hot':'')+'">>7d: '+g.p7d+'</span></span></div></div>';
+ const grows=(g.newest||[]).map(m=>
+ '<div class="row"><div class="l1"><span class="name memo-link" onclick="openMemo(\''+esc(m.f).replace(/'/g,"\\'")+'\')">'+esc(m.f)+'</span>'+
+ '<span class="meta">'+ago(m.ts)+'</span></div></div>').join('');
+ $('p-gate').innerHTML = ghdr + (grows || '<div class="empty">Queue empty.</div>');
+
+ // ledger
+ $('n-ledger').textContent = (s.ledger||[]).length;
+ $('p-ledger').innerHTML = (s.ledger&&s.ledger.length) ? s.ledger.map(r=>
+ '<div class="row"><div class="l1"><span class="name">'+esc(r.agent||'—')+'</span>'+
+ '<span class="meta">'+esc(r.ticket||'')+' · '+ago(r.ts)+'</span></div>'+
+ '<div class="txt">'+esc(r.action)+'</div>'+
+ (r.undo_cmd?'<div class="undo">↩ '+esc(r.undo_cmd)+'</div>':'')+'</div>').join('')
+ : '<div class="empty">No ledger rows.</div>';
+}
+function pill(k,v){ return '<span class="pill">'+k+' '+v+'</span>'; }
+
+async function openMemo(f){
+ $('m-title').textContent=f; $('m-body').textContent='loading…'; $('modal').classList.add('show');
+ try{ const r=await fetch('/api/memo?f='+encodeURIComponent(f)); const j=await r.json();
+ $('m-body').textContent = j.error ? ('error: '+j.error) : (j.lines||'(empty)'); }
+ catch(e){ $('m-body').textContent='fetch failed'; }
+}
+function closeModal(){ $('modal').classList.remove('show'); }
+$('modal').addEventListener('click',e=>{ if(e.target.id==='modal') closeModal(); });
+
+$('run-audit').addEventListener('click', async ()=>{
+ const btn=$('run-audit'); btn.disabled=true; btn.textContent='running…';
+ $('p-audit').className=''; $('p-audit').textContent='running audit-overnight-closes.mjs (up to 30s)…';
+ try{ const r=await fetch('/api/audit'); const j=await r.json();
+ $('p-audit').textContent = j.error ? ('error: '+j.error) : (j.output||'(no output)'); }
+ catch(e){ $('p-audit').textContent='fetch failed'; }
+ finally{ btn.disabled=false; btn.textContent='Run audit'; }
+});
+
+load();
+setInterval(load, 10000); // auto-refresh 10s (audit excluded)
+</script>
+</body>
+</html>
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..aea1749
--- /dev/null
+++ b/server.js
@@ -0,0 +1,216 @@
+#!/usr/bin/env node
+// Harness Cockpit — live viewer for Steve's agent-orchestration harness.
+// Basic-auth admin/DW2024! · dep-free (Node built-in http) · live-scan + ~6s cache.
+// Mirrors the architecture of ~/Projects/stack-map-viewer/server.js.
+const http = require('http');
+const https = require('https');
+const { execSync } = require('child_process');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+
+const HOME = os.homedir();
+const EVENTS = path.join(HOME, '.claude/tickets/events.jsonl');
+const PENDING = path.join(HOME, '.claude/yolo-queue/pending-approval');
+const LEDGER = path.join(HOME, '.claude/yolo-queue/executed-reversible/ledger.jsonl');
+const AUDIT_SCRIPT = path.join(HOME, 'Projects/ticket-system/scripts/audit-overnight-closes.mjs');
+const AUTH = 'Basic ' + Buffer.from('admin:DW2024!').toString('base64');
+const PORT = process.env.PORT || 9770;
+
+// ── safe shell: 6s timeout, graceful empty on any failure ────────────────────
+const sh = (c, ms = 6000) => { try { return execSync(c, { encoding: 'utf8', timeout: ms, maxBuffer: 32 * 1024 * 1024 }); } catch { return ''; } };
+const shq = (s) => "'" + String(s).replace(/'/g, "'\\''") + "'";
+const trunc = (s, n) => { s = String(s || ''); return s.length > n ? s.slice(0, n - 1) + '…' : s; };
+const shortId = (id) => { const m = String(id || '').match(/^(TK-\d+)/); return m ? m[1] : String(id || ''); };
+
+// Read the last N lines of a big file WITHOUT loading it whole. Never read
+// events.jsonl (57MB) or ledger (86k+ lines) in full — tail + per-line parse.
+function tailJSON(file, n) {
+ const raw = sh(`tail -n ${n} ${shq(file)} 2>/dev/null`);
+ if (!raw) return [];
+ const out = [];
+ for (const ln of raw.split('\n')) {
+ const t = ln.trim();
+ if (!t) continue;
+ try { out.push(JSON.parse(t)); } catch { /* skip malformed line */ }
+ }
+ return out;
+}
+
+// ── ENGINES: probe the "full AI setup" (2s each) ─────────────────────────────
+function probeHTTP(url, timeoutMs = 2000) {
+ return new Promise((resolve) => {
+ let done = false;
+ const finish = (up) => { if (!done) { done = true; resolve(up); } };
+ try {
+ const lib = url.startsWith('https') ? https : http;
+ const req = lib.get(url, (r) => { finish(r.statusCode === 200); r.resume(); });
+ req.on('error', () => finish(false));
+ req.setTimeout(timeoutMs, () => { req.destroy(); finish(false); });
+ } catch { finish(false); }
+ });
+}
+const ENGINE_DEFS = [
+ { key: 'exo', label: 'exo ring', kind: 'http', endpoint: 'http://127.0.0.1:52415/' },
+ { key: 'openclaw', label: 'openclaw MLX', kind: 'http', endpoint: 'http://127.0.0.1:8000/v1/models' },
+ { key: 'gateway', label: 'ai-cluster gateway', kind: 'http', endpoint: 'http://127.0.0.1:8080/' },
+ { key: 'codex', label: 'codex CLI', kind: 'exec', endpoint: '~/.local/bin/codex', test: path.join(HOME, '.local/bin/codex') },
+ { key: 'askopenai', label: 'ask-openai', kind: 'exec', endpoint: '~/bin/ask-openai', test: path.join(HOME, 'bin/ask-openai') },
+];
+async function probeEngines() {
+ const now = new Date().toISOString();
+ const results = await Promise.all(ENGINE_DEFS.map(async (e) => {
+ let up = false;
+ if (e.kind === 'http') up = await probeHTTP(e.endpoint, 2000);
+ else { try { fs.accessSync(e.test, fs.constants.X_OK); up = true; } catch { up = false; } }
+ return { key: e.key, label: e.label, endpoint: e.endpoint, up, ts: now };
+ }));
+ return results;
+}
+
+// ── LIVE LANES + TICKETS: from a single tail of events.jsonl ─────────────────
+function analyzeEvents() {
+ const ev = tailJSON(EVENTS, 4000);
+ const now = Date.now();
+ const WINDOW = 30 * 60 * 1000; // 30 min
+ const lanes = new Map(); // agent -> {agent, actions, lastText, lastTs}
+ const tickets = new Map(); // shortId -> {id, agent, lastText, ts, state}
+
+ for (const e of ev) {
+ if (!e || !e.ts) continue;
+ const t = Date.parse(e.ts);
+ if (isNaN(t)) continue;
+ const text = e.text || (e.status ? '[' + e.status + ']' : e.type || '');
+ // --- lanes: last 30 min, grouped by agent ---
+ if (e.agent && now - t <= WINDOW) {
+ let ln = lanes.get(e.agent);
+ if (!ln) { ln = { agent: e.agent, actions: 0, lastText: '', lastTs: 0 }; lanes.set(e.agent, ln); }
+ ln.actions++;
+ if (t >= ln.lastTs) { ln.lastTs = t; ln.lastText = trunc(text, 140); }
+ }
+ // --- tickets: reduce by short id, derive state ---
+ if (e.id) {
+ const sid = shortId(e.id);
+ let tk = tickets.get(sid);
+ if (!tk) { tk = { id: sid, fullId: e.id, agent: e.agent || '', lastText: '', ts: 0, state: 'DOING' }; tickets.set(sid, tk); }
+ let state = tk.state;
+ if (e.type === 'status') { const s = (e.status || '').toLowerCase(); if (s === 'done') state = 'DONE'; else if (s === 'blocked') state = 'BLOCKED'; else state = 'DOING'; }
+ else if (e.type === 'new' || e.type === 'take') state = 'DOING';
+ else if (e.type === 'comment' || e.type === 'action' || e.type === 'note' || e.type === 'log') state = 'DOING';
+ if (t >= tk.ts) { tk.ts = t; tk.lastText = trunc(text, 160); tk.state = state; if (e.agent) tk.agent = e.agent; }
+ else if (e.type === 'status') { tk.state = state; } // status is authoritative even if slightly older
+ }
+ }
+
+ const laneArr = [...lanes.values()].sort((a, b) => b.lastTs - a.lastTs)
+ .map(l => ({ agent: l.agent, actions: l.actions, lastText: l.lastText, ts: new Date(l.lastTs).toISOString() }));
+ const tkArr = [...tickets.values()].sort((a, b) => b.ts - a.ts).slice(0, 25)
+ .map(t => ({ id: t.id, agent: t.agent, lastText: t.lastText, ts: new Date(t.ts).toISOString(), state: t.state }));
+ const doing = [...tickets.values()].filter(t => t.state === 'DOING').length;
+ return { lanes: laneArr, tickets: tkArr, doing };
+}
+
+// ── GATE QUEUE ───────────────────────────────────────────────────────────────
+function scanGateQueue() {
+ let files = [];
+ try { files = fs.readdirSync(PENDING).filter(f => f.endsWith('.md')); } catch { return { total: 0, p3: 0, p24: 0, p7d: 0, newest: [] }; }
+ const now = Date.now();
+ let p3 = 0, p24 = 0, p7d = 0;
+ const withMtime = [];
+ for (const f of files) {
+ let m = 0;
+ try { m = fs.statSync(path.join(PENDING, f)).mtimeMs; } catch { continue; }
+ const h = (now - m) / 3.6e6;
+ if (h >= 3) p3++;
+ if (h >= 24) p24++;
+ if (h >= 168) p7d++;
+ withMtime.push({ f, m });
+ }
+ withMtime.sort((a, b) => b.m - a.m);
+ const newest = withMtime.slice(0, 25).map(x => ({ f: x.f, ts: new Date(x.m).toISOString() }));
+ return { total: files.length, p3, p24, p7d, newest };
+}
+
+// ── REVERSIBLE LEDGER ────────────────────────────────────────────────────────
+function scanLedger() {
+ const rows = tailJSON(LEDGER, 40);
+ return rows.reverse().map(r => ({
+ ts: r.ts || '', agent: r.agent || '', ticket: r.ticket || '',
+ action: trunc(r.action, 220), undo_cmd: trunc(r.undo_cmd, 260),
+ }));
+}
+
+// ── aggregate state (all panels except on-demand audit), ~6s cache ───────────
+let CACHE = null, CACHE_TS = 0;
+async function buildState() {
+ if (CACHE && Date.now() - CACHE_TS < 6000) return CACHE;
+ const engines = await probeEngines();
+ const ev = analyzeEvents();
+ const gate = scanGateQueue();
+ const ledger = scanLedger();
+ const enginesUp = engines.filter(e => e.up).length;
+ CACHE = {
+ ts: new Date().toISOString(),
+ engines, enginesUp, enginesTotal: engines.length,
+ lanes: ev.lanes, tickets: ev.tickets, doing: ev.doing,
+ gate, ledger,
+ hasAudit: (() => { try { fs.accessSync(AUDIT_SCRIPT); return true; } catch { return false; } })(),
+ };
+ CACHE_TS = Date.now();
+ return CACHE;
+}
+
+// ── memo reader (path-jailed to PENDING) ─────────────────────────────────────
+function readMemo(name) {
+ if (!name || typeof name !== 'string') return { error: 'no file' };
+ const resolved = path.resolve(PENDING, name);
+ const jail = path.resolve(PENDING) + path.sep;
+ if (!(resolved + path.sep).startsWith(jail) && resolved !== path.resolve(PENDING)) return { error: 'path escape rejected' };
+ if (!resolved.startsWith(jail)) return { error: 'path escape rejected' };
+ if (!resolved.endsWith('.md')) return { error: 'not a memo' };
+ let txt = '';
+ try { txt = fs.readFileSync(resolved, 'utf8'); } catch { return { error: 'not found' }; }
+ const lines = txt.split('\n').slice(0, 60).join('\n');
+ return { file: path.basename(resolved), lines };
+}
+
+// ── audit (on-demand only) ───────────────────────────────────────────────────
+function runAudit() {
+ try { fs.accessSync(AUDIT_SCRIPT); } catch { return { error: 'audit script not found', script: AUDIT_SCRIPT }; }
+ const out = sh(`node ${shq(AUDIT_SCRIPT)} 2>&1`, 30000);
+ return { script: AUDIT_SCRIPT, output: out || '(no output / timed out)' };
+}
+
+const PAGE = fs.readFileSync(path.join(__dirname, 'index.html'), 'utf8');
+
+const server = http.createServer(async (req, res) => {
+ if (req.headers.authorization !== AUTH) {
+ res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="harness-cockpit"' });
+ return res.end('auth required');
+ }
+ const u = new URL(req.url, 'http://x');
+ try {
+ if (u.pathname === '/api/state') {
+ const s = await buildState();
+ res.writeHead(200, { 'content-type': 'application/json' });
+ return res.end(JSON.stringify(s));
+ }
+ if (u.pathname === '/api/memo') {
+ const out = readMemo(u.searchParams.get('f') || '');
+ res.writeHead(out.error ? 400 : 200, { 'content-type': 'application/json' });
+ return res.end(JSON.stringify(out));
+ }
+ if (u.pathname === '/api/audit') {
+ const out = runAudit();
+ res.writeHead(200, { 'content-type': 'application/json' });
+ return res.end(JSON.stringify(out));
+ }
+ } catch (e) {
+ res.writeHead(500, { 'content-type': 'application/json' });
+ return res.end(JSON.stringify({ error: String(e && e.message || e) }));
+ }
+ res.writeHead(200, { 'content-type': 'text/html' });
+ res.end(PAGE);
+});
+
+server.listen(PORT, () => console.log(`harness cockpit → http://127.0.0.1:${PORT} (admin/DW2024!)`));
(oldest)
·
back to Harness Viewer
·
TK-12268: add creds-in-URL fetch guard (fleet pattern) to ga 45a5532 →