← back to Stack Map Viewer
viewer: click any skill/canary → description + who/what/where/why/when 5W card; auto-open one on load
18c85c09ef924166c94796d41fd1a2d405d33150 · 2026-09-22 12:39:57 -0700 · Steve Abrams
Files touched
Diff
commit 18c85c09ef924166c94796d41fd1a2d405d33150
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Sep 22 12:39:57 2026 -0700
viewer: click any skill/canary → description + who/what/where/why/when 5W card; auto-open one on load
---
index.html | 30 +++++++++++++++++++++--
server.js | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
2 files changed, 109 insertions(+), 4 deletions(-)
diff --git a/index.html b/index.html
index b67637e..e8cbabd 100644
--- a/index.html
+++ b/index.html
@@ -53,6 +53,13 @@
#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}
+ /* 5W info card */
+ #dinfo{margin:0 0 14px}
+ .icard{background:#0e1420;border:1px solid #2a3247;border-radius:10px;padding:12px 12px 8px}
+ .idesc{font-size:12.5px;line-height:1.55;color:#c7d2e6;margin-bottom:10px;border-bottom:1px solid #222a3a;padding-bottom:10px}
+ .iw{display:flex;gap:10px;padding:5px 0;align-items:baseline}
+ .iw .ik{flex:0 0 52px;font-size:10px;font-weight:700;letter-spacing:.6px;text-align:right;padding-top:1px}
+ .iw .iv{flex:1;font-size:12px;line-height:1.5;color:#dde3f0;word-break:break-word}
#drawer ul{list-style:none;margin:0;padding:0;font-size:13px}
#drawer li{padding:6px 8px;border-radius:6px;cursor:pointer;display:flex;gap:8px;align-items:baseline}
#drawer li:nth-child(odd){background:#181c28}
@@ -91,7 +98,7 @@
<div class="foot" id="foot"></div>
<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>
+ <h2 id="dtitle"></h2><div class="cnt" id="dcnt"></div><div id="dinfo"></div><ul id="dlist"></ul></div>
<script>
const N=(v)=>v==null?'…':v.toLocaleString();
let DATA=null;
@@ -273,14 +280,30 @@ async function fetchDrill(type,id){
}
function startDrill(type,id,label){CRUMBS=[];openDrill(type,id,label);}
function openDrill(type,id,label){CRUMBS.push({type,id,label:label||id});renderDrill();}
+function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,m=>({'&':'&','<':'<','>':'>','"':'"'}[m]));}
+// ── 5W info card (description + what/where/who/why/when) ──────────────────────
+function renderInfo(info){
+ const el=document.getElementById('dinfo');
+ if(!info){el.innerHTML='';return;}
+ const row=(k,v,c)=>v?'<div class="iw"><span class="ik" style="color:'+c+'">'+k+'</span><span class="iv">'+esc(v)+'</span></div>':'';
+ el.innerHTML='<div class="icard">'
+ +(info.description?'<div class="idesc">'+esc(info.description)+'</div>':'')
+ +row('WHAT',info.what,'#8fe3b4')
+ +row('WHY',info.why,'#ffd27a')
+ +row('WHERE',info.where,'#9fc4ff')
+ +row('WHO',info.who,'#d6b8ff')
+ +row('WHEN',info.when,'#f3b6b6')
+ +'</div>';
+}
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='';
+ document.getElementById('dtitle').textContent='…';document.getElementById('dlist').innerHTML='';document.getElementById('dcnt').textContent='';document.getElementById('dinfo').innerHTML='';
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;
+ renderInfo(j.info);
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>';
@@ -389,8 +412,11 @@ function renderFolders(folders){
}
// ── Data / refresh ───────────────────────────────────────────────────────────
+let bootDrilled=false;
function applyData(d){
DATA=d;
+ // on load, auto-open one skill's 5W card so the description/who-what-where-why-when panel is visible to test
+ if(!bootDrilled){bootDrilled=true;setTimeout(()=>startDrill('skill','greenland-scraper-manager','greenland-scraper-manager'),450);}
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();
diff --git a/server.js b/server.js
index 02a0457..15f3c3e 100644
--- a/server.js
+++ b/server.js
@@ -173,6 +173,85 @@ function heartbeatKids(dir) {
return out;
}
+// ── skill "5W" info block: description + what/why/where/who/when ─────────────
+// Robust to BOTH SKILL.md shapes: YAML frontmatter (name:/description:) AND the
+// bare "# Heading + prose" style. Always returns a filled card — never blank.
+const firstSentence = (s) => { const m = String(s || '').match(/^[\s\S]*?[.!?](\s|$)/); return (m ? m[0] : String(s || '')).trim(); };
+const sentences = (s) => String(s || '').split(/(?<=[.!?])\s+/).map(x => x.trim()).filter(Boolean);
+
+function parseSkillMd(id) {
+ const dir = path.join(SKILLS, id);
+ const md = readText(path.join(dir, 'SKILL.md'));
+ let name = id, desc = '', body = md;
+ const fm = md.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
+ if (fm) {
+ body = fm[2] || '';
+ const nm = fm[1].match(/^name:\s*(.+)$/m); if (nm) name = nm[1].trim();
+ const dm = fm[1].match(/^description:\s*([\s\S]*?)(?:\n[a-zA-Z_-]+:\s|\n*$)/m); if (dm) desc = dm[1].replace(/\s+/g, ' ').trim();
+ }
+ if (!desc) { // no frontmatter description — take the first real prose paragraph, stopping at any structural line
+ const h = body.match(/^#\s+(.+)$/m); if (h) name = h[1].replace(/[*#]/g, '').trim();
+ const isStruct = (l) => { const t = l.trim(); return t === '' || /^[#>`|]/.test(t) || /^\*\*[^*]+\*\*\s*:/.test(t) || /^(?:CREATE|SELECT|INSERT|UPDATE|DELETE|ALTER|\{|\}|<|\d+[.)])/i.test(t); };
+ const buf = []; let started = false;
+ for (const l of body.split('\n')) {
+ if (!started) { if (isStruct(l) || l.trim().length < 25) continue; started = true; buf.push(l.trim()); }
+ else { if (isStruct(l)) break; buf.push(l.trim()); if (buf.length >= 4) break; }
+ }
+ desc = buf.join(' ').replace(/\*\*/g, '').replace(/\s+/g, ' ').trim();
+ }
+ return { dir, name, desc, body };
+}
+
+function skillInfo(id) {
+ const { dir, name, desc, body } = parseSkillMd(id);
+ const hay = desc + ' \n' + body.slice(0, 4000);
+ // WHERE — vendor domains, staging tables, SKU prefix, target systems, dir
+ const domains = [...new Set([...hay.matchAll(/\b([a-z0-9][a-z0-9-]*\.(?:com|co\.uk|fr|net|org|io|us|design))\b/gi)].map(m => m[1].toLowerCase()))]
+ .filter(d => !/\.(?:js|md|json|sh|mjs|py)$/.test(d) && !/^com\.steve/.test(d)).slice(0, 4);
+ const tables = [...new Set([...hay.matchAll(/\b([a-z][a-z0-9_]*_(?:catalog|colorways|pricing|registry))\b/g)].map(m => m[1]))].slice(0, 4);
+ const prefix = (hay.match(/\bDW[A-Z]{2,4}-/) || [])[0] || '';
+ const systems = ['Shopify', 'FileMaker', 'dw_unified', 'Merchant Center', 'Postgres', 'Browserbase'].filter(s => new RegExp(s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i').test(hay));
+ let scripts = [];
+ try { scripts = fs.readdirSync(dir).filter(f => /\.(?:mjs|js|py|sh)$/.test(f)).slice(0, 6); } catch {}
+ const whereBits = [];
+ if (domains.length) whereBits.push('source ' + domains.join(', '));
+ if (tables.length) whereBits.push('tables ' + tables.join(', '));
+ if (prefix) whereBits.push('SKU ' + prefix);
+ if (systems.length) whereBits.push('writes ' + systems.join(' · '));
+ if (scripts.length) whereBits.push('scripts ' + scripts.join(', '));
+ whereBits.push('~/.claude/skills/' + id);
+ // WHO — referenced-by (cron plists · agents · sibling skills)
+ const refP = lines(sh(`grep -lF ${shq(id)} ${LA}/com.steve.*.plist 2>/dev/null`)).map(base);
+ const refA = lines(sh(`grep -lF ${shq(id)} ${AGENTS}/*.md 2>/dev/null`)).map(f => base(f).replace(/\.md$/, ''));
+ const refS = lines(sh(`grep -rlF ${shq(id)} ${SKILLS}/*/SKILL.md 2>/dev/null`)).map(f => base(path.dirname(f))).filter(n => n !== id);
+ const whoBits = [];
+ if (refA.length) whoBits.push(refA.length + ' agent' + (refA.length > 1 ? 's' : '') + ' (' + refA.slice(0, 4).join(', ') + (refA.length > 4 ? '…' : '') + ')');
+ if (refP.length) whoBits.push(refP.length + ' cron plist' + (refP.length > 1 ? 's' : ''));
+ if (refS.length) whoBits.push(refS.length + ' skill' + (refS.length > 1 ? 's' : ''));
+ // WHEN — cadence from a driving plist, else on-demand
+ let when = 'on-demand (invoked via Skill tool)';
+ if (refP.length) {
+ const xml = readText(path.join(LA, refP[0]));
+ const iv = xml.match(/<key>StartInterval<\/key>\s*<integer>(\d+)/);
+ const cal = /StartCalendarInterval/.test(xml);
+ if (iv) { const s = +iv[1]; when = s >= 3600 ? 'every ' + (s / 3600) + 'h (cron)' : s >= 60 ? 'every ' + (s / 60) + 'm (cron)' : 'every ' + s + 's (cron)'; }
+ else if (cal) { const hrs = [...xml.matchAll(/<key>Hour<\/key>\s*<integer>(\d+)/g)].map(x => x[1]); when = 'daily' + (hrs.length ? ' @ ' + hrs.join(',') + 'h' : '') + ' (cron)'; }
+ else when = 'cron (' + refP[0].replace(/\.plist$/, '') + ')';
+ }
+ // WHY — pull rationale/trigger sentences from the description
+ const why = sentences(desc).filter(s => /\b(use when|use proactively|born|catch|guard|because|so that|must never|never customer-facing|enforce|ensure|prevent|keeps?|refuses?|private label)\b/i.test(s)).slice(0, 2).join(' ')
+ || sentences(desc).slice(1, 3).find(s => s.length > 25) || firstSentence(desc) || '—';
+ // WHAT — one-line summary
+ const what = firstSentence(desc) || desc || '—';
+ return {
+ description: desc || '(no description found in SKILL.md)',
+ what, why,
+ who: whoBits.length ? whoBits.join(' · ') : 'nothing references it (pure on-demand)',
+ where: whereBits.join(' · '),
+ when, name,
+ };
+}
+
function catChildType(kind) {
if (kind === 'canaries' || kind === 'sk_canary') return 'canary';
if (kind === 'officers' || kind === 'ag_officers') return 'officer';
@@ -205,7 +284,7 @@ const DRILLERS = {
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 };
+ return { title: 'skill · ' + id, breadcrumb: id, info: skillInfo(id), children };
},
refby(id) {
const children = [];
@@ -295,7 +374,7 @@ const DRILLERS = {
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 };
+ return { title: 'canary · ' + id, breadcrumb: 'canary/' + id, info: skillInfo(id), children };
},
project(id) {
const dir = path.join(PROJECTS, id);
← a676017 viewer: draggable nodes + recursive drill + view switcher
·
back to Stack Map Viewer
·
viewer: add LAST RUN to scraper cards (newest scrape timesta 4c7440f →