Spaces:
Running
Running
| <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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""); } | |
| 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> |