← back to Doing Viewer
public/index.html
233 lines
<!doctype html>
<html lang="en">
<head>
<script>
// Credential-safe fetch guard (fleet drop-in) — if this page is opened with
// credentials in the URL (a saved bookmark / Chrome-remembered basic-auth:
// https://user:pass@host/…), the browser poisons document.baseURI, so any
// relative or root-relative fetch('/api/…') throws "Request cannot be constructed
// from a URL that includes credentials" and callers silently fall to an empty
// state. Resolve every non-absolute request URL against the credential-free
// location instead of baseURI. Placed first so it wraps window.fetch before any
// app script runs. Ref: creds-in-url-fetch-guard-fleet-pattern.
(function () {
var _fetch = window.fetch.bind(window);
var cleanBase = function () { return location.origin + location.pathname; };
window.fetch = function (input, init) {
try {
if (typeof input === 'string' && !/^[a-z]+:\/\//i.test(input) && input.indexOf('//') !== 0) {
input = new URL(input, cleanBase()).href;
} else if (input instanceof Request && !/^[a-z]+:\/\//i.test(input.url)) {
input = new Request(new URL(input.url, cleanBase()).href, input);
}
} catch (_) { /* fall through to native */ }
return _fetch(input, init);
};
})();
</script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>What's Doing — live board</title>
<style>
:root{
--bg:#0e1116; --panel:#161b22; --panel2:#1b2230; --line:#252c38;
--ink:#e6edf3; --dim:#8b98a9; --accent:#4aa8ff; --doing:#ffb454;
--kid:#7ee787; --kidbg:#12241a;
}
*{box-sizing:border-box}
html{scroll-behavior:smooth}
body{margin:0;background:var(--bg);color:var(--ink);
font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}
header{display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;
padding:14px 20px;border-bottom:1px solid var(--line);background:var(--panel);
position:sticky;top:0;z-index:5}
header h1{margin:0;font-size:18px;font-weight:700;letter-spacing:.2px}
.pill{font-size:12px;color:var(--dim);background:var(--panel2);border:1px solid var(--line);
padding:2px 9px;border-radius:999px}
.pill b{color:var(--doing)}
.spacer{flex:1}
#clock{font-variant-numeric:tabular-nums;color:var(--dim);font-size:12px}
.dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--kid);
margin-right:6px;animation:pulse 2s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.35}}
.head-row,.card{display:grid;grid-template-columns:1fr 1.35fr 1.1fr 1.05fr;gap:0}
.head-row{position:sticky;top:53px;z-index:4;background:var(--bg);
border-bottom:1px solid var(--line)}
.head-row div{padding:10px 18px;font-size:11px;letter-spacing:.14em;text-transform:uppercase;
color:var(--dim);font-weight:700}
.head-row .h3{color:var(--kid)}
.list{padding-bottom:60px}
.card{border-bottom:1px solid var(--line)}
.card:hover{background:#12161d}
.cell{padding:16px 18px;border-right:1px solid var(--line)}
.cell:last-child{border-right:0}
/* col 1 — what's doing */
.c1 .id{font-size:11px;color:var(--accent);font-family:ui-monospace,Menlo,monospace}
.c1 .title{font-weight:650;margin:3px 0 7px;font-size:14.5px}
.c1 .meta{font-size:11.5px;color:var(--dim)}
.c1 .meta .proj{color:#c9a6ff}
.badge{display:inline-block;font-size:10.5px;padding:1px 7px;border-radius:5px;
background:rgba(255,180,84,.14);color:var(--doing);border:1px solid rgba(255,180,84,.3);
margin-top:8px;font-weight:600}
/* col 2 — what it's doing */
.c2 .who{font-size:11px;color:var(--dim);margin-bottom:5px}
.c2 .who b{color:#8fd3ff}
.c2 .act{font-size:13px;color:#d7dee7;white-space:pre-wrap;
display:-webkit-box;-webkit-line-clamp:8;-webkit-box-orient:vertical;overflow:hidden}
.c2 .act.open{-webkit-line-clamp:unset}
.c2 .more{font-size:11px;color:var(--accent);cursor:pointer;margin-top:4px;display:inline-block}
.c2 .age{font-size:11px;color:var(--dim);margin-top:8px}
.fresh{color:var(--kid)!important}
.warn{color:var(--doing)!important}
.stale{color:#ff7b72!important}
/* col 3 — in plain words */
.c3{background:var(--kidbg)}
.c3 .kid{font-size:14.5px;line-height:1.6;color:#d9f7e4}
.c3 .thinking{color:var(--dim);font-style:italic}
.c3 .via{font-size:10.5px;color:#4d7a5e;margin-top:9px}
.head-row .h4{color:#ffd479}
/* col 4 — why so slow / restart */
.verdict{display:inline-block;font-size:12px;font-weight:700;padding:3px 10px;border-radius:6px;margin-bottom:9px}
.v-RUNNING{background:rgba(126,231,135,.14);color:#7ee787;border:1px solid rgba(126,231,135,.35)}
.v-NUDGE{background:rgba(255,212,121,.14);color:#ffd479;border:1px solid rgba(255,212,121,.35)}
.v-RESTART{background:rgba(255,123,114,.16);color:#ff9b93;border:1px solid rgba(255,123,114,.4)}
.v-WAITING{background:rgba(122,162,255,.14);color:#9db4ff;border:1px solid rgba(122,162,255,.35)}
.c4 .why{font-size:12.5px;color:#c6cfda;line-height:1.55}
button.verdict{cursor:pointer;font-family:inherit;transition:filter .12s,transform .05s}
button.verdict:hover{filter:brightness(1.18)}
button.verdict:active{transform:translateY(1px)}
button.verdict:disabled{cursor:not-allowed;opacity:.5;filter:none}
.verdict.busy{opacity:.7;pointer-events:none}
.actnote{font-size:11px;margin-top:7px}
.actnote.ok{color:#7ee787}.actnote.err{color:#ff7b72}
.empty{padding:60px;text-align:center;color:var(--dim)}
@media(max-width:1000px){
.head-row{display:none}
.card{grid-template-columns:1fr 1fr}
.cell{border-right:0}
}
@media(max-width:640px){ .card{grid-template-columns:1fr} .c3{border-top:1px dashed #2c4a38} }
</style>
</head>
<body>
<header>
<h1><span class="dot"></span>What's Doing</h1>
<span class="pill"><b id="count">–</b> tasks in flight</span>
<span class="pill" id="pend"></span>
<div class="spacer"></div>
<span id="unlock"></span>
<span id="clock">connecting…</span>
</header>
<div class="head-row">
<div>1 · What's doing</div>
<div>2 · What it's doing right now</div>
<div class="h3">3 · In plain words 🧒</div>
<div class="h4">4 · Why so slow / restart?</div>
</div>
<div class="list" id="list"><div class="empty">Loading the board…</div></div>
<script>
const esc = s => (s||'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
let IS_MIRROR = false, CAN_ACT = false;
function renderVerdict(i){
const v = i.diag.verdict, label = esc(i.diag.label), id = esc(i.id);
if (v === 'RUNNING') return `<div class="verdict v-RUNNING">${label}</div>`;
if (v === 'WAITING') return `<button class="verdict v-WAITING" onclick="window.open('https://approvals.agentabrams.com','_blank')" title="Open the approvals console">${label}</button>`;
// NUDGE / RESTART -> real launch button
const verb = (v === 'RESTART') ? 'restart' : 'nudge';
if (!CAN_ACT) return `<button class="verdict v-${v}" onclick="needUnlock('${id}')" title="View-only on this device. Click to unlock it (owner key), or open the board locally on Mac2 / your home network.">${label} 🔒</button>`;
return `<button class="verdict v-${v}" onclick="act('${id}','${verb}',this)">${label}</button>`;
}
function needUnlock(id){
const note = document.getElementById('note-'+id);
if(note){ note.className='actnote err'; note.textContent='🔒 View-only on this device — unlock it to enable Restart / Nudge.'; }
unlockDevice();
}
function unlockDevice(){
const k = prompt('Enter your owner key to enable the Restart/Nudge buttons on this device:');
if (k) window.location = '/unlock?key=' + encodeURIComponent(k.trim());
}
async function act(id, verb, btn){
const note = document.getElementById('note-'+id);
const orig = btn.textContent;
btn.classList.add('busy'); btn.textContent = (verb==='restart'?'🔄 launching…':'🔄 kicking…');
try{
const r = await fetch('/api/action',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,verb})});
const d = await r.json();
if(r.ok && d.ok){ btn.textContent='✓ launched'; if(note){note.className='actnote ok';note.textContent='An iTerm window is opening on Mac2 to drive '+id+'.';} }
else { btn.textContent=orig; btn.classList.remove('busy'); if(note){note.className='actnote err';note.textContent=(d.error||d.msg||'failed');} }
}catch(e){ btn.textContent=orig; btn.classList.remove('busy'); if(note){note.className='actnote err';note.textContent='request failed';} }
}
function ageClass(h){ return h<6?'fresh':h<48?'warn':'stale'; }
function agoTxt(ts,h){
const m=Math.round(h*60);
if(m<60) return m+' min ago';
if(h<48) return h.toFixed(1)+' h ago';
return (h/24).toFixed(1)+' days ago';
}
async function load(){
let d;
try{ d = await (await fetch('/api/doing',{cache:'no-store'})).json(); }
catch(e){ document.getElementById('clock').textContent='reconnecting…'; return; }
IS_MIRROR = !!d.mirror; CAN_ACT = !!d.canAct;
const un = document.getElementById('unlock');
if (!IS_MIRROR && !CAN_ACT) un.innerHTML = '<a href="#" onclick="unlockDevice();return false" style="color:#ffd479;text-decoration:none;font-size:12px">🔓 Unlock this device</a>';
else if (!IS_MIRROR && CAN_ACT) un.innerHTML = '<a href="/lock" style="color:#8b98a9;text-decoration:none;font-size:11px">lock</a>';
else un.innerHTML = '';
document.getElementById('count').textContent = d.count;
const pend = document.getElementById('pend');
if (d.mirror) {
const sa = d.syncedAt ? new Date(d.syncedAt) : null;
const min = sa ? Math.round((Date.now()-sa.getTime())/60000) : null;
const stale = min===null || min>15;
pend.innerHTML = sa
? `🔁 Kamatera mirror · synced <b style="color:${stale?'#ff7b72':'#7ee787'}">${min} min ago</b>${stale?' (Mac2 may be offline)':''}`
: `🔁 Kamatera mirror · <b style="color:#ff7b72">no snapshot yet</b>`;
} else {
pend.textContent = d.pending ? ('✍️ writing '+d.pending+' plain-word summaries…') : '✅ all summarized';
}
document.getElementById('clock').innerHTML = (d.mirror?'mirror · ':'live · ')+'as of '+new Date(d.now).toLocaleTimeString()
+ (CAN_ACT ? ' · <span style="color:#7ee787">🔓 actions on</span>' : ' · <span style="color:#8b98a9">🔒 view-only</span>');
const list = document.getElementById('list');
if(!d.items.length){ list.innerHTML='<div class="empty">Nothing is doing right now. 🎉</div>'; return; }
list.innerHTML = d.items.map(i=>`
<div class="card">
<div class="cell c1">
<div class="id">${esc(i.id)}</div>
<div class="title">${esc(i.title)}</div>
<div class="meta"><span class="proj">${esc(i.project||'—')}</span></div>
<div class="badge">DOING</div>
</div>
<div class="cell c2">
<div class="who">agent <b>${esc(i.agent||'—')}</b></div>
<div class="act">${esc(i.doingText)||'<span style="color:var(--dim)">just started — nothing logged yet</span>'}</div>
${ (i.doingText||'').length>320 ? '<span class="more" onclick="this.previousElementSibling.classList.toggle(\'open\');this.textContent=this.textContent[0]==\'▾\'?\'▴ less\':\'▾ more\'">▾ more</span>' : '' }
<div class="age ${ageClass(i.ageH)}">last activity ${agoTxt(i.doingTs,i.ageH)}</div>
</div>
<div class="cell c3">
${ i.eli12
? `<div class="kid">${esc(i.eli12)}</div><div class="via">explained by a local model, free</div>`
: `<div class="kid thinking">🤔 getting the simple version…</div>` }
</div>
<div class="cell c4">
${ i.diag ? renderVerdict(i) : `<div class="why" style="color:var(--dim)">—</div>` }
<div class="why">${i.diag?esc(i.diag.why):''}</div>
<div class="actnote" id="note-${esc(i.id)}"></div>
</div>
</div>`).join('');
}
load(); setInterval(load, 8000);
</script>
</body>
</html>