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); +}