Scaffold: fork of Chambers architecture, renamed to Boardroom Map
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>Boardroom Map</title>
|
||||
<style>
|
||||
:root { --bg:#10131a; --panel:#181d27; --edge:#2a313f; --ink:#e9edf5; --dim:#97a1b5;
|
||||
--accent:#c9a24b; --ok:#5fd08a; --warn:#ffcf66; --bad:#ff7a7a; }
|
||||
* { box-sizing:border-box; }
|
||||
body { margin:0; font:14px/1.5 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;
|
||||
background:var(--bg); color:var(--ink); }
|
||||
header { display:flex; align-items:center; gap:12px; padding:18px 22px; border-bottom:1px solid var(--edge); }
|
||||
header .seal { width:26px;height:26px;border-radius:50%;border:2px solid var(--accent);
|
||||
display:flex;align-items:center;justify-content:center;color:var(--accent);font-weight:700;font-size:13px; }
|
||||
h1 { font-size:18px; margin:0; letter-spacing:2px; }
|
||||
.wrap { max-width:1100px; margin:0 auto; padding:22px; display:grid; gap:18px; grid-template-columns:1fr 1fr; }
|
||||
.card { background:var(--panel); border:1px solid var(--edge); border-radius:14px; padding:16px 18px; }
|
||||
.card.full { grid-column:1 / -1; }
|
||||
.card h2 { margin:0 0 10px; font-size:13px; text-transform:uppercase; letter-spacing:1px; color:var(--dim); }
|
||||
.pill { display:inline-block; padding:2px 10px; border-radius:999px; font-size:12px; font-weight:600; }
|
||||
.pill.idle,.pill.done{background:#26303f;color:var(--dim)}
|
||||
.pill.reviewing,.pill.synthesizing,.pill.collecting,.pill.extracting{background:#3b3414;color:var(--warn)}
|
||||
.pill.error{background:#3b1414;color:var(--bad)}
|
||||
.kv { display:flex; justify-content:space-between; padding:4px 0; border-bottom:1px dashed var(--edge); }
|
||||
.kv:last-child{border:0} .kv .k{color:var(--dim)}
|
||||
.dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px}
|
||||
.dot.on{background:var(--ok)} .dot.off{background:#55608f}
|
||||
pre { background:#0c0f16; border:1px solid var(--edge); border-radius:10px; padding:12px; margin:0;
|
||||
max-height:340px; overflow:auto; font:12px/1.5 ui-monospace,Menlo,monospace; color:#cdd6ff; white-space:pre-wrap; }
|
||||
.row{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
|
||||
button{background:#222a3a;color:var(--ink);border:1px solid var(--edge);border-radius:9px;
|
||||
padding:8px 12px;font-size:13px;cursor:pointer} button:hover{background:#2c3650}
|
||||
button.primary{background:#3a2f12;border-color:#6b551f;color:#ffdf9a}
|
||||
.drop{border:1.5px dashed var(--edge);border-radius:12px;padding:18px;text-align:center;color:var(--dim);cursor:pointer}
|
||||
.drop.hot{border-color:var(--accent);color:var(--ink)}
|
||||
.badge{font-size:11px;color:var(--dim)}
|
||||
.muted{color:var(--dim);font-size:12px}
|
||||
a{color:#9ab8ff}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="seal">§</div>
|
||||
<h1>BOARDROOM MAP</h1>
|
||||
<span id="phase" class="pill idle">idle</span>
|
||||
<span class="muted" id="netmode"></span>
|
||||
</header>
|
||||
|
||||
<div class="wrap">
|
||||
<div class="card">
|
||||
<h2>Documents</h2>
|
||||
<div id="drop" class="drop">Drop confidential documents here, or click to choose<br>
|
||||
<span class="badge">PDF · DOCX · TXT · MD</span></div>
|
||||
<input id="file" type="file" multiple style="display:none"/>
|
||||
<div id="inbox" style="margin-top:10px"></div>
|
||||
<div class="row">
|
||||
<button class="primary" onclick="runReview()">Run Review</button>
|
||||
<button onclick="act('/api/inbox/clear','POST')">Clear Inbox</button>
|
||||
<button onclick="refresh()">Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Setup</h2>
|
||||
<div id="ready"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Panel</h2>
|
||||
<div id="panel"><div class="muted">No reviewers configured.</div></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Serving / Job</h2>
|
||||
<div id="job"><div class="muted">—</div></div>
|
||||
<div class="row">
|
||||
<button onclick="act('/api/reviewer/build-image','POST')">Build reviewer image</button>
|
||||
<button onclick="act('/api/stop','POST')">Stop serving</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card full">
|
||||
<h2>Activity log</h2>
|
||||
<pre id="log">loading…</pre>
|
||||
</div>
|
||||
|
||||
<div class="card full">
|
||||
<h2>Latest report</h2>
|
||||
<pre id="report">—</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function getJSON(u){ const r=await fetch(u); if(!r.ok) throw new Error(await r.text()); return r.json(); }
|
||||
async function act(u,m){ try{ const r=await fetch(u,{method:m}); const t=await r.text();
|
||||
alert(r.ok? 'OK: '+t.slice(0,400) : 'Error: '+t.slice(0,400)); refresh(); }catch(e){ alert(e); } }
|
||||
function dot(on,label){ return `<div><span class="dot ${on?'on':'off'}"></span>${label}</div>`; }
|
||||
async function runReview(){ act('/api/run','POST'); }
|
||||
|
||||
const drop=document.getElementById('drop'), file=document.getElementById('file');
|
||||
drop.onclick=()=>file.click();
|
||||
file.onchange=()=>upload(file.files);
|
||||
['dragover','dragenter'].forEach(e=>drop.addEventListener(e,ev=>{ev.preventDefault();drop.classList.add('hot');}));
|
||||
['dragleave','drop'].forEach(e=>drop.addEventListener(e,ev=>{ev.preventDefault();drop.classList.remove('hot');}));
|
||||
drop.addEventListener('drop',ev=>{ if(ev.dataTransfer.files.length) upload(ev.dataTransfer.files); });
|
||||
async function upload(files){
|
||||
const fd=new FormData(); for(const f of files) fd.append('files',f);
|
||||
try{ const r=await fetch('/api/upload',{method:'POST',body:fd});
|
||||
if(!r.ok) alert('Upload failed: '+(await r.text()).slice(0,300)); refresh(); }catch(e){ alert(e); }
|
||||
}
|
||||
|
||||
async function refresh(){
|
||||
try{
|
||||
const s = await getJSON('/api/status');
|
||||
const rt = s.runtime||{};
|
||||
const ph = document.getElementById('phase');
|
||||
ph.textContent = rt.phase||'idle'; ph.className = 'pill '+(rt.phase||'idle');
|
||||
document.getElementById('netmode').textContent =
|
||||
(s.networkMode==='airgapped'?'air-gapped':'local-services')
|
||||
+ (s.synthesis?' · synthesis on':'') + (s.autoRunOnDrop?' · auto-run':'');
|
||||
|
||||
document.getElementById('inbox').innerHTML = (s.inbox||[]).length
|
||||
? (s.inbox||[]).map(f=>`<div class="kv"><span>${f.name} ${f.supported?'':'<span class="badge">unsupported</span>'}</span><span class="muted">${(f.bytes/1024).toFixed(0)} KB</span></div>`).join('')
|
||||
: '<div class="muted">Inbox empty.</div>';
|
||||
|
||||
document.getElementById('ready').innerHTML =
|
||||
dot(s.configured.sparks,'Sparks configured') +
|
||||
dot(s.configured.models>0,`${s.configured.models} model(s)`) +
|
||||
dot(s.configured.reviewers>0,`${s.configured.reviewers} reviewer(s)`) +
|
||||
`<div class="kv"><span class="k">network mode</span><span>${s.networkMode}</span></div>` +
|
||||
`<div class="kv"><span class="k">wipe docs after</span><span>${s.wipeRemoteDocs?'yes':'no'}</span></div>` +
|
||||
(s.models||[]).map(m=>`<div class="kv"><span>${m.alias}</span><span class="muted">${m.hfModel} · ${m.spark}</span></div>`).join('');
|
||||
|
||||
document.getElementById('panel').innerHTML = (s.panel||[]).length
|
||||
? (s.panel||[]).map(r=>`<div class="kv"><span>${r.name} ${r.known?'':'<span class="badge">unknown model</span>'}</span><span class="muted">${r.model}${r.persona?' · persona ✓':''}</span></div>`).join('')
|
||||
: '<div class="muted">No reviewers configured.</div>';
|
||||
|
||||
const panelRt=(rt.panel||[]);
|
||||
document.getElementById('job').innerHTML =
|
||||
`<div class="kv"><span class="k">job</span><span>${rt.job_id||'—'}</span></div>` +
|
||||
(rt.waves_total?`<div class="kv"><span class="k">wave</span><span>${rt.wave_index}/${rt.waves_total}</span></div>`:'') +
|
||||
(rt.message?`<div class="muted" style="margin-top:6px">${rt.message}</div>`:'') +
|
||||
panelRt.map(p=>`<div class="kv"><span>${p.name}</span><span class="muted">${p.status||''}</span></div>`).join('');
|
||||
|
||||
const ev = await getJSON('/api/events');
|
||||
document.getElementById('log').textContent = (ev.events||[]).slice(-200).join('\n') || '(no activity yet)';
|
||||
try{ const rep = await (await fetch('/api/report')).text();
|
||||
document.getElementById('report').textContent = rep; }catch(e){}
|
||||
}catch(e){ document.getElementById('log').textContent = 'status error: '+e; }
|
||||
}
|
||||
refresh(); setInterval(refresh, 6000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user