← 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 &amp; 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,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;');
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>