← 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 & 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 & 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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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>