← 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
A .gitignoreA package.jsonA server.js
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 => ({'&':'&','<':'<','>':'>','"':'"'}[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 →