[object Object]

← back to Approval Command Center

initial scaffold: approval command center over the 105-draft digest

d7fbed4fabaa2ba5e260cd0c2a7b7eb728dc99c1 · 2026-08-09 10:33:57 -0700 · Steve Abrams

Files touched

Diff

commit d7fbed4fabaa2ba5e260cd0c2a7b7eb728dc99c1
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Aug 9 10:33:57 2026 -0700

    initial scaffold: approval command center over the 105-draft digest
---
 .gitignore        |   8 ++
 README.md         |   6 ++
 public/index.html | 217 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 server.js         | 180 ++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 411 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/README.md b/README.md
new file mode 100644
index 0000000..532b09b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,6 @@
+# Approval Command Center
+Button-pushing surface over the 105-item pending-approval digest (2026-08-09).
+Steve's click is the human gate. Approve → logs the go + appends to
+~/.claude/yolo-queue/execute-queue.jsonl for the live officer-yolo verified pass.
+Reject → _rejected/, Hold → _held/. Zero deps. Basic Auth admin/DW2024!.
+Run: PORT=9795 node server.js
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..1e1b7b1
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,217 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8"/>
+<meta name="viewport" content="width=device-width, initial-scale=1"/>
+<title>Approval Command Center — 105 gated drafts</title>
+<style>
+  :root{ --cols:3; --bg:#0f1115; --panel:#171a21; --line:#252a34; --ink:#e7ebf0; --dim:#98a2b3; --teal:#33c2b8; --gold:#d9b166; }
+  *{box-sizing:border-box}
+  body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}
+  header{position:sticky;top:0;z-index:20;background:linear-gradient(180deg,#12151b,#12151bee);border-bottom:1px solid var(--line);padding:14px 20px;backdrop-filter:blur(6px)}
+  h1{margin:0 0 2px;font-size:17px;font-weight:600;letter-spacing:.2px}
+  .sub{color:var(--dim);font-size:12px}
+  .bar{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-top:10px}
+  input,select,button{font:inherit}
+  input[type=search],select{background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px 10px}
+  input[type=search]{min-width:230px}
+  .range{display:flex;align-items:center;gap:6px;color:var(--dim);font-size:12px}
+  .chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px}
+  .chip{cursor:pointer;user-select:none;border:1px solid var(--line);background:var(--panel);color:var(--dim);border-radius:999px;padding:4px 11px;font-size:12px}
+  .chip.on{color:#0f1115;font-weight:600}
+  .chip.on[data-sev=hard]{background:#e06b6b;border-color:#e06b6b}
+  .chip.on[data-sev=soft]{background:var(--teal);border-color:var(--teal)}
+  .chip.on{background:var(--gold);border-color:var(--gold)}
+  .legend{margin-top:9px;color:var(--dim);font-size:11.5px;display:flex;gap:16px;flex-wrap:wrap}
+  .dotp{display:inline-block;width:9px;height:9px;border-radius:2px;vertical-align:middle;margin-right:5px}
+  main{padding:18px 20px 80px}
+  .grid{display:grid;grid-template-columns:repeat(var(--cols),minmax(0,1fr));gap:14px}
+  .card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:13px 14px;display:flex;flex-direction:column;gap:8px;position:relative}
+  .card.hard{border-left:3px solid #e06b6b}
+  .card.soft{border-left:3px solid var(--teal)}
+  .ctitle{font-weight:600;font-size:14px;line-height:1.35}
+  .meta{display:flex;flex-wrap:wrap;gap:6px;align-items:center}
+  .badge{font-size:11px;padding:2px 8px;border-radius:6px;border:1px solid var(--line);color:var(--dim);white-space:nowrap}
+  .badge.gate{background:#20242d}
+  .badge.irrev{color:#e06b6b;border-color:#5a2b2b;background:#241618}
+  .badge.rev{color:var(--teal);border-color:#1f4a47;background:#12211f}
+  .badge.state{background:#1a1e26}
+  .when{color:var(--dim);font-size:11.5px}
+  .officer{color:var(--gold);font-size:11.5px}
+  .body{display:none;white-space:pre-wrap;font:12px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace;background:#0e1116;border:1px solid var(--line);border-radius:8px;padding:10px;max-height:340px;overflow:auto;color:#c7d0dc}
+  .card.open .body{display:block}
+  .row{display:flex;gap:6px;flex-wrap:wrap;margin-top:2px}
+  .btn{cursor:pointer;border:1px solid var(--line);background:#20242d;color:var(--ink);border-radius:8px;padding:6px 11px;font-size:12.5px}
+  .btn:hover{border-color:#3a4150}
+  .btn.approve{background:#12332e;border-color:#1f5c52;color:#7ee0d3}
+  .btn.reject{background:#2c1618;border-color:#5a2b2b;color:#f0a0a0}
+  .btn.hold{background:#241f12;border-color:#5c4c1f;color:#e6c66b}
+  .btn.sel{background:#161d2b;border-color:#2a3a5c;color:#9ec0ff}
+  .btn.done{opacity:.5;pointer-events:none}
+  .card.acted{opacity:.55}
+  .selbox{position:absolute;top:11px;right:11px;transform:scale(1.25)}
+  .bulk{position:fixed;bottom:0;left:0;right:0;background:#12151bfa;border-top:1px solid var(--line);padding:10px 20px;display:none;gap:10px;align-items:center;z-index:30}
+  .bulk.show{display:flex}
+  .bulk .n{color:var(--teal);font-weight:600}
+  .toast{position:fixed;bottom:64px;right:18px;background:#12332e;border:1px solid #1f5c52;color:#7ee0d3;padding:9px 14px;border-radius:8px;z-index:40;display:none}
+  a{color:var(--teal)}
+  .count-pills{display:flex;flex-wrap:wrap;gap:8px;margin-top:9px}
+  .cp{font-size:11.5px;color:var(--dim);background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:4px 9px;cursor:pointer}
+  .cp b{color:var(--ink)}
+</style>
+</head>
+<body>
+<header>
+  <h1>Approval Command Center <span class="sub">— the 105-item pending-approval digest · 2026-08-09</span></h1>
+  <div class="sub">Your click is the human gate. <b>Approve</b> logs your go and queues the item for the live officer-yolo pass (DTD → officer → contrarian → verify). Nothing irreversible fires from this page unattended.</div>
+  <div class="bar">
+    <input type="search" id="q" placeholder="Search title / body / slug…"/>
+    <select id="sort">
+      <option value="sev">Sort: Risk (hard first)</option>
+      <option value="newest">Newest</option>
+      <option value="oldest">Oldest</option>
+      <option value="gate">Gate A→Z</option>
+      <option value="title">Title A→Z</option>
+      <option value="officer">Officer</option>
+    </select>
+    <label class="range">Density
+      <input type="range" id="dens" min="1" max="5" value="3"/>
+    </label>
+    <button class="btn sel" id="selall">Select all visible</button>
+  </div>
+  <div class="chips" id="chips"></div>
+  <div class="count-pills" id="pills"></div>
+  <div class="legend">
+    <span><span class="dotp" style="background:#e06b6b"></span>HARD gate — irreversible / spend / DNS / bulk-send / canonical → live officer-yolo only</span>
+    <span><span class="dotp" style="background:#33c2b8"></span>SOFT gate — reversible → safe to fast-track</span>
+  </div>
+</header>
+<main>
+  <div class="grid" id="grid"></div>
+</main>
+<div class="bulk" id="bulk">
+  <span class="n" id="bulkn">0</span> selected
+  <button class="btn approve" onclick="bulk('approve')">Approve + queue selected</button>
+  <button class="btn reject" onclick="bulk('reject')">Reject selected</button>
+  <button class="btn hold" onclick="bulk('hold')">Hold selected</button>
+  <button class="btn" onclick="clearSel()">Clear</button>
+</div>
+<div class="toast" id="toast"></div>
+<script>
+let ITEMS=[], SEL=new Set();
+const activeGates=new Set(), activeSev=new Set(), activeState=new Set();
+const $=s=>document.querySelector(s);
+const gates=["destructive","dw_unified canonical","Shopify write","deploy","DNS / domain","send (comms-law — NEVER auto)","other"];
+
+function fmtWhen(ms){ if(!ms) return "—"; return new Date(ms).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}); }
+
+function load(){
+  fetch('/api/items').then(r=>r.json()).then(d=>{ ITEMS=d.items; render(); });
+}
+function passesFilter(it){
+  if(activeGates.size && !activeGates.has(it.gate)) return false;
+  if(activeSev.size && !activeSev.has(it.severity)) return false;
+  if(activeState.size && !activeState.has(it.where)) return false;
+  const q=$('#q').value.trim().toLowerCase();
+  if(q && !((it.title+it.slug+it.body).toLowerCase().includes(q))) return false;
+  return true;
+}
+function sortItems(a){
+  const s=$('#sort').value;
+  const sevRank={hard:0,soft:1};
+  return a.sort((x,y)=>{
+    if(s==='sev') return (sevRank[x.severity]-sevRank[y.severity])|| (y.created-x.created);
+    if(s==='newest') return (y.created||0)-(x.created||0);
+    if(s==='oldest') return (x.created||0)-(y.created||0);
+    if(s==='gate') return x.gate.localeCompare(y.gate);
+    if(s==='title') return x.title.localeCompare(y.title);
+    if(s==='officer') return (x.officer||'').localeCompare(y.officer||'');
+    return 0;
+  });
+}
+function render(){
+  // chips
+  const cw=$('#chips'); cw.innerHTML='';
+  const mkchip=(label,set,val,sev)=>{ const c=document.createElement('span'); c.className='chip'+(set.has(val)?' on':''); if(sev)c.dataset.sev=sev; c.textContent=label;
+    c.onclick=()=>{ set.has(val)?set.delete(val):set.add(val); render(); }; cw.appendChild(c); };
+  mkchip('● HARD only',activeSev,'hard','hard'); mkchip('● SOFT only',activeSev,'soft','soft');
+  gates.forEach(g=>mkchip(g,activeGates,g));
+  ['pending','approved','rejected','held'].forEach(s=>mkchip('state:'+s,activeState,s));
+
+  // pills (counts by gate) — clickable → filter
+  const pills=$('#pills'); pills.innerHTML='';
+  const byGate={}; ITEMS.forEach(i=>byGate[i.gate]=(byGate[i.gate]||0)+1);
+  const total=ITEMS.length, irrev=ITEMS.filter(i=>!i.reversible).length;
+  const addpill=(html,fn)=>{ const p=document.createElement('span'); p.className='cp'; p.innerHTML=html; if(fn)p.onclick=fn; pills.appendChild(p); };
+  addpill(`<b>${total}</b> total`);
+  addpill(`<b>${irrev}</b> irreversible`, ()=>{activeSev.clear();activeSev.add('hard');render();});
+  addpill(`<b>${ITEMS.filter(i=>i.reversible).length}</b> reversible`, ()=>{activeSev.clear();activeSev.add('soft');render();});
+  gates.forEach(g=>addpill(`<b>${byGate[g]||0}</b> ${g}`, ()=>{activeGates.clear();activeGates.add(g);render();}));
+
+  const grid=$('#grid'); grid.innerHTML='';
+  const list=sortItems(ITEMS.filter(passesFilter));
+  list.forEach(it=>grid.appendChild(cardEl(it)));
+  updateBulk();
+}
+function cardEl(it){
+  const c=document.createElement('div'); c.className='card '+it.severity+(SEL.has(it.slug)?'':'');
+  const acted = it.where!=='pending';
+  if(acted) c.classList.add('acted');
+  c.innerHTML=`
+    ${!acted?`<input type="checkbox" class="selbox" ${SEL.has(it.slug)?'checked':''}>`:''}
+    <div class="ctitle">${esc(it.title)}</div>
+    <div class="when">🕓 ${fmtWhen(it.created)}</div>
+    <div class="meta">
+      <span class="badge gate">${esc(it.gate)}</span>
+      <span class="badge ${it.reversible?'rev':'irrev'}">${it.reversible?'reversible':'IRREVERSIBLE'}</span>
+      <span class="badge state">${it.where}</span>
+      ${it.officer&&it.officer!=='unassigned'?`<span class="officer">${esc(it.officer)}</span>`:''}
+    </div>
+    <div class="row">
+      <button class="btn toggle">▸ memo</button>
+      ${acted?`<span class="badge">${it.where.toUpperCase()}</span>`
+        :`<button class="btn approve">Approve + queue</button>
+          <button class="btn reject">Reject</button>
+          <button class="btn hold">Hold</button>`}
+    </div>
+    <div class="body" title="${it.mdname||''}">${esc(it.body||'(no memo body found on disk)')}</div>`;
+  c.querySelector('.toggle').onclick=()=>c.classList.toggle('open');
+  const cb=c.querySelector('.selbox');
+  if(cb) cb.onchange=()=>{ cb.checked?SEL.add(it.slug):SEL.delete(it.slug); updateBulk(); };
+  if(!acted){
+    c.querySelector('.approve').onclick=()=>act(it,'approve');
+    c.querySelector('.reject').onclick=()=>act(it,'reject');
+    c.querySelector('.hold').onclick=()=>act(it,'hold');
+  }
+  return c;
+}
+function act(it,action){
+  if(action==='approve' && it.severity==='hard'){
+    if(!confirm(`HARD gate — ${it.gate}${it.reversible?'':' · IRREVERSIBLE'}\n\n"${it.title}"\n\nApprove logs your go and QUEUES it for the live officer-yolo pass (verified, per-item). It does NOT fire from this page.\n\nProceed?`)) return;
+  }
+  post('/api/action',{slug:it.slug,action}).then(()=>{ toast(`${action.toUpperCase()} · ${it.slug}`); load(); });
+}
+function bulk(action){
+  const slugs=[...SEL]; if(!slugs.length) return;
+  const hard=slugs.filter(s=>{const i=ITEMS.find(x=>x.slug===s);return i&&i.severity==='hard';}).length;
+  if(action==='approve' && hard){
+    if(!confirm(`${slugs.length} selected, ${hard} are HARD gates.\n\nApprove logs your go + queues them for the live officer-yolo verified pass. Nothing fires from this page. Proceed?`)) return;
+  }
+  post('/api/bulk',{slugs,action}).then(()=>{ toast(`${action.toUpperCase()} · ${slugs.length} items`); SEL.clear(); load(); });
+}
+function updateBulk(){ $('#bulkn').textContent=SEL.size; $('#bulk').classList.toggle('show',SEL.size>0); }
+function clearSel(){ SEL.clear(); render(); }
+$('#selall').onclick=()=>{ sortItems(ITEMS.filter(passesFilter)).forEach(i=>{ if(i.where==='pending') SEL.add(i.slug); }); render(); };
+function post(url,b){ return fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b)}).then(r=>r.json()); }
+function toast(m){ const t=$('#toast'); t.textContent=m; t.style.display='block'; setTimeout(()=>t.style.display='none',2200); }
+function esc(s){ return (s||'').replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c])); }
+
+// density + sort persistence
+const dens=$('#dens'); dens.value=localStorage.acc_dens||3; document.documentElement.style.setProperty('--cols',6-dens.value);
+dens.oninput=()=>{ localStorage.acc_dens=dens.value; document.documentElement.style.setProperty('--cols',6-dens.value); };
+$('#sort').value=localStorage.acc_sort||'sev'; $('#sort').onchange=()=>{ localStorage.acc_sort=$('#sort').value; render(); };
+$('#q').oninput=render;
+load();
+</script>
+</body>
+</html>
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..275b15f
--- /dev/null
+++ b/server.js
@@ -0,0 +1,180 @@
+'use strict';
+/*
+ * Approval Command Center — a button-pushing surface over the 105-item
+ * pending-approval digest (2026-08-09). Steve's click IS the human gate.
+ *
+ * Model (realizes option 1 from the 10:00 AM email):
+ *   - Every draft is a GATED runbook memo. 81/105 are irreversible.
+ *   - The unattended web server NEVER auto-fires an irreversible prod runbook.
+ *   - Steve triages each item with a click:
+ *       APPROVE  -> records his approval + appends to execute-queue.jsonl
+ *                   (drained by a live officer-yolo session: DTD -> officer ->
+ *                    contrarian -> verify per item). Draft moves to approved/.
+ *       REJECT   -> moves to _rejected/ with a note.
+ *       HOLD     -> moves to _held/.
+ *   - Reversible items are labeled green so Steve can safely fast-track them.
+ *
+ * Zero dependencies (Node http only). Basic Auth admin/DW2024!.
+ */
+const http = require('http');
+const fs = require('fs');
+const path = require('path');
+const os = require('os');
+const { execFile } = require('child_process');
+
+const PORT = process.env.PORT || 9795;
+const AUTH_USER = process.env.ACC_USER || 'admin';
+const AUTH_PASS = process.env.ACC_PASS || 'DW2024!';
+const HOME = os.homedir();
+const Q = path.join(HOME, '.claude', 'yolo-queue');
+const PENDING = path.join(Q, 'pending-approval');
+const APPROVED = path.join(Q, 'approved');
+const REJECTED = path.join(Q, '_rejected');
+const HELD = path.join(Q, '_held');
+const MANIFEST = path.join(Q, 'digest-manifest-2026-08-09.json');
+const EXEC_QUEUE = path.join(Q, 'execute-queue.jsonl');
+const ACTION_LOG = path.join(Q, 'command-center-actions.jsonl');
+
+for (const d of [APPROVED, REJECTED, HELD]) {
+  try { fs.mkdirSync(d, { recursive: true }); } catch (e) { /* ignore */ }
+}
+
+// ---- gate severity classification -------------------------------------------
+// Which gates the running server may NEVER fire itself even on Steve's click
+// (his own standing rules: spend / bulk-send / irreversible / DNS confirm-first).
+const HARD_GATES = new Set([
+  'destructive',
+  'DNS / domain',
+  'send (comms-law — NEVER auto)',
+  'dw_unified canonical',
+]);
+function severity(gate, reversible) {
+  if (!reversible || HARD_GATES.has(gate)) return 'hard';   // live officer-yolo only
+  return 'soft';                                            // reversible fast-track ok
+}
+
+// ---- data -------------------------------------------------------------------
+function loadManifest() {
+  const m = JSON.parse(fs.readFileSync(MANIFEST, 'utf8'));
+  return m.drafts.map((d) => {
+    const item = { ...d };
+    // find the backing .md across queue folders (state may have changed)
+    const candidates = [
+      [PENDING, 'pending'], [APPROVED, 'approved'],
+      [REJECTED, 'rejected'], [HELD, 'held'],
+    ];
+    let body = '', where = 'missing', created = null, mdname = null;
+    for (const [dir, label] of candidates) {
+      const f = path.join(dir, d.slug + '.md');
+      if (fs.existsSync(f)) {
+        try {
+          body = fs.readFileSync(f, 'utf8');
+          const st = fs.statSync(f);
+          created = st.mtimeMs;
+          where = label; mdname = d.slug + '.md';
+        } catch (e) { /* ignore */ }
+        break;
+      }
+    }
+    item.body = body;
+    item.where = where;
+    item.created = created;
+    item.mdname = mdname;
+    item.severity = severity(d.gate, d.reversible);
+    return item;
+  });
+}
+
+function logAction(entry) {
+  try { fs.appendFileSync(ACTION_LOG, JSON.stringify(entry) + '\n'); } catch (e) { /* ignore */ }
+}
+
+function moveDraft(slug, destDir) {
+  const src = path.join(PENDING, slug + '.md');
+  const dst = path.join(destDir, slug + '.md');
+  if (fs.existsSync(src)) { fs.renameSync(src, dst); return true; }
+  // already moved from pending — try to relocate from wherever it is
+  for (const dir of [APPROVED, REJECTED, HELD]) {
+    const f = path.join(dir, slug + '.md');
+    if (fs.existsSync(f) && dir !== destDir) { fs.renameSync(f, dst); return true; }
+  }
+  return false;
+}
+
+function doAction(slug, action, note) {
+  const ts = new Date().toISOString();
+  const manifest = JSON.parse(fs.readFileSync(MANIFEST, 'utf8'));
+  const meta = manifest.drafts.find((d) => d.slug === slug) || { slug };
+  const rec = { ts, slug, action, note: note || '', gate: meta.gate, reversible: meta.reversible, by: 'steve', via: 'command-center' };
+  if (action === 'approve') {
+    moveDraft(slug, APPROVED);
+    // enqueue for the live officer-yolo session to execute with verification
+    fs.appendFileSync(EXEC_QUEUE, JSON.stringify({
+      ...rec, status: 'approved-queued',
+      execution_path: meta.reversible ? 'reversible-fast-track' : 'officer-yolo-verified',
+    }) + '\n');
+  } else if (action === 'reject') {
+    moveDraft(slug, REJECTED);
+  } else if (action === 'hold') {
+    moveDraft(slug, HELD);
+  } else {
+    throw new Error('unknown action: ' + action);
+  }
+  logAction(rec);
+  return rec;
+}
+
+// ---- http -------------------------------------------------------------------
+function unauthorized(res) {
+  res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Approval Command Center"' });
+  res.end('Auth required');
+}
+function checkAuth(req) {
+  const h = req.headers.authorization || '';
+  if (!h.startsWith('Basic ')) return false;
+  const [u, p] = Buffer.from(h.slice(6), 'base64').toString().split(':');
+  return u === AUTH_USER && p === AUTH_PASS;
+}
+function body(req) {
+  return new Promise((resolve) => {
+    let d = ''; req.on('data', (c) => (d += c)); req.on('end', () => resolve(d));
+  });
+}
+
+const server = http.createServer(async (req, res) => {
+  if (!checkAuth(req)) return unauthorized(res);
+  try {
+    if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {
+      const html = fs.readFileSync(path.join(__dirname, 'public', 'index.html'));
+      res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
+      return res.end(html);
+    }
+    if (req.method === 'GET' && req.url === '/api/items') {
+      const items = loadManifest();
+      res.writeHead(200, { 'Content-Type': 'application/json' });
+      return res.end(JSON.stringify({ count: items.length, items }));
+    }
+    if (req.method === 'POST' && req.url === '/api/action') {
+      const { slug, action, note } = JSON.parse((await body(req)) || '{}');
+      if (!slug || !action) { res.writeHead(400); return res.end('slug+action required'); }
+      const rec = doAction(slug, action, note);
+      res.writeHead(200, { 'Content-Type': 'application/json' });
+      return res.end(JSON.stringify({ ok: true, rec }));
+    }
+    if (req.method === 'POST' && req.url === '/api/bulk') {
+      const { slugs, action, note } = JSON.parse((await body(req)) || '{}');
+      if (!Array.isArray(slugs) || !action) { res.writeHead(400); return res.end('slugs[]+action required'); }
+      const recs = slugs.map((s) => { try { return doAction(s, action, note); } catch (e) { return { slug: s, error: e.message }; } });
+      res.writeHead(200, { 'Content-Type': 'application/json' });
+      return res.end(JSON.stringify({ ok: true, recs }));
+    }
+    res.writeHead(404); res.end('not found');
+  } catch (e) {
+    res.writeHead(500, { 'Content-Type': 'application/json' });
+    res.end(JSON.stringify({ error: e.message }));
+  }
+});
+
+server.listen(PORT, () => {
+  console.log(`Approval Command Center on http://127.0.0.1:${PORT}  (auth ${AUTH_USER}/${AUTH_PASS})`);
+});

(oldest)  ·  back to Approval Command Center  ·  viewer: add reversible fast-track tray (Approve all N) + per 4781118 →