← back to Stack Map Viewer
viewer: draggable nodes + recursive drill + view switcher
a676017beff35cca400361a667e5772e6b21a6cd · 2026-09-22 11:42:17 -0700 · Steve Abrams
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D2KFGQK7p7ahEzug7HBMZF
Files touched
Diff
commit a676017beff35cca400361a667e5772e6b21a6cd
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Sep 22 11:42:17 2026 -0700
viewer: draggable nodes + recursive drill + view switcher
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D2KFGQK7p7ahEzug7HBMZF
---
index.html | 476 ++++++++++++++++++++++++++++++++++++++++++++-----------------
server.js | 202 ++++++++++++++++++++++++++
2 files changed, 544 insertions(+), 134 deletions(-)
diff --git a/index.html b/index.html
index 236ae14..b67637e 100644
--- a/index.html
+++ b/index.html
@@ -12,18 +12,54 @@
.WARN{background:#2e2712;color:#ffd27a;border:1px solid #8a6a2a}
.FAIL{background:#2e1c1c;color:#f3b6b6;border:1px solid #8a4a4a}
.UNKNOWN{background:#20233a;color:#b9c0ff;border:1px solid #4a5388}
+ /* view switcher */
+ #toolbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:10px 20px;border-bottom:1px solid #222a3a}
+ .vbtn{background:#181c28;border:1px solid #39507a;color:#c7d2e6;border-radius:8px;padding:6px 14px;cursor:pointer;font-family:inherit;font-size:13px}
+ .vbtn.on{background:#20304d;color:#eef1f7;border-color:#5f7bb0;font-weight:700}
+ .tbtn{background:#231a1a;border:1px solid #6b4141;color:#e6c7c7;border-radius:8px;padding:6px 12px;cursor:pointer;font-family:inherit;font-size:12px}
+ .tbtn:hover{background:#3a2323}
+ .tspace{margin-left:auto;color:var(--dim);font-size:12px}
#wrap{overflow:auto;padding:8px}
svg{display:block}
- rect.node{cursor:pointer;transition:filter .12s}
- rect.node:hover{filter:brightness(1.35)}
- #drawer{position:fixed;top:0;right:-420px;width:400px;height:100%;background:var(--panel);border-left:1px solid #2a3247;
- transition:right .18s;overflow:auto;padding:18px;box-shadow:-8px 0 30px rgba(0,0,0,.5)}
+ g.dnode{cursor:grab}
+ g.dnode:active{cursor:grabbing}
+ g.dnode rect.node{transition:filter .12s}
+ g.dnode:hover rect.node{filter:brightness(1.2)}
+ g.sub{cursor:pointer}
+ g.sub:hover rect{filter:brightness(1.4)}
+ /* tree */
+ #treebox{padding:10px 22px 40px;display:none}
+ .tnode{margin:1px 0}
+ .tlabel{display:inline-flex;align-items:center;gap:8px;padding:3px 8px;border-radius:6px;cursor:pointer;font-size:13px;color:#c7d2e6}
+ .tlabel:hover{background:#181c28}
+ .tlabel .tw{color:#6f7d97;width:12px;display:inline-block;text-align:center}
+ .tlabel .tt{color:#5f7bb0;font-size:11px;background:#181c28;border:1px solid #2a3247;border-radius:8px;padding:0 6px}
+ .tlabel .tm{color:#6f7d97;font-size:11px}
+ .kids{margin-left:18px;border-left:1px solid #222a3a;padding-left:6px}
+ .kids.hide{display:none}
+ .tleaf{color:#6f7d97;font-size:12px;padding:2px 8px}
+ /* treemap */
+ #treemapbox{padding:8px;display:none}
+ #treemapbox svg text{pointer-events:none;font-family:inherit}
+ #treemapbox rect{cursor:pointer;stroke:#0f1117;stroke-width:1.5}
+ #treemapbox rect:hover{filter:brightness(1.3)}
+ /* drawer */
+ #drawer{position:fixed;top:0;right:-460px;width:440px;height:100%;background:var(--panel);border-left:1px solid #2a3247;
+ transition:right .18s;overflow:auto;padding:18px;box-shadow:-8px 0 30px rgba(0,0,0,.5);z-index:50}
#drawer.open{right:0}
- #drawer h2{font-size:16px;margin:0 0 4px}
+ #crumbs{font-size:12px;margin:0 0 8px;color:#9fc4ff;line-height:1.7}
+ #crumbs a{cursor:pointer;color:#9fc4ff;text-decoration:none}
+ #crumbs a:hover{text-decoration:underline}
+ #crumbs .sep{color:#3a4761;margin:0 6px}
+ #drawer h2{font-size:16px;margin:0 0 4px;word-break:break-all}
#drawer .cnt{color:var(--dim);font-size:12px;margin-bottom:12px}
#drawer ul{list-style:none;margin:0;padding:0;font-size:13px}
- #drawer li{padding:6px 8px;border-radius:6px}
+ #drawer li{padding:6px 8px;border-radius:6px;cursor:pointer;display:flex;gap:8px;align-items:baseline}
#drawer li:nth-child(odd){background:#181c28}
+ #drawer li:hover{background:#20304d}
+ #drawer li .li-l{flex:1;word-break:break-all}
+ #drawer li .li-m{color:#6f7d97;font-size:11px;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+ #drawer li .li-t{color:#5f7bb0;font-size:10px;background:#12151f;border:1px solid #2a3247;border-radius:8px;padding:0 6px}
#close{float:right;cursor:pointer;color:var(--dim);font-size:20px;line-height:1}
.foot{color:var(--dim);font-size:12px;padding:8px 20px}
#folders{padding:4px 20px 30px}
@@ -42,153 +78,325 @@
<span class="sub" id="sub">scanning…</span>
<span class="badge UNKNOWN" id="verdict">…</span>
</header>
-<div id="wrap"><div id="svgbox"></div></div>
+<div id="toolbar">
+ <span style="color:var(--dim);font-size:12px">View:</span>
+ <button class="vbtn" data-view="map">Map</button>
+ <button class="vbtn" data-view="tree">Tree</button>
+ <button class="vbtn" data-view="treemap">Treemap</button>
+ <button class="tbtn" id="resetLayout" title="Clear saved node positions">Reset layout</button>
+ <span class="tspace" id="viewhint"></span>
+</div>
+<div id="wrap"><div id="svgbox"></div><div id="treebox"></div><div id="treemapbox"></div></div>
<div id="folders"></div>
<div class="foot" id="foot"></div>
-<div id="drawer"><span id="close" onclick="document.getElementById('drawer').classList.remove('open')">✕</span>
+<div id="drawer"><span id="close">✕</span>
+ <div id="crumbs"></div>
<h2 id="dtitle"></h2><div class="cnt" id="dcnt"></div><ul id="dlist"></ul></div>
<script>
const N=(v)=>v==null?'…':v.toLocaleString();
-function box(x,y,w,h,fill,stroke,kind){return `<rect class="node" x="${x}" y="${y}" width="${w}" height="${h}" rx="10" fill="${fill}" stroke="${stroke}"${kind?` data-kind="${kind}"`:''}/>`}
-function txt(x,y,s,fill,size,extra=''){return `<text x="${x}" y="${y}" fill="${fill}" font-size="${size}" ${extra}>${s}</text>`}
+let DATA=null;
-function render(d){
- const s=`<svg width="1680" height="1120" viewBox="0 0 1680 1120" xmlns="http://www.w3.org/2000/svg">
- <defs>
- <marker id="arw" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#7d8aa5"/></marker>
- <marker id="arwG" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#4fae7a"/></marker>
- <marker id="arwR" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#d98b3a"/></marker>
- </defs>
- <rect width="1680" height="1120" fill="#0f1117"/>
- ${box(60,120,220,66,'#243b53','#4a6c92')}
- ${txt(170,150,'STEVE','#eef1f7',20,'text-anchor="middle" font-weight="700"')}
- ${txt(170,172,'/dtd · /officer-yolo · /yolo','#a9b6cc',12,'text-anchor="middle"')}
+// ── localStorage helpers (try/catch — may throw / return empty) ──────────────
+function lsGet(k,d){try{const v=localStorage.getItem(k);return v==null?d:v;}catch{return d;}}
+function lsSet(k,v){try{localStorage.setItem(k,v);}catch{}}
+function loadPos(){try{return JSON.parse(localStorage.getItem('smv_pos')||'{}')||{};}catch{return {};}}
+function savePos(p){try{localStorage.setItem('smv_pos',JSON.stringify(p));}catch{}}
+let POS=loadPos();
+let VIEW=lsGet('smv_view','map');
- ${box(40,240,480,116,'#1c2740','#3f5a86','officers')}
- ${txt(56,268,`${N(d.officers)} CABINET OFFICERS (vp-*)`,'#9fc4ff',16,'font-weight="700"')}
- ${txt(56,294,'commerce · marketing · engineering · operations · security · cncp','#c7d2e6',12)}
- ${txt(56,314,'research · directories · compliance · special-projects · consulting · abramsego','#c7d2e6',12)}
- ${txt(56,340,'route + final sign-off on gated work — click to list','#6f7d97',11,'font-style="italic"')}
- <line x1="170" y1="186" x2="230" y2="238" stroke="#7d8aa5" stroke-width="2" marker-end="url(#arw)"/>
-
- ${box(40,406,480,120,'#1c2740','#3f5a86','ag_workers')}
- ${txt(56,432,`~${N(d.agents-d.officers)} WORKER AGENTS (${N(d.agents)} total) ▸`,'#9fc4ff',16,'font-weight="700"')}
- ${txt(56,456,'vendor stewards · site/build · marketing · directory builders','#c7d2e6',12)}
- ${txt(56,476,'contrarian · approval-agent · fleet-lifecycle · masterdot · google-merchant','#c7d2e6',12)}
- ${txt(56,500,'own & supervise; recruit subagents (plan→split→argue→merge)','#6f7d97',11,'font-style="italic"')}
- <line x1="280" y1="356" x2="280" y2="404" stroke="#7d8aa5" stroke-width="2" marker-end="url(#arw)"/>
-
- ${box(40,576,480,214,'#182135','#39507a','sk_all')}
- ${txt(56,602,`${N(d.skills)} SKILLS (invoked via Skill tool) ▸`,'#9fc4ff',16,'font-weight="700"')}
- ${box(56,616,200,34,'#20304d','#20304d','canaries')}${txt(66,637,`${N(d.canaries)} canaries / monitors`,'#c7d2e6',12)}
- ${box(266,616,238,34,'#20304d','#20304d','scrapers')}${txt(276,637,`${N(d.scrapers)} vendor scraper-managers`,'#c7d2e6',12)}
- ${box(56,658,200,34,'#20304d','#20304d','dots')}${txt(66,679,`${N(d.dots)} terminal dot/color`,'#c7d2e6',12)}
- ${box(266,658,238,34,'#20304d','#20304d','video')}${txt(276,679,`${N(d.video)} hyperframes / video`,'#c7d2e6',12)}
- ${txt(56,724,'+ deploy · pricing · settlement gate · storefront builders · research','#c7d2e6',12)}
- ${txt(56,760,`${N(d.health)} emit data/latest.json health heartbeat`,'#6f7d97',11,'font-style="italic"')}
- <line x1="280" y1="526" x2="280" y2="574" stroke="#7d8aa5" stroke-width="2" marker-end="url(#arw)"/>
-
- ${box(600,120,300,120,'#2a2136','#6b4f86')}
- ${txt(616,146,`${N(d.cronLoaded)} CRON JOBS (launchd)`,'#d6b8ff',16,'font-weight="700"')}
- ${txt(616,170,`com.steve.* — ${N(d.cronPlists)} plists on disk`,'#cfc3e0',12)}
- ${txt(616,190,'canaries · drips · refreshes · rollups','#cfc3e0',12)}
- ${txt(616,214,`${N(d.cronToSkill)} point directly at a skill dir`,'#8f7fb0',11,'font-style="italic"')}
- <path d="M600,200 C560,210 540,540 524,646" fill="none" stroke="#d98b3a" stroke-width="2.4" marker-end="url(#arwR)"/>
- ${txt(548,470,`drives (${N(d.cronToSkill)})`,'#d98b3a',12,'font-weight="700" transform="rotate(-82 548 470)"')}
-
- ${box(600,300,300,96,'#14261d','#2f6a48')}
- ${txt(616,326,`${N(d.canaries)} canaries + ${N(d.health)} health skills`,'#8fe3b4',15,'font-weight="700"')}
- ${txt(616,350,'each writes data/latest.json','#bfe6cf',12)}
- ${txt(616,372,'PASS / WARN / FAIL vocabulary','#5f9c78',11,'font-style="italic"')}
- <path d="M520,686 C560,540 560,430 598,372" fill="none" stroke="#4fae7a" stroke-width="2.2" marker-end="url(#arwG)"/>
-
- ${box(600,440,300,80,'#14261d','#3f8a5f')}
- ${txt(616,470,'fleet-health-rollup','#8fe3b4',15,'font-weight="700"')}
- ${txt(616,492,'→ single 7am "is anything on fire?" panel','#bfe6cf',12)}
- <line x1="750" y1="396" x2="750" y2="438" stroke="#4fae7a" stroke-width="2.2" marker-end="url(#arwG)"/>
- ${box(600,560,300,52,'#0f2016','#2f6a48')}
- ${txt(750,591,`MORNING PANEL → Steve [${d.verdict}]`,'#8fe3b4',14,'text-anchor="middle" font-weight="700"')}
- <line x1="750" y1="520" x2="750" y2="558" stroke="#4fae7a" stroke-width="2.2" marker-end="url(#arwG)"/>
-
- ${box(600,680,300,150,'#2e1c1c','#8a4a4a','parked')}
- ${txt(616,708,`${N(d.parked)} PARKED (pending-approval)`,'#f3b6b6',15,'font-weight="700"')}
- ${txt(616,732,'gated: spend · send-list · publish · DNS','#e6c7c7',12)}
- ${txt(616,752,'remote push · canonical dw_unified/Shopify','#e6c7c7',12)}
- ${txt(616,778,`⚠ ${N(d.p24)} > 24h old · ${N(d.p7d)} > 7 days`,'#ff9b6a',12,'font-weight="700"')}
- ${txt(616,800,'cleared by /ungate · approval-agent — click to list','#a87a7a',11,'font-style="italic"')}
- <path d="M280,790 C420,880 520,800 598,760" fill="none" stroke="#d98b3a" stroke-width="2.2" marker-end="url(#arwR)"/>
- ${txt(410,862,'draft gated →','#d98b3a',12,'font-weight="700"')}
+// ── Node/edge model for the draggable Map ────────────────────────────────────
+const VBW=1680, VBH=920;
+function nodes(d){
+ const T=(x,y,t,c,s,extra)=>({x,y,t,c,s,extra:extra||''});
+ return [
+ {id:'steve',x:60,y:20,w:220,h:66,fill:'#243b53',stroke:'#4a6c92',lines:[
+ T(110,30,'STEVE','#eef1f7',20,'text-anchor="middle" font-weight="700"'),
+ T(110,52,'/dtd · /officer-yolo · /yolo','#a9b6cc',12,'text-anchor="middle"')]},
+ {id:'officers',x:40,y:140,w:480,h:116,fill:'#1c2740',stroke:'#3f5a86',drill:{type:'cat',id:'officers'},lines:[
+ T(16,28,N(d.officers)+' CABINET OFFICERS (vp-*) ▸','#9fc4ff',16,'font-weight="700"'),
+ T(16,54,'commerce · marketing · engineering · operations · security · cncp','#c7d2e6',12),
+ T(16,74,'research · directories · compliance · special · consulting · abramsego','#c7d2e6',12),
+ T(16,100,'route + final sign-off on gated work — click to drill','#6f7d97',11,'font-style="italic"')]},
+ {id:'workers',x:40,y:306,w:480,h:120,fill:'#1c2740',stroke:'#3f5a86',drill:{type:'cat',id:'ag_workers'},lines:[
+ T(16,26,'~'+N(d.agents-d.officers)+' WORKER AGENTS ('+N(d.agents)+' total) ▸','#9fc4ff',16,'font-weight="700"'),
+ T(16,50,'vendor stewards · site/build · marketing · directory builders','#c7d2e6',12),
+ T(16,70,'contrarian · approval-agent · fleet-lifecycle · masterdot · merchant','#c7d2e6',12),
+ T(16,94,'own & supervise; recruit subagents (plan→split→argue→merge)','#6f7d97',11,'font-style="italic"')]},
+ {id:'skills',x:40,y:476,w:480,h:214,fill:'#182135',stroke:'#39507a',drill:{type:'cat',id:'sk_all'},lines:[
+ T(16,26,N(d.skills)+' SKILLS (invoked via Skill tool) ▸','#9fc4ff',16,'font-weight="700"'),
+ T(16,148,'+ deploy · pricing · settlement · storefront · research','#c7d2e6',12),
+ T(16,184,N(d.health)+' emit data/latest.json health heartbeat','#6f7d97',11,'font-style="italic"')],
+ subs:[
+ {x:16,y:40,w:200,h:34,fill:'#20304d',label:N(d.canaries)+' canaries / monitors',type:'cat',id:'canaries'},
+ {x:226,y:40,w:238,h:34,fill:'#20304d',label:N(d.scrapers)+' scraper-managers',type:'cat',id:'scrapers'},
+ {x:16,y:82,w:200,h:34,fill:'#20304d',label:N(d.dots)+' terminal dot/color',type:'cat',id:'dots'},
+ {x:226,y:82,w:238,h:34,fill:'#20304d',label:N(d.video)+' hyperframes / video',type:'cat',id:'video'}]},
+ {id:'cron',x:600,y:20,w:300,h:120,fill:'#2a2136',stroke:'#6b4f86',drill:{type:'cat',id:'cronPlists'},lines:[
+ T(16,26,N(d.cronLoaded)+' CRON JOBS (launchd) ▸','#d6b8ff',16,'font-weight="700"'),
+ T(16,50,'com.steve.* — '+N(d.cronPlists)+' plists on disk','#cfc3e0',12),
+ T(16,70,'canaries · drips · refreshes · rollups','#cfc3e0',12),
+ T(16,94,N(d.cronToSkill)+' point directly at a skill dir','#8f7fb0',11,'font-style="italic"')]},
+ {id:'canaries',x:600,y:200,w:300,h:96,fill:'#14261d',stroke:'#2f6a48',drill:{type:'cat',id:'canaries'},lines:[
+ T(16,26,N(d.canaries)+' canaries + '+N(d.health)+' health skills ▸','#8fe3b4',15,'font-weight="700"'),
+ T(16,50,'each writes data/latest.json','#bfe6cf',12),
+ T(16,72,'PASS / WARN / FAIL vocabulary','#5f9c78',11,'font-style="italic"')]},
+ {id:'rollup',x:600,y:336,w:300,h:80,fill:'#14261d',stroke:'#3f8a5f',drill:{type:'skill',id:'fleet-health-rollup'},lines:[
+ T(16,30,'fleet-health-rollup ▸','#8fe3b4',15,'font-weight="700"'),
+ T(16,52,'→ single 7am "is anything on fire?" panel','#bfe6cf',12)]},
+ {id:'morning',x:600,y:456,w:300,h:52,fill:'#0f2016',stroke:'#2f6a48',lines:[
+ T(150,31,'MORNING PANEL → Steve ['+d.verdict+']','#8fe3b4',14,'text-anchor="middle" font-weight="700"')]},
+ {id:'parked',x:600,y:540,w:300,h:150,fill:'#2e1c1c',stroke:'#8a4a4a',drill:{type:'cat',id:'parked'},lines:[
+ T(16,28,N(d.parked)+' PARKED (pending-approval) ▸','#f3b6b6',15,'font-weight="700"'),
+ T(16,52,'gated: spend · send-list · publish · DNS','#e6c7c7',12),
+ T(16,72,'remote push · canonical dw_unified/Shopify','#e6c7c7',12),
+ T(16,98,'⚠ '+N(d.p24)+' > 24h old · '+N(d.p7d)+' > 7 days','#ff9b6a',12,'font-weight="700"'),
+ T(16,120,'cleared by /ungate · approval-agent — click to drill','#a87a7a',11,'font-style="italic"')]},
+ {id:'ledger',x:950,y:540,w:290,h:150,fill:'#14261d',stroke:'#2f6a48',lines:[
+ T(16,28,N(d.ledger)+' REVERSIBLE ACTIONS','#8fe3b4',15,'font-weight="700"'),
+ T(16,52,'auto-executed (3-question tier):','#bfe6cf',12),
+ T(16,72,'reversible + no externality + bounded','#bfe6cf',12),
+ T(16,98,'→ executed-reversible/ledger.jsonl','#bfe6cf',12),
+ T(16,120,'post-hoc /rollback-runbook','#5f9c78',11,'font-style="italic"')]},
+ {id:'terminal',x:950,y:20,w:290,h:150,fill:'#20233a',stroke:'#4a5388',drill:{type:'cat',id:'dots'},lines:[
+ T(16,28,'TERMINAL FLEET (iTerm2) ▸','#b9c0ff',15,'font-weight="700"'),
+ T(16,52,'🟢 working · 🟡 needs direction','#cfd3ee',12),
+ T(16,72,'🟣 gated · 🟠 paste · 🔵 needs-Steve','#cfd3ee',12),
+ T(16,92,'🩷 parked','#cfd3ee',12),
+ T(16,118,'painted by '+N(d.dots)+' dot skills + masterdot','#7f88ad',11,'font-style="italic"')]},
+ {id:'projects',x:950,y:200,w:290,h:150,fill:'#2a2136',stroke:'#6b4f86',drill:{type:'cat',id:'pj_all'},lines:[
+ T(16,28,N(d.projects)+' PROJECTS (~/Projects) ▸','#d6b8ff',15,'font-weight="700"'),
+ T(16,98,'DW · scrapers · websites · directories','#cfc3e0',12),
+ T(16,118,'viewers · media · infra — click to drill','#8f7fb0',11,'font-style="italic"')],
+ subs:[{x:16,y:44,w:130,h:30,fill:'#3a2f52',label:N(d.projectRepos)+' git repos',type:'cat',id:'pj_repos'}]},
+ {id:'legend',x:1270,y:20,w:370,h:560,fill:'#12151f',stroke:'#2a3247',lines:[
+ T(18,30,'Reading the map','#eef1f7',17,'font-weight="700"'),
+ T(18,62,'Drag any node to rearrange — layout saves.','#c7d2e6',13),
+ T(18,84,'Click a bordered node to drill infinitely.','#c7d2e6',13),
+ T(18,120,'1 · Cron → Skills','#d98b3a',13,'font-weight="700"'),
+ T(30,140,N(d.cronLoaded)+' jobs fire skills on a clock.','#c7d2e6',13),
+ T(18,170,'2 · Canaries → fleet-health-rollup','#4fae7a',13,'font-weight="700"'),
+ T(30,190,N(d.health)+' heartbeats fold into one panel.','#c7d2e6',13),
+ T(18,220,'3 · Officers → Agents → Skills','#9fc4ff',13,'font-weight="700"'),
+ T(30,240,'the cabinet hierarchy does the work.','#c7d2e6',13),
+ T(18,270,'4 · Gated → pending-approval','#f3b6b6',13,'font-weight="700"'),
+ T(30,290,'risky pool for Steve; reversible auto-run.','#c7d2e6',13),
+ T(18,332,'Streamline signals','#ffd27a',14,'font-weight="700"'),
+ T(18,358,'• '+N(d.parked)+' parked, '+N(d.p24)+' >24h — backlog','#e6c7c7',13),
+ T(18,380,'• '+N(d.skills)+' skills — heavy sprawl','#e6c7c7',13),
+ T(18,402,'• '+N(d.canaries)+' canaries + '+N(d.cronLoaded)+' cron — dense','#e6c7c7',13),
+ T(18,424,'• '+N(d.agents)+' agents / '+N(d.officers)+' officers — deep','#e6c7c7',13),
+ T(18,470,'Views: Map · Tree · Treemap (top).','#6f7d97',11,'font-style="italic"')]},
+ ];
+}
+const EDGES=[
+ {from:'steve',to:'officers',color:'#7d8aa5',m:'arw',fa:'b',ta:'t'},
+ {from:'officers',to:'workers',color:'#7d8aa5',m:'arw',fa:'b',ta:'t'},
+ {from:'workers',to:'skills',color:'#7d8aa5',m:'arw',fa:'b',ta:'t'},
+ {from:'cron',to:'skills',color:'#d98b3a',w:2.4,m:'arwR',fa:'b',ta:'t',label:'drives'},
+ {from:'skills',to:'canaries',color:'#4fae7a',w:2.2,m:'arwG',fa:'r',ta:'l'},
+ {from:'canaries',to:'rollup',color:'#4fae7a',w:2.2,m:'arwG',fa:'b',ta:'t'},
+ {from:'rollup',to:'morning',color:'#4fae7a',w:2.2,m:'arwG',fa:'b',ta:'t'},
+ {from:'skills',to:'parked',color:'#d98b3a',w:2.2,m:'arwR',fa:'r',ta:'l'},
+ {from:'parked',to:'ledger',color:'#3a4761',w:1.6,m:'arw',dash:1,fa:'r',ta:'l'},
+ {from:'workers',to:'projects',color:'#3a4761',w:1.4,m:'arw',dash:1,fa:'r',ta:'l'},
+ {from:'skills',to:'terminal',color:'#3a4761',w:1.4,m:'arw',dash:1,fa:'r',ta:'t'},
+];
+let NODEMAP={};
+function defXY(id){const n=NODEMAP[id];return n?{x:n.x,y:n.y}:{x:0,y:0};}
+function curXY(id){const n=NODEMAP[id];if(!n)return{x:0,y:0,w:0,h:0};const p=POS[id]||{x:n.x,y:n.y};return{x:p.x,y:p.y,w:n.w,h:n.h};}
+function anchor(id,side){const b=curXY(id);switch(side){case 'b':return[b.x+b.w/2,b.y+b.h];case 't':return[b.x+b.w/2,b.y];case 'l':return[b.x,b.y+b.h/2];case 'r':return[b.x+b.w,b.y+b.h/2];default:return[b.x+b.w/2,b.y+b.h/2];}}
+function drawEdges(){
+ const layer=document.getElementById('edges');if(!layer)return;
+ layer.innerHTML=EDGES.map(e=>{const a=anchor(e.from,e.fa||'b'),b=anchor(e.to,e.ta||'t');
+ return '<line x1="'+a[0]+'" y1="'+a[1]+'" x2="'+b[0]+'" y2="'+b[1]+'" stroke="'+(e.color||'#7d8aa5')+'" stroke-width="'+(e.w||2)+'"'+(e.dash?' stroke-dasharray="5 4"':'')+' marker-end="url(#'+(e.m||'arw')+')"/>';
+ }).join('');
+}
+function renderMap(d){
+ const ns=nodes(d);NODEMAP={};ns.forEach(n=>NODEMAP[n.id]=n);
+ const grp=n=>{const xy=POS[n.id]||{x:n.x,y:n.y};
+ const txt=(n.lines||[]).map(l=>'<text x="'+l.x+'" y="'+l.y+'" fill="'+l.c+'" font-size="'+l.s+'" '+l.extra+'>'+l.t+'</text>').join('');
+ const subs=(n.subs||[]).map(sb=>'<g class="sub" data-dtype="'+sb.type+'" data-did="'+encodeURIComponent(sb.id)+'"><rect x="'+sb.x+'" y="'+sb.y+'" width="'+sb.w+'" height="'+sb.h+'" rx="6" fill="'+sb.fill+'"/><text x="'+(sb.x+10)+'" y="'+(sb.y+21)+'" fill="#c7d2e6" font-size="12">'+sb.label+'</text></g>').join('');
+ const dd=n.drill?(' data-dtype="'+n.drill.type+'" data-did="'+encodeURIComponent(n.drill.id)+'"'):'';
+ return '<g class="dnode" data-id="'+n.id+'"'+dd+' transform="translate('+xy.x+','+xy.y+')"><rect class="node" x="0" y="0" width="'+n.w+'" height="'+n.h+'" rx="10" fill="'+n.fill+'" stroke="'+n.stroke+'"/>'+txt+subs+'</g>';
+ };
+ const svg='<svg id="mapsvg" width="'+VBW+'" height="'+VBH+'" viewBox="0 0 '+VBW+' '+VBH+'" xmlns="http://www.w3.org/2000/svg">'
+ +'<defs>'
+ +'<marker id="arw" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#7d8aa5"/></marker>'
+ +'<marker id="arwG" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#4fae7a"/></marker>'
+ +'<marker id="arwR" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#d98b3a"/></marker>'
+ +'</defs><rect width="'+VBW+'" height="'+VBH+'" fill="#0f1117"/><g id="edges"></g>'
+ +ns.map(grp).join('')+'</svg>';
+ document.getElementById('svgbox').innerHTML=svg;
+ drawEdges();
+ wireMap();
+}
+function wireMap(){
+ const svg=document.getElementById('mapsvg');if(!svg)return;
+ let cur=null,sx=0,sy=0,ox=0,oy=0,moved=false,scale=1;
+ svg.querySelectorAll('g.dnode').forEach(g=>{
+ g.addEventListener('pointerdown',e=>{
+ if(e.target.closest('g.sub'))return; // sub handles its own click
+ cur=g;moved=false;
+ const xy=POS[g.dataset.id]||defXY(g.dataset.id);ox=xy.x;oy=xy.y;sx=e.clientX;sy=e.clientY;
+ const r=svg.getBoundingClientRect();scale=VBW/(r.width||VBW);
+ try{g.setPointerCapture(e.pointerId);}catch{}
+ e.preventDefault();
+ });
+ g.addEventListener('pointermove',e=>{
+ if(cur!==g)return;
+ const dx=(e.clientX-sx)*scale,dy=(e.clientY-sy)*scale;
+ if(Math.abs(e.clientX-sx)+Math.abs(e.clientY-sy)>3)moved=true;
+ const nx=Math.round(ox+dx),ny=Math.round(oy+dy);
+ POS[g.dataset.id]={x:nx,y:ny};
+ g.setAttribute('transform','translate('+nx+','+ny+')');
+ drawEdges();
+ });
+ g.addEventListener('pointerup',e=>{
+ if(cur!==g)return;cur=null;
+ if(moved){savePos(POS);}
+ else if(g.dataset.dtype){startDrill(g.dataset.dtype,decodeURIComponent(g.dataset.did),decodeURIComponent(g.dataset.did));}
+ });
+ });
+ svg.querySelectorAll('g.sub').forEach(s=>{
+ s.addEventListener('pointerdown',e=>e.stopPropagation());
+ s.addEventListener('click',e=>{e.stopPropagation();startDrill(s.dataset.dtype,decodeURIComponent(s.dataset.did),decodeURIComponent(s.dataset.did));});
+ });
+}
- ${box(950,680,290,150,'#14261d','#2f6a48')}
- ${txt(966,708,`${N(d.ledger)} REVERSIBLE ACTIONS`,'#8fe3b4',15,'font-weight="700"')}
- ${txt(966,732,'auto-executed (3-question tier):','#bfe6cf',12)}
- ${txt(966,752,'reversible + no externality + bounded','#bfe6cf',12)}
- ${txt(966,778,'→ executed-reversible/ledger.jsonl','#bfe6cf',12)}
- ${txt(966,800,'post-hoc /rollback-runbook','#5f9c78',11,'font-style="italic"')}
- <line x1="900" y1="770" x2="948" y2="760" stroke="#4fae7a" stroke-width="2" marker-end="url(#arwG)"/>
+// ── Recursive drill drawer ───────────────────────────────────────────────────
+let CRUMBS=[];
+const drillCache=new Map();
+async function fetchDrill(type,id){
+ const k=type+'|'+id;if(drillCache.has(k))return drillCache.get(k);
+ try{const r=await fetch('/api/drill?type='+encodeURIComponent(type)+'&id='+encodeURIComponent(id));const j=await r.json();drillCache.set(k,j);return j;}
+ catch(e){return {title:type+' · '+id,children:[],error:String(e)};}
+}
+function startDrill(type,id,label){CRUMBS=[];openDrill(type,id,label);}
+function openDrill(type,id,label){CRUMBS.push({type,id,label:label||id});renderDrill();}
+async function renderDrill(){
+ const c=CRUMBS[CRUMBS.length-1];
+ const dw=document.getElementById('drawer');dw.classList.add('open');
+ document.getElementById('dtitle').textContent='…';document.getElementById('dlist').innerHTML='';document.getElementById('dcnt').textContent='';
+ renderCrumbs();
+ const j=await fetchDrill(c.type,c.id);
+ if(CRUMBS[CRUMBS.length-1]!==c)return; // superseded by a faster click
+ document.getElementById('dtitle').textContent=j.title||c.label;
+ document.getElementById('dcnt').textContent=((j.children||[]).length)+' items'+(j.error?(' · error: '+j.error):'');
+ const html=(j.children||[]).map((ch,i)=>'<li data-type="'+ch.type+'" data-id="'+encodeURIComponent(ch.id)+'"><span class="li-l">'+(ch.label||ch.id)+'</span>'+(ch.meta?'<span class="li-m" title="'+String(ch.meta).replace(/"/g,'"')+'">'+ch.meta+'</span>':'')+'<span class="li-t">'+ch.type+'</span></li>').join('');
+ document.getElementById('dlist').innerHTML=html||'<li>(no deeper detail)</li>';
+ document.querySelectorAll('#dlist li[data-type]').forEach(li=>li.addEventListener('click',()=>openDrill(li.dataset.type,decodeURIComponent(li.dataset.id),li.querySelector('.li-l').textContent)));
+}
+function renderCrumbs(){
+ document.getElementById('crumbs').innerHTML=CRUMBS.map((c,i)=>'<a data-i="'+i+'">'+c.label+'</a>').join('<span class="sep">›</span>');
+ document.querySelectorAll('#crumbs a').forEach(a=>a.addEventListener('click',()=>{CRUMBS=CRUMBS.slice(0,+a.dataset.i+1);renderDrill();}));
+}
+document.getElementById('close').addEventListener('click',()=>{document.getElementById('drawer').classList.remove('open');CRUMBS=[];});
- ${box(950,120,290,150,'#20233a','#4a5388')}
- ${txt(966,148,'TERMINAL FLEET (iTerm2)','#b9c0ff',15,'font-weight="700"')}
- ${txt(966,172,'🟢 working · 🟡 needs direction','#cfd3ee',12)}
- ${txt(966,192,'🟣 gated · 🟠 paste · 🔵 needs-Steve','#cfd3ee',12)}
- ${txt(966,212,'🩷 parked','#cfd3ee',12)}
- ${txt(966,238,`painted by ${N(d.dots)} dot skills + masterdot + router`,'#7f88ad',11,'font-style="italic"')}
- <path d="M300,585 C620,560 820,360 948,240" fill="none" stroke="#3a4761" stroke-width="1.6" stroke-dasharray="5 4" marker-end="url(#arw)"/>
+// ── Tree view (recursive collapsible, same /api/drill) ───────────────────────
+function treeRoots(d){return [
+ {type:'cat',id:'sk_all',label:'SKILLS ('+N(d.skills)+')'},
+ {type:'cat',id:'ag_all',label:'AGENTS ('+N(d.agents)+')'},
+ {type:'cat',id:'officers',label:'OFFICERS ('+N(d.officers)+')'},
+ {type:'cat',id:'pj_all',label:'PROJECTS ('+N(d.projects)+')'},
+ {type:'cat',id:'cronPlists',label:'CRON PLISTS ('+N(d.cronPlists)+')'},
+ {type:'cat',id:'canaries',label:'CANARIES ('+N(d.canaries)+')'},
+ {type:'cat',id:'parked',label:'PARKED ('+N(d.parked)+')'},
+];}
+function treeRowHTML(c){
+ return '<div class="tnode" data-type="'+c.type+'" data-id="'+encodeURIComponent(c.id)+'">'
+ +'<span class="tlabel"><span class="tw">▸</span><span class="tn">'+(c.label||c.id)+'</span><span class="tt">'+c.type+'</span>'+(c.meta?'<span class="tm">'+c.meta+'</span>':'')+'</span>'
+ +'<div class="kids hide"></div></div>';
+}
+function renderTree(d){
+ const box=document.getElementById('treebox');
+ box.innerHTML='<div class="fbar">Click any node to expand its children inline — recurses arbitrarily deep via /api/drill.</div>'+treeRoots(d).map(treeRowHTML).join('');
+ box.querySelectorAll(':scope > .tnode').forEach(wireTreeNode);
+}
+function wireTreeNode(node){
+ const label=node.querySelector(':scope > .tlabel');
+ label.addEventListener('click',async()=>{
+ const kids=node.querySelector(':scope > .kids');const arrow=label.querySelector('.tw');
+ if(node.dataset.loaded){const h=kids.classList.toggle('hide');arrow.textContent=h?'▸':'▾';return;}
+ arrow.textContent='⋯';
+ const j=await fetchDrill(node.dataset.type,decodeURIComponent(node.dataset.id));
+ kids.innerHTML=(j.children||[]).map(treeRowHTML).join('')||'<div class="tleaf">(no deeper detail)</div>';
+ node.dataset.loaded='1';kids.classList.remove('hide');arrow.textContent='▾';
+ kids.querySelectorAll(':scope > .tnode').forEach(wireTreeNode);
+ });
+}
- ${box(950,300,290,150,'#2a2136','#6b4f86','pj_all')}
- ${txt(966,328,`${N(d.projects)} PROJECTS (~/Projects) ▸`,'#d6b8ff',15,'font-weight="700"')}
- ${box(966,344,120,30,'#3a2f52','#3a2f52','pj_repos')}${txt(976,364,`${N(d.projectRepos)} git repos`,'#cfc3e0',12)}
- ${txt(966,398,'DW · scrapers · websites · directories','#cfc3e0',12)}
- ${txt(966,418,'viewers · media · infra — click to drill','#8f7fb0',11,'font-style="italic"')}
- ${txt(966,440,'each build is a git repo (standing rule)','#8f7fb0',11,'font-style="italic"')}
- <line x1="520" y1="466" x2="948" y2="400" stroke="#3a4761" stroke-width="1.4" stroke-dasharray="5 4" marker-end="url(#arw)"/>
+// ── Treemap view (d3, area = count; leaf drillable) ──────────────────────────
+let d3ready=false;
+function loadD3(){return new Promise((res,rej)=>{if(window.d3){d3ready=true;return res();}const s=document.createElement('script');s.src='https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js';s.onload=()=>{d3ready=true;res();};s.onerror=rej;document.head.appendChild(s);});}
+async function renderTreemap(d){
+ const box=document.getElementById('treemapbox');
+ box.innerHTML='<div class="fbar">Loading d3 (CDN)…</div>';
+ try{await loadD3();}catch{box.innerHTML='<div class="fbar">Could not load d3 from CDN — use Map or Tree.</div>';return;}
+ const folders=(d.folders||[]);
+ const groups={};folders.forEach(f=>{(groups[f.group]=groups[f.group]||[]).push(f);});
+ const gcolor={SKILLS:'#39507a',AGENTS:'#3f5a86',PROJECTS:'#6b4f86'};
+ const data={name:'stack',children:Object.keys(groups).map(g=>({name:g,color:gcolor[g]||'#39507a',children:groups[g].map(f=>({name:f.label,value:Math.max(1,f.n),kind:f.kind,count:f.n}))}))};
+ const W=Math.max(900,(box.clientWidth||1200)-16),H=760;
+ const root=d3.hierarchy(data).sum(x=>x.value).sort((a,b)=>b.value-a.value);
+ d3.treemap().size([W,H]).paddingInner(2).paddingTop(20).paddingOuter(3)(root);
+ box.innerHTML='';
+ const svg=d3.select(box).append('svg').attr('width',W).attr('height',H);
+ const gnode=svg.selectAll('g.gg').data(root.descendants().filter(x=>x.depth===1)).enter().append('g');
+ gnode.append('text').attr('x',x=>x.x0+4).attr('y',x=>x.y0+14).attr('fill','#9fc4ff').attr('font-size',13).attr('font-weight',700).text(x=>x.data.name);
+ const leaves=svg.selectAll('g.leaf').data(root.leaves()).enter().append('g').attr('class','leaf');
+ leaves.append('rect').attr('x',x=>x.x0).attr('y',x=>x.y0).attr('width',x=>Math.max(0,x.x1-x.x0)).attr('height',x=>Math.max(0,x.y1-x.y0))
+ .attr('fill',x=>x.parent.data.color).attr('opacity',.82)
+ .on('click',(ev,x)=>startDrill('cat',x.data.kind,x.data.name));
+ leaves.append('text').attr('x',x=>x.x0+5).attr('y',x=>x.y0+16).attr('fill','#eef1f7').attr('font-size',11)
+ .text(x=>((x.x1-x.x0)>70&&(x.y1-x.y0)>20)?x.data.name:'');
+ leaves.append('text').attr('x',x=>x.x0+5).attr('y',x=>x.y0+30).attr('fill','#c7d2e6').attr('font-size',10)
+ .text(x=>((x.x1-x.x0)>40&&(x.y1-x.y0)>34)?x.data.count:'');
+}
- ${box(1270,120,370,700,'#12151f','#2a3247')}
- ${txt(1288,150,'Reading the map','#eef1f7',17,'font-weight="700"')}
- ${txt(1288,184,'Four control planes tie it together:','#c7d2e6',13)}
- ${txt(1288,214,'1 · Cron → Skills','#d98b3a',13,'font-weight="700"')}${txt(1300,234,'365 jobs fire skills on a clock.','#c7d2e6',13)}
- ${txt(1288,264,'2 · Canaries → fleet-health-rollup','#4fae7a',13,'font-weight="700"')}${txt(1300,284,`${N(d.health)} heartbeats fold into one panel.`,'#c7d2e6',13)}
- ${txt(1288,314,'3 · Officers → Agents → Skills','#9fc4ff',13,'font-weight="700"')}${txt(1300,334,'the cabinet hierarchy does the work.','#c7d2e6',13)}
- ${txt(1288,364,'4 · Gated → pending-approval','#f3b6b6',13,'font-weight="700"')}${txt(1300,384,'risky pool for Steve; reversible auto-run.','#c7d2e6',13)}
- <line x1="1288" y1="410" x2="1620" y2="410" stroke="#2a3247"/>
- ${txt(1288,438,'Streamline signals','#ffd27a',14,'font-weight="700"')}
- ${txt(1288,464,`• ${N(d.parked)} parked, ${N(d.p24)} >24h — backlog`,'#e6c7c7',13)}
- ${txt(1288,486,`• ${N(d.skills)} skills — heavy sprawl`,'#e6c7c7',13)}
- ${txt(1288,508,`• ${N(d.canaries)} canaries + ${N(d.cronLoaded)} cron — dense`,'#e6c7c7',13)}
- ${txt(1288,530,'• ~130 vendor aliases — collapsible','#e6c7c7',13)}
- ${txt(1288,552,`• ${N(d.agents)} agents / ${N(d.officers)} officers — deep`,'#e6c7c7',13)}
- ${txt(1288,596,'Click any bordered node to drill in.','#6f7d97',11,'font-style="italic"')}
- ${txt(1288,614,`Live scan · updated ${new Date(d.ts).toLocaleTimeString()}`,'#6f7d97',11,'font-style="italic"')}
- </svg>`;
- document.getElementById('svgbox').innerHTML=s;
- document.getElementById('sub').textContent=`${N(d.skills)} skills · ${N(d.agents)} agents · ${N(d.projects)} projects · ${N(d.cronLoaded)} cron · ${N(d.parked)} parked`;
- renderFolders(d.folders||[]);
- const vb=document.getElementById('verdict'); vb.textContent='fleet: '+d.verdict; vb.className='badge '+d.verdict;
- document.getElementById('foot').textContent=`Live from ~/.claude · ~/Library/LaunchAgents · yolo-queue — auto-refresh 30s — last scan ${new Date(d.ts).toLocaleTimeString()}`;
- document.querySelectorAll('rect.node[data-kind]').forEach(r=>r.addEventListener('click',()=>drill(r.dataset.kind)));
+// ── View switching ───────────────────────────────────────────────────────────
+function setView(v){
+ VIEW=v;lsSet('smv_view',v);
+ document.querySelectorAll('.vbtn').forEach(b=>b.classList.toggle('on',b.dataset.view===v));
+ document.getElementById('svgbox').style.display=v==='map'?'block':'none';
+ document.getElementById('treebox').style.display=v==='tree'?'block':'none';
+ document.getElementById('treemapbox').style.display=v==='treemap'?'block':'none';
+ document.getElementById('resetLayout').style.display=v==='map'?'inline-block':'none';
+ const hint={map:'drag nodes to rearrange · click to drill',tree:'click any row to expand deeper',treemap:'rectangle area = member count · click a leaf to drill'};
+ document.getElementById('viewhint').textContent=hint[v]||'';
+ if(!DATA)return;
+ if(v==='map')renderMap(DATA);
+ else if(v==='tree')renderTree(DATA);
+ else if(v==='treemap')renderTreemap(DATA);
}
+document.querySelectorAll('.vbtn').forEach(b=>b.addEventListener('click',()=>setView(b.dataset.view)));
+document.getElementById('resetLayout').addEventListener('click',()=>{POS={};savePos(POS);if(DATA)renderMap(DATA);});
+
+// ── Folders (chips) — shared across views ────────────────────────────────────
function renderFolders(folders){
const el=document.getElementById('folders');
- if(!folders.length){el.innerHTML='';return;}
- const groups={}; folders.forEach(f=>{(groups[f.group]=groups[f.group]||[]).push(f)});
+ if(!folders||!folders.length){el.innerHTML='';return;}
+ const groups={};folders.forEach(f=>{(groups[f.group]=groups[f.group]||[]).push(f);});
const order=['SKILLS','AGENTS','PROJECTS'];
const gcolor={SKILLS:'#39507a',AGENTS:'#3f5a86',PROJECTS:'#6b4f86'};
- let html='<div class="fbar">Folders — click any to drill into its members (live-scanned)</div>';
- order.forEach(g=>{ const arr=groups[g]; if(!arr)return;
+ let html='<div class="fbar">Folders — click any to drill into its members (live-scanned, recurses infinitely)</div>';
+ order.forEach(g=>{const arr=groups[g];if(!arr)return;
const tot=arr.reduce((a,f)=>a+f.n,0);
- html+=`<div class="fgroup"><div class="ghead">${g}<span class="gn">${arr.length} folders · ${N(tot)} members</span></div><div class="chips">`+
- arr.map(f=>`<button class="chip" style="border-color:${gcolor[g]||'#39507a'}" onclick="drill('${f.kind}')" title="${f.label}"><span class="cl">${f.label}</span><span class="cn">${N(f.n)}</span></button>`).join('')+
- `</div></div>`;
+ html+='<div class="fgroup"><div class="ghead">'+g+'<span class="gn">'+arr.length+' folders · '+N(tot)+' members</span></div><div class="chips">'+
+ arr.map(f=>'<button class="chip" style="border-color:'+(gcolor[g]||'#39507a')+'" onclick="startDrill(\'cat\',\''+f.kind+'\',\''+f.label.replace(/'/g,"\\'")+'\')" title="'+f.label+'"><span class="cl">'+f.label+'</span><span class="cn">'+N(f.n)+'</span></button>').join('')+
+ '</div></div>';
});
el.innerHTML=html;
}
-async function drill(kind){
- const r=await fetch('/api/list?kind='+kind); const j=await r.json();
- document.getElementById('dtitle').textContent=kind;
- document.getElementById('dcnt').textContent=j.items.length+' items';
- document.getElementById('dlist').innerHTML=j.items.map(i=>`<li>${i}</li>`).join('')||'<li>(none)</li>';
- document.getElementById('drawer').classList.add('open');
+
+// ── Data / refresh ───────────────────────────────────────────────────────────
+function applyData(d){
+ DATA=d;
+ document.getElementById('sub').textContent=N(d.skills)+' skills · '+N(d.agents)+' agents · '+N(d.projects)+' projects · '+N(d.cronLoaded)+' cron · '+N(d.parked)+' parked';
+ const vb=document.getElementById('verdict');vb.textContent='fleet: '+d.verdict;vb.className='badge '+d.verdict;
+ document.getElementById('foot').textContent='Live from ~/.claude · ~/Library/LaunchAgents · yolo-queue — auto-refresh 30s — last scan '+new Date(d.ts).toLocaleTimeString();
+ renderFolders(d.folders||[]);
+ setView(VIEW);
}
-async function refresh(){ try{const r=await fetch('/api/stats'); render(await r.json());}catch(e){document.getElementById('sub').textContent='scan error';} }
-refresh(); setInterval(refresh,30000);
+async function refresh(){try{const r=await fetch('/api/stats');applyData(await r.json());}catch(e){document.getElementById('sub').textContent='scan error';}}
+refresh();setInterval(refresh,30000);
</script></body></html>
diff --git a/server.js b/server.js
index 4ecfd5f..02a0457 100644
--- a/server.js
+++ b/server.js
@@ -104,6 +104,7 @@ function scan() {
pj_all: projectDirs,
pj_repos: projectRepos.slice().sort(),
parked: parked.slice().sort(),
+ cronPlists: cronPlists.slice().sort(),
};
const folders = [];
const addCats = (group, names, cats) => {
@@ -132,6 +133,198 @@ function scan() {
return CACHE;
}
+// ── Recursive drill graph ────────────────────────────────────────────────────
+// Generic typed-children endpoint: /api/drill?type=<t>&id=<id>
+// Every function returns { title, breadcrumb, children:[{type,id,label,meta}] }.
+// Children whose type has a handler are drillable; leaf types (kv/file-leaf) just
+// return empty children → the UI shows "(no deeper detail)" gracefully.
+const CABINET = path.join(PROJECTS, 'agent-cabinet/cabinet.yaml');
+const readText = (p) => { try { return fs.readFileSync(p, 'utf8'); } catch { return ''; } };
+const existsP = (p) => { try { return fs.existsSync(p); } catch { return false; } };
+const isDirP = (p) => { try { return fs.statSync(p).isDirectory(); } catch { return false; } };
+const mtimeP = (p) => { try { return fs.statSync(p).mtimeMs; } catch { return 0; } };
+const sizeP = (p) => { try { return fs.statSync(p).size; } catch { return 0; } };
+const fmtSize = (n) => n < 1024 ? n + 'B' : n < 1048576 ? (n / 1024).toFixed(1) + 'K' : (n / 1048576).toFixed(1) + 'M';
+const shq = (s) => "'" + String(s).replace(/'/g, "'\\''") + "'"; // single-quote for shell
+const lines = (s) => s.split('\n').map(x => x.trim()).filter(Boolean);
+const base = (p) => path.basename(p);
+
+const DRILL = new Map();
+function dcache(key, fn) {
+ const hit = DRILL.get(key);
+ if (hit && Date.now() - hit.ts < 15000) return hit.v;
+ const v = fn();
+ DRILL.set(key, { ts: Date.now(), v });
+ return v;
+}
+
+function heartbeatKids(dir) {
+ const hb = path.join(dir, 'data/latest.json');
+ if (!existsP(hb)) return [];
+ const out = [{ type: 'kv', id: hb, label: '♥ heartbeat: data/latest.json', meta: new Date(mtimeP(hb)).toLocaleString() }];
+ try {
+ const j = JSON.parse(readText(hb));
+ for (const k of Object.keys(j)) {
+ const v = j[k];
+ if (v === null || typeof v === 'object') continue;
+ out.push({ type: 'kv', id: dir + '#' + k, label: k, meta: String(v) });
+ }
+ } catch { out.push({ type: 'kv', id: hb + '#err', label: '(unparseable latest.json)', meta: '' }); }
+ return out;
+}
+
+function catChildType(kind) {
+ if (kind === 'canaries' || kind === 'sk_canary') return 'canary';
+ if (kind === 'officers' || kind === 'ag_officers') return 'officer';
+ if (kind === 'cronPlists') return 'plist';
+ if (kind === 'parked') return 'parked';
+ if (kind.startsWith('ag_') || kind === 'workers' || kind === 'ag_workers') return 'agent';
+ if (kind.startsWith('pj_') || kind === 'projectRepos') return 'project';
+ return 'skill'; // sk_*, scrapers, dots, video, sk_all + default
+}
+
+const DRILLERS = {
+ cat(id) {
+ const s = scan();
+ const items = (s._lists[id] || []).slice().sort();
+ const ct = catChildType(id);
+ return {
+ title: 'folder · ' + id + ' (' + items.length + ')',
+ breadcrumb: id,
+ children: items.map(it => ({ type: ct, id: it, label: it })),
+ };
+ },
+ skill(id) {
+ const dir = path.join(SKILLS, id);
+ if (!existsP(dir)) return { title: 'skill · ' + id, breadcrumb: id, children: [{ type: 'kv', id, label: '(no such skill dir)', meta: '' }] };
+ const children = [{ type: 'refby', id, label: '⇦ referenced-by (plists · agents · skills)', meta: 'who names this skill' }];
+ children.push(...heartbeatKids(dir));
+ let entries = [];
+ try { entries = fs.readdirSync(dir).filter(f => !f.startsWith('.')).sort(); } catch {}
+ for (const e of entries) {
+ const abs = path.join(dir, e);
+ children.push({ type: 'file', id: abs, label: e + (isDirP(abs) ? '/' : ''), meta: isDirP(abs) ? 'dir' : fmtSize(sizeP(abs)) });
+ }
+ return { title: 'skill · ' + id, breadcrumb: id, children };
+ },
+ refby(id) {
+ const children = [];
+ const seen = new Set();
+ const gp = sh(`grep -lF ${shq(id)} ${LA}/com.steve.*.plist 2>/dev/null`);
+ for (const f of lines(gp)) { const b = base(f); if (!seen.has('p' + b)) { seen.add('p' + b); children.push({ type: 'plist', id: b, label: b, meta: 'launchd' }); } }
+ const ga = sh(`grep -lF ${shq(id)} ${AGENTS}/*.md 2>/dev/null`);
+ for (const f of lines(ga)) { const b = base(f).replace(/\.md$/, ''); if (!seen.has('a' + b)) { seen.add('a' + b); children.push({ type: 'agent', id: b, label: b, meta: 'agent' }); } }
+ const gs = sh(`grep -rlF ${shq(id)} ${SKILLS}/*/SKILL.md 2>/dev/null`);
+ for (const f of lines(gs)) { const nm = base(path.dirname(f)); if (nm !== id && !seen.has('s' + nm)) { seen.add('s' + nm); children.push({ type: 'skill', id: nm, label: nm, meta: 'skill' }); } }
+ return { title: 'referenced-by · ' + id + ' (' + children.length + ')', breadcrumb: 'refby/' + id, children };
+ },
+ file(id) {
+ if (isDirP(id)) {
+ let entries = [];
+ try { entries = fs.readdirSync(id).filter(f => !f.startsWith('.')).sort(); } catch {}
+ return {
+ title: 'dir · ' + base(id), breadcrumb: id,
+ children: entries.map(e => { const abs = path.join(id, e); return { type: 'file', id: abs, label: e + (isDirP(abs) ? '/' : ''), meta: isDirP(abs) ? 'dir' : fmtSize(sizeP(abs)) }; }),
+ };
+ }
+ const txt = readText(id);
+ const nl = txt ? txt.split('\n').length : 0;
+ const children = [{ type: 'kv', id: id + '#size', label: 'size', meta: fmtSize(sizeP(id)) }, { type: 'kv', id: id + '#lines', label: 'lines', meta: String(nl) }, { type: 'kv', id: id + '#mtime', label: 'modified', meta: new Date(mtimeP(id)).toLocaleString() }];
+ txt.split('\n').slice(0, 12).forEach((ln, i) => { if (ln.trim()) children.push({ type: 'kv', id: id + '#L' + i, label: String(i + 1).padStart(3), meta: ln.slice(0, 120) }); });
+ return { title: 'file · ' + base(id), breadcrumb: id, children };
+ },
+ plist(id) {
+ if (!/\.plist$/.test(id)) id += '.plist';
+ const p = path.join(LA, id);
+ const xml = readText(p);
+ const children = [];
+ const m = xml.match(/\/skills\/([^\/<"' ]+)/);
+ if (m) children.push({ type: 'skill', id: m[1], label: 'skill: ' + m[1], meta: 'invokes' });
+ const scripts = [...xml.matchAll(/<string>([^<]*\.(?:sh|mjs|js|py))<\/string>/g)].map(x => x[1]);
+ for (const s of scripts) children.push({ type: 'file', id: s, label: '▶ ' + base(s), meta: s });
+ const iv = xml.match(/<key>StartInterval<\/key>\s*<integer>(\d+)/);
+ if (iv) children.push({ type: 'kv', id: id + '#iv', label: 'StartInterval', meta: iv[1] + 's' });
+ if (/StartCalendarInterval/.test(xml)) {
+ const hrs = [...xml.matchAll(/<key>Hour<\/key>\s*<integer>(\d+)/g)].map(x => x[1]).join(', ');
+ children.push({ type: 'kv', id: id + '#cal', label: 'StartCalendarInterval', meta: hrs ? 'hours ' + hrs : 'calendar' });
+ }
+ const label = id.replace(/\.plist$/, '');
+ const ll = sh(`launchctl list 2>/dev/null | grep -F ${shq(label)}`).trim();
+ if (ll) { const parts = ll.split(/\s+/); children.push({ type: 'kv', id: id + '#pid', label: 'launchctl pid', meta: parts[0] }); children.push({ type: 'kv', id: id + '#exit', label: 'last exit status', meta: parts[1] }); }
+ else children.push({ type: 'kv', id: id + '#unloaded', label: 'launchctl', meta: 'not loaded' });
+ return { title: 'plist · ' + label, breadcrumb: 'plist/' + label, children };
+ },
+ officer(id) {
+ const children = [];
+ const seen = new Set();
+ // parse cabinet.yaml block for this vp
+ const y = readText(CABINET).split('\n');
+ let inBlock = false;
+ for (const raw of y) {
+ const t = raw.trim();
+ const vpM = raw.match(/^\s*-\s*vp:\s*(\S+)/);
+ if (vpM) { inBlock = vpM[1] === id; continue; }
+ if (!inBlock) continue;
+ const sub = t.match(/^-?\s*subagent:\s*(\S+)/);
+ const sk = t.match(/^-?\s*skill:\s*(\S+)/);
+ if (sub && !seen.has('a' + sub[1])) { seen.add('a' + sub[1]); children.push({ type: 'agent', id: sub[1], label: sub[1], meta: 'subagent' }); }
+ if (sk && !seen.has('s' + sk[1])) { seen.add('s' + sk[1]); children.push({ type: 'skill', id: sk[1], label: sk[1], meta: 'skill' }); }
+ }
+ // officer .md "Delegates to" line
+ const md = readText(path.join(AGENTS, id + '.md'));
+ const del = md.match(/Delegates to ([^.]+)\./i);
+ if (del) for (const nm of del[1].split(/,|and/).map(x => x.trim().replace(/\s+subagents?$/, '')).filter(Boolean)) {
+ if (existsP(path.join(AGENTS, nm + '.md')) && !seen.has('a' + nm)) { seen.add('a' + nm); children.push({ type: 'agent', id: nm, label: nm, meta: 'delegate' }); }
+ }
+ return { title: 'officer · ' + id + ' (' + children.length + ')', breadcrumb: 'officer/' + id, children };
+ },
+ agent(id) {
+ const md = readText(path.join(AGENTS, id + '.md'));
+ if (!md) return { title: 'agent · ' + id, breadcrumb: id, children: [{ type: 'kv', id, label: '(no such agent .md)', meta: '' }] };
+ const children = [];
+ const tools = md.match(/^tools:\s*(.+)$/m);
+ if (tools) children.push({ type: 'kv', id: id + '#tools', label: 'tools', meta: tools[1].trim() });
+ const names = new Set([...md.matchAll(/skills\/([a-z0-9][a-z0-9-]+)/gi)].map(x => x[1]));
+ for (const nm of names) if (existsP(path.join(SKILLS, nm))) children.push({ type: 'skill', id: nm, label: 'skill: ' + nm, meta: 'invokes' });
+ if (id.startsWith('vp-')) { const off = DRILLERS.officer(id); for (const c of off.children) children.push(c); }
+ return { title: 'agent · ' + id + ' (' + children.length + ')', breadcrumb: 'agent/' + id, children };
+ },
+ canary(id) {
+ const dir = path.join(SKILLS, id);
+ const children = heartbeatKids(dir);
+ const gp = sh(`grep -lF ${shq(id)} ${LA}/com.steve.*.plist 2>/dev/null`);
+ for (const f of lines(gp)) children.push({ type: 'plist', id: base(f), label: '⏰ ' + base(f), meta: 'cron driver' });
+ if (!children.length) children.push({ type: 'kv', id, label: '(no heartbeat / no cron found)', meta: '' });
+ return { title: 'canary · ' + id, breadcrumb: 'canary/' + id, children };
+ },
+ project(id) {
+ const dir = path.join(PROJECTS, id);
+ const children = [];
+ const isRepo = existsP(path.join(dir, '.git'));
+ children.push({ type: 'kv', id: id + '#git', label: 'git repo', meta: isRepo ? 'yes' : 'no' });
+ if (isRepo) { const st = sh(`cd ${shq(dir)} && git log -1 --format='%h %s' 2>/dev/null`).trim(); if (st) children.push({ type: 'kv', id: id + '#head', label: 'HEAD', meta: st }); }
+ let entries = [];
+ try { entries = fs.readdirSync(dir).filter(f => !f.startsWith('.') && f !== 'node_modules').sort().slice(0, 60); } catch {}
+ for (const e of entries) { const abs = path.join(dir, e); children.push({ type: 'file', id: abs, label: e + (isDirP(abs) ? '/' : ''), meta: isDirP(abs) ? 'dir' : fmtSize(sizeP(abs)) }); }
+ return { title: 'project · ' + id, breadcrumb: 'project/' + id, children };
+ },
+ parked(id) {
+ const p = path.join(PENDING, id);
+ const txt = readText(p);
+ const children = [{ type: 'kv', id: id + '#age', label: 'modified', meta: new Date(mtimeP(p)).toLocaleString() }];
+ txt.split('\n').slice(0, 40).forEach((ln, i) => { if (ln.trim()) children.push({ type: 'kv', id: id + '#L' + i, label: String(i + 1).padStart(3), meta: ln.slice(0, 140) }); });
+ if (children.length === 1) children.push({ type: 'kv', id, label: '(empty memo)', meta: '' });
+ return { title: 'parked · ' + id, breadcrumb: 'parked/' + id, children };
+ },
+ kv(id) { return { title: id, breadcrumb: id, children: [] }; },
+};
+
+function drill(type, id) {
+ const fn = DRILLERS[type];
+ if (!fn) return { title: type + ' · ' + id, breadcrumb: type + '/' + id, children: [], error: 'unknown type ' + type };
+ return fn(id);
+}
+
const PAGE = fs.readFileSync(path.join(__dirname, 'index.html'), 'utf8');
const server = http.createServer((req, res) => {
@@ -149,6 +342,15 @@ const server = http.createServer((req, res) => {
res.writeHead(200, { 'content-type': 'application/json' });
return res.end(JSON.stringify({ kind, items: (s._lists[kind] || []).slice().sort() }));
}
+ if (u.pathname === '/api/drill') {
+ const type = u.searchParams.get('type') || '';
+ const id = u.searchParams.get('id') || '';
+ let out;
+ try { out = dcache(type + '|' + id, () => drill(type, id)); }
+ catch (e) { out = { title: type + ' · ' + id, breadcrumb: type + '/' + id, children: [], error: String(e && e.message || e) }; }
+ res.writeHead(200, { 'content-type': 'application/json' });
+ return res.end(JSON.stringify(out));
+ }
res.writeHead(200, { 'content-type': 'text/html' }); res.end(PAGE);
});
← da88d74 stack-map: add live folders + drill-downs for skills, agents
·
back to Stack Map Viewer
·
viewer: click any skill/canary → description + who/what/wher 18c85c0 →