Files
2026-09-11 06:14:38 +02:00

18 lines
7.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const $=s=>document.querySelector(s);let snapshot=null,timer=null,raf=0,lastFetch=0;
const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const pct=(a,b)=>b>0?Math.max(0,Math.min(100,a/b*100)):0;const fmtPct=v=>Number.isFinite(v)?`${Math.round(v)}%`:'–';
const uptime=s=>{s=Math.max(0,Math.floor(s||0));const d=Math.floor(s/86400);s%=86400;const h=Math.floor(s/3600);s%=3600;const m=Math.floor(s/60);return d?`${d}d ${h}h`:h?`${h}h ${m}m`:`${m}m`};
const stateLabel=s=>({queued:'Queue',routing:'Routing',running:'Compute',streaming:'Streaming',completed:'Fertig',failed:'Fehler',cancelled:'Abbruch'}[s]||s||'–');
function setText(id,v){const e=$(id);if(e)e.textContent=v}
function render(s){snapshot=s;document.title=s.title||'Gateway Status';setText('#title',s.title||'Gateway Status');setText('#subtitle',s.subtitle||'Live-Auslastung und Infrastruktur');setText('#k-queue',s.counts?.queued??0);setText('#k-active',s.counts?.active??0);setText('#k-workers',`${s.counts?.healthy_workers??0}/${s.counts?.workers??0}`);setText('#k-models',s.counts?.models??0);setText('#k-streaming',s.counts?.streaming??0);setText('#k-uptime',uptime(s.uptime_seconds));setText('#live-count',`${s.requests?.length||0} EVENTS`);setText('#footer-refresh',`Refresh ${Math.round((s.refresh_interval_ms||2000)/100)/10}s`);const when=new Date(s.generated_at);setText('#updated',Number.isNaN(when.getTime())?'–':`Update ${when.toLocaleTimeString()}`);const wrap=$('.status-wrap');wrap?.classList.remove('ok','bad');const healthy=(s.counts?.workers||0)>0&&(s.counts?.healthy_workers||0)===(s.counts?.workers||0);if(healthy){wrap?.classList.add('ok');setText('#status-text',(s.counts?.queued||0)>0?'Ausgelastet':'Operational')}else{wrap?.classList.add('bad');setText('#status-text','Degraded')}renderWorkers(s.workers||[]);renderLive(s.requests||[]);$('#map-empty')?.classList.toggle('hidden',(s.workers||[]).length>0);drawMap();}
function renderWorkers(ws){const root=$('#workers');if(!root)return;if(!ws.length){root.innerHTML='<div class="empty">Keine Worker-Telemetrie verfügbar.</div>';return}root.innerHTML=ws.map(w=>{const slots=`${w.active||0}/${w.max_concurrent||0} Slots`;const mods=(w.loaded_models||[]).map(m=>`<span>${esc(m)}</span>`).join('');const metrics=[];if(w.resource_metrics){const r=w.resource_metrics;if(r.vram_total_bytes>0)metrics.push(metric('VRAM',fmtPct(pct(r.vram_used_bytes,r.vram_total_bytes)),pct(r.vram_used_bytes,r.vram_total_bytes)));if(r.memory_total_bytes>0)metrics.push(metric('RAM',fmtPct(pct(r.memory_used_bytes,r.memory_total_bytes)),pct(r.memory_used_bytes,r.memory_total_bytes)));if(Number.isFinite(r.gpu_utilization_percent))metrics.push(metric('GPU',fmtPct(r.gpu_utilization_percent),r.gpu_utilization_percent));if(r.gpu_temperature_c>0)metrics.push(metric('Temp',`${Math.round(r.gpu_temperature_c)}°C`,Math.min(100,r.gpu_temperature_c)));}return `<article class="worker"><div class="worker-head"><div><strong>${esc(w.name)}</strong><small>${esc(slots)} · ${esc(w.maintenance||'active')}</small></div><span class="chip ${w.healthy?'':'bad'}">${w.healthy?'online':'offline'}</span></div>${metrics.length?`<div class="metrics">${metrics.join('')}</div>`:''}${mods?`<div class="models">${mods}</div>`:''}</article>`}).join('')}
function metric(k,v,p){return `<div class="metric"><span>${esc(k)}</span><b>${esc(v)}</b><div class="bar"><i style="width:${Math.max(0,Math.min(100,p||0))}%"></i></div></div>`}
function renderLive(rs){const root=$('#live');if(!root)return;if(!rs.length){root.innerHTML='<div class="empty">Aktuell keine sichtbare Request-Aktivität.</div>';return}root.innerHTML=rs.map(r=>{const cls=r.state==='completed'?'done':r.state;const model=r.model?` · ${esc(r.model)}`:'';const worker=r.worker?esc(r.worker):'wartet';const toks=(r.prompt_tokens||0)+(r.completion_tokens||0);return `<article class="event ${esc(cls)}"><div class="event-head"><strong>${esc(r.id)}</strong><span class="state">${esc(stateLabel(r.state))}</span></div><small>${worker}${model}</small><div class="event-meta"><span>Queue ${r.queue_ms||0}ms</span><span>${toks?`${toks} tok`:'–'}</span></div></article>`}).join('')}
async function poll(){try{const r=await fetch('./api/snapshot',{headers:{Accept:'application/json'}});if(!r.ok)throw new Error(`HTTP ${r.status}`);const s=await r.json();render(s);lastFetch=Date.now();const delay=Math.max(1000,s.refresh_interval_ms||2000);clearTimeout(timer);timer=setTimeout(poll,delay)}catch(e){const wrap=$('.status-wrap');wrap?.classList.remove('ok');wrap?.classList.add('bad');setText('#status-text','Unavailable');setText('#updated',e.message);clearTimeout(timer);timer=setTimeout(poll,5000)}}
function fitCanvas(){const c=$('#map-canvas'),stage=$('#map-stage');if(!c||!stage)return null;const dpr=Math.min(devicePixelRatio||1,2),r=stage.getBoundingClientRect();const w=Math.max(1,Math.floor(r.width)),h=Math.max(1,Math.floor(r.height));if(c.width!==Math.floor(w*dpr)||c.height!==Math.floor(h*dpr)){c.width=Math.floor(w*dpr);c.height=Math.floor(h*dpr)}const x=c.getContext('2d');x.setTransform(dpr,0,0,dpr,0,0);return{x,w,h}}
function drawMap(ts=performance.now()){const fit=fitCanvas();if(!fit){raf=requestAnimationFrame(drawMap);return}const {x,w,h}=fit;x.clearRect(0,0,w,h);const s=snapshot;if(s){const ws=s.workers||[],req=s.requests||[];const queue={x:w*.18,y:h*.5},gateway={x:w*.48,y:h*.5};const nodes=ws.map((v,i)=>({v,x:w*.80,y:h*(i+1)/(ws.length+1)}));line(x,queue,gateway,'#b9c7d3',1.4);for(const n of nodes)line(x,gateway,n,'#b9c7d3',1.2);node(x,queue,54,'#a56600','QUEUE',String(s.counts?.queued||0));node(x,gateway,66,'#147fa5','GATEWAY',`${s.counts?.active||0} active`);for(const n of nodes)node(x,n,52,n.v.healthy?'#1a805f':'#b83d4b',n.v.name,`${n.v.active||0}/${n.v.max_concurrent||0}`);req.slice(0,40).forEach((r,i)=>{let a=queue,b=gateway;if(r.worker){const n=nodes.find(n=>n.v.name===r.worker);if(n){a=gateway;b=n}}const phase=((ts/1800)+(hash(r.id)%100)/100)%1;const p=r.state==='queued'?Math.min(.25,phase*.25):phase;const px=a.x+(b.x-a.x)*p,py=a.y+(b.y-a.y)*p;const col=r.state==='streaming'?'#6b5bb5':r.state==='queued'?'#a56600':'#147fa5';x.beginPath();x.fillStyle=col;x.arc(px,py,3.5,0,Math.PI*2);x.fill()})}raf=requestAnimationFrame(drawMap)}
function line(x,a,b,c,l){x.beginPath();x.moveTo(a.x,a.y);x.lineTo(b.x,b.y);x.strokeStyle=c;x.lineWidth=l;x.stroke()}
function node(x,n,r,c,title,sub){x.save();x.beginPath();x.fillStyle='#fff';x.strokeStyle=c;x.lineWidth=2;x.arc(n.x,n.y,r,0,Math.PI*2);x.fill();x.stroke();x.textAlign='center';x.fillStyle='#2a4054';x.font='700 9px '+getComputedStyle(document.documentElement).getPropertyValue('--mono');x.fillText(String(title).slice(0,18),n.x,n.y-2);x.fillStyle='#7b8c9d';x.font='8px '+getComputedStyle(document.documentElement).getPropertyValue('--mono');x.fillText(String(sub).slice(0,18),n.x,n.y+12);x.restore()}
function hash(s){let h=2166136261;for(const c of String(s))h=Math.imul(h^c.charCodeAt(0),16777619);return h>>>0}
document.addEventListener('visibilitychange',()=>{if(!document.hidden&&Date.now()-lastFetch>3000)poll()});poll();drawMap();