[object Object]

← back to Agent Cabinet

Add /activity viewer — live date/time timeline aggregating git commits, canary runs, watch ticks, CNCP wins, and paid API calls; /api/activity endpoint (epoch-sorted, mtime-dated ticks); type filter chips + 30s auto-refresh; linked from pyramid toolbar

101c9bc852fa6178da94fc0d364f4faa8e435098 · 2026-06-16 17:39:51 -0700 · SteveStudio2

Files touched

Diff

commit 101c9bc852fa6178da94fc0d364f4faa8e435098
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Tue Jun 16 17:39:51 2026 -0700

    Add /activity viewer — live date/time timeline aggregating git commits, canary runs, watch ticks, CNCP wins, and paid API calls; /api/activity endpoint (epoch-sorted, mtime-dated ticks); type filter chips + 30s auto-refresh; linked from pyramid toolbar
---
 activity.html | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 pyramid.html  |  1 +
 server.js     | 51 +++++++++++++++++++++++++++++++++-
 3 files changed, 139 insertions(+), 1 deletion(-)

diff --git a/activity.html b/activity.html
new file mode 100644
index 0000000..0becaf1
--- /dev/null
+++ b/activity.html
@@ -0,0 +1,88 @@
+<!doctype html>
+<html lang="en"><head>
+<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
+<title>What it's doing — Activity Timeline</title>
+<style>
+  :root{--bg:#0b0e14;--panel:#11151f;--ink:#e7ecf3;--dim:#8a94a6;--line:#1f2735}
+  *{box-sizing:border-box}
+  html,body{margin:0;background:var(--bg);color:var(--ink);font:13.5px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,Roboto,Helvetica,Arial,sans-serif}
+  header{padding:20px 28px 12px;border-bottom:1px solid var(--line);position:sticky;top:0;background:#0b0e14;z-index:5}
+  h1{margin:0;font-size:21px;font-weight:600;letter-spacing:.2px}
+  h1 small{color:var(--dim);font-weight:400;font-size:13px;margin-left:10px}
+  .meta{color:var(--dim);font-size:12px;margin-top:5px}
+  .meta a{color:#34f4ce;text-decoration:none}
+  .chips{display:flex;gap:8px;flex-wrap:wrap;margin-top:13px}
+  .chip{cursor:pointer;user-select:none;font-size:12px;padding:5px 12px;border-radius:20px;border:1px solid var(--line);background:#121826;color:var(--dim);display:flex;align-items:center;gap:6px}
+  .chip.on{color:#fff}
+  .chip .dot{width:9px;height:9px;border-radius:50%}
+  .chip .n{font:11px ui-monospace,monospace;opacity:.8}
+  main{max-width:1000px;margin:0 auto;padding:18px 28px 70px}
+  .day{margin:22px 0 8px;font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:var(--dim);position:sticky;top:128px;background:#0b0e14;padding:4px 0;z-index:3}
+  .ev{display:grid;grid-template-columns:64px 92px 1fr;gap:12px;align-items:baseline;padding:8px 10px;border-radius:9px;border-left:3px solid var(--c,#2b3650);background:#0e1320;margin-bottom:5px}
+  .ev:hover{background:#121829}
+  .ev .tm{font:12px ui-monospace,monospace;color:#9fb0c9}
+  .ev .ty{font-size:9.5px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:var(--c);align-self:center}
+  .ev .ti{font-weight:600}
+  .ev .dt{color:var(--dim);font-size:11.5px;font-weight:400}
+  .empty{color:var(--dim);padding:40px;text-align:center}
+  .pulse{display:inline-block;width:8px;height:8px;border-radius:50%;background:#34f4ce;margin-right:6px;animation:p 1.6s infinite}
+  @keyframes p{0%,100%{opacity:.3}50%{opacity:1}}
+</style></head>
+<body>
+<header>
+  <h1>What it's doing <small>live activity timeline — agents, canaries, watch ticks, commits, spend</small></h1>
+  <div class="meta"><span class="pulse"></span><span id="meta">loading…</span> · auto-refreshes every 30s · <a href="/pyramid">← cabinet pyramid</a></div>
+  <div class="chips" id="chips"></div>
+</header>
+<main id="feed"><div class="empty">Loading activity…</div></main>
+<script>
+const TYPES={
+  commit:{label:'Commits', col:'#a78bfa'},
+  canary:{label:'Canaries', col:'#34d399'},
+  win:{label:'Wins', col:'#fbbf24'},
+  cost:{label:'Spend', col:'#fb7185'},
+  tick:{label:'Watch ticks', col:'#60a5fa'},
+};
+const VCOL={FAIL:'#ef4444',WARN:'#fbbf24',PASS:'#34d399',NA:'#6b7280'};
+let DATA=[], OFF_FILTER=new Set();   // types hidden
+function colorFor(e){ if(e.type==='canary') return VCOL[e.verdict]||TYPES.canary.col; return (TYPES[e.type]||{}).col||'#2b3650'; }
+function fmtTime(iso){ try{ return new Date(iso).toLocaleTimeString(undefined,{hour:'2-digit',minute:'2-digit'}); }catch(e){ return '--:--'; } }
+function fmtDay(iso){ try{ return new Date(iso).toLocaleDateString(undefined,{weekday:'short',month:'short',day:'numeric',year:'numeric'}); }catch(e){ return iso; } }
+function dayKey(iso){ try{ return new Date(iso).toISOString().slice(0,10); }catch(e){ return iso.slice(0,10); } }
+
+function renderChips(){
+  const counts={}; DATA.forEach(e=>counts[e.type]=(counts[e.type]||0)+1);
+  const c=document.getElementById('chips');
+  c.innerHTML='<span class="chip '+(OFF_FILTER.size?'':'on')+'" data-t="__all"><b>All</b> <span class="n">'+DATA.length+'</span></span>'+
+    Object.keys(TYPES).map(t=>`<span class="chip ${OFF_FILTER.has(t)?'':'on'}" data-t="${t}"><span class="dot" style="background:${TYPES[t].col}"></span>${TYPES[t].label} <span class="n">${counts[t]||0}</span></span>`).join('');
+  c.querySelectorAll('.chip').forEach(ch=>ch.onclick=()=>{
+    const t=ch.dataset.t;
+    if(t==='__all'){ OFF_FILTER.clear(); } else { OFF_FILTER.has(t)?OFF_FILTER.delete(t):OFF_FILTER.add(t); }
+    renderChips(); renderFeed();
+  });
+}
+function renderFeed(){
+  const shown=DATA.filter(e=>!OFF_FILTER.has(e.type));
+  const feed=document.getElementById('feed');
+  if(!shown.length){ feed.innerHTML='<div class="empty">No events for this filter.</div>'; return; }
+  let html='', curDay=null;
+  shown.forEach(e=>{
+    const dk=dayKey(e.ts);
+    if(dk!==curDay){ curDay=dk; html+=`<div class="day">${fmtDay(e.ts)}</div>`; }
+    const ty=e.type==='canary'?(e.verdict||'canary'):(TYPES[e.type]||{}).label||e.type;
+    html+=`<div class="ev" style="--c:${colorFor(e)}"><span class="tm">${fmtTime(e.ts)}</span><span class="ty">${ty}</span><span><span class="ti">${esc(e.title)}</span>${e.detail?` <span class="dt">— ${esc(e.detail)}</span>`:''}</span></div>`;
+  });
+  feed.innerHTML=html;
+}
+function esc(s){ return (s==null?'':String(s)).replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c])); }
+async function load(){
+  try{
+    const r=await(await fetch('/api/activity')).json();
+    DATA=r.events||[];
+    document.getElementById('meta').textContent=DATA.length+' events · updated '+new Date(r.generated_at||Date.now()).toLocaleTimeString();
+    renderChips(); renderFeed();
+  }catch(e){ document.getElementById('feed').innerHTML='<div class="empty">Couldn’t reach the server (:9766).</div>'; }
+}
+load(); setInterval(load, 30000);
+</script>
+</body></html>
diff --git a/pyramid.html b/pyramid.html
index 7cba1c9..d224181 100644
--- a/pyramid.html
+++ b/pyramid.html
@@ -529,6 +529,7 @@ function buildToolbar(){
     `<label><input type="checkbox" id="tg-shared" checked><span class="tog-line" style="border-top:2px dashed #cbd5e1"></span>Shared now (${SHARED.length})</label>`+
     `<label><input type="checkbox" id="tg-sug" checked><span class="tog-line" style="border-top:2px dotted #fbbf24"></span>Suggested moves (${SUGGEST.length})</label>`+
     `<button class="tb-btn" onclick="openIdeasGallery()">💡 Ideas</button>`+
+    `<button class="tb-btn" style="margin-left:0" onclick="location.href='/activity'">🕓 Activity</button>`+
     `<button class="tb-btn" style="margin-left:0" onclick="showAnalysis()">▤ List</button>`;
   ['tg-hier','tg-shared','tg-sug'].forEach(id=>document.getElementById(id).addEventListener('change',applyToggles));
 }
diff --git a/server.js b/server.js
index 0e41232..040b873 100644
--- a/server.js
+++ b/server.js
@@ -5,7 +5,7 @@ const fs = require('fs');
 const path = require('path');
 const http = require('http');
 const os = require('os');
-const { execFile } = require('child_process');
+const { execFile, execSync } = require('child_process');
 
 const PORT = 9766;
 const ROOT = __dirname;
@@ -606,6 +606,11 @@ const server = http.createServer((req, res) => {
     } catch (e) { res.writeHead(500); res.end('pyramid.html missing: ' + e.message); }
     return;
   }
+  if (u.pathname === '/activity' || u.pathname === '/activity/') {
+    try { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(fs.readFileSync(path.join(ROOT, 'activity.html'), 'utf8')); }
+    catch (e) { res.writeHead(500); res.end('activity.html missing: ' + e.message); }
+    return;
+  }
   if (u.pathname === '/api/source') {
     const r = resolveSource(q.get('kind'), q.get('name'));
     if (!r) return J({ ok: false, error: 'bad name' });
@@ -684,6 +689,50 @@ const server = http.createServer((req, res) => {
     const sortObj = (o) => Object.entries(o).map(([k, v]) => ({ name: k, usd: +v.toFixed(4) })).sort((a, b) => b.usd - a.usd);
     return J({ ok: true, total_usd: +total.toFixed(2), entries: n, first, last, by_app: sortObj(byApp).slice(0, 80), by_vendor: sortObj(byVendor).slice(0, 20) });
   }
+  if (u.pathname === '/api/activity') {
+    const ev = [];
+    // 1) git commits from active repos
+    [['agent-cabinet', ROOT], ['cncp-starter', path.join(HOME, 'cncp-starter')]].forEach(([nm, r]) => {
+      try {
+        execSync('git -C "' + r + '" log -20 --pretty=format:%cI%x09%h%x09%s', { encoding: 'utf8' }).split('\n').forEach(l => {
+          const p = l.split('\t'); if (p[0]) ev.push({ ts: p[0], type: 'commit', title: p.slice(2).join(' '), detail: nm + ' · ' + p[1] });
+        });
+      } catch (e) {}
+    });
+    // 2) canary runs (last verdict each)
+    ['dw-uptime-probe', 'dw-scraper-canary', 'dw-map-auditor', 'dw-leak-scanner', 'dw-backup-canary', 'dw-canary-meta-watchdog'].forEach(sk => {
+      const c = readCanary(sk); if (c.ts) ev.push({ ts: c.ts, type: 'canary', verdict: c.sev, title: sk + ' — ' + c.sev, detail: c.summary || '' });
+    });
+    // 3) recent paid API calls
+    try {
+      const lines = fs.readFileSync(path.join(HOME, '.claude', 'cost-ledger.jsonl'), 'utf8').trim().split('\n');
+      lines.slice(-30).forEach(l => { try { const j = JSON.parse(l); ev.push({ ts: j.ts, type: 'cost', title: '$' + (+j.cost_usd).toFixed(4) + ' · ' + j.app, detail: (j.vendor || '') + (j.note ? ' · ' + j.note : '') }); } catch (e) {} });
+    } catch (e) {}
+    // 4) overnight watch ticks (lines carry no date → use the run-notes file's mtime day)
+    try {
+      const rnf = path.join(HOME, '.claude', 'run-notes', 'overnight-2026-06-15.md');
+      const md = new Date(fs.statSync(rnf).mtimeMs);
+      const dstr = md.getFullYear() + '-' + ('0' + (md.getMonth() + 1)).slice(-2) + '-' + ('0' + md.getDate()).slice(-2);
+      fs.readFileSync(rnf, 'utf8').split('\n').forEach(l => {
+        const m = l.match(/tick\s+(\d{1,2}):(\d{2})\s*PT/i);
+        if (m) ev.push({ ts: dstr + 'T' + ('0' + m[1]).slice(-2) + ':' + m[2] + ':00-07:00', type: 'tick', title: 'watch tick ' + m[1] + ':' + m[2], detail: l.replace(/^[-*\s]+/, '').replace(/\*\*/g, '').slice(0, 140) });
+      });
+    } catch (e) {}
+    // 5) CNCP wins (async) → finalize
+    const finish = (wins) => {
+      (wins || []).forEach(w => {
+        const t = w.createdAt ? new Date(w.createdAt) : (w.date ? new Date(w.date) : null);
+        ev.push({ ts: (t || new Date()).toISOString(), type: 'win', title: w.title || 'win', detail: (w.project || '') + (w.commit ? ' · ' + w.commit : '') });
+      });
+      ev.sort((a, b) => (Date.parse(b.ts) || 0) - (Date.parse(a.ts) || 0));
+      J({ ok: true, count: ev.length, generated_at: new Date().toISOString(), events: ev.slice(0, 250) });
+    };
+    const wr = http.request({ host: '127.0.0.1', port: 3333, path: '/api/wins', method: 'GET', timeout: 4000 }, (cr) => {
+      let d = ''; cr.on('data', c => d += c); cr.on('end', () => { let arr = []; try { const j = JSON.parse(d); arr = j.wins || j.items || (Array.isArray(j) ? j : []); } catch (e) {} finish((arr || []).slice(0, 50)); });
+    });
+    wr.on('error', () => finish([])); wr.on('timeout', () => { wr.destroy(); finish([]); }); wr.end();
+    return;
+  }
 
   if (req.url === '/cabinet.yaml') {
     try {

← 9634ba2 Build out both proposals LIVE: 🩺 Health Overlay (reads 6 DW  ·  back to Agent Cabinet  ·  Activity viewer: add Upcoming/scheduled lane — /api/schedule 5b4da1d →