← back to Resume Queue Viewer
server.js
451 lines
// 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 os = require('os');
const path = require('path');
const { execFile } = require('child_process');
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 []; }
}
// Write a JSON response in one call.
function sendJSON(res, code, obj) {
res.writeHead(code, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(obj));
}
// Collect and JSON-parse a request body (capped at 100 KB).
function readBody(req) {
return new Promise((resolve, reject) => {
let body = '';
req.on('data', c => {
body += c;
if (body.length > 1e5) { req.destroy(); reject(new Error('body too large')); }
});
req.on('end', () => {
try { resolve(JSON.parse(body || '{}')); }
catch (e) { reject(new Error('bad JSON')); }
});
req.on('error', reject);
});
}
// Build the unified queue from the three CNCP stores.
function buildQueue() {
const parking = (readJSON('cncp-parking-lot.json') || []).map(p => ({
type: 'parking',
id: p.id || '',
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',
discussed: !!p.discussed,
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 } };
}
// ── Run an item ────────────────────────────────────────────────────────────
// Opens a fresh iTerm2 tab and boots a `claude` session pre-loaded with a
// task prompt built from the queue item. Returns a Promise<{ok,message}>.
function escAS(s) { return String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"'); }
function buildPrompt(item) {
if (item.type === 'recap') {
return [
'Resume this prior work session from its CNCP recap.',
item.path ? 'Recap file: ' + item.path : '',
'',
item.title,
item.body || '',
'',
'Read the recap, re-establish full context, then continue where it left off.',
].filter(Boolean).join('\n');
}
// parking-lot TODO (default)
return [
'Resume-queue item from the CNCP parking lot — work this now.',
'',
'TITLE: ' + item.title,
'NOTE: ' + (item.body || ''),
item.link ? 'REF: ' + item.link : '',
'',
'Investigate, propose a concrete approach, confirm anything risky with Steve, then proceed.',
].filter(Boolean).join('\n');
}
function runItem(item) {
return new Promise((resolve) => {
const prompt = buildPrompt(item);
// Stash the prompt in a temp file so no shell/AppleScript escaping of the
// prompt body is needed — the tab just cats it into `claude`.
const tmp = path.join(os.tmpdir(), 'rq-run-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8) + '.txt');
try { fs.writeFileSync(tmp, prompt, 'utf8'); }
catch (e) { return resolve({ ok: false, message: 'temp write failed: ' + e.message }); }
const home = process.env.HOME;
const shellCmd = "cd '" + home + "' && claude \"$(cat '" + tmp + "')\"; rm -f '" + tmp + "'";
const script =
'tell application "iTerm2"\n' +
' activate\n' +
' if (count of windows) = 0 then\n' +
' create window with default profile\n' +
' else\n' +
' tell current window to create tab with default profile\n' +
' end if\n' +
' tell current session of current window to write text "' + escAS(shellCmd) + '"\n' +
'end tell';
execFile('osascript', ['-e', script], { timeout: 15000 }, (err, stdout, stderr) => {
if (err) return resolve({ ok: false, message: (stderr || err.message || 'osascript failed').trim() });
resolve({ ok: true, message: 'Launched a claude tab for "' + item.title + '"' });
});
});
}
// Toggle the `discussed` flag on a parking-lot entry, written straight back
// to the live CNCP store. Returns {ok,message}.
function setDiscussed(id, discussed) {
const file = path.join(CNCP, 'cncp-parking-lot.json');
let list;
try { list = JSON.parse(fs.readFileSync(file, 'utf8')); }
catch (e) { return { ok: false, message: 'cannot read parking lot: ' + e.message }; }
if (!Array.isArray(list)) return { ok: false, message: 'parking lot is not an array' };
const entry = list.find(p => p.id === id);
if (!entry) return { ok: false, message: 'no parking-lot entry "' + id + '"' };
entry.discussed = !!discussed;
try { fs.writeFileSync(file, JSON.stringify(list, null, 2) + '\n', 'utf8'); }
catch (e) { return { ok: false, message: 'write failed: ' + e.message }; }
return { ok: true, message: (discussed ? 'Marked done: ' : 'Reopened: ') + id };
}
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, input[type=search] { background:var(--card); color:var(--txt); border:1px solid var(--line); border-radius:7px; padding:5px 8px; font-size:13px; }
input[type=search] { width:170px; }
input[type=search]:focus { outline:none; border-color:var(--accent); }
.bar button .ct { color:var(--dim); font-size:11px; margin-left:4px; }
.bar button.on .ct { color:#fff; opacity:.8; }
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; }
.card .actions { display:flex; gap:8px; }
button.run { background:var(--accent); color:#fff; border:none; border-radius:7px;
padding:7px 14px; font-size:12.5px; font-weight:600; cursor:pointer; }
button.run:hover { filter:brightness(1.12); }
button.run:disabled { opacity:.55; cursor:progress; }
button.done { background:transparent; color:var(--dim); border:1px solid var(--line);
border-radius:7px; padding:7px 12px; font-size:12.5px; cursor:pointer; }
button.done:hover { color:var(--txt); border-color:var(--dim); }
button.done:disabled { opacity:.55; cursor:progress; }
#toast { position:fixed; left:50%; bottom:26px; transform:translateX(-50%) translateY(20px);
background:#1f2230; border:1px solid var(--line); color:var(--txt);
padding:11px 18px; border-radius:9px; font-size:13px; opacity:0;
transition:.25s; pointer-events:none; max-width:80vw; }
#toast.show { opacity:1; transform:translateX(-50%) translateY(0); }
#toast.err { border-color:#7a2336; }
</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<span class="ct" data-ct="all"></span></button>
<button data-f="parking">Parking Lot<span class="ct" data-ct="parking"></span></button>
<button data-f="recap">Recaps<span class="ct" data-ct="recap"></span></button>
<button data-f="win">Wins<span class="ct" data-ct="win"></span></button>
<input type="search" id="q" placeholder="search…" autocomplete="off">
<button id="refresh" title="Reload CNCP data now">↻ Refresh</button>
<span class="grow"></span>
<label>Sort</label>
<select id="sort">
<option value="new">Newest</option>
<option value="old">Oldest</option>
<option value="status">Status (open first)</option>
<option value="priority">Priority (★)</option>
<option value="title">Title A→Z</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>
<div id="toast"></div>
<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);
let query = '';
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 all = [...DATA.parking, ...DATA.recaps, ...DATA.wins];
const q = query.trim().toLowerCase();
if (q) all = all.filter(i =>
((i.title||'')+' '+(i.body||'')+' '+(i.project||'')).toLowerCase().includes(q));
const counts = { all: all.length, parking:0, recap:0, win:0 };
all.forEach(i => { counts[i.type] = (counts[i.type]||0) + 1; });
document.querySelectorAll('.ct').forEach(el => {
el.textContent = ' ' + (counts[el.dataset.ct] || 0); });
let items = filter === 'all' ? all : all.filter(i => i.type === filter);
// Status rank: open parking items > resumable recaps > everything done.
const statusRank = i => (i.type==='parking' && !i.discussed) ? 0
: (i.type==='recap') ? 1
: (i.type==='parking' && i.discussed) ? 2 : 3;
if (sort === 'new') items.sort((a,b)=>b.ts-a.ts);
else if (sort === 'old') items.sort((a,b)=>a.ts-b.ts);
else if (sort === 'status') items.sort((a,b)=>statusRank(a)-statusRank(b) || b.ts-a.ts);
else if (sort === 'priority') items.sort((a,b)=>(b.rank||0)-(a.rank||0) || b.ts-a.ts);
else if (sort === 'title') items.sort((a,b)=>(a.title||'').localeCompare(b.title||''));
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">' +
(q ? 'No matches for “' + esc(query.trim()) + '”.' : 'Nothing in the queue.') + '</div>';
return;
}
window.LASTITEMS = items;
g.innerHTML = items.map((i, idx) => {
const stars = i.type==='win' ? '<span class="stars">'+'★'.repeat(i.rank||3)+'</span>' : '';
const link = i.link ? '<a href="'+esc(i.link)+'" target="_blank" rel="noopener noreferrer">open ↗</a>' : '';
const proj = i.project ? esc(i.project) : '';
const runnable = i.type==='parking' || i.type==='recap';
const runLabel = i.type==='recap' ? 'Resume ▸' : 'Run ▸';
let actionBtns = '';
if (runnable) actionBtns += '<button class="run" data-idx="'+idx+'">'+runLabel+'</button>';
if (i.type==='parking')
actionBtns += '<button class="done" data-idx="'+idx+'">'+(i.discussed?'↩ Reopen':'✓ Done')+'</button>';
const actions = actionBtns ? '<div class="actions">'+actionBtns+'</div>' : '';
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>'
+ actions
+ '<div class="meta">'+(i.date?'<span>'+esc(i.date)+'</span>':'')
+ (proj?'<span>'+proj+'</span>':'') + stars + link + '</div>'
+ '</div>';
}).join('');
}
let toastTimer;
function toast(msg, isErr) {
const t = document.getElementById('toast');
t.textContent = msg;
t.className = 'show' + (isErr ? ' err' : '');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => { t.className = ''; }, 4200);
}
async function runItem(idx, btn) {
const item = (window.LASTITEMS || [])[idx];
if (!item) return;
btn.disabled = true;
const orig = btn.textContent;
btn.textContent = 'Launching…';
try {
const r = await fetch('/api/run', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(item),
});
const j = await r.json();
toast(j.message || (j.ok ? 'launched' : 'failed'), !j.ok);
} catch (e) {
toast('request failed: ' + e.message, true);
} finally {
btn.disabled = false;
btn.textContent = orig;
}
}
async function markDone(idx, btn) {
const item = (window.LASTITEMS || [])[idx];
if (!item || item.type !== 'parking') return;
btn.disabled = true;
try {
const r = await fetch('/api/discussed', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: item.id, discussed: !item.discussed }),
});
const j = await r.json();
toast(j.message || (j.ok ? 'updated' : 'failed'), !j.ok);
if (j.ok) return load();
} catch (e) { toast('request failed: ' + e.message, true); }
btn.disabled = false;
}
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(manual) {
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();
if (manual) toast('Refreshed — '+(c.parking+c.recaps+c.wins)+' items');
} catch(e){
document.getElementById('sub').textContent = 'failed to load CNCP data';
if (manual) toast('Refresh failed: '+e.message, true);
}
}
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('grid').addEventListener('click', e => {
const run = e.target.closest('button.run');
if (run) return runItem(+run.dataset.idx, run);
const done = e.target.closest('button.done');
if (done) return markDone(+done.dataset.idx, done);
});
document.getElementById('q').addEventListener('input', e => { query = e.target.value; render(); });
document.getElementById('refresh').onclick = () => load(true);
document.getElementById('sort').value = sort;
document.getElementById('dens').value = dens;
setFilter(filter);
load();
setInterval(load, 30000);
</script>
</body>
</html>`;
// Defensive 404-guard: even though this app serves no static files, refuse to
// answer any URL that looks like a snapshot/backup leak (*.bak, *.pre-*,
// *.orig, *.rej, *.swp, *~) before any other routing. Belt-and-braces in case
// a future static-mount is bolted on top.
const SNAPSHOT_RE = /(\.bak(\.|$)|\.pre-|\.orig$|\.rej$|\.swp$|~$)/i;
const server = http.createServer((req, res) => {
if (SNAPSHOT_RE.test(req.url || '')) {
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('404 — not found');
return;
}
if (req.url === '/api/queue') return sendJSON(res, 200, buildQueue());
if (req.url === '/api/run' && req.method === 'POST') {
readBody(req)
.then(item => {
if (!item || !item.title) return sendJSON(res, 400, { ok: false, message: 'missing item' });
return runItem(item).then(r => sendJSON(res, r.ok ? 200 : 500, r));
})
.catch(e => sendJSON(res, 400, { ok: false, message: e.message }));
return;
}
if (req.url === '/api/discussed' && req.method === 'POST') {
readBody(req)
.then(p => {
if (!p || !p.id) return sendJSON(res, 400, { ok: false, message: 'missing id' });
const r = setDiscussed(p.id, !!p.discussed);
sendJSON(res, r.ok ? 200 : 500, r);
})
.catch(e => sendJSON(res, 400, { ok: false, message: e.message }));
return;
}
if (req.url === '/healthz') { res.writeHead(200); res.end('ok'); return; }
if (req.url === '/' || req.url === '/index.html') {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(PAGE);
return;
}
// Anything else is genuinely not found — don't masquerade as the page.
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('404 — not found');
});
server.listen(PORT, '127.0.0.1', () => {
console.log('Resume Queue Viewer → http://127.0.0.1:' + PORT);
});