From 706306d7b84a76e210779f8193d1bafc6941ec6c Mon Sep 17 00:00:00 2001
From: Mikei386 <44135113+Mikei386@users.noreply.github.com>
Date: Wed, 30 Sep 2026 12:36:05 +0200
Subject: [PATCH] Restore dashboard meters under strict content security policy
---
dashboard-ui.js | 21 ++++++++++++++++-----
dashboard.css | 3 +++
themes.css | 4 ++++
3 files changed, 23 insertions(+), 5 deletions(-)
diff --git a/dashboard-ui.js b/dashboard-ui.js
index 37177ec..a2bba13 100644
--- a/dashboard-ui.js
+++ b/dashboard-ui.js
@@ -6,9 +6,18 @@ 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.querySelector(`#${id}`);
+// HTML style attributes are blocked by Deck's CSP; assign individual CSS properties.
+function applyDashboardStyles(container=root){
+ container.querySelectorAll('[data-bar-percent]').forEach(el=>{
+ const percent=Number(el.dataset.barPercent);
+ el.style.width=`${Number.isFinite(percent)?Math.max(0,Math.min(100,percent)):0}%`;
+ });
+ container.querySelectorAll('[data-bar-color]').forEach(el=>el.style.setProperty('--usage-color',el.dataset.barColor));
+ container.querySelectorAll('[data-series-color]').forEach(el=>el.style.setProperty('--series',el.dataset.seriesColor));
+}
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 `GPU ${esc(g.index)}
${esc(g.name)}
${esc(g.pstate)}${pct(g.gpu_percent)}GPU-Kern
${memoryText}VRAM
${esc(g.temperature_c)} °CTemperatur
${esc(g.power_w)} / ${esc(g.power_limit_w)} WLeistung
${esc(g.memory_controller_percent)} %Speichercontroller
${esc(g.graphics_clock_mhz)} MHzGPU-Takt
${esc(g.memory_clock_mhz)} MHzSpeichertakt
${esc(g.fan_percent)} %Lüfter
GPU-Auslastung${pct(load)}
VRAM${pct(p)}
`}
+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 `GPU ${esc(g.index)}
${esc(g.name)}
${esc(g.pstate)}${pct(g.gpu_percent)}GPU-Kern
${memoryText}VRAM
${esc(g.temperature_c)} °CTemperatur
${esc(g.power_w)} / ${esc(g.power_limit_w)} WLeistung
${esc(g.memory_controller_percent)} %Speichercontroller
${esc(g.graphics_clock_mhz)} MHzGPU-Takt
${esc(g.memory_clock_mhz)} MHzSpeichertakt
${esc(g.fan_percent)} %Lüfter
GPU-Auslastung${pct(load)}
VRAM${pct(p)}
`}
function mount(){
historyColors=document.documentElement.dataset.theme==='athena-classic'?['#45d7ff','#ffc65c','#66e3a4','#bda7ff']:['#c5edaf','#ffb454','#7fc8b6','#c39bff'];
controller=new AbortController();
@@ -83,7 +92,7 @@ function slotHtml(s,pool){
let state=s.processing?'arbeitet':'frei';
let capacity=pool?`${deNum(used)} / ${deNum(available)} Token aktuell möglich`:`${deNum(used)} / ${deNum(total)} Token`;
let shared=pool?`Gemeinsamer Pool: ${deNum(pool.free)} Token frei`:'';
- return `
Slot ${s.id??'–'} · ${state}${s.speculative?'MTP aktiv':'Standard'}
${capacity}${p.toFixed(1)} %
${shared?`
${shared}
`:''}
${metric(deNum(s.prompt_tokens),'Prompt')}${metric(deNum(s.prompt_cached),'Cache-Token')}${metric(deNum(s.decoded_tokens),'generiert')}${metric(deNum(s.remaining_generation),'Ausgabe übrig')}${metric(s.task_id??'–','Task-ID')}${metric(s.temperature??'–','Temperatur')}${metric(boolLabel(s.stream),'Streaming')}${metric(deNum(s.max_tokens),'Ausgabelimit')}
`;
+ return `Slot ${s.id??'–'} · ${state}${s.speculative?'MTP aktiv':'Standard'}
${capacity}${p.toFixed(1)} %
${shared?`
${shared}
`:''}
${metric(deNum(s.prompt_tokens),'Prompt')}${metric(deNum(s.prompt_cached),'Cache-Token')}${metric(deNum(s.decoded_tokens),'generiert')}${metric(deNum(s.remaining_generation),'Ausgabe übrig')}${metric(s.task_id??'–','Task-ID')}${metric(s.temperature??'–','Temperatur')}${metric(boolLabel(s.stream),'Streaming')}${metric(deNum(s.max_tokens),'Ausgabelimit')}
`;
}
async function refreshFull(){
try{
@@ -105,6 +114,7 @@ async function refreshFull(){
$('requestSub').textContent=`${(lt.slots||[]).filter(s=>!s.processing).length} freie Slots`;
let slots=lt.slots||[],slotPool=sharedSlotPool(slots,lr.kv_unified===true);
$('slotCards').innerHTML=slots.map(s=>slotHtml(s,slotPool)).join('')||'Slot-Telemetrie momentan nicht verfügbar
';
+ applyDashboardStyles();
let drafted=Number(met.spec_decode_num_draft_tokens_total||0),accepted=Number(met.spec_decode_num_accepted_tokens_total||0),acceptance=drafted?accepted/drafted*100:null;
$('mtpAcceptance').textContent=acceptance==null?'–':`${acceptance.toFixed(1)} % akzeptiert`;$('mtpBar').style.width=`${acceptance||0}%`;
$('mtpDrafted').textContent=deNum(drafted);$('mtpAccepted').textContent=deNum(accepted);$('mtpSteps').textContent=deNum(met.spec_decode_num_drafts_total);$('mtpDepth').textContent=lr.mtp_draft_tokens??'–';
@@ -149,7 +159,7 @@ function drawHistory(points){
}
x.textAlign='start';
const ids=[...new Set(points.map(p=>p.gpu_index))].sort();
- $('gpuLegend').innerHTML=ids.flatMap((id,idx)=>[['load',`GPU ${id} Auslastung`,historyColors[idx*2%historyColors.length]],['temp',`GPU ${id} Temperatur`,historyColors[(idx*2+1)%historyColors.length]]].map(([kind,label,color])=>{let key=`${id}:${kind}`;return ``})).join('');
+ $('gpuLegend').innerHTML=ids.flatMap((id,idx)=>[['load',`GPU ${id} Auslastung`,historyColors[idx*2%historyColors.length]],['temp',`GPU ${id} Temperatur`,historyColors[(idx*2+1)%historyColors.length]]].map(([kind,label,color])=>{let key=`${id}:${kind}`;return ``})).join('');applyDashboardStyles();
ids.forEach((id,idx)=>{let rows=points.filter(p=>p.gpu_index===id),load=historyColors[idx*2%historyColors.length],temp=historyColors[(idx*2+1)%historyColors.length];
[[load,'gpu_util','load'],[temp,'temperature_c','temp']].forEach(([color,key,kind])=>{if(hiddenHistorySeries.has(`${id}:${kind}`))return;x.beginPath();x.strokeStyle=color;x.lineWidth=2;let first=true;rows.forEach(p=>{if(p[key]==null)return;let xx=px(p.ts),yy=py(Number(p[key]));first?(x.moveTo(xx,yy),first=false):x.lineTo(xx,yy)});x.stroke()})});
}
@@ -170,11 +180,11 @@ function drawSlotHistory(points){
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 ``}).join('');
+ $('slotLegend').innerHTML=ids.map((id,idx)=>{let color=historyColors[idx%historyColors.length],key=String(id);return ``}).join('');applyDashboardStyles();
ids.forEach((id,idx)=>{let key=String(id);if(hiddenSlotSeries.has(key))return;let rows=points.filter(p=>p.slot_id===id);x.beginPath();x.strokeStyle=historyColors[idx%historyColors.length];x.lineWidth=2;let first=true;rows.forEach(p=>{if(p.context_percent==null)return;let xx=px(p.ts),yy=py(Number(p.context_percent));first?(x.moveTo(xx,yy),first=false):x.lineTo(xx,yy)});x.stroke()});
}
async function refreshHistory(){try{let r=await fetch(`/api/v1/dashboard/history?range=${historyRange}`,{cache:'no-store'});if(!r.ok)throw Error(`HTTP ${r.status}`);let d=await r.json(),t=d.token_totals||{},input=Number(t.prompt_tokens||0)+Number(t.cached_tokens||0);$('historyInput').textContent=deNum(input);$('historyInputSub').textContent=`${deNum(t.prompt_tokens)} neu · ${deNum(t.cached_tokens)} aus Cache`;$('historyOutput').textContent=deNum(t.output_tokens||0);$('historyStorage').textContent=`${deNum((d.storage||{}).rows||0)} Messpunkte`;drawHistory(d.points||[]);drawSlotHistory(d.slot_points||[]);renderProfileUsage(d.profile_usage||[]);let note=`Bereich ${d.range} · Messung alle ${d.sample_interval_seconds}s · Detaildaten ${d.detail_retention_days} Tage`;$('historyNote').textContent=note;$('slotHistoryNote').textContent=`${note} · ${deNum((d.slot_storage||{}).rows||0)} Slot-Messpunkte`;$('historyEvents').innerHTML=(d.model_events||[]).slice(0,15).map(e=>`| ${new Date(e.ts*1000).toLocaleString('de-DE')} | ${esc(e.previous_profile||'–')} → ${esc(e.profile||'–')} | ${esc(e.previous_model||'–')} → ${esc(e.model||'–')} |
`).join('')||'| Noch keine Wechsel aufgezeichnet |
'}catch(e){$('historyNote').textContent=`Historie nicht verfügbar: ${e.message}`;$('slotHistoryNote').textContent=`Historie nicht verfügbar: ${e.message}`}}
-function renderProfileUsage(rows){lastProfileUsage=rows;let value=r=>usageMode==='output'?Number(r.output_tokens||0):Number(r.prompt_tokens||0)+Number(r.cached_tokens||0)+Number(r.output_tokens||0),sum=rows.reduce((n,r)=>n+value(r),0);$('profileUsage').innerHTML=rows.map((r,i)=>{let v=value(r),p=sum?v/sum*100:0,input=Number(r.prompt_tokens||0)+Number(r.cached_tokens||0);return `${esc(r.profile||'unbekannt')} · ${esc(r.model||'–')}${p.toFixed(1)} %
${deNum(input)} Eingabe · ${deNum(r.output_tokens||0)} Ausgabe${deNum(v)} gewertet
`}).join('')||'In diesem Zeitraum wurden noch keine Token aufgezeichnet.
'}
+function renderProfileUsage(rows){lastProfileUsage=rows;let value=r=>usageMode==='output'?Number(r.output_tokens||0):Number(r.prompt_tokens||0)+Number(r.cached_tokens||0)+Number(r.output_tokens||0),sum=rows.reduce((n,r)=>n+value(r),0);$('profileUsage').innerHTML=rows.map((r,i)=>{let v=value(r),p=sum?v/sum*100:0,input=Number(r.prompt_tokens||0)+Number(r.cached_tokens||0);return `${esc(r.profile||'unbekannt')} · ${esc(r.model||'–')}${p.toFixed(1)} %
${deNum(input)} Eingabe · ${deNum(r.output_tokens||0)} Ausgabe${deNum(v)} gewertet
`}).join('')||'In diesem Zeitraum wurden noch keine Token aufgezeichnet.
';applyDashboardStyles($('profileUsage'))}
$('usageModes').addEventListener('click',e=>{let b=e.target.closest('button[data-mode]');if(!b)return;usageMode=b.dataset.mode;root.querySelectorAll('#usageModes button').forEach(x=>x.classList.toggle('active',x===b));renderProfileUsage(lastProfileUsage)});
$('historyRanges').addEventListener('click',e=>setHistoryRange(e));
$('slotHistoryRanges').addEventListener('click',e=>setHistoryRange(e));
@@ -197,6 +207,7 @@ async function refreshSummary(){
$('load').textContent=`${c.logical_cpus||'–'} Threads · Load ${(c.load||[]).join(' / ')}`;
const rp=m.total?m.used/m.total*100:0;$('ram').textContent=pct(m.total?rp:null);$('ramBar').style.width=`${rp}%`;$('ramSub').textContent=`${gib(m.used)} / ${gib(m.total)}`;
$('gpuCards').innerHTML=(d.gpus||[]).map(gpuCard).join('')||'Keine GPU-Daten verfügbar';
+ applyDashboardStyles();
$('availability').textContent=imageActive?'Bildauftrag läuft':(q.available?'bereit':'nicht geladen');
$('availability').className=`value status ${q.available||imageActive?'':'bad'}`;
$('activeChats').textContent=q.active_chats??'–';$('routerUptime').textContent=dur(rt.uptime_seconds);
diff --git a/dashboard.css b/dashboard.css
index 167fcd9..654e2bb 100644
--- a/dashboard.css
+++ b/dashboard.css
@@ -61,3 +61,6 @@
@media(max-width:1180px){#deck-dashboard-host .span3{grid-column:span 6}#deck-dashboard-host .span4{grid-column:span 6}#deck-dashboard-host .row{align-items:flex-start;flex-direction:column}}
@media(max-width:800px){#deck-dashboard-host .span3,#deck-dashboard-host .span4,#deck-dashboard-host .span6{grid-column:span 12}#deck-dashboard-host .top{align-items:flex-start;flex-direction:column}#deck-dashboard-host .profiles{grid-template-columns:1fr}}
@media(max-width:500px){#deck-dashboard-host .card{padding:18px}#deck-dashboard-host .metrics{grid-template-columns:repeat(2,minmax(0,1fr))}#deck-dashboard-host .gpu-title{align-items:flex-start;flex-direction:column}#deck-dashboard-host .usage-meta{flex-direction:column;gap:3px}}
+
+/* Per-profile history colors are applied through CSSOM under the strict CSP. */
+#deck-dashboard-host .usage-row .fill{background:var(--usage-color,var(--db-accent))}
diff --git a/themes.css b/themes.css
index 2b26ced..f36bb69 100644
--- a/themes.css
+++ b/themes.css
@@ -91,3 +91,7 @@
:root[data-theme="athena-classic"] #deck-dashboard-host .fill.gpu-load{
background:linear-gradient(90deg,#8b7cff,#45d7ff);
}
+
+:root[data-theme="athena-classic"] #deck-dashboard-host .usage-row .fill{
+ background:linear-gradient(90deg,var(--usage-color,#45d7ff),#66e3a4);
+}