feat(dashboard): live GPU health scoring + real KPIs
- Added /metrics/gpu-health endpoint with live health scores (VRAM 40%, temp 30%, load 30%) - Added /metrics/latency endpoint for dashboard KPIs - Added GPU_LABELS for human-readable model names - Dashboard v2: rewired to real data endpoints - KPI cards: GPUs online, circuit trips, avg latency, req/min, active requests - Health scores from actual gpu_health_score() function - Rolling 60-sample history chart (real data, no simulation) - Status: green/yellow/red based on tripped circuits - No CDN dependency (pure CSS) - Auto-refresh every 15s - nginx: /dashboard/ serves static files with cache headers - docker-compose: dashboard volume mount Co-authored-by: Abiba <abiba@sysloggh.com>
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Inference Harness - Dashboard</title>
|
||||
<style>
|
||||
:root{--bg:#0b0f17;--text:#bcc3cd;--panel:rgba(31,41,55,0.7);--border:rgba(75,85,99,0.4)}
|
||||
body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;margin:0;padding:1.5rem}
|
||||
.glass-panel{background:var(--panel);backdrop-filter:blur(12px);border:1px solid var(--border);border-radius:0.75rem;overflow:hidden}
|
||||
.stat-value{font-size:28px;font-weight:700;line-height:1.1}
|
||||
.stat-label{font-size:11px;text-transform:uppercase;letter-spacing:0.6px;color:#64748b}
|
||||
.health-bar{height:0.5rem;background:#374151;border-radius:0.375rem;overflow:hidden}
|
||||
.health-fill{height:100%;transition:width .5s ease}
|
||||
.dot-green{background:#10b981;animation:pulse 2s infinite}
|
||||
.dot-yellow{background:#f59e0b;animation:pulse 2s infinite}
|
||||
.dot-red{background:#ef4444;animation:pulse 2s infinite}
|
||||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.8}}
|
||||
.container{max-width:1400px;margin:0 auto}
|
||||
.grid-5{display:grid;grid-template-columns:repeat(5,1fr);gap:1rem}
|
||||
.grid-3{display:grid;grid-template-columns:repeat(3,1fr);gap:1rem}
|
||||
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem}
|
||||
.mb-6{margin-bottom:1.5rem}
|
||||
.p-4{padding:1rem}
|
||||
.text-center{text-align:center}
|
||||
.font-bold{font-weight:700}
|
||||
.text-white{color:#fff}
|
||||
.text-sm{font-size:.875rem}
|
||||
.text-xs{font-size:.75rem}
|
||||
.text-gray-400{color:#9ca3af}
|
||||
.text-gray-500{color:#6b7280}
|
||||
.flex{display:flex}
|
||||
.items-center{align-items:center}
|
||||
.justify-between{justify-content:space-between}
|
||||
.gap-4{gap:1rem}
|
||||
.w-3{width:.75rem}.h-3{height:.75rem}.rounded-full{border-radius:9999px}
|
||||
.bg-blue-600{background:#2563eb}.bg-blue-600:hover{background:#1d4ed8}
|
||||
.px-4{padding-left:1rem;padding-right:1rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}
|
||||
.rounded-lg{border-radius:.5rem}.text-white{color:#fff}
|
||||
.mt-2{margin-top:.5rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}
|
||||
.border-t{border-top:1px solid #374151}.pt-4{padding-top:1rem}
|
||||
.text-xl{font-size:1.25rem}.text-2xl{font-size:1.5rem}.text-lg{font-size:1.125rem}
|
||||
.font-semibold{font-weight:600}.font-normal{font-weight:400}
|
||||
.text-emerald-400{color:#34d399}.text-amber-400{color:#fbbf24}.text-red-400{color:#f87171}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div><h1 class="text-2xl font-bold text-white">Inference Harness</h1><p class="text-sm text-gray-400">Syslog Solution LLC · Real-time Monitoring</p></div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span id="statusBadge" class="flex items-center gap-2"><span id="statusDot" class="w-3 h-3 rounded-full dot-green"></span><span id="statusText" class="text-emerald-400 text-lg font-semibold">healthy</span></span>
|
||||
<span id="lastUpdate" class="text-sm text-gray-500"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-5 mb-6">
|
||||
<div class="glass-panel p-4 text-center"><p class="stat-value text-white" id="kpiGpus">-</p><p class="stat-label">GPUs Online</p></div>
|
||||
<div class="glass-panel p-4 text-center"><p class="stat-value text-white" id="kpiTrips">-</p><p class="stat-label">Circuit Trips</p></div>
|
||||
<div class="glass-panel p-4 text-center"><p class="stat-value text-white" id="kpiLatency">-</p><p class="stat-label">Avg Latency</p></div>
|
||||
<div class="glass-panel p-4 text-center"><p class="stat-value text-white" id="kpiReqs">-</p><p class="stat-label">Requests/min</p></div>
|
||||
<div class="glass-panel p-4 text-center"><p class="stat-value text-white" id="kpiActive">-</p><p class="stat-label">Active Requests</p></div>
|
||||
</div>
|
||||
|
||||
<h2 class="text-xl font-semibold text-white mb-3">GPU Health Scoring <span class="text-sm text-gray-400 font-normal">Live: VRAM 40% · Temp 30% · Load 30%</span></h2>
|
||||
<div id="gpuCards" class="grid-3 mb-6"></div>
|
||||
<div class="glass-panel p-4 mb-6"><h3 class="text-sm font-semibold text-gray-400 mb-3">Health Score History (60s rolling)</h3><div id="healthChart" style="height:220px"></div></div>
|
||||
|
||||
<div class="text-center text-sm text-gray-500 pt-4 border-t"><p>Inference Harness Dashboard · Syslog Solution LLC · Auto-refresh 15s</p></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const COLORS={'qwen3.6-35B-A3B':'#10b981','qwen3.6-27B-code':'#8b5cf6','gemma-4-12b':'#3b82f6'};
|
||||
const HISTORY=[]; // rolling 60 sample history for chart
|
||||
function Q(id){return document.getElementById(id)}
|
||||
|
||||
function updateStatus(trips,degraded){
|
||||
const d=Q('statusDot'),t=Q('statusText');
|
||||
if(degraded){d.className='w-3 h-3 rounded-full dot-red';t.textContent='degraded';t.className='text-red-400 text-lg font-semibold'}
|
||||
else if(trips>0){d.className='w-3 h-3 rounded-full dot-yellow';t.textContent='trips:'+trips;t.className='text-amber-400 text-lg font-semibold'}
|
||||
else{d.className='w-3 h-3 rounded-full dot-green';t.textContent='healthy';t.className='text-emerald-400 text-lg font-semibold'}
|
||||
}
|
||||
|
||||
function fetchAll(){
|
||||
Promise.all([
|
||||
fetch('/metrics/gpu-health').then(r=>r.json()),
|
||||
fetch('/metrics/latency').then(r=>r.json())
|
||||
]).then(function(_a){var health=_a[0],latency=_a[1];
|
||||
Q('lastUpdate').textContent=new Date().toLocaleTimeString();
|
||||
var gpus=health.gpus||[],kpi=health.kpi||{};
|
||||
// KPIs
|
||||
Q('kpiGpus').textContent=kpi.gpus_online+'/'+kpi.total_gpus;
|
||||
Q('kpiTrips').textContent=kpi.total_trips;
|
||||
Q('kpiLatency').textContent=(latency.avg_ms||0)+'ms';
|
||||
Q('kpiReqs').textContent=latency.requests_per_min||0;
|
||||
var active=0;gpus.forEach(function(g){active+=g.active_requests||0});
|
||||
Q('kpiActive').textContent=active;
|
||||
// Status
|
||||
var degraded=gpus.some(function(g){return g.status==='down'||g.circuit_tripped});
|
||||
updateStatus(kpi.total_trips||0,degraded);
|
||||
// GPU cards
|
||||
var html='';
|
||||
gpus.forEach(function(g,i){
|
||||
var s=g.health_score||0,clr=s>=70?'#10b981':(s>=40?'#f59e0b':'#ef4444');
|
||||
var badge='';
|
||||
if(g.circuit_tripped)badge='<span class="text-xs px-2 py-1 bg-red-900 rounded text-red-300">TRIPPED</span>';
|
||||
else if(i===0)badge='<span class="text-xs px-2 py-1 bg-emerald-900 rounded text-emerald-300">Best</span>';
|
||||
html+='<div class="glass-panel p-4"><div class="flex justify-between items-center mb-2"><p class="text-lg font-bold text-white">'+g.label+'</p>'+badge+'</div>'+
|
||||
'<p class="text-4xl font-bold mb-2" style="color:'+clr+'">'+Math.round(s)+'</p>'+
|
||||
'<div class="health-bar"><div class="health-fill" style="width:'+s+'%;background:'+clr+'"></div></div>'+
|
||||
'<div class="flex justify-between mt-2 text-xs text-gray-400"><span>VRAM '+g.vram_pct+'%</span><span>'+g.temp_c+'°C</span><span>Active '+g.active_requests+'/'+g.max_concurrent+'</span><span>Trips '+g.circuit_trip_count+'</span></div></div>';
|
||||
});
|
||||
Q('gpuCards').innerHTML=html;
|
||||
// Rolling history
|
||||
var now=Date.now();HISTORY.push({ts:now,gpus:gpus.map(function(g){return{id:g.id,score:g.health_score}})});
|
||||
if(HISTORY.length>60)HISTORY.shift();
|
||||
renderChart();
|
||||
}).catch(function(){});
|
||||
}
|
||||
|
||||
function renderChart(){
|
||||
var W=800,H=220,svg='<svg viewBox="0 0 '+W+' '+H+'" style="width:100%;height:220px" preserveAspectRatio="none">';
|
||||
// Grid lines
|
||||
for(var i=0;i<=4;i++){var y=(i/4)*H;svg+='<line x1="0" y1="'+y+'" x2="'+W+'" y2="'+y+'" stroke="#1e293b" stroke-width="1"/>';svg+='<text x="5" y="'+(y+10)+'" font-size="9" fill="#64748b">'+(100-i*25)+'</text>'}
|
||||
// Time labels
|
||||
for(var i=0;i<=4;i++){var lx=(i/4)*W,lt=HISTORY.length>0?new Date(HISTORY[Math.floor(i/4*(HISTORY.length-1))].ts).toLocaleTimeString():'';svg+='<text x="'+lx+'" y="'+(H-2)+'" font-size="8" fill="#475569" text-anchor="middle">'+lt+'</text>'}
|
||||
// Plot lines per GPU
|
||||
var ids=HISTORY.length>0?HISTORY[0].gpus.map(function(g){return g.id}):[];
|
||||
ids.forEach(function(id){
|
||||
var c=COLORS[id]||'#94a3b8',pts='',first=true;
|
||||
HISTORY.forEach(function(h,i){
|
||||
var gpu=h.gpus.find(function(g){return g.id===id});
|
||||
if(!gpu)return;
|
||||
var x=(i/(HISTORY.length-1||1))*W,y=H-(gpu.score/100*H);
|
||||
pts+=(first?'M':'L')+x.toFixed(1)+','+y.toFixed(1);first=false;
|
||||
});
|
||||
if(pts)svg+='<path d="'+pts+'" fill="none" stroke="'+c+'" stroke-width="2" opacity="0.9"/>';
|
||||
});
|
||||
svg+='</svg>';Q('healthChart').innerHTML=svg;
|
||||
}
|
||||
|
||||
fetchAll();setInterval(fetchAll,15000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user