← back to Api Token Dashboard

public/index.html

116 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>API &amp; Token Connections</title>
<style>
  :root{--bg:#0f1216;--card:#171b22;--line:#252b34;--fg:#e6e9ee;--dim:#93a0b0;
        --ok:#2ecc71;--dead:#ff5252;--missing:#ffb020;--unknown:#7f8ea3;--accent:#34f4ab}
  *{box-sizing:border-box}
  body{margin:0;background:var(--bg);color:var(--fg);font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}
  header{position:sticky;top:0;background:linear-gradient(#0f1216,#0f1216ee);backdrop-filter:blur(6px);
         padding:18px 24px;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:16px;flex-wrap:wrap;z-index:5}
  h1{font-size:18px;margin:0;font-weight:650;letter-spacing:.2px}
  .counts{display:flex;gap:10px;flex-wrap:wrap;font-size:12.5px}
  .pill{padding:3px 10px;border-radius:999px;border:1px solid var(--line);white-space:nowrap}
  .pill b{font-variant-numeric:tabular-nums}
  .c-ok{color:var(--ok)} .c-dead{color:var(--dead)} .c-missing{color:var(--missing)} .c-unknown{color:var(--unknown)}
  button{background:var(--accent);color:#04140d;border:0;padding:8px 16px;border-radius:8px;font-weight:650;cursor:pointer}
  button.ghost{background:transparent;color:var(--fg);border:1px solid var(--line)}
  main{padding:20px 24px 60px;max-width:1150px;margin:0 auto}
  .filters{display:flex;gap:8px;margin:0 0 18px;flex-wrap:wrap}
  .filters .ghost.active{border-color:var(--accent);color:var(--accent)}
  .cat{margin:26px 0 10px;font-size:12px;text-transform:uppercase;letter-spacing:1.4px;color:var(--dim)}
  .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:14px}
  .card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:15px 16px;display:flex;flex-direction:column;gap:9px}
  .card.dead{border-color:#5a2222}.card.missing{border-color:#5a4718}
  .top{display:flex;align-items:center;justify-content:space-between;gap:10px}
  .name{font-weight:620}
  .tag{font-size:12px;color:var(--dim);font-variant-numeric:tabular-nums}
  .status{display:inline-flex;align-items:center;gap:6px;font-size:12.5px;font-weight:650;padding:3px 9px;border-radius:999px;border:1px solid var(--line)}
  .dot{width:8px;height:8px;border-radius:50%}
  .s-connected .dot{background:var(--ok)} .s-connected{color:var(--ok)}
  .s-dead .dot{background:var(--dead)} .s-dead{color:var(--dead)}
  .s-missing .dot{background:var(--missing)} .s-missing{color:var(--missing)}
  .s-unknown .dot{background:var(--unknown)} .s-unknown{color:var(--unknown)}
  .detail{font-size:12px;color:var(--dim)}
  .instructions{font-size:12.5px;color:#c4ccd6;border-top:1px dashed var(--line);padding-top:9px}
  .actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
  a.fix{font-size:12.5px;color:var(--accent);text-decoration:none;border:1px solid var(--line);padding:5px 10px;border-radius:7px}
  a.fix:hover{border-color:var(--accent)}
  code{background:#0c0f13;padding:1px 6px;border-radius:5px;font-size:12px;color:#c8d2dd}
  .muted{color:var(--dim);font-size:12.5px}
  .ts{font-size:11.5px;color:var(--dim)}
</style>
</head>
<body>
<header>
  <h1>🔌 API &amp; Token Connections</h1>
  <div class="counts" id="counts"></div>
  <div style="flex:1"></div>
  <span class="ts" id="ts"></span>
  <button id="recheck" onclick="load()">Re-check all</button>
</header>
<main>
  <div class="filters" id="filters">
    <button class="ghost active" data-f="all">All</button>
    <button class="ghost" data-f="action">⚠︎ Needs action</button>
    <button class="ghost" data-f="connected">Connected</button>
  </div>
  <div id="body"><p class="muted">Probing connections…</p></div>
</main>
<script>
let DATA=[], FILTER='all';
const SEV={dead:0,missing:1,unknown:2,connected:3};
function esc(s){return (s||'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]))}
async function load(){
  document.getElementById('recheck').textContent='Checking…';
  try{
    const r=await fetch('/api/status'); const j=await r.json();
    DATA=j.results.sort((a,b)=>SEV[a.status]-SEV[b.status]||a.name.localeCompare(b.name));
    document.getElementById('ts').textContent='updated '+new Date(j.generatedAt).toLocaleString();
    render();
  }catch(e){ document.getElementById('body').innerHTML='<p class="muted">Failed to load status: '+esc(e.message)+'</p>'; }
  document.getElementById('recheck').textContent='Re-check all';
}
function render(){
  const by={connected:0,dead:0,missing:0,unknown:0};
  DATA.forEach(d=>by[d.status]++);
  document.getElementById('counts').innerHTML=
    `<span class="pill c-ok">✅ Connected <b>${by.connected}</b></span>`+
    `<span class="pill c-dead">❌ Dead <b>${by.dead}</b></span>`+
    `<span class="pill c-missing">⚠︎ Missing <b>${by.missing}</b></span>`+
    `<span class="pill c-unknown">• Unknown <b>${by.unknown}</b></span>`;
  let rows=DATA;
  if(FILTER==='action') rows=DATA.filter(d=>d.status==='dead'||d.status==='missing');
  if(FILTER==='connected') rows=DATA.filter(d=>d.status==='connected');
  const cats=[...new Set(rows.map(d=>d.category))];
  const html=cats.map(cat=>{
    const items=rows.filter(d=>d.category===cat);
    return `<div class="cat">${esc(cat)}</div><div class="grid">`+items.map(card).join('')+`</div>`;
  }).join('');
  document.getElementById('body').innerHTML=html||'<p class="muted">Nothing in this filter.</p>';
}
function card(d){
  const cls=d.status==='connected'?'':d.status;
  const label={connected:'Connected',dead:'Disconnected',missing:'Not set',unknown:'Unknown'}[d.status];
  const showFix=d.status!=='connected';
  return `<div class="card ${cls}">
    <div class="top"><span class="name">${esc(d.name)}</span>
      <span class="status s-${d.status}"><span class="dot"></span>${label}</span></div>
    <div class="detail">${d.tag?`<code>${esc(d.envName)}</code> ${esc(d.tag)}`:`<code>${esc(d.envName||'—')}</code>`}${d.detail?` · ${esc(d.detail)}`:''}</div>
    ${showFix?`<div class="instructions">${esc(d.instructions)}</div>
    <div class="actions"><a class="fix" href="${esc(d.fixUrl)}" target="_blank" rel="noopener noreferrer">Open fix ↗</a></div>`:''}
  </div>`;
}
document.getElementById('filters').addEventListener('click',e=>{
  const b=e.target.closest('button'); if(!b)return;
  [...document.querySelectorAll('#filters button')].forEach(x=>x.classList.remove('active'));
  b.classList.add('active'); FILTER=b.dataset.f; render();
});
load();
</script>
</body>
</html>