← back to Ungate Console

index.html

173 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<script>
// Credential-safe fetch guard (fleet drop-in) — if this page is opened with
// credentials in the URL (a saved bookmark / Chrome-remembered basic-auth:
// https://user:pass@host/…), the browser poisons document.baseURI, so any
// relative or root-relative fetch('/api/…') throws "Request cannot be constructed
// from a URL that includes credentials" and callers silently fall to an empty
// state. Resolve every non-absolute request URL against the credential-free
// location instead of baseURI. Placed first so it wraps window.fetch before any
// app script runs. Ref: creds-in-url-fetch-guard-fleet-pattern.
(function () {
  var _fetch = window.fetch.bind(window);
  var cleanBase = function () { return location.origin + location.pathname; };
  window.fetch = function (input, init) {
    try {
      if (typeof input === 'string' && !/^[a-z]+:\/\//i.test(input) && input.indexOf('//') !== 0) {
        input = new URL(input, cleanBase()).href;
      } else if (input instanceof Request && !/^[a-z]+:\/\//i.test(input.url)) {
        input = new Request(new URL(input.url, cleanBase()).href, input);
      }
    } catch (_) { /* fall through to native */ }
    return _fetch(input, init);
  };
})();
</script>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Ungate Console</title>
<style>
  :root{ --bg:#0f1115; --card:#1a1d24; --card2:#20242d; --ink:#e8eaed; --dim:#9aa0ab;
    --line:#2c313b; --steve:#ff5252; --ext:#ffb300; --agent:#7c8794; --chip:#2a3f5f; --accent:#4dd0a0; }
  *{box-sizing:border-box}
  body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}
  header{position:sticky;top:0;z-index:5;background:linear-gradient(180deg,#12151b,#0f1115);border-bottom:1px solid var(--line);padding:16px 22px}
  h1{margin:0;font-size:20px;letter-spacing:.2px}
  .sub{color:var(--dim);font-size:13px;margin-top:3px}
  .counts{display:flex;gap:18px;margin-top:10px;flex-wrap:wrap}
  .count{font-size:13px;color:var(--dim)}
  .count b{font-size:22px;color:var(--ink);display:block}
  main{max-width:1100px;margin:0 auto;padding:22px}
  .sec{margin:28px 0 8px;display:flex;align-items:center;gap:10px}
  .sec h2{margin:0;font-size:16px}
  .sec .n{background:var(--card2);border:1px solid var(--line);border-radius:20px;padding:2px 11px;font-size:13px;color:var(--dim)}
  .dot{width:11px;height:11px;border-radius:50%}
  .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(330px,1fr));gap:12px}
  .card{background:var(--card);border:1px solid var(--line);border-left:4px solid var(--agent);border-radius:12px;padding:14px 15px}
  .card.steve{border-left-color:var(--steve)}
  .card.external{border-left-color:var(--ext)}
  .card.agent{border-left-color:var(--agent);opacity:.9}
  .top{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:6px}
  .tk{font-family:ui-monospace,Menlo,monospace;font-size:12px;color:var(--accent);font-weight:600}
  .type{font-size:10.5px;text-transform:uppercase;letter-spacing:.4px;color:var(--dim);border:1px solid var(--line);border-radius:6px;padding:2px 7px;white-space:nowrap}
  .title{font-weight:600;font-size:14.5px;margin:2px 0 4px}
  .proj{display:inline-block;background:var(--chip);color:#cfe0ff;font-size:11px;border-radius:5px;padding:1px 7px;margin-bottom:8px}
  .action{font-size:13.5px;color:var(--ink)}
  .action b{color:var(--accent)}
  .paste{margin-top:9px;background:#0b0d11;border:1px solid var(--line);border-radius:7px;padding:8px 10px;font-family:ui-monospace,Menlo,monospace;font-size:11.5px;color:#cfe0ff;position:relative;overflow-x:auto;white-space:pre}
  .copy{position:absolute;top:5px;right:5px;background:var(--card2);border:1px solid var(--line);color:var(--dim);border-radius:6px;font-size:10.5px;padding:2px 7px;cursor:pointer}
  .copy:hover{color:var(--ink)}
  .hint{color:var(--dim);font-size:12.5px;margin:2px 0 14px}
  .btns{display:flex;gap:7px;margin-top:11px}
  .btn{flex:1;border:1px solid var(--line);background:var(--card2);color:var(--dim);border-radius:7px;padding:6px 0;font-size:12px;font-weight:600;cursor:pointer;text-align:center}
  .btn:hover{color:var(--ink)}
  .btn.go.on{background:#0f5132;border-color:#1a7f4f;color:#7dffbb}
  .btn.defer.on{background:#5a4a00;border-color:#8a7200;color:#ffe08a}
  .btn.close.on{background:#5a1f1f;border-color:#8a3030;color:#ff9d9d}
  .card.decided{outline:1px solid var(--accent)}
  .bar{position:sticky;bottom:0;background:linear-gradient(0deg,#12151b,rgba(18,21,27,.94));border-top:1px solid var(--line);padding:12px 22px;display:flex;justify-content:space-between;align-items:center;z-index:6}
  .bar .msg{font-size:13.5px}
  .bar b{color:var(--accent)}
  .bar button{background:var(--accent);color:#04120c;border:0;border-radius:8px;padding:9px 16px;font-weight:700;font-size:13px;cursor:pointer}
  .bar button:disabled{opacity:.4;cursor:default}
  @media (prefers-color-scheme:light){
    :root{--bg:#f5f6f8;--card:#fff;--card2:#eef1f5;--ink:#1a1d24;--dim:#5b626e;--line:#dde1e8;--chip:#dce9ff}
    header{background:linear-gradient(180deg,#fff,#f5f6f8)}
    .paste{background:#f0f2f5;color:#1a3a6b}
  }
</style>
</head>
<body>
<header>
  <h1>🔓 Ungate Console</h1>
  <div class="sub" id="sub">loading…</div>
  <div class="counts" id="counts"></div>
</header>
<main>
  <div class="sec"><span class="dot" style="background:var(--steve)"></span><h2>You must act</h2><span class="n" id="n-steve">0</span></div>
  <div class="hint">Each of these is blocked ONLY on you — an approval, a credential, a value, a paste, an identity click, or a decision. These are the ones that shrink the wall.</div>
  <div class="grid" id="g-steve"></div>

  <div class="sec"><span class="dot" style="background:var(--ext)"></span><h2>Waiting on vendors / Apple</h2><span class="n" id="n-external">0</span></div>
  <div class="hint">Nothing you can do — these move only when the outside party responds.</div>
  <div class="grid" id="g-external"></div>

  <div class="sec"><span class="dot" style="background:var(--agent)"></span><h2>Agents will finish (not yours)</h2><span class="n" id="n-agent">0</span></div>
  <div class="hint">Open engineering / loop work parked here — the automation can complete these without you.</div>
  <div class="grid" id="g-agent"></div>
</main>
<div class="bar">
  <div class="msg" id="barmsg">No selections yet — click <b>GO</b>, <b>Defer</b>, or <b>Close</b> on any card.</div>
  <button id="tell" disabled onclick="copyForClaude()">Copy decisions for Claude</button>
</div>
<script>
function esc(s){return (s||'').replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]))}
const DEC = {};        // tk -> decision
const ITEMS = {};      // tk -> item
function decide(tk, choice){
  DEC[tk] = choice;
  fetch('/api/decide',{method:'POST',headers:{'Content-Type':'application/json'},
    body:JSON.stringify({tk,decision:choice})}).catch(()=>{});
  const el=document.getElementById('card-'+tk);
  el.classList.add('decided');
  ['go','defer','close'].forEach(c=>el.querySelector('.btn.'+c).classList.toggle('on',c===choice));
  refreshBar();
}
function refreshBar(){
  const n=Object.keys(DEC).length;
  const msg=document.getElementById('barmsg'), btn=document.getElementById('tell');
  if(!n){ msg.innerHTML='No selections yet — click <b>GO</b>, <b>Defer</b>, or <b>Close</b> on any card.'; btn.disabled=true; return; }
  const tally={go:0,defer:0,close:0}; Object.values(DEC).forEach(c=>tally[c]++);
  msg.innerHTML=`<b>${n}</b> selected — ${tally.go} GO · ${tally.defer} defer · ${tally.close} close. Then tell Claude "process the console" (or copy →).`;
  btn.disabled=false;
}
function copyForClaude(){
  const lines=Object.entries(DEC).map(([tk,c])=>`${tk} ${c.toUpperCase()}`);
  navigator.clipboard.writeText('process the console decisions:\n'+lines.join('\n'));
  document.getElementById('tell').textContent='copied ✓';
  setTimeout(()=>document.getElementById('tell').textContent='Copy decisions for Claude',1500);
}
function card(it){
  ITEMS[it.tk]=it;
  const paste = it.paste ? `<div class="paste"><button class="copy" onclick="navigator.clipboard.writeText(this.parentNode.dataset.p);this.textContent='copied'">copy</button>${esc(it.paste)}</div>` : '';
  const el = document.createElement('div');
  el.className = 'card '+it.bucket; el.id='card-'+it.tk;
  el.innerHTML = `<div class="top"><span class="tk">${esc(it.tk)}</span><span class="type">${esc(it.type)}</span></div>
    <div class="title">${esc(it.title)}</div>
    <span class="proj">${esc(it.project)}</span>
    <div class="action">→ ${esc(it.action)}</div>${paste}
    <div class="btns">
      <button class="btn go" onclick="decide('${it.tk}','go')">✓ GO</button>
      <button class="btn defer" onclick="decide('${it.tk}','defer')">⏸ Defer</button>
      <button class="btn close" onclick="decide('${it.tk}','close')">✕ Close</button>
    </div>`;
  if(it.paste){ el.querySelector('.paste').dataset.p = it.paste; }
  return el;
}
Promise.all([fetch('/api/items').then(r=>r.json()), fetch('/api/decisions').then(r=>r.json()).catch(()=>({}))])
.then(([d,saved])=>{
  const by={steve:[],external:[],agent:[]};
  d.items.forEach(it=>by[it.bucket].push(it));
  ['steve','external','agent'].forEach(b=>{
    const g=document.getElementById('g-'+b);
    by[b].forEach(it=>g.appendChild(card(it)));
    document.getElementById('n-'+b).textContent=by[b].length;
  });
  // restore previously-saved decisions
  Object.values(saved||{}).forEach(r=>{ if(document.getElementById('card-'+r.tk)){ DEC[r.tk]=r.decision;
    const el=document.getElementById('card-'+r.tk); el.classList.add('decided');
    ['go','defer','close'].forEach(c=>el.querySelector('.btn.'+c).classList.toggle('on',c===r.decision)); }});
  refreshBar();
  document.getElementById('sub').textContent =
    `${d.items.length} blocked tickets · generated ${new Date(d.generated).toLocaleString(undefined,{month:'short',day:'numeric',hour:'numeric',minute:'2-digit'})}`;
  document.getElementById('counts').innerHTML =
    `<div class="count"><b style="color:var(--steve)">${by.steve.length}</b>need you</div>`+
    `<div class="count"><b style="color:var(--ext)">${by.external.length}</b>vendors/Apple</div>`+
    `<div class="count"><b style="color:var(--agent)">${by.agent.length}</b>agents</div>`;
});
</script>
</body>
</html>