← back to Approvals Viewer

server.js

78 lines

#!/usr/bin/env node
// Approvals yes/no swipe viewer — zero-dependency. Basic-auth admin/DW2024!.
const http=require('http'),fs=require('fs'),path=require('path');
// TK-11685 approval-time freshness re-check (READ-ONLY, ADVISORY). Never mutates a memo.
let freshnessGuard=null;try{freshnessGuard=require('./freshness-guard');}catch(e){}
const QUEUE=path.join(process.env.HOME,'.claude/yolo-queue/pending-approval');
const APPROVED=path.join(QUEUE,'_approved'),REJECTED=path.join(QUEUE,'_rejected');
const LOG=path.join(QUEUE,'_decisions.jsonl');
const RATINGS_FILE=path.join(__dirname,'ratings.json');
const USER='admin',PASS='DW2024!';
for(const d of [APPROVED,REJECTED]) fs.mkdirSync(d,{recursive:true});

function loadRatings(){try{return JSON.parse(fs.readFileSync(RATINGS_FILE,'utf8'));}catch{return {};}}
function saveRatings(r){fs.writeFileSync(RATINGS_FILE,JSON.stringify(r,null,2));}
function categorize(name){const n=name.toLowerCase();
 if(/tk10045|tk-10045|critical-incident|credential|godaddy|alert-|secret-rotat|tk-10024|security-harden|git-history|blocker-a|rotation/.test(n))return{key:'security',label:'\u{1F534} Security / rotation'};
 if(/golive|go-live|deploy|cutover|consolidation|homesonspec|spechomes|consulting-portal|nineoh|testflight|rubikscube/.test(n))return{key:'golive',label:'\u{1F7E0} Go-live / deploy'};
 if(/kravet|eur-|reprice|reid-witlin|rebel|tk10029|sku-collision|debadge|prwn|banned-word|fentucci|versa|stale-needs|imageless|image-less|quote-only|commercial-use|maharam|stout|osborne|momentum|greenland|astek|pr-quote|price/.test(n))return{key:'catalog',label:'\u{1F7E1} Catalog / pricing'};
 if(/linkedin|tiktok|ig-tiktok|cc-fm|cc-import|marketing|reels|social/.test(n))return{key:'marketing',label:'\u{1F7E2} Marketing / send'};
 if(/usre|usrealestate|rentv|houston|foia|parcel|firm-resolve|listing/.test(n))return{key:'project',label:'\u{1F535} usre / rentv'};
 if(/gmc|dedup|cadence|model-arena|swap-pressure|shopify-storage|fm-mfr/.test(n))return{key:'misc',label:'⚪ GMC / recurring'};
 return{key:'other',label:'⚫ Other'};}
function listMemos(sort='mtime'){
 const ratings=loadRatings();
 const items=fs.readdirSync(QUEUE).filter(f=>f.endsWith('.md')&&!f.startsWith('_')).map(f=>{
  const fp=path.join(QUEUE,f),st=fs.statSync(fp),body=fs.readFileSync(fp,'utf8');
  const title=(body.match(/^#\s+(.+)$/m)||[,f.replace(/\.md$/,'')])[1].slice(0,140);
  const r=ratings[f]||{};
  return{file:f,title,category:categorize(f),mtime:st.mtime.toISOString(),size:st.size,body,rating:r.rating||0,ratingNote:r.note||''};
 });
 if(sort==='rating') return items.sort((a,b)=>b.rating-a.rating||new Date(b.mtime)-new Date(a.mtime));
 if(sort==='category') return items.sort((a,b)=>a.category.key.localeCompare(b.category.key)||new Date(b.mtime)-new Date(a.mtime));
 if(sort==='security') return items.sort((a,b)=>{const s=x=>x.category.key==='security'?0:1;return s(a)-s(b)||new Date(b.mtime)-new Date(a.mtime);});
 return items.sort((a,b)=>new Date(b.mtime)-new Date(a.mtime));
}
function send(res,c,b,t='application/json'){res.writeHead(c,{'Content-Type':t});res.end(b);}
http.createServer((req,res)=>{
 // /healthz stays open (uptime/keep-alive probes); everything else requires Basic auth.
 // NOTE: exposed publicly via CF tunnel at approvals.agentabrams.com — auth MUST stay on.
 if(req.url==='/healthz')return send(res,200,'ok','text/plain');
 const hdr=req.headers.authorization||'';
 if(!(hdr.startsWith('Basic ')&&Buffer.from(hdr.slice(6),'base64').toString()===USER+':'+PASS)){
   res.writeHead(401,{'WWW-Authenticate':'Basic realm="Approval Command Center"'});return res.end('auth required');}
 if(req.url==='/'||req.url==='/index.html')return send(res,200,fs.readFileSync(path.join(__dirname,'public/index.html')),'text/html');
 if(req.url.startsWith('/api/memos')){const u=new URL(req.url,'http://x');return send(res,200,JSON.stringify(listMemos(u.searchParams.get('sort')||'mtime')));}
 // READ-ONLY advisory: approval-time freshness re-check for one memo. Surfaces
 // "N of M targets are now stale" so Steve never approves a delete list blind.
 // It only READS; it never approves/rejects/edits/moves/executes anything.
 if(req.url.startsWith('/api/freshness')){const u=new URL(req.url,'http://x');const file=u.searchParams.get('file')||'';
   if(!/^[\w.\-]+\.md$/.test(file))return send(res,400,'{"error":"bad file"}');
   if(!freshnessGuard)return send(res,200,'{"verdict":"UNAVAILABLE","status":"WARN","reason":"freshness-guard module not loaded"}');
   try{const rpt=freshnessGuard.checkMemo(path.join(QUEUE,file));return send(res,200,JSON.stringify(rpt));}
   catch(e){return send(res,200,JSON.stringify({verdict:'NOT_MEASURED',status:'WARN',reason:String(e.message)}));}}
 if(req.url==='/api/rate'&&req.method==='POST'){let d='';req.on('data',c=>d+=c);req.on('end',()=>{try{
   const{file,rating,note}=JSON.parse(d);if(!/^[\w.\-]+\.md$/.test(file))throw new Error('bad file');
   const r=loadRatings();r[file]={rating:Math.min(5,Math.max(0,parseInt(rating)||0)),note:String(note||'').slice(0,200),ts:new Date().toISOString()};saveRatings(r);
   send(res,200,'{"ok":true}');
 }catch(e){send(res,400,JSON.stringify({error:String(e.message)}));}}); return;}
 if(req.url==='/api/decide'&&req.method==='POST'){let d='';req.on('data',c=>d+=c);req.on('end',()=>{try{
   const{file,decision}=JSON.parse(d);if(!/^[\w.\-]+\.md$/.test(file))throw new Error('bad file');
   const src=path.join(QUEUE,file);if(!fs.existsSync(src))throw new Error('gone');
   let dest=decision==='approve'?path.join(APPROVED,file):decision==='reject'?path.join(REJECTED,file):null;
   if(dest)fs.renameSync(src,dest);
   fs.appendFileSync(LOG,JSON.stringify({ts:new Date().toISOString(),file,decision})+'\n');send(res,200,'{"ok":true}');
 }catch(e){send(res,400,JSON.stringify({error:String(e.message)}));}});return;}
 if(req.url==='/api/undo'&&req.method==='POST'){let d='';req.on('data',c=>d+=c);req.on('end',()=>{try{
   const{file,decision}=JSON.parse(d);if(!/^[\w.\-]+\.md$/.test(file))throw new Error('bad file');if(decision!=='approve'&&decision!=='reject')throw new Error('bad decision');const from=decision==='approve'?APPROVED:REJECTED;const src=path.join(from,file);
   if(fs.existsSync(src))fs.renameSync(src,path.join(QUEUE,file));
   fs.appendFileSync(LOG,JSON.stringify({ts:new Date().toISOString(),file,decision:'undo:'+decision})+'\n');send(res,200,'{"ok":true}');
 }catch(e){send(res,400,JSON.stringify({error:String(e.message)}));}});return;}
 // static assets under public/ (e.g. /nav-agent/nav-agent.js|css) — additive, path-traversal-guarded
 {const MIME={'.js':'text/javascript','.css':'text/css','.html':'text/html','.json':'application/json','.png':'image/png','.jpg':'image/jpeg','.svg':'image/svg+xml','.ico':'image/x-icon','.woff2':'font/woff2'};
  const pub=path.join(__dirname,'public'),urlPath=decodeURIComponent(req.url.split('?')[0]),fp=path.normalize(path.join(pub,urlPath));
  if(fp.startsWith(pub+path.sep)&&fs.existsSync(fp)&&fs.statSync(fp).isFile())
    return send(res,200,fs.readFileSync(fp),MIME[path.extname(fp).toLowerCase()]||'application/octet-stream');}
 send(res,404,'nf','text/plain');
}).listen(process.env.PORT||9795,'127.0.0.1',function(){const port=this.address().port;fs.writeFileSync(path.join(__dirname,'.port'),String(port));console.log('approvals-viewer http://127.0.0.1:'+port+' (admin/DW2024!)');});