← back to Draft Viewer
server.js
110 lines
const http=require('http'),fs=require('fs'),path=require('path');
const USER='admin',PASS='DW2024!';
const DATA=path.join(__dirname,'data','board.json');
// Gmail deep link. Steve decided (2026-09-10) that HE clicks Send in Gmail —
// this board never sends, never deletes, it only makes the right draft findable.
const gm=(a,id)=>`https://mail.google.com/mail/u/${a==='info'?'1':'0'}/#drafts/${id}`;
const esc=s=>String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
const when=iso=>iso?new Date(iso).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}):'—';
const BUCKET={
send_now:{t:'SEND THESE',k:'go',n:'Open each and click Send. Nothing here is fired for you.'},
keep:{t:'KEEP AS DRAFTS',k:'keep',n:'Your call: “put in drafts.” Not deleted, not sent, bodies unedited.'},
superseded:{t:'SUPERSEDED — NO ACTION',k:'old',n:'Duplicates and wrong-scope asks already replaced. Inert on steve-office.'}
};
function card(x){
// days_left only exists for info@, the one account the 04:15 drainer purges.
const fuse = x.days_left!=null && x.days_left<=7
? `<span class="fuse${x.days_left<=4?' hot':''}">${x.days_left}d until permanent delete</span>`:'';
return `<article class="c ${x.bucket}${x.fuse?' isfuse':''}">
<header><span class="v">${esc(x.vendor||(x.to||'').split('@')[0]||'—')}</span>
<span class="acct">${esc(x.account)}</span>${fuse}</header>
<p class="s">${esc(x.subject)}</p>
${x.to?`<p class="to">→ ${esc(x.to)}</p>`:''}
${x.gap?`<p class="gap">${esc(x.gap)}</p>`:''}
${x.why?`<p class="why">${esc(x.why)}</p>`:''}
<p class="pv">${esc(x.preview)}…</p>
<footer>
<time title="${esc(x.created||'')}">🕓 ${esc(when(x.created))} · ${esc(x.age_days)}d old</time>
<a href="${gm(x.account,x.id)}" target="_blank" rel="noopener noreferrer">open in gmail →</a>
</footer>
<code title="snapshot on disk: ${esc(x.snapshot_file)}">💾 ${esc(x.id)}</code></article>`;
}
function page(d){
const g=b=>d.rows.filter(r=>r.bucket===b);
const sec=b=>{const rows=g(b),m=BUCKET[b];return !rows.length?'':`
<section><h2 class="${m.k}">${m.t} <b>${rows.length}</b></h2>
<p class="note">${esc(m.n)}</p><p class="note dim">${esc(d.decisions[b])}</p>
<div class="grid">${rows.map(card).join('')}</div></section>`;};
const hot=g('send_now').filter(r=>r.days_left!=null&&r.days_left<=7)
.sort((a,b)=>a.days_left-b.days_left);
return `<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1">
<title>TK-11231 drafts — ${g('send_now').length} to send</title><style>
:root{--bg:#0e0e10;--fg:#e8e6e3;--dim:#8b8781;--line:#26262a;--go:#3fb950;--keep:#58a6ff;--old:#6e7681;--hot:#f85149;--warn:#d29922;--cols:3}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.55 ui-sans-serif,-apple-system,system-ui,sans-serif}
.wrap{max-width:1500px;margin:0 auto;padding:28px 22px 60px}
h1{font-size:15px;letter-spacing:.16em;text-transform:uppercase;color:var(--dim);font-weight:600;margin:0 0 4px}
.sub{color:var(--dim);font-size:12.5px;margin:0 0 18px}
.alarm{border:1px solid var(--hot);background:#f851491a;border-radius:10px;padding:14px 16px;margin:0 0 24px}
.alarm h3{margin:0 0 8px;font-size:13px;letter-spacing:.1em;text-transform:uppercase;color:var(--hot)}
.alarm ul{margin:0;padding-left:18px}.alarm a{color:var(--fg)}
.safe{border:1px solid var(--line);border-left:3px solid var(--go);border-radius:8px;padding:11px 14px;margin:0 0 24px;color:var(--dim);font-size:12.5px}
.safe b{color:var(--go)}
.bar{display:flex;gap:14px;align-items:center;margin:0 0 22px;color:var(--dim);font-size:12px}
.bar input{accent-color:var(--keep)}
h2{font-size:12.5px;letter-spacing:.14em;text-transform:uppercase;margin:30px 0 4px;font-weight:600}
h2 b{font-weight:600;opacity:.6}
h2.go{color:var(--go)}h2.keep{color:var(--keep)}h2.old{color:var(--old)}
.note{color:var(--dim);font-size:12.5px;margin:0 0 4px}.note.dim{opacity:.62;margin-bottom:14px}
.grid{display:grid;grid-template-columns:repeat(var(--cols),minmax(0,1fr));gap:12px}
.c{border:1px solid var(--line);border-radius:9px;padding:13px 14px;background:#141417;display:flex;flex-direction:column;gap:6px;min-width:0}
.c.send_now{border-left:3px solid var(--go)}.c.keep{border-left:3px solid var(--keep)}
.c.superseded{border-left:3px solid var(--old);opacity:.62}
.c.isfuse{border-color:var(--hot);background:#17100f}
header{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.v{font-weight:600;font-size:13px}
.acct{font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--dim);border:1px solid var(--line);border-radius:20px;padding:1px 7px}
.fuse{font-size:10.5px;color:var(--warn);border:1px solid var(--warn);border-radius:20px;padding:1px 7px}
.fuse.hot{color:var(--hot);border-color:var(--hot)}
.s{margin:0;font-size:13px;line-height:1.4}
.to{margin:0;font-size:12px;color:var(--keep);word-break:break-all}
.gap,.why{margin:0;font-size:12px;color:var(--dim)}
.pv{margin:4px 0 0;font-size:12px;color:var(--dim);opacity:.8;border-left:2px solid var(--line);padding-left:9px}
footer{display:flex;justify-content:space-between;gap:10px;align-items:baseline;margin-top:auto;padding-top:8px;flex-wrap:wrap}
time{font-size:11px;color:var(--dim)}
footer a{font-size:12px;color:var(--go);text-decoration:none;white-space:nowrap}footer a:hover{text-decoration:underline}
code{font-size:10px;color:var(--dim);opacity:.5}
@media(max-width:900px){.grid{grid-template-columns:1fr}}
</style><div class=wrap>
<h1>TK-11231 · unsent draft graveyard</h1>
<p class=sub>Decided ${esc(d.decided_at)} · built ${esc(when(d.built_at))} · this board never sends and never deletes.</p>
${hot.length?`<div class=alarm><h3>⏳ Permanently deleted in ${hot[0].days_left} days — no Trash</h3>
<ul>${hot.map(r=>`<li><b>${r.days_left}d</b> — <a href="${gm(r.account,r.id)}" target="_blank" rel="noopener noreferrer">${esc(r.to)}</a> — ${esc(r.subject)}</li>`).join('')}</ul></div>`:''}
<div class=safe><b>The text is safe either way.</b> All ${d.rows.length} draft bodies are snapshotted to
<code>data/bodies/</code>, so even if the 04:15 drainer destroys one, the words survive on disk.</div>
<div class=bar><label>density <input type=range min=1 max=5 value=3 id=dz></label>
<span>${d.counts.send_now||0} to send · ${d.counts.keep||0} kept · ${d.counts.superseded||0} superseded</span></div>
${sec('send_now')}${sec('keep')}${sec('superseded')}
</div><script>
var dz=document.getElementById('dz'),K='tk11231.cols';
function ap(v){document.documentElement.style.setProperty('--cols',v);dz.value=v;try{localStorage.setItem(K,v)}catch(e){}}
try{var s=localStorage.getItem(K);if(s)ap(s)}catch(e){}
dz.addEventListener('input',function(){ap(dz.value)});
</script>`;
}
http.createServer((req,res)=>{
const h=req.headers.authorization||'';
const ok=h.startsWith('Basic ')&&Buffer.from(h.slice(6),'base64').toString()===USER+':'+PASS;
if(!ok){res.writeHead(401,{'WWW-Authenticate':'Basic realm="drafts"'});return res.end('auth required');}
let d;try{d=JSON.parse(fs.readFileSync(DATA,'utf8'));}catch(e){res.writeHead(500);return res.end('board.json missing — run: node build-board.mjs');}
if(req.url.startsWith('/api/board')){res.writeHead(200,{'content-type':'application/json'});return res.end(JSON.stringify(d));}
res.writeHead(200,{'content-type':'text/html; charset=utf-8'});res.end(page(d));
}).listen(0,'127.0.0.1',function(){console.log('PORT='+this.address().port);});