← 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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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>