[object Object]

← back to Resume Queue Viewer

initial scaffold — resume queue viewer (CNCP parking-lot + recaps + wins)

598f4ad6888e3bc3c152529dd746ae2bf07a2d3d · 2026-05-18 16:39:20 -0700 · SteveStudio2

Files touched

Diff

commit 598f4ad6888e3bc3c152529dd746ae2bf07a2d3d
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date:   Mon May 18 16:39:20 2026 -0700

    initial scaffold — resume queue viewer (CNCP parking-lot + recaps + wins)
---
 .gitignore   |   8 +++
 package.json |   8 +++
 server.js    | 198 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 214 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1924158
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..56f018b
--- /dev/null
+++ b/package.json
@@ -0,0 +1,8 @@
+{
+  "name": "resume-queue-viewer",
+  "version": "1.0.0",
+  "private": true,
+  "description": "Local web viewer for Steve's resume queue — CNCP parking-lot TODOs, session recaps, and recent wins.",
+  "main": "server.js",
+  "scripts": { "start": "node server.js" }
+}
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..4ba9b92
--- /dev/null
+++ b/server.js
@@ -0,0 +1,198 @@
+// Resume Queue Viewer — single-file, zero-dependency.
+// Surfaces everything in Steve's "resume queue": CNCP parking-lot TODOs,
+// session recaps (resumable sessions), and recent wins (done).
+// Reads the live CNCP JSON stores on every request so the board never goes stale.
+
+const http = require('http');
+const fs = require('fs');
+const path = require('path');
+
+const PORT = process.env.PORT || 9768;
+const CNCP = path.join(process.env.HOME, 'cncp-starter');
+
+function readJSON(name) {
+  try { return JSON.parse(fs.readFileSync(path.join(CNCP, name), 'utf8')); }
+  catch (e) { return []; }
+}
+
+// Build the unified queue from the three CNCP stores.
+function buildQueue() {
+  const parking = (readJSON('cncp-parking-lot.json') || []).map(p => ({
+    type: 'parking',
+    title: p.id || 'untitled',
+    body: p.note || '',
+    date: p.saved ? new Date(p.saved).toISOString().slice(0, 10) : '',
+    ts: p.saved || 0,
+    badge: p.discussed ? 'discussed' : 'open',
+    link: p.url || '',
+  }));
+
+  const recaps = (readJSON('cncp-session-recaps.json') || []).map(r => ({
+    type: 'recap',
+    title: r.name || r.file || 'recap',
+    body: (r.summary || '').replace(/^name:.*?description:\s*/s, '').slice(0, 320),
+    date: r.added || '',
+    ts: r.added ? Date.parse(r.added) || 0 : 0,
+    badge: 'resumable',
+    link: '',
+    path: r.path || '',
+  }));
+
+  const wins = (readJSON('cncp-wins.json') || []).map(w => ({
+    type: 'win',
+    title: w.title || 'win',
+    body: w.summary || '',
+    date: w.date || '',
+    ts: w.date ? Date.parse(w.date) || 0 : 0,
+    badge: 'done',
+    rank: w.rank || 3,
+    project: w.project || '',
+    link: w.live || '',
+  }));
+
+  return { parking, recaps, wins,
+    counts: { parking: parking.length, recaps: recaps.length, wins: wins.length } };
+}
+
+const PAGE = `<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Resume Queue</title>
+<style>
+  :root { --bg:#0e0f13; --card:#181a21; --line:#272a34; --txt:#e7e8ee; --dim:#9aa0b0;
+          --accent:#f4348c; --cols:3; }
+  * { box-sizing:border-box; margin:0; padding:0; }
+  body { background:var(--bg); color:var(--txt); font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }
+  header { padding:26px 32px 14px; border-bottom:1px solid var(--line); }
+  h1 { font-size:22px; font-weight:650; letter-spacing:-.01em; }
+  h1 span { color:var(--accent); }
+  .sub { color:var(--dim); font-size:13px; margin-top:3px; }
+  .bar { display:flex; gap:8px; align-items:center; flex-wrap:wrap; padding:14px 32px; border-bottom:1px solid var(--line); }
+  .bar button { background:var(--card); color:var(--txt); border:1px solid var(--line);
+                 padding:6px 13px; border-radius:7px; cursor:pointer; font-size:13px; }
+  .bar button.on { background:var(--accent); border-color:var(--accent); color:#fff; }
+  .bar .grow { flex:1; }
+  .bar label { color:var(--dim); font-size:12px; }
+  select, input[type=range] { vertical-align:middle; }
+  select { background:var(--card); color:var(--txt); border:1px solid var(--line); border-radius:7px; padding:5px 8px; font-size:13px; }
+  main { padding:22px 32px 60px; }
+  .grid { display:grid; grid-template-columns:repeat(var(--cols),1fr); gap:14px; }
+  .card { background:var(--card); border:1px solid var(--line); border-radius:11px; padding:15px 16px;
+          display:flex; flex-direction:column; gap:8px; }
+  .card .top { display:flex; align-items:center; gap:8px; }
+  .tag { font-size:10.5px; font-weight:700; letter-spacing:.06em; text-transform:uppercase;
+         padding:3px 8px; border-radius:20px; }
+  .tag.parking { background:#3a2f10; color:#f5c451; }
+  .tag.recap   { background:#10303a; color:#5fd0e8; }
+  .tag.win     { background:#123a1c; color:#5fe07f; }
+  .badge { margin-left:auto; font-size:10.5px; color:var(--dim); border:1px solid var(--line);
+           padding:2px 7px; border-radius:20px; }
+  .badge.open { color:#f5c451; border-color:#5a4a18; }
+  .badge.done { color:#5fe07f; border-color:#1f5a30; }
+  .card h3 { font-size:14px; font-weight:600; word-break:break-word; }
+  .card p { font-size:12.5px; color:var(--dim); }
+  .card .meta { font-size:11px; color:var(--dim); display:flex; gap:10px; flex-wrap:wrap;
+                border-top:1px solid var(--line); padding-top:7px; margin-top:auto; }
+  .card a { color:var(--accent); text-decoration:none; }
+  .stars { color:#f5c451; }
+  .empty { color:var(--dim); padding:40px; text-align:center; }
+</style>
+</head>
+<body>
+<header>
+  <h1>Resume <span>Queue</span></h1>
+  <div class="sub" id="sub">loading…</div>
+</header>
+<div class="bar">
+  <button data-f="all" class="on">All</button>
+  <button data-f="parking">Parking Lot</button>
+  <button data-f="recap">Recaps</button>
+  <button data-f="win">Wins</button>
+  <span class="grow"></span>
+  <label>Sort</label>
+  <select id="sort">
+    <option value="new">Newest</option>
+    <option value="old">Oldest</option>
+    <option value="type">Type</option>
+  </select>
+  <label>Density</label>
+  <input type="range" id="dens" min="1" max="5" value="3">
+</div>
+<main><div class="grid" id="grid"></div></main>
+<script>
+let DATA = { parking:[], recaps:[], wins:[] };
+let filter = localStorage.getItem('rq.filter') || 'all';
+let sort = localStorage.getItem('rq.sort') || 'new';
+let dens = +(localStorage.getItem('rq.dens') || 3);
+
+const TAGCLASS = { parking:'parking', recap:'recap', win:'win' };
+const esc = s => String(s||'').replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
+
+function render() {
+  document.documentElement.style.setProperty('--cols', dens);
+  let items = [...DATA.parking, ...DATA.recaps, ...DATA.wins];
+  if (filter !== 'all') items = items.filter(i => i.type === filter);
+  if (sort === 'new') items.sort((a,b)=>b.ts-a.ts);
+  else if (sort === 'old') items.sort((a,b)=>a.ts-b.ts);
+  else items.sort((a,b)=>a.type.localeCompare(b.type) || b.ts-a.ts);
+
+  const g = document.getElementById('grid');
+  if (!items.length) { g.innerHTML = '<div class="empty">Nothing in the queue.</div>'; return; }
+  g.innerHTML = items.map(i => {
+    const stars = i.type==='win' ? '<span class="stars">'+'★'.repeat(i.rank||3)+'</span>' : '';
+    const link = i.link ? '<a href="'+esc(i.link)+'" target="_blank">open ↗</a>' : '';
+    const proj = i.project ? esc(i.project) : '';
+    return '<div class="card">'
+      + '<div class="top"><span class="tag '+TAGCLASS[i.type]+'">'+i.type+'</span>'
+      + '<span class="badge '+(i.badge==='open'?'open':i.badge==='done'?'done':'')+'">'+esc(i.badge)+'</span></div>'
+      + '<h3>'+esc(i.title)+'</h3>'
+      + '<p>'+esc(i.body)+'</p>'
+      + '<div class="meta">'+(i.date?'<span>'+esc(i.date)+'</span>':'')
+      + (proj?'<span>'+proj+'</span>':'') + stars + link + '</div>'
+      + '</div>';
+  }).join('');
+}
+
+function setFilter(f){ filter=f; localStorage.setItem('rq.filter',f);
+  document.querySelectorAll('.bar button').forEach(b=>b.classList.toggle('on',b.dataset.f===f));
+  render(); }
+
+async function load() {
+  try {
+    const r = await fetch('/api/queue'); DATA = await r.json();
+    const c = DATA.counts;
+    document.getElementById('sub').textContent =
+      c.parking+' parking-lot · '+c.recaps+' recaps · '+c.wins+' wins — auto-refreshes every 30s';
+    render();
+  } catch(e){ document.getElementById('sub').textContent = 'failed to load CNCP data'; }
+}
+
+document.querySelectorAll('.bar button').forEach(b=>b.onclick=()=>setFilter(b.dataset.f));
+document.getElementById('sort').onchange = e=>{ sort=e.target.value; localStorage.setItem('rq.sort',sort); render(); };
+document.getElementById('dens').oninput = e=>{ dens=+e.target.value; localStorage.setItem('rq.dens',dens); render(); };
+
+document.getElementById('sort').value = sort;
+document.getElementById('dens').value = dens;
+setFilter(filter);
+load();
+setInterval(load, 30000);
+</script>
+</body>
+</html>`;
+
+const server = http.createServer((req, res) => {
+  if (req.url === '/api/queue') {
+    res.writeHead(200, { 'Content-Type': 'application/json' });
+    res.end(JSON.stringify(buildQueue()));
+    return;
+  }
+  if (req.url === '/healthz') { res.writeHead(200); res.end('ok'); return; }
+  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
+  res.end(PAGE);
+});
+
+server.listen(PORT, '127.0.0.1', () => {
+  console.log('Resume Queue Viewer → http://127.0.0.1:' + PORT);
+});

(oldest)  ·  back to Resume Queue Viewer  ·  Add Run/Resume action — launches a claude iTerm2 tab per que bf816e7 →