const DashboardUI=(()=>{ let host=null,root=null,timers=[],controller=null; const esc=v=>String(v??'–').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); 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}`); 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 mount(){ controller=new AbortController(); root=host; root.innerHTML=`
ATHENA / TELEMETRIE

Dashboard

System, Inferenz und Verlauf auf einen Blick.

verbinde …
Aktives Profil
–
Deck wird abgefragt
Modell
–
–
CPU
–
–
System-RAM
–
–
Inferenz · Live
Generierung
–
Token pro Sekunde
Prompt-Einlesen
–
Token pro Sekunde
Prompt-Cache
–
–
Anfragen
–
–
Slots und Kontextfenster
Aktuelle Belegung und Verlauf des genutzten Kontextfensters
Telemetrie wird geladen …
Historie wird geladen …
MTP / Speculative Decoding
–
–Draft-Token
–akzeptiert
–Prüfschritte
–Draft-Tiefe
Tokenzähler seit Modellstart
Modell-Eigenschaften
Langzeitstatistik
Eingabe-Tokens gesamt
–
neu + Prompt-Cache
Ausgabe-Tokens gesamt
–
seit Beginn der Aufzeichnung
Historie
–
21 Tage detailliert, danach Stundenwerte
GPU-Verlauf
Auslastung und Temperatur beider Karten
Historie wird geladen …
Nutzung nach Profil und Modell
Prozentanteil im oben gewählten Zeitraum
Nutzungsverteilung wird geladen …
Dauerhafte Modellwechsel
ZeitProfilModell
Noch keine Wechsel aufgezeichnet
Deck · Profile · Dienste
Inferenz
–
–aktive Anfragen
–Deck-Uptime
–Profilwechsel
–/data belegt
Verfügbare Profile
Zusatzdienste
Netzwerk und Host
Hardware · Dateien · Laufzeit
GPU-Prozesse
GPUProzessPIDVRAM
–
Ereignisse seit Dashboard-Start
Noch keine Zustandsänderung
llama.cpp Laufzeitkonfiguration
–wird gelesen
Verfügbare GGUF-Dateien
–
DateiPfadGrößeGeändert
–
Notfall-Backups · herunterladen
Verschlüsselte portable Sicherungen
Unersetzliche Daten ohne erneut ladbare Modellgewichte · maximal fünf Generationen
ErstelltGrößeSHA256
Backups werden geladen …
Zum Wiederherstellen wird der separat verwahrte Age-Schlüssel benötigt.
`; const deNum=n=>n==null?'–':Number(n).toLocaleString('de-DE',{maximumFractionDigits:1}); const bytesRate=n=>n==null?'–':n>=1048576?`${(n/1048576).toFixed(1)} MiB/s`:`${(n/1024).toFixed(1)} KiB/s`; const metric=(value,label)=>`
${esc(value)}${esc(label)}
`; const boolLabel=v=>v===true?'ja':v===false?'nein':'–'; const liveRateState={prompt:{tokens:null,seconds:null,value:null,seen:0},generation:{tokens:null,seconds:null,value:null,seen:0}}; function stableLiveRate(kind,tokensTotal,secondsTotal,gauge,active){ const now=Date.now(),state=liveRateState[kind],tokens=Number(tokensTotal),seconds=Number(secondsTotal),raw=Number(gauge); let measured=Number.isFinite(raw)&&raw>0?raw:null; if(Number.isFinite(tokens)&&Number.isFinite(seconds)&&state.tokens!=null&&tokens>=state.tokens&&seconds>state.seconds){ const derived=(tokens-state.tokens)/(seconds-state.seconds); if(Number.isFinite(derived)&&derived>0) measured=measured??derived; } if(Number.isFinite(tokens)&&Number.isFinite(seconds)){ state.tokens=tokens;state.seconds=seconds; } if(measured!=null&&measured<100000){state.value=measured;state.seen=now;} if(state.value!=null&&(active||now-state.seen<8000)) return {text:`${deNum(state.value)} tok/s`,fresh:now-state.seen<2500}; return {text:active?'misst …':'–',fresh:false}; } function sharedSlotPool(slots,unified){ if(!unified||!Array.isArray(slots)||slots.length<2)return null; const total=Math.max(0,...slots.map(s=>Number(s.n_ctx)||0)); const used=slots.reduce((sum,s)=>sum+Math.max(0,Number(s.context_used)||0),0); return total?{total,used,free:Math.max(0,total-used)}:null; } function slotHtml(s,pool){ let used=Math.max(0,Number(s.context_used)||0),total=Math.max(0,Number(s.n_ctx)||0); let available=pool?Math.min(total,used+pool.free):total; let p=available?Math.min(100,used/available*100):0; 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')}
`; } async function refreshFull(){ try{ let response=await fetch('/api/v1/dashboard',{cache:'no-store'}); if(!response.ok) return; let d=await response.json(),rt=d.router||{},lt=rt.llama_telemetry||{},met=lt.metrics||{},props=lt.props||{},lr=d.llama_runtime||{},cpu=d.cpu||{},net=cpu.network||{}; let running=Number(met.requests_processing??(rt.qwen||{}).active_chats??0),waiting=Number(met.requests_deferred||0),active=running>0; let gen=stableLiveRate('generation',met.tokens_predicted_total,met.tokens_predicted_seconds_total,met.predicted_tokens_seconds,active); let prompt=stableLiveRate('prompt',met.prompt_tokens_total,met.prompt_seconds_total,met.prompt_tokens_seconds,active); $('generationRate').textContent=gen.text; let genAvg=met.tokens_predicted_seconds_total?met.tokens_predicted_total/met.tokens_predicted_seconds_total:null; $('generationSub').textContent=active&&!gen.fresh?'Anfrage läuft · letzter Messwert':(genAvg==null?'Aktuelle llama.cpp-Messung':`Gesamtdurchschnitt ${deNum(genAvg)} tok/s`); $('promptRate').textContent=prompt.text; let promptAvg=met.prompt_seconds_total?met.prompt_tokens_total/met.prompt_seconds_total:null; $('promptSub').textContent=active&&!prompt.fresh?'Prompt wird verarbeitet · letzter Messwert':(promptAvg==null?'Aktuelle llama.cpp-Messung':`Gesamtdurchschnitt ${deNum(promptAvg)} tok/s`); let cached=Number(met.prompt_tokens_cached_total||0),processed=Number(met.prompt_tokens_total||0),hit=(cached+processed)?cached/(cached+processed)*100:null; $('cacheHit').textContent=hit==null?'–':`${hit.toFixed(1)} %`;$('cacheBar').style.width=`${hit||0}%`; $('cacheSub').textContent=hit==null?'Keine Cache-Metrik':`${deNum(cached)} wiederverwendet · ${deNum(processed)} neu`; $('requestState').textContent=`${running} aktiv · ${waiting} wartet`; $('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
'; 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??'–'; $('counters').innerHTML=metric(deNum(met.prompt_tokens_total),'Prompt neu')+metric(deNum(met.prompt_tokens_cached_total),'Prompt aus Cache')+metric(deNum(met.tokens_predicted_total),'generierte Token')+metric(deNum(met.n_decode_total),'Decode-Aufrufe')+metric(deNum(met.n_tokens_max),'größte Sequenz')+metric(deNum(met.n_busy_slots_per_decode),'Slots je Decode'); let modalities=Object.entries(props.modalities||{}).filter(([,v])=>v).map(([k])=>k).join(', ')||'–'; $('modelDetails').innerHTML=metric(props.model_ftype||'–','Quantisierung')+metric(props.total_slots??lr.parallel??'–','Slots')+metric(modalities,'Modalitäten')+metric(deNum(props.default_context||lr.context_size),'Kontext')+metric(props.model_alias||lr.alias||'–','Alias')+metric(lr.reasoning_budget??'–','Reasoning-Budget'); $('profiles').innerHTML=Object.entries(rt.profiles||{}).map(([name,ctx])=>`
${esc(name)}${Number(ctx).toLocaleString('de-DE')} Token${name===rt.current_profile?' · aktiv':''}
`).join('')||'
Keine Profile gemeldet
'; let tts=rt.tts||{},stt=rt.stt||{},img=rt.image||{}; $('services').innerHTML=metric(tts.ready?'bereit':'nicht bereit',`TTS · ${tts.engine||'–'}`)+metric(tts.speaker||'–','Stimme')+metric(stt.reachable?'bereit':'aus','STT')+metric(img.worker||'–','Bild-Worker')+metric(img.model||'–','Bildmodell')+metric(img.phase==='idle'?'inaktiv':imagePhaseLabel(img.phase),'Bildstatus')+metric(img.model_loaded?'geladen':'entladen','Modellzustand')+metric(img.last_seconds==null?'–':`${deNum(img.last_seconds)} s`,'letztes Bild'); $('hostMetrics').innerHTML=metric(bytesRate(net.rx_bytes_per_second),'Netzwerk empfangen')+metric(bytesRate(net.tx_bytes_per_second),'Netzwerk gesendet')+metric(dur(cpu.host_uptime_seconds),'Host-Uptime')+metric(dur(d.dashboard_uptime_seconds),'Dashboard-Uptime')+metric((cpu.load||[]).join(' / ')||'–','Load 1/5/15')+metric(net.interfaces??'–','Interfaces'); let summary=d.model_summary||{};$('modelSummary').textContent=`${summary.count??0} Dateien · ${gib(summary.total_size||0)} gesamt`; $('modelFiles').innerHTML=(d.models||[]).map(f=>`${esc(f.name)}${esc(f.relative_path)}${gib(f.size)}${new Date(f.modified*1000).toLocaleString('de-DE')}`).join('')||'Keine GGUF-Dateien im eingebundenen Modellordner'; $('events').innerHTML=(d.events||[]).map(e=>`
${esc(e.name)}: ${esc(e.from)} → ${esc(e.to)}
`).join('')||'
Noch keine Zustandsänderung
'; }catch(_){/* Die bestehende Verbindungsanzeige meldet Fehler bereits sichtbar. */} } refreshFull();timers.push(setInterval(()=>{if(host?.isConnected)refreshFull();else cleanup()},1000)); let historyRange='24h',usageMode='total',lastProfileUsage=[]; const historyColors=['#c5edaf','#ffb454','#7fc8b6','#c39bff']; const hiddenHistorySeries=new Set();let lastHistoryPoints=[]; const hiddenSlotSeries=new Set();let lastSlotPoints=[]; function drawHistory(points){ lastHistoryPoints=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='#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)); const px=t=>pad.l+(t-min)/Math.max(1,max-min)*(w-pad.l-pad.r), py=v=>pad.t+(100-Math.max(0,Math.min(100,v)))/100*(h-pad.t-pad.b); const span=max-min,ticks=5; for(let i=0;ip.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(''); 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()})}); } function drawSlotHistory(points){ lastSlotPoints=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='#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)); const px=t=>pad.l+(t-min)/Math.max(1,max-min)*(w-pad.l-pad.r),py=v=>pad.t+(100-Math.max(0,Math.min(100,v)))/100*(h-pad.t-pad.b); const span=max-min,ticks=5; for(let i=0;ip.slot_id))].sort((a,b)=>a-b); $('slotLegend').innerHTML=ids.map((id,idx)=>{let color=historyColors[idx%historyColors.length],key=String(id);return ``}).join(''); 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.
'} $('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)); function setHistoryRange(e){let b=e.target.closest('button[data-range]');if(!b)return;historyRange=b.dataset.range;root.querySelectorAll('#historyRanges button,#slotHistoryRanges button').forEach(x=>x.classList.toggle('active',x.dataset.range===historyRange));refreshHistory()} $('gpuLegend').addEventListener('click',e=>{let b=e.target.closest('button[data-series]');if(!b)return;let key=b.dataset.series;hiddenHistorySeries.has(key)?hiddenHistorySeries.delete(key):hiddenHistorySeries.add(key);drawHistory(lastHistoryPoints)}); $('slotLegend').addEventListener('click',e=>{let b=e.target.closest('button[data-slot-series]');if(!b)return;let key=b.dataset.slotSeries;hiddenSlotSeries.has(key)?hiddenSlotSeries.delete(key):hiddenSlotSeries.add(key);drawSlotHistory(lastSlotPoints)}); window.addEventListener('resize',()=>{if(host?.isConnected){drawHistory(lastHistoryPoints);drawSlotHistory(lastSlotPoints)}},{signal:controller.signal});refreshHistory();timers.push(setInterval(()=>{if(host?.isConnected)refreshHistory();else cleanup()},15000)); async function refreshSummary(){ try{ const response=await fetch('/api/v1/dashboard',{cache:'no-store'}); if(!response.ok)throw Error(`HTTP ${response.status}`); const d=await response.json(),c=d.cpu||{},m=c.memory||{},rt=d.router||{},up=rt.upstream||{},q=rt.qwen||{},lr=d.llama_runtime||{},img=rt.image||{}; const imageActive=img.phase&&img.phase!=='idle'; $('profile').textContent=imageActive?'Bildgenerierung':(rt.current_profile||'nicht geladen'); $('profileSub').textContent=imageActive?'Bildauftrag läuft':(rt.switching?'Profilwechsel läuft':`Kontext: ${up.ctx?Number(up.ctx).toLocaleString('de-DE'):'–'} Token`); $('model').textContent=imageActive?(img.model||'Bildmodell'):(up.model||'–'); $('modelSub').textContent=imageActive?(img.model_loaded?'geladen':'wird vorbereitet'):(lr.model_file||(up.reachable?'llama.cpp erreichbar':'llama.cpp nicht geladen')); $('cpu').textContent=pct(c.usage_percent);$('cpuBar').style.width=`${c.usage_percent||0}%`; $('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
'; $('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); $('switching').textContent=rt.switching||'nein'; const disk=c.disk_data||{};$('dataDisk').textContent=pct(disk.total?100*disk.used/disk.total:null); $('processes').innerHTML=(d.gpu_processes||[]).map(p=>`${esc((d.gpus||[]).find(g=>g.uuid===p.gpu_uuid)?.index)}${esc(p.name)}${esc(p.pid)}${esc(p.memory_mib)} MiB`).join('')||'Keine Compute-Prozesse gemeldet'; const runtime=[['Modell-Datei',lr.model_file],['PID',lr.pid],['Kontext',lr.context_size?Number(lr.context_size).toLocaleString('de-DE'):'–'],['Batch / µBatch',`${lr.batch_size??'–'} / ${lr.ubatch_size??'–'}`],['Parallel',lr.parallel],['Threads',`${lr.threads??'–'} / ${lr.threads_batch??'–'}`],['Geräte',lr.device],['Tensor-Split',lr.tensor_split],['KV-Cache',`${lr.cache_k??'–'} / ${lr.cache_v??'–'}`],['Flash Attention',lr.flash_attention==null?'–':lr.flash_attention?'an':'aus'],['Prompt-Cache',lr.prompt_cache==null?'–':lr.prompt_cache?'an':'aus'],['MTP Draft',lr.mtp_draft_tokens]]; $('runtime').innerHTML=runtime.map(([k,v])=>metric(v,k)).join(''); const errors=Object.entries(d.errors||{}).filter(([,v])=>v); $('errors').hidden=!errors.length;$('errors').textContent=errors.map(([k,v])=>`${k}: ${v}`).join('\n'); $('updated').textContent=`Live · ${new Date(d.timestamp*1000).toLocaleTimeString('de-DE')}`;$('dot').style.background='var(--green)'; }catch(e){if(!host?.isConnected)return;$('updated').textContent=`Verbindung gestört: ${e.message}`;$('dot').style.background='var(--red)'} } async function refreshBackups(){ try{ const response=await fetch('/api/v1/dashboard/backups',{cache:'no-store'}); if(!response.ok)throw Error(`HTTP ${response.status}`); const d=await response.json(),rows=d.backups||[]; $('backupFiles').innerHTML=rows.map(b=>`${new Date(b.modified*1000).toLocaleString('de-DE')}${gib(b.size)}${esc(b.sha256||'Prüfsumme fehlt')}Herunterladen`).join('')||'Keine portablen Backups gefunden'; $('backupNote').textContent=d.available?`${rows.length} von maximal 5 Generationen · verschlüsselt mit Age`:'Backup-Verzeichnis auf diesem Host nicht eingebunden.'; }catch(e){if(host?.isConnected)$('backupNote').textContent=`Backup-Liste nicht verfügbar: ${e.message}`} } refreshSummary();refreshBackups(); timers.push(setInterval(()=>{if(host?.isConnected)refreshSummary();else cleanup()},1000)); timers.push(setInterval(()=>{if(host?.isConnected)refreshBackups();else cleanup()},60000)); } return {render(){ const view=document.getElementById('view'); if(host?.isConnected)return; if(host)cleanup(); view.innerHTML='
'; host=view.firstElementChild; mount(); }}; })();