Files
the-nexus/web/dashboard.html
2026-04-12 21:35:30 -04:00

140 lines
6.1 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>MUD Bridge - Health Dashboard</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Courier New',monospace;background:#1a1a2e;color:#e0e0e0;min-height:100vh;padding:24px}
h1{color:#e94560;font-size:20px;margin-bottom:20px;text-align:center}
.nav{text-align:center;margin-bottom:24px}
.nav a{color:#533483;text-decoration:none;font-size:13px;margin:0 8px}
.nav a:hover{color:#e94560}
#status{font-size:13px;text-align:center;margin-bottom:20px;color:#a0a0a0}
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;max-width:900px;margin:0 auto 24px}
.card{background:#16213e;border:1px solid #0f3460;border-radius:8px;padding:20px;text-align:center}
.card .label{font-size:12px;color:#a0a0a0;text-transform:uppercase;margin-bottom:8px}
.card .value{font-size:32px;font-weight:bold;color:#e94560}
.card .value.ok{color:#4ecca3}
.card .value.warn{color:#f0a500}
.card .value.err{color:#e94560}
.card .sub{font-size:11px;color:#666;margin-top:4px}
.panel{background:#16213e;border:1px solid #0f3460;border-radius:8px;max-width:900px;margin:0 auto 16px;padding:16px}
.panel h3{color:#e94560;font-size:14px;margin-bottom:12px}
table{width:100%;border-collapse:collapse;font-size:13px}
th{text-align:left;color:#a0a0a0;padding:6px 8px;border-bottom:1px solid #0f3460;font-size:11px;text-transform:uppercase}
td{padding:6px 8px;border-bottom:1px solid #0f346044}
tr:last-child td{border-bottom:none}
.badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:bold}
.badge.active{background:#4ecca322;color:#4ecca3}
.badge.idle{background:#f0a50022;color:#f0a500}
</style>
</head>
<body>
<h1>== MUD Bridge Health Dashboard ==</h1>
<div class="nav"><a href="/">Chat</a> | <a href="/dashboard.html">Dashboard</a></div>
<div id="status">Loading...</div>
<div class="cards">
<div class="card"><div class="label">Bridge Status</div><div class="value" id="bridge-status">--</div><div class="sub" id="bridge-version"></div></div>
<div class="card"><div class="label">Uptime</div><div class="value" id="uptime">--</div><div class="sub" id="uptime-detail"></div></div>
<div class="card"><div class="label">Active Sessions</div><div class="value" id="sessions">--</div></div>
<div class="card"><div class="label">Rooms With Players</div><div class="value" id="rooms">--</div></div>
<div class="card"><div class="label">Total Messages</div><div class="value" id="messages">--</div></div>
<div class="card"><div class="label">Avg Latency</div><div class="value" id="latency">--</div><div class="sub">ms</div></div>
</div>
<div class="panel">
<h3>Active Sessions</h3>
<table><thead><tr><th>User</th><th>Room</th><th>Status</th><th>Messages</th><th>Latency</th><th>Connected</th></tr></thead><tbody id="session-list"><tr><td colspan="6">Loading...</td></tr></tbody></table>
</div>
<div class="panel">
<h3>Room Activity</h3>
<table><thead><tr><th>Room</th><th>Players</th><th>Messages</th></tr></thead><tbody id="room-list"><tr><td colspan="3">Loading...</td></tr></tbody></table>
</div>
<script>
const API='http://127.0.0.1:4004';
const $=id=>document.getElementById(id);
let startTime=Date.now();
function fmtUptime(ms){
const s=Math.floor(ms/1000),m=Math.floor(s/60),h=Math.floor(m/60),d=Math.floor(h/24);
if(d>0)return d+'d '+h%24+'h';
if(h>0)return h+'h '+m%60+'m';
if(m>0)return m+'m '+s%60+'s';
return s+'s';
}
function fmtDate(ts){return new Date(ts).toLocaleTimeString()}
async function refresh(){
try{
const[health,sessions,stats]=await Promise.all([
fetch(API+'/bridge/health').then(r=>r.json()),
fetch(API+'/bridge/sessions').then(r=>r.json()),
fetch(API+'/bridge/stats').then(r=>r.json())
]);
// Health
const healthy=health.status==='ok'||health.status==='healthy';
$('bridge-status').textContent=healthy?'OK':'DEGRADED';
$('bridge-status').className='value '+(healthy?'ok':'err');
$('bridge-version').textContent=health.version||'';
$('uptime').textContent=fmtUptime(health.uptime_ms||Date.now()-startTime);
$('uptime-detail').textContent='since '+fmtDate(Date.now()-(health.uptime_ms||0));
// Stats
$('sessions').textContent=stats.active_sessions||stats.sessions||0;
$('messages').textContent=stats.total_messages||stats.messages||0;
$('latency').textContent=Math.round(stats.avg_latency_ms||stats.latency||0);
// Sessions table
const sList=sessions.sessions||sessions||[];
if(Array.isArray(sList)&&sList.length>0){
$('session-list').innerHTML=sList.map(s=>`<tr>
<td>${esc(s.username||s.user||'?')}</td>
<td>${esc(s.room||'?')}</td>
<td><span class="badge active">Active</span></td>
<td>${s.message_count||s.messages||0}</td>
<td>${Math.round(s.latency_ms||0)}ms</td>
<td>${fmtDate(s.connected_at||s.created||Date.now())}</td>
</tr>`).join('');
}else{
$('session-list').innerHTML='<tr><td colspan="6" style="color:#a0a0a0">No active sessions</td></tr>';
}
// Room activity - aggregate from sessions
const roomMap={};
if(Array.isArray(sList)){
sList.forEach(s=>{
const r=s.room||'?';
if(!roomMap[r])roomMap[r]={players:0,messages:0};
roomMap[r].players++;
roomMap[r].messages+=(s.message_count||s.messages||0);
});
}
const rooms=Object.entries(roomMap);
$('rooms').textContent=rooms.length;
if(rooms.length>0){
$('room-list').innerHTML=rooms.map(([name,data])=>`<tr>
<td>${esc(name)}</td>
<td>${data.players}</td>
<td>${data.messages}</td>
</tr>`).join('');
}else{
$('room-list').innerHTML='<tr><td colspan="3" style="color:#a0a0a0">No rooms active</td></tr>';
}
$('status').textContent='Last updated: '+new Date().toLocaleTimeString();
}catch(e){
$('status').textContent='Error: '+e.message;
$('bridge-status').textContent='ERR';
$('bridge-status').className='value err';
}
}
function esc(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML}
refresh();
setInterval(refresh,5000);
</script>
</body>
</html>