vidyax-Coder / templates /admin.html
NADEV23's picture
fix(admin): buka akses dashboard tanpa password + rapikan HTML
037fa97
Raw
History Blame Contribute Delete
5.21 kB
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bord AI — Monitoring</title>
<style>
* { box-sizing: border-box; }
body { margin: 0; background: #0d1117; color: #c9d1d9; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13px; }
a { color: #58a6ff; }
.container { max-width: 1200px; margin: 0 auto; padding: 16px; }
.header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #30363d; padding: 12px 0; }
.header h1 { margin: 0; font-size: 16px; }
.badge { font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid #30363d; }
.metrics { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin-top: 16px; }
@media (max-width: 640px) { .metrics { grid-template-columns: 1fr 1fr; } }
.card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 12px; }
.card .label { color: #8b949e; font-size: 12px; text-transform: uppercase; }
.card .value { font-size: 22px; margin-top: 4px; }
.events { margin-top: 18px; background: #161b22; border: 1px solid #30363d; border-radius: 8px; height: calc(100vh - 240px); max-height: 540px; overflow: hidden; display: flex; flex-direction: column; }
.events-header { padding: 10px 12px; border-bottom: 1px solid #30363d; display: flex; justify-content: space-between; color: #d29922; }
.table { overflow-y: auto; flex: 1; }
table { width: 100%; border-collapse: collapse; font-size: 12px; table-layout: fixed; }
thead th { position: sticky; top: 0; background: #161b22; text-align: left; color: #8b949e; font-weight: 500; padding: 6px 8px; border-bottom: 1px solid #30363d; }
tbody tr { border-bottom: 1px solid #21262d; }
tbody tr:hover { background: #1c2129; }
td { padding: 5px 8px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.col-ago { width: 80px; }
.col-type { width: 90px; }
.dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; }
.dot-ok { background: #2ea043; }
.dot-chat { background: #1f6feb; }
.dot-sys { background: #d29922; }
.dot-warn { background: #f85149; }
.error { color: #f85149; margin-top: 12px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Bord AI · Monitoring</h1>
<div class="badge" id="status" style="color:#2ea043">● live</div>
</div>
<div id="app">
<div class="metrics">
<div class="card"><div class="label">Chat</div><div class="value" id="m-chat">0</div></div>
<div class="card"><div class="label">Token</div><div class="value" id="m-token">0</div></div>
<div class="card"><div class="label">Foto</div><div class="value" id="m-foto">0</div></div>
<div class="card"><div class="label">Sesi aktif</div><div class="value" id="m-sessions">0</div></div>
</div>
<div class="events">
<div class="events-header"><div>Event stream</div></div>
<div class="table">
<table>
<thead>
<tr><th class="col-ago">Waktu</th><th class="col-type">Tipe</th><th>Konteks</th></tr>
</thead>
<tbody id="rows"></tbody>
</table>
</div>
</div>
</div>
</div>
<script>
const rows = document.getElementById('rows');
function escapeHtml(s){ return s.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"); }
function appendRow(ev){ const tr=document.createElement('tr'); tr.innerHTML=`<td>${escapeHtml(ev.elapsed||'-')}</td><td><span class="dot ${dotFor(ev.type)}"></span>${escapeHtml(ev.type)}</td><td title="${escapeHtml(ev.summary||'')}">${escapeHtml(ev.summary||'')}</td>`; rows.insertBefore(tr,rows.firstChild); if(rows.children.length>300) rows.removeChild(rows.lastChild); }
function dotFor(t){ if(!t) return 'dot-sys'; const b=t.split('/')[0]; if(b==='chat') return 'dot-chat'; if(b==='error'||b==='rate_limit') return 'dot-warn'; if(b==='healthz') return 'dot-ok'; return 'dot-sys'; }
function updateMetrics(data){ const st=data.stats||{}; document.getElementById('m-chat').textContent=(st.chats??0).toLocaleString('id-ID'); document.getElementById('m-token').textContent=(st.tokens??0).toLocaleString('id-ID'); document.getElementById('m-foto').textContent=(st.foto??0).toLocaleString('id-ID'); document.getElementById('m-sessions').textContent=data.active_sessions??0; }
fetch('/admin/json').then(r=>r.json()).then(data=>{ updateMetrics(data); (data.events||[]).forEach(appendRow); startStream(); });
function startStream(){
const es = new EventSource('/admin/events');
es.addEventListener('open',()=>{ document.getElementById('status').style.color='#2ea043'; document.getElementById('status').textContent='● live'; });
es.addEventListener('error',()=>{ document.getElementById('status').style.color='#f85149'; document.getElementById('status').textContent='● disconnected'; });
es.addEventListener('message',(e)=>{ try{ appendRow(JSON.parse(e.data)); }catch(err){} });
}
</script>
</body>
</html>