← 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
A .gitignoreA README.mdA public/index.htmlA server.js
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=>({'&':'&','<':'<','>':'>'}[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 →