← back to Agent Cabinet
Activity viewer: click any process/event for details — detail drawer + /api/job (plist schedule, launchctl status, command, log tail), /api/commit (git show), /api/canary (full verdict); jobs, continuous chips, commits, canaries, wins/cost/ticks all drillable; injection-guarded
9727194a40c0dcfa021999f0aa8b523d68c55760 · 2026-06-16 18:13:46 -0700 · SteveStudio2
Files touched
M activity.htmlM server.js
Diff
commit 9727194a40c0dcfa021999f0aa8b523d68c55760
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Tue Jun 16 18:13:46 2026 -0700
Activity viewer: click any process/event for details — detail drawer + /api/job (plist schedule, launchctl status, command, log tail), /api/commit (git show), /api/canary (full verdict); jobs, continuous chips, commits, canaries, wins/cost/ticks all drillable; injection-guarded
---
activity.html | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
server.js | 33 ++++++++++++++++++++++++++++--
2 files changed, 92 insertions(+), 5 deletions(-)
diff --git a/activity.html b/activity.html
index 6947808..f5ff26f 100644
--- a/activity.html
+++ b/activity.html
@@ -48,6 +48,19 @@
.up-cont .cc{background:#0e1320;border:1px solid var(--line);border-radius:14px;padding:2px 9px;color:#aeb8c8}
.up-more{color:#34f4ce;font-size:11.5px;margin-top:8px;cursor:pointer}
.up-more:hover{text-decoration:underline}
+ .up-row,.cc,.ev{cursor:pointer}
+ .up-row:hover,.ev:hover{outline:1px solid #2b4a55}
+ .cc:hover{border-color:#34f4ce;color:#fff}
+ /* detail drawer */
+ #det{position:fixed;top:0;right:0;height:100vh;width:min(580px,95vw);background:#0d1119;border-left:1px solid #243049;box-shadow:-14px 0 50px rgba(0,0,0,.6);transform:translateX(102%);transition:transform .2s cubic-bezier(.4,0,.2,1);z-index:200;display:flex;flex-direction:column}
+ #det.open{transform:translateX(0)}
+ .det-head{padding:15px 18px;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:10px}
+ #det-title{font-size:15px;font-weight:700;flex:1;word-break:break-word}
+ .det-x{background:transparent;border:1px solid var(--line);color:var(--dim);border-radius:7px;width:30px;height:30px;cursor:pointer;font-size:15px}
+ .det-x:hover{color:#fff;border-color:#4b5b7a}
+ #det-body{padding:14px 18px;overflow:auto;flex:1;font-size:12.5px}
+ .det-k{color:#34f4ce;font-size:10.5px;letter-spacing:.06em;text-transform:uppercase;margin:14px 0 3px}
+ .det-pre{background:#0a0e16;border:1px solid var(--line);border-radius:8px;padding:11px;font:11.5px/1.5 ui-monospace,monospace;color:#c8d2e0;white-space:pre-wrap;word-break:break-word;margin:2px 0}
</style></head>
<body>
<header>
@@ -55,6 +68,7 @@
<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>
+<div id="det"><div class="det-head"><span id="det-title">—</span><button class="det-x" onclick="closeDet()">✕</button></div><div id="det-body"></div></div>
<main class="cols">
<aside class="col-up"><section id="upcoming"></section></aside>
<div class="col-resize" id="splitter" title="Drag to resize · double-click to reset"></div>
@@ -95,11 +109,12 @@ function renderFeed(){
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>`;
+ html+=`<div class="ev" style="--c:${colorFor(e)}" data-type="${escA(e.type)}" data-ts="${escA(e.ts)}" data-title="${escA(e.title)}" data-detail="${escA(e.detail||'')}"${e.repo?` data-repo="${escA(e.repo)}" data-hash="${escA(e.hash)}"`:''}${e.skill?` data-skill="${escA(e.skill)}"`:''}><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])); }
+function escA(s){ return esc(s).replace(/"/g,'"'); }
async function load(){
try{
const r=await(await fetch('/api/activity')).json();
@@ -116,11 +131,54 @@ async function loadSchedule(){
const up=r.upcoming||[], cont=r.continuous||[];
const n=SHOW_ALL_UP?up.length:14;
let h='<div class="up-head">⏭ Upcoming <span class="s">— '+up.length+' scheduled jobs · next fire</span></div>';
- h+=up.slice(0,n).map(e=>`<div class="up-row"><span class="rel">${rel(Date.parse(e.next_ts))}</span><span class="abs">${fmtAbs(e.next_ts)}</span><span class="lab">${esc(e.label)}</span><span class="cad">${esc(e.cadence)}</span></div>`).join('');
+ h+=up.slice(0,n).map(e=>`<div class="up-row" data-label="${escA(e.label)}"><span class="rel">${rel(Date.parse(e.next_ts))}</span><span class="abs">${fmtAbs(e.next_ts)}</span><span class="lab">${esc(e.label)}</span><span class="cad">${esc(e.cadence)}</span></div>`).join('');
if(up.length>14) h+=`<div class="up-more" onclick="SHOW_ALL_UP=!SHOW_ALL_UP;loadSchedule()">${SHOW_ALL_UP?'▲ show less':'▼ show all '+up.length}</div>`;
- if(cont.length) h+='<div class="up-cont"><b style="color:#8a94a6">Continuous</b>'+cont.map(c=>`<span class="cc">${esc(c.label)} · ${esc(c.cadence)}</span>`).join('')+'</div>';
+ if(cont.length) h+='<div class="up-cont"><b style="color:#8a94a6">Continuous</b>'+cont.map(c=>`<span class="cc" data-label="${escA(c.label)}">${esc(c.label)} · ${esc(c.cadence)}</span>`).join('')+'</div>';
document.getElementById('upcoming').innerHTML=h;
}
+// ── click any process / event → detail drawer ──
+function closeDet(){ document.getElementById('det').classList.remove('open'); }
+function openDet(title, html){ document.getElementById('det-title').innerHTML=title; document.getElementById('det-body').innerHTML=html; document.getElementById('det').classList.add('open'); }
+async function detJob(label){
+ openDet('⚙️ '+esc(label), '<div class="det-pre">loading…</div>');
+ let d; try{ d=await(await fetch('/api/job?label='+encodeURIComponent(label))).json(); }catch(e){ return openDet(esc(label),'<div class="det-pre">server unreachable</div>'); }
+ if(!d.ok) return openDet(esc(label), '<div class="det-pre">'+esc(d.error||'error')+'</div>');
+ const st=d.status||{};
+ const statusTxt = st.loaded ? ('loaded · '+(st.pid?('running PID '+st.pid):'idle')+(st.lastExit!=null?(' · last exit '+st.lastExit):'')) : 'not loaded';
+ let h='<div class="det-k">Schedule</div>'+esc(d.schedule)+
+ '<div class="det-k">Status</div>'+esc(statusTxt)+
+ '<div class="det-k">Command</div><div class="det-pre">'+esc((d.program||[]).join(' '))+'</div>';
+ if(d.stdout) h+='<div class="det-k">Log file</div><div class="det-pre">'+esc(d.stdout)+'</div>';
+ if(d.logTail) h+='<div class="det-k">Recent log (tail)</div><div class="det-pre">'+esc(d.logTail)+'</div>';
+ openDet('⚙️ '+esc(d.label), h);
+}
+async function detCommit(repo,hash){
+ openDet('🟣 '+esc(repo)+' · '+esc(hash), '<div class="det-pre">loading…</div>');
+ let d; try{ d=await(await fetch('/api/commit?repo='+encodeURIComponent(repo)+'&hash='+encodeURIComponent(hash))).json(); }catch(e){ return openDet('commit','<div class="det-pre">server unreachable</div>'); }
+ if(!d.ok) return openDet('commit','<div class="det-pre">'+esc(d.error)+'</div>');
+ openDet('🟣 '+esc(repo)+' · '+esc(hash), '<div class="det-pre">'+esc(d.detail)+'</div>');
+}
+async function detCanary(skill){
+ openDet('🩺 '+esc(skill), '<div class="det-pre">loading…</div>');
+ let d; try{ d=await(await fetch('/api/canary?skill='+encodeURIComponent(skill))).json(); }catch(e){ return openDet(esc(skill),'<div class="det-pre">server unreachable</div>'); }
+ if(!d.ok) return openDet(esc(skill),'<div class="det-pre">'+esc(d.error)+'</div>');
+ openDet('🩺 '+esc(skill), '<div class="det-pre">'+esc(JSON.stringify(d.data,null,2))+'</div>');
+}
+function detSimple(el){
+ let when=''; try{ when=new Date(el.dataset.ts).toLocaleString(); }catch(e){}
+ openDet(esc(el.dataset.title||''), '<div class="det-k">When</div>'+esc(when)+'<div class="det-k">'+esc(el.dataset.type||'')+'</div>'+esc(el.dataset.detail||'(no extra detail)'));
+}
+document.addEventListener('click', e=>{
+ if(e.target.closest('#det')||e.target.closest('#splitter')||e.target.closest('.chip')||e.target.closest('.up-more')) return;
+ const job=e.target.closest('[data-label]'); if(job){ detJob(job.dataset.label); return; }
+ const ev=e.target.closest('.ev'); if(ev){ const ty=ev.dataset.type;
+ if(ty==='commit'&&ev.dataset.repo) detCommit(ev.dataset.repo, ev.dataset.hash);
+ else if(ty==='canary'&&ev.dataset.skill) detCanary(ev.dataset.skill);
+ else detSimple(ev);
+ }
+});
+document.addEventListener('keydown', e=>{ if(e.key==='Escape') closeDet(); });
+
// draggable splitter — resize the two columns (persisted; double-click resets)
(function(){
const cols=document.querySelector('.cols'), sp=document.getElementById('splitter'), KEY='cabinet-activity-colw';
diff --git a/server.js b/server.js
index 8a2a5f3..51beba6 100644
--- a/server.js
+++ b/server.js
@@ -728,13 +728,13 @@ const server = http.createServer((req, res) => {
[['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] });
+ const p = l.split('\t'); if (p[0]) ev.push({ ts: p[0], type: 'commit', title: p.slice(2).join(' '), detail: nm + ' · ' + p[1], repo: nm, hash: 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 || '' });
+ const c = readCanary(sk); if (c.ts) ev.push({ ts: c.ts, type: 'canary', verdict: c.sev, title: sk + ' — ' + c.sev, detail: c.summary || '', skill: sk });
});
// 3) recent paid API calls
try {
@@ -788,6 +788,35 @@ const server = http.createServer((req, res) => {
continuous.sort((a, b) => a.every_s - b.every_s);
return J({ ok: true, generated_at: new Date().toISOString(), upcoming, continuous });
}
+ if (u.pathname === '/api/job') {
+ const lab = (q.get('label') || '').replace(/^com\.steve\./, '');
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(lab)) return J({ ok: false, error: 'bad label' });
+ const full = 'com.steve.' + lab, pf = path.join(HOME, 'Library', 'LaunchAgents', full + '.plist');
+ let d; try { d = JSON.parse(execSync('plutil -convert json -o - "' + pf + '"', { encoding: 'utf8' })); } catch (e) { return J({ ok: false, error: 'plist not found' }); }
+ const program = d.ProgramArguments || (d.Program ? [d.Program] : []);
+ let pid = null, lastExit = null, loaded = false;
+ try { const s = execSync('launchctl list ' + full + ' 2>/dev/null', { encoding: 'utf8' }); loaded = true; const mp = s.match(/"PID"\s*=\s*(\d+)/); if (mp) pid = +mp[1]; const me = s.match(/"LastExitStatus"\s*=\s*(-?\d+)/); if (me) lastExit = +me[1]; } catch (e) {}
+ let sched = '—';
+ if (d.StartInterval) sched = humanInt(d.StartInterval);
+ else if (d.StartCalendarInterval) { const sc = d.StartCalendarInterval; sched = Array.isArray(sc) ? sc.map(calCadence).join(' · ') : calCadence(sc); }
+ else if (d.KeepAlive) sched = 'KeepAlive (always running)';
+ let logTail = ''; const lp = d.StandardOutPath || d.StandardErrorPath;
+ if (lp && lp[0] === '/') { try { logTail = execSync('tail -n 25 "' + lp + '" 2>/dev/null', { encoding: 'utf8' }); } catch (e) {} }
+ return J({ ok: true, label: full, schedule: sched, program, stdout: d.StandardOutPath || null, stderr: d.StandardErrorPath || null, runAtLoad: !!d.RunAtLoad, status: { loaded, pid, lastExit }, logTail: logTail.slice(-4000) });
+ }
+ if (u.pathname === '/api/commit') {
+ const repo = q.get('repo'), hash = q.get('hash');
+ const map = { 'agent-cabinet': ROOT, 'cncp-starter': path.join(HOME, 'cncp-starter') };
+ if (!map[repo]) return J({ ok: false, error: 'unknown repo' });
+ if (!/^[0-9a-f]{6,40}$/.test(hash || '')) return J({ ok: false, error: 'bad hash' });
+ try { const out = execSync('git -C "' + map[repo] + '" show --stat --format="%H%n%an <%ae>%n%cI%n%n%B" ' + hash, { encoding: 'utf8', maxBuffer: 1 << 20 }); return J({ ok: true, repo, hash, detail: out.slice(0, 8000) }); }
+ catch (e) { return J({ ok: false, error: 'git show failed' }); }
+ }
+ if (u.pathname === '/api/canary') {
+ const sk = q.get('skill'); if (!/^[a-z0-9][a-z0-9._-]*$/.test(sk || '')) return J({ ok: false, error: 'bad skill' });
+ try { return J({ ok: true, skill: sk, data: JSON.parse(fs.readFileSync(path.join(HOME, '.claude', 'skills', sk, 'data', 'latest.json'), 'utf8')) }); }
+ catch (e) { return J({ ok: false, error: 'no data' }); }
+ }
if (req.url === '/cabinet.yaml') {
try {
← be0315f Activity viewer: draggable middle splitter to resize the two
·
back to Agent Cabinet
·
Activity viewer: live search box — filters the feed AND sche c791b73 →