Integrate dashboard natively into Deck UI

This commit is contained in:
Mikei386
2026-09-29 22:38:06 +02:00
parent aaf9f570e9
commit c2fcd5c509
4 changed files with 78 additions and 26 deletions
+10 -10
View File
@@ -4,14 +4,14 @@ const esc=v=>String(v??'–').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>'
const pct=n=>n==null?'–':`${Number(n).toFixed(1)}%`;
const gib=b=>b==null?'–':`${(Number(b)/1073741824).toFixed(1)} GiB`;
const dur=s=>{if(s==null)return'–';let d=Math.floor(s/86400),h=Math.floor(s%86400/3600),m=Math.floor(s%3600/60);return d?`${d}d ${h}h`:`${h}h ${m}m`};
const $=id=>root.getElementById(id);
const $=id=>root.querySelector(`#${id}`);
const imagePhaseLabel=p=>({'running':'Bildauftrag läuft','loading':'Bildmodell lädt','complete':'bereit','failed':'Fehler'})[p]||p||'inaktiv';
function cleanup(){for(const timer of timers)clearInterval(timer);timers=[];controller?.abort();controller=null;host=null;root=null}
function gpuCard(g){let total=g.memory_total_mib||0,used=g.memory_used_mib||0,p=total&&g.memory_used_mib!=null?used/total*100:0,load=Math.max(0,Math.min(100,g.gpu_percent||0)),memoryText=total&&g.memory_used_mib!=null?`${(used/1024).toFixed(1)} / ${(total/1024).toFixed(1)} GiB`:'–';return `<article class="card span6"><div class="gpu-title"><div><div class="label">GPU ${esc(g.index)}</div><div class="value">${esc(g.name)}</div></div><span class="badge">${esc(g.pstate)}</span></div><div class="metrics"><div class="metric"><b>${pct(g.gpu_percent)}</b><span>GPU-Kern</span></div><div class="metric"><b>${memoryText}</b><span>VRAM</span></div><div class="metric"><b>${esc(g.temperature_c)} °C</b><span>Temperatur</span></div><div class="metric"><b>${esc(g.power_w)} / ${esc(g.power_limit_w)} W</b><span>Leistung</span></div><div class="metric"><b>${esc(g.memory_controller_percent)} %</b><span>Speichercontroller</span></div><div class="metric"><b>${esc(g.graphics_clock_mhz)} MHz</b><span>GPU-Takt</span></div><div class="metric"><b>${esc(g.memory_clock_mhz)} MHz</b><span>Speichertakt</span></div><div class="metric"><b>${esc(g.fan_percent)} %</b><span>Lüfter</span></div></div><div class="bar-label"><span>GPU-Auslastung</span><span>${pct(load)}</span></div><div class="bar"><div class="fill gpu-load" style="width:${load}%"></div></div><div class="bar-label"><span>VRAM</span><span>${pct(p)}</span></div><div class="bar"><div class="fill" style="width:${p}%"></div></div></article>`}
function mount(){
controller=new AbortController();
root=host.attachShadow({mode:'open'});
root.innerHTML=`<link rel="stylesheet" href="/dashboard.css"><div class="dashboard-root"><main><div class="top"><div><div class="eyebrow">Athena Deck · Live Telemetry</div><h1>Dashboard</h1></div><div class="live"><span class="dot" id="dot"></span><span id="updated">verbinde …</span></div></div><section class="grid"> <article class="card span3"><div class="label">Aktives Profil</div><div class="value" id="profile">–</div><div class="sub" id="profileSub">Deck wird abgefragt</div></article>
root=host;
root.innerHTML=`<div class="dashboard-root"><div class="top"><div><div class="kicker">ATHENA / TELEMETRIE</div><h1>Dashboard</h1><p>System, Inferenz und Verlauf auf einen Blick.</p></div><div class="live"><span class="dot" id="dot"></span><span id="updated">verbinde …</span></div></div><section class="grid"> <article class="card span3"><div class="label">Aktives Profil</div><div class="value" id="profile">–</div><div class="sub" id="profileSub">Deck wird abgefragt</div></article>
<article class="card span3"><div class="label">Modell</div><div class="value" id="model">–</div><div class="sub" id="modelSub">–</div></article>
<article class="card span3"><div class="label">CPU</div><div class="value" id="cpu">–</div><div class="bar"><div class="fill" id="cpuBar"></div></div><div class="sub" id="load">–</div></article>
<article class="card span3"><div class="label">System-RAM</div><div class="value" id="ram">–</div><div class="bar"><div class="fill" id="ramBar"></div></div><div class="sub" id="ramSub">–</div></article>
@@ -45,8 +45,8 @@ root.innerHTML=`<link rel="stylesheet" href="/dashboard.css"><div class="dashboa
<div class="section-title">Notfall-Backups · herunterladen</div>
<article class="card span12"><div class="row"><div><div class="label">Verschlüsselte portable Sicherungen</div><div class="sub">Unersetzliche Daten ohne erneut ladbare Modellgewichte · maximal fünf Generationen</div></div></div><div class="scroll"><table><thead><tr><th>Erstellt</th><th>Größe</th><th>SHA256</th><th></th></tr></thead><tbody id="backupFiles"><tr><td colspan="4">Backups werden geladen …</td></tr></tbody></table></div><div class="sub" id="backupNote">Zum Wiederherstellen wird der separat verwahrte Age-Schlüssel benötigt.</div></article>
<article class="card span12 error" id="errors" hidden></article>
</section><div class="footer">Aktualisierung jede Sekunde · Steuerung über die Deck-Übersicht</div></main></div>`;
root.querySelector('link').addEventListener('load',()=>{if(host?.isConnected)refreshHistory()},{once:true});
</section><div class="footer">Aktualisierung jede Sekunde · Steuerung über die Deck-Übersicht</div></div>`;
const deNum=n=>n==null?'–':Number(n).toLocaleString('de-DE',{maximumFractionDigits:1});
@@ -130,7 +130,7 @@ function drawHistory(points){
const canvas=$('gpuHistoryChart'),rect=canvas.getBoundingClientRect(),ratio=window.devicePixelRatio||1;
canvas.width=Math.max(1,Math.floor(rect.width*ratio));canvas.height=Math.max(1,Math.floor(rect.height*ratio));
const x=canvas.getContext('2d');x.scale(ratio,ratio);const w=rect.width,h=rect.height,pad={l:42,r:44,t:16,b:28};
x.clearRect(0,0,w,h);x.strokeStyle='#213044';x.fillStyle='#8fa1b5';x.font='11px system-ui';x.lineWidth=1;
x.clearRect(0,0,w,h);x.strokeStyle='#303a37';x.fillStyle='#97a79e';x.font='11px system-ui';x.lineWidth=1;
for(let i=0;i<=4;i++){let y=pad.t+(h-pad.t-pad.b)*i/4;x.beginPath();x.moveTo(pad.l,y);x.lineTo(w-pad.r,y);x.stroke();x.fillText(`${100-i*25}%`,4,y+4);x.fillText(`${100-i*25}°`,w-pad.r+7,y+4)}
if(!points.length){x.fillText('Noch keine historischen Messwerte',pad.l+10,h/2);return}
const min=Math.min(...points.map(p=>p.ts)),max=Math.max(...points.map(p=>p.ts));
@@ -143,8 +143,8 @@ function drawHistory(points){
:span<=45*86400
?date.toLocaleString('de-DE',{day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'})
:date.toLocaleDateString('de-DE',{day:'2-digit',month:'2-digit',year:'2-digit'});
x.strokeStyle='#172538';x.beginPath();x.moveTo(xx,pad.t);x.lineTo(xx,h-pad.b);x.stroke();
x.fillStyle='#8fa1b5';x.textAlign=i===0?'left':i===ticks-1?'right':'center';x.fillText(label,xx,h-7);
x.strokeStyle='#2b3532';x.beginPath();x.moveTo(xx,pad.t);x.lineTo(xx,h-pad.b);x.stroke();
x.fillStyle='#97a79e';x.textAlign=i===0?'left':i===ticks-1?'right':'center';x.fillText(label,xx,h-7);
}
x.textAlign='start';
const ids=[...new Set(points.map(p=>p.gpu_index))].sort();
@@ -157,7 +157,7 @@ function drawSlotHistory(points){
const canvas=$('slotHistoryChart'),rect=canvas.getBoundingClientRect(),ratio=window.devicePixelRatio||1;
canvas.width=Math.max(1,Math.floor(rect.width*ratio));canvas.height=Math.max(1,Math.floor(rect.height*ratio));
const x=canvas.getContext('2d');x.scale(ratio,ratio);const w=rect.width,h=rect.height,pad={l:42,r:18,t:16,b:28};
x.clearRect(0,0,w,h);x.strokeStyle='#213044';x.fillStyle='#8fa1b5';x.font='11px system-ui';x.lineWidth=1;
x.clearRect(0,0,w,h);x.strokeStyle='#303a37';x.fillStyle='#97a79e';x.font='11px system-ui';x.lineWidth=1;
for(let i=0;i<=4;i++){let y=pad.t+(h-pad.t-pad.b)*i/4;x.beginPath();x.moveTo(pad.l,y);x.lineTo(w-pad.r,y);x.stroke();x.fillText(`${100-i*25}%`,4,y+4)}
if(!points.length){x.fillText('Noch keine historischen Slot-Messwerte',pad.l+10,h/2);$('slotLegend').innerHTML='';return}
const min=Math.min(...points.map(p=>p.ts)),max=Math.max(...points.map(p=>p.ts));
@@ -166,7 +166,7 @@ function drawSlotHistory(points){
for(let i=0;i<ticks;i++){
const ts=min+span*i/(ticks-1),xx=px(ts),date=new Date(ts*1000);
const label=span<=2*86400?date.toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit'}):span<=45*86400?date.toLocaleString('de-DE',{day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'}):date.toLocaleDateString('de-DE',{day:'2-digit',month:'2-digit',year:'2-digit'});
x.strokeStyle='#172538';x.beginPath();x.moveTo(xx,pad.t);x.lineTo(xx,h-pad.b);x.stroke();x.fillStyle='#8fa1b5';x.textAlign=i===0?'left':i===ticks-1?'right':'center';x.fillText(label,xx,h-7);
x.strokeStyle='#2b3532';x.beginPath();x.moveTo(xx,pad.t);x.lineTo(xx,h-pad.b);x.stroke();x.fillStyle='#97a79e';x.textAlign=i===0?'left':i===ticks-1?'right':'center';x.fillText(label,xx,h-7);
}
x.textAlign='start';const ids=[...new Set(points.map(p=>p.slot_id))].sort((a,b)=>a-b);
$('slotLegend').innerHTML=ids.map((id,idx)=>{let color=historyColors[idx%historyColors.length],key=String(id);return `<button data-slot-series="${key}" class="${hiddenSlotSeries.has(key)?'off':''}" style="--series:${color}" aria-pressed="${hiddenSlotSeries.has(key)?'false':'true'}">Slot ${id} · Kontext</button>`}).join('');