[object Object]

← back to Approval Viewer

snapshot before TK-10995 five-pass verification

8f90371d55d9d1e80a4626d1792c0d0c23c5be6e · 2026-08-31 01:32:32 -0700 · Steve Abrams

Files touched

Diff

commit 8f90371d55d9d1e80a4626d1792c0d0c23c5be6e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 31 01:32:32 2026 -0700

    snapshot before TK-10995 five-pass verification
---
 public/index.html |  4 ++--
 server.js         | 18 ++++++++++--------
 2 files changed, 12 insertions(+), 10 deletions(-)

diff --git a/public/index.html b/public/index.html
index 5179bc4..3364f0d 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1,4 +1,4 @@
 <!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Gated Approval Viewer</title><style>
-:root{color-scheme:dark;--bg:#101216;--panel:#191d24;--line:#303744;--ink:#f1f3f5;--muted:#aab3c0;--gold:#d9b166;--ok:#64d1ba;--red:#ed8e8e}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.45 system-ui,sans-serif}header{padding:24px max(18px,4vw);border-bottom:1px solid var(--line);background:#15181e}h1{margin:0 0 5px;font-size:25px}p{color:var(--muted);margin:5px 0}.notice{margin-top:14px;padding:10px 12px;border:1px solid #3e4b60;border-radius:9px;color:#c8d9f4}.tools{display:flex;gap:10px;flex-wrap:wrap;margin-top:17px}input,select,button,textarea{font:inherit;border:1px solid var(--line);border-radius:8px;padding:9px 11px;background:var(--panel);color:var(--ink)}input{min-width:240px}.grid{padding:22px max(18px,4vw);display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:15px}.card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:16px;display:flex;flex-direction:column;gap:10px}.card.done{opacity:.62}.title{font-weight:700;font-size:17px}.meta{display:flex;gap:7px;flex-wrap:wrap}.badge{font-size:12px;padding:3px 8px;border-radius:999px;border:1px solid var(--line);color:var(--muted)}.hard{color:var(--red);border-color:#704244}.soft{color:var(--ok);border-color:#32675d}.summary{color:#d2d7df;min-height:45px}.actions{display:flex;gap:7px;flex-wrap:wrap;margin-top:auto}.actions button{cursor:pointer}.approve{border-color:#32675d;color:var(--ok)}.revise{border-color:#76602e;color:var(--gold)}.block{border-color:#704244;color:var(--red)}.status{font-size:13px;color:var(--muted)}.empty{padding:30px;color:var(--muted)}textarea{width:100%;min-height:44px;resize:vertical}
+:root{color-scheme:dark;--bg:#101216;--panel:#191d24;--line:#303744;--ink:#f1f3f5;--muted:#aab3c0;--gold:#d9b166;--ok:#64d1ba;--red:#ed8e8e}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.45 system-ui,sans-serif}header{padding:24px max(18px,4vw);border-bottom:1px solid var(--line);background:#15181e}h1{margin:0 0 5px;font-size:25px}p{color:var(--muted);margin:5px 0}.notice{margin-top:14px;padding:10px 12px;border:1px solid #3e4b60;border-radius:9px;color:#c8d9f4}.tools{display:flex;gap:10px;flex-wrap:wrap;margin-top:17px}input,select,button,textarea{font:inherit;border:1px solid var(--line);border-radius:8px;padding:9px 11px;background:var(--panel);color:var(--ink)}input{min-width:240px}.grid{padding:22px max(18px,4vw);display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:15px}.card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:16px;display:flex;flex-direction:column;gap:10px}.card.done{border-color:#4b5b69}.title{font-weight:700;font-size:17px}.meta{display:flex;gap:7px;flex-wrap:wrap}.badge{font-size:12px;padding:3px 8px;border-radius:999px;border:1px solid var(--line);color:var(--muted)}.hard{color:var(--red);border-color:#704244}.soft{color:var(--ok);border-color:#32675d}.summary{color:#d2d7df;min-height:45px}.actions{display:flex;gap:7px;flex-wrap:wrap;margin-top:auto}.actions button{cursor:pointer}.approve{border-color:#32675d;color:var(--ok)}.revise{border-color:#76602e;color:var(--gold)}.block{border-color:#704244;color:var(--red)}.status{font-size:13px;color:var(--muted)}.when{font-variant-numeric:tabular-nums}.empty{padding:30px;color:var(--muted)}textarea{width:100%;min-height:44px;resize:vertical}
 </style></head><body><header><h1>Gated approvals</h1><p>Read the request. Choose one clear next step.</p><div class="notice">Safe mode: buttons record a local decision only. They do not publish, email, spend, deploy, delete, or call outside systems.</div><div class="tools"><input id="search" type="search" placeholder="Search tickets…"><select id="filter"><option value="all">All gates</option><option value="hard">Hard gates</option><option value="soft">Safe local items</option></select><span id="count" class="status"></span></div></header><main id="grid" class="grid"></main><script>
-let data={items:[],decisions:[]};const $=s=>document.querySelector(s);const latest=()=>Object.fromEntries(data.decisions.map(d=>[d.id,d]));function esc(s){return String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}function render(){let q=$('#search').value.toLowerCase(),f=$('#filter').value,st=latest();let list=data.items.filter(i=>(f==='all'||i.risk===f)&&(!q||JSON.stringify(i).toLowerCase().includes(q)));$('#count').textContent=`${list.length} item${list.length===1?'':'s'} shown`;$('#grid').innerHTML=list.length?list.map(i=>{let d=st[i.id];return `<article class="card ${d?'done':''}"><div class="title">${esc(i.title)}</div><div class="meta"><span class="badge">${esc(i.id)}</span><span class="badge ${i.risk}">${esc(i.gate)}</span><span class="badge">${esc(i.owner)}</span></div><div class="summary">${esc(i.summary)}</div><textarea id="note-${esc(i.id)}" placeholder="Optional note" aria-label="Note for ${esc(i.title)}"></textarea>${d?`<div class="status">Recorded: <b>${esc(d.action)}</b> · ${new Date(d.at).toLocaleString()}</div>`:`<div class="actions"><button class="approve" data-id="${esc(i.id)}" data-action="approve">Approve</button><button class="revise" data-id="${esc(i.id)}" data-action="revise">Revise</button><button class="block" data-id="${esc(i.id)}" data-action="block">Block</button></div>`}</article>`}).join(''):'<div class="empty">No matching items.</div>';document.querySelectorAll('[data-action]').forEach(b=>b.onclick=()=>decide(b.dataset.id,b.dataset.action));}async function decide(id,action){let note=(document.getElementById(`note-`)||{}).value||'';let r=await fetch('/api/decision',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,action,note})});let j=await r.json();if(!r.ok)return alert(j.error||'Could not record decision');data.decisions.push(j.decision);render();}async function load(){data=await (await fetch('/api/items')).json();render();}$('#search').oninput=render;$('#filter').onchange=render;load();</script></body></html>
+let data={items:[],decisions:[]};const $=s=>document.querySelector(s);const latest=()=>Object.fromEntries(data.decisions.map(d=>[d.id,d]));function esc(s){return String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}function fmtDate(value){let date=new Date(value);return Number.isNaN(date.valueOf())?'Unknown':date.toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'});}function render(){let q=$('#search').value.toLowerCase(),f=$('#filter').value,st=latest();let list=data.items.filter(i=>(f==='all'||i.risk===f)&&(!q||JSON.stringify(i).toLowerCase().includes(q)));$('#count').textContent=`${list.length} item${list.length===1?'':'s'} shown`;$('#grid').innerHTML=list.length?list.map(i=>{let d=st[i.id];return `<article class="card ${d?'done':''}"><div class="title">${esc(i.title)}</div><div class="meta"><span class="badge">${esc(i.id)}</span><span class="badge ${i.risk}">${esc(i.gate)}</span><span class="badge">${esc(i.owner)}</span><span class="badge when" title="${esc(i.created_at)}">🕓 ${esc(fmtDate(i.created_at))}</span></div><div class="summary">${esc(i.summary)}</div><textarea id="note-${esc(i.id)}" placeholder="Optional note" aria-label="Note for ${esc(i.title)}"></textarea>${d?`<div class="status">Recorded: <b>${esc(d.action)}</b> · ${esc(fmtDate(d.at))}</div>`:''}<div class="actions"><button class="approve" data-id="${esc(i.id)}" data-action="approve">Approve</button><button class="revise" data-id="${esc(i.id)}" data-action="revise">Revise</button><button class="block" data-id="${esc(i.id)}" data-action="block">Block</button></div></article>`}).join(''):'<div class="empty">No matching items.</div>';document.querySelectorAll('[data-action]').forEach(b=>b.onclick=()=>decide(b.dataset.id,b.dataset.action));}async function decide(id,action){let note=(document.getElementById(`note-${id}`)||{}).value||'';let r=await fetch('/api/decision',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,action,note})});let j=await r.json();if(!r.ok)return alert(j.error||'Could not record decision');data.decisions.push(j.decision);render();}async function load(){data=await (await fetch('/api/items')).json();render();}$('#search').oninput=render;$('#filter').onchange=render;load();</script></body></html>
diff --git a/server.js b/server.js
index beedab2..3555bd6 100644
--- a/server.js
+++ b/server.js
@@ -3,22 +3,24 @@ const http = require('http');
 const fs = require('fs');
 const path = require('path');
 const PORT = Number(process.env.PORT || 9788);
-const decisionsPath = path.join(__dirname, 'decisions.jsonl');
+const decisionsPath = process.env.DECISIONS_PATH || path.join(__dirname, 'decisions.jsonl');
 const demoItems = [
-  { id:'TK-10409-A', title:'Publish campaign to social accounts', gate:'external publish', risk:'hard', owner:'Marketing', summary:'Would publish to selected social accounts. Review targets and copy first.' },
-  { id:'TK-10409-B', title:'Deploy pending website change', gate:'production deploy', risk:'hard', owner:'Engineering', summary:'Would change the live site. Confirm tests, rollback, and timing.' },
-  { id:'TK-10409-C', title:'Send campaign email', gate:'outbound email', risk:'hard', owner:'Marketing', summary:'Would contact a list. Confirm audience, consent, and message.' },
-  { id:'TK-10409-D', title:'Run approval dry-run report', gate:'read-only', risk:'soft', owner:'Operations', summary:'Creates a local report only; no external side effects.' },
-  { id:'TK-10409-E', title:'Update campaign draft copy', gate:'local draft', risk:'soft', owner:'Marketing', summary:'Local draft edit. Nothing is published until separately approved.' }
+  { id:'TK-10409-A', title:'Publish campaign to social accounts', gate:'external publish', risk:'hard', owner:'Marketing', created_at:'2026-08-26T16:00:00.000Z', summary:'Would publish to selected social accounts. Review targets and copy first.' },
+  { id:'TK-10409-B', title:'Deploy pending website change', gate:'production deploy', risk:'hard', owner:'Engineering', created_at:'2026-08-26T16:05:00.000Z', summary:'Would change the live site. Confirm tests, rollback, and timing.' },
+  { id:'TK-10409-C', title:'Send campaign email', gate:'outbound email', risk:'hard', owner:'Marketing', created_at:'2026-08-26T16:10:00.000Z', summary:'Would contact a list. Confirm audience, consent, and message.' },
+  { id:'TK-10409-D', title:'Run approval dry-run report', gate:'read-only', risk:'soft', owner:'Operations', created_at:'2026-08-26T16:15:00.000Z', summary:'Creates a local report only; no external side effects.' },
+  { id:'TK-10409-E', title:'Update campaign draft copy', gate:'local draft', risk:'soft', owner:'Marketing', created_at:'2026-08-26T16:20:00.000Z', summary:'Local draft edit. Nothing is published until separately approved.' }
 ];
 const queueDir = path.join(process.env.HOME || '/Users/macstudio3', '.claude', 'yolo-queue', 'pending-approval');
 function queueItems(){
   try {
     return fs.readdirSync(queueDir).filter(n=>n.endsWith('.md')||n.endsWith('.json')).map(name=>{
-      const raw=fs.readFileSync(path.join(queueDir,name),'utf8');
+      const itemPath=path.join(queueDir,name);
+      const raw=fs.readFileSync(itemPath,'utf8');
+      const stat=fs.statSync(itemPath);
       const heading=(raw.match(/^#\s+(.+)$/m)||[])[1] || name.replace(/\.[^.]+$/,'').replace(/[-_]+/g,' ');
       const gate=(raw.match(/(?:GATE|gate|approval(?: required)?)[^\n:]*[:\-]\s*([^\n]+)/i)||[])[1] || 'gated approval';
-      return {id:'QUEUE-'+name,title:heading.slice(0,180),gate:gate.slice(0,180),risk:'hard',owner:'pending-approval',summary:raw.replace(/\s+/g,' ').slice(0,360),source:name};
+      return {id:'QUEUE-'+name,title:heading.slice(0,180),gate:gate.slice(0,180),risk:'hard',owner:'pending-approval',created_at:stat.birthtime.toISOString(),summary:raw.replace(/\s+/g,' ').slice(0,360),source:name};
     });
   } catch { return []; }
 }

← fab2e2b record revise decision for TK-10878 disk phase  ·  back to Approval Viewer  ·  verify approval viewer across five local sweeps c103c15 →