← back to Secrets Manager
update-viewer/index.html
113 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Credentials & Tokens — Update Tracker</title>
<style>
:root{--bg:#0d0f13;--panel:#161a21;--ink:#e9e7e2;--muted:#8b919c;--rule:#262b34;--p1:#ef4444;--p2:#fbbf24;--ok:#4ade80;--accent:#9c7a4d}
*{box-sizing:border-box;margin:0;padding:0}
body{background:var(--bg);color:var(--ink);font-family:'Inter',system-ui,-apple-system,sans-serif;-webkit-font-smoothing:antialiased;padding:0 0 60px}
header{position:sticky;top:0;z-index:5;background:rgba(13,15,19,.95);backdrop-filter:blur(8px);border-bottom:1px solid var(--rule);padding:18px 28px}
h1{font-family:'Cormorant Garamond',Georgia,serif;font-weight:600;font-size:27px;letter-spacing:.02em}
.sub{color:var(--muted);font-size:13px;margin-top:3px}
.counts{display:flex;gap:10px;margin-top:12px;flex-wrap:wrap}
.pill{font-size:12px;padding:4px 11px;border-radius:999px;border:1px solid var(--rule);color:var(--muted)}
.pill b{color:var(--ink)}
.wrap{max-width:980px;margin:24px auto;padding:0 24px;display:flex;flex-direction:column;gap:16px}
.card{background:var(--panel);border:1px solid var(--rule);border-radius:12px;padding:18px 20px;border-left:4px solid var(--rule)}
.card.P1{border-left-color:var(--p1)} .card.P2{border-left-color:var(--p2)} .card.prep{border-left-color:var(--muted)} .card.done{opacity:.62}
.top{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
.title{font-size:17px;font-weight:600}
.sev{font-size:10px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;padding:2px 8px;border-radius:5px;border:1px solid currentColor}
.sev.P1{color:var(--p1)}.sev.P2{color:var(--p2)}.sev.prep,.sev.P3{color:var(--muted)}
.type{font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted)}
.togg{margin-left:auto;font-size:12px;font-weight:600;padding:6px 13px;border-radius:7px;border:1px solid var(--rule);background:transparent;color:var(--muted);cursor:pointer}
.togg.outstanding{color:var(--p2);border-color:#fbbf2455}
.togg.done{color:var(--ok);border-color:#4ade8055;background:#4ade8011}
.gates{margin-top:8px;font-size:12px;color:var(--p1);background:#ef444411;border:1px solid #ef444433;border-radius:6px;padding:6px 10px;display:inline-block}
.exposed{margin-top:10px;font-size:12px;color:var(--muted);line-height:1.5}
.exposed b{color:#cbb}
.steps{margin-top:12px;display:flex;flex-direction:column;gap:7px}
.step{display:flex;gap:9px;align-items:flex-start;font-size:12px;color:#c7ccd4}
.step .n{color:var(--accent);font-weight:700;flex-shrink:0}
.cmd{margin-top:6px;background:#0a0c10;border:1px solid var(--rule);border-radius:6px;padding:9px 11px;font-family:'SF Mono',ui-monospace,monospace;font-size:11.5px;color:#9fd;white-space:pre-wrap;word-break:break-all;position:relative;cursor:pointer}
.cmd:hover{border-color:var(--accent)}
.cmd .lbl{position:absolute;top:6px;right:9px;font-size:9px;color:var(--muted);letter-spacing:.1em;text-transform:uppercase}
.mint{display:inline-block;margin-top:10px;font-size:12px;color:#7dd3fc;text-decoration:none;border:1px solid #7dd3fc44;border-radius:6px;padding:5px 11px}
.mint:hover{background:#7dd3fc11}
.runnable{margin-top:10px;font-size:11px;color:var(--ok)}
.row-label{font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted);margin:14px 0 2px}
</style>
</head>
<body>
<header>
<h1>Credentials & Tokens — Update Tracker</h1>
<div class="sub" id="origin">loading…</div>
<div class="counts" id="counts"></div>
</header>
<div class="wrap" id="wrap"></div>
<script>
const esc=s=>String(s==null?'':s).replace(/&/g,'&').replace(/</g,'<').replace(/"/g,'"');
function cmdBox(label,txt){ if(!txt)return ''; return '<div class="cmd" onclick="navigator.clipboard&&navigator.clipboard.writeText(this.dataset.c)" data-c="'+esc(txt)+'"><span class="lbl">'+label+' · copy</span>'+esc(txt)+'</div>'; }
async function load(){
const d=await (await fetch('/api/items')).json();
document.getElementById('origin').textContent=d.origin;
const out=d.items.filter(i=>i.status==='outstanding');
const p1=out.filter(i=>i.severity==='P1').length;
document.getElementById('counts').innerHTML=
'<span class="pill"><b>'+out.length+'</b> outstanding</span>'+
'<span class="pill" style="color:var(--p1)"><b>'+p1+'</b> P1 (blocks Kamatera)</span>'+
'<span class="pill"><b>'+d.items.filter(i=>i.status==='done').length+'</b> done</span>';
const w=document.getElementById('wrap'); w.innerHTML='';
for(const it of d.items){
const div=document.createElement('div');
div.className='card '+it.severity+(it.status==='done'?' done':'');
const steps=(it.console||[]).map((s,i)=>'<div class="step"><span class="n">'+(i+1)+'</span><span>'+esc(s)+'</span></div>').join('');
div.innerHTML=
'<div class="top">'+
'<span class="sev '+it.severity+'">'+esc(it.severity)+'</span>'+
'<span class="title">'+esc(it.title)+'</span>'+
'<span class="type">'+esc(it.type)+'</span>'+
'<button class="togg '+it.status+'" data-id="'+it.id+'" data-st="'+it.status+'">'+(it.status==='done'?'✓ done':'mark done')+'</button>'+
'</div>'+
(it.gates?'<div class="gates">⛔ gates: '+esc(it.gates)+'</div>':'')+
(it.exposed?'<div class="exposed"><b>Exposed:</b> '+esc(it.exposed)+'</div>':'')+
(steps?'<div class="row-label">Console (Steve)</div><div class="steps">'+steps+'</div>':'')+
(it.mintUrl?'<a class="mint" target="_blank" rel="noopener" href="'+esc(it.mintUrl)+'">↗ mint new key</a>':'')+
(it.routeBack?'<div class="row-label">Route back</div>'+cmdBox('route',it.routeBack):'')+
(it.restart?'<div class="row-label">Restart</div>'+cmdBox('restart',it.restart):'')+
(it.verify?'<div class="row-label">Verify</div>'+cmdBox('verify',it.verify):'')+
((it.claudeCanRun&&it.claudeCanRun.length)?'<div class="runnable">⚡ Claude can run: '+it.claudeCanRun.join(', ')+'</div>':'');
w.appendChild(div);
}
// ── To configure (not-set keys) ──
const tc = d.toConfigure || [];
if (tc.length) {
const h = document.createElement('div');
h.className = 'row-label';
h.style.cssText = 'margin:18px 4px 2px;font-size:13px;color:#cbb';
h.textContent = 'To configure — ' + tc.length + ' keys not set';
w.appendChild(h);
for (const k of tc) {
const c = document.createElement('div');
c.className = 'card prep';
c.style.padding = '13px 18px';
c.innerHTML =
'<div class="top"><span class="sev prep">not set</span><span class="title" style="font-size:15px">' + esc(k.name) + '</span></div>' +
'<div class="exposed">' + esc(k.desc) + '</div>' +
(k.mintUrl ? '<a class="mint" target="_blank" rel="noopener" href="' + esc(k.mintUrl) + '">↗ get key</a>' : '') +
cmdBox('route', "printf '" + k.name + "=<VALUE>' | node ~/Projects/secrets-manager/cli.js import-paste");
w.appendChild(c);
}
}
w.querySelectorAll('.togg').forEach(b=>b.onclick=async()=>{
const ns=b.dataset.st==='done'?'outstanding':'done';
await fetch('/api/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:b.dataset.id,status:ns})});
load();
});
}
load();
</script>
</body>
</html>