← 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
A activity.htmlM pyramid.htmlM server.js
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=>({'&':'&','<':'<','>':'>'}[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 →