'use strict'; const $ = id => document.getElementById(id); const colors = ['#6977db','#8d9ae7','#9e83c9','#50a994','#548ba9','#344779','#e9954c','#b6bdca']; const names = {NEW:'Eingang',VALIDATED:'Angenommen',BUILDING_CASE:'Fallaufbau',INTERPRETING_CASE:'Fallplanung',GENERATING_HYPOTHESES:'Hypothesen',INVESTIGATING_HYPOTHESES:'Untersuchung',BUILDING_DIAGNOSIS:'Abschluss',BLOCKED:'Wartet',REOPENED:'Wiedereröffnet'}; const finished = new Set(['OUTPUT_READY','FAILED_TO_ANALYZE','HANDED_OFF']); const number = n => new Intl.NumberFormat('de-DE').format(n); let token = '', timer = null, overview = null, projects = [], sending = false, submission = null, session = 0; const text = (tag, value, cls) => { const e=document.createElement(tag); e.textContent=value; if(cls)e.className=cls; return e; }; function error(message='') { $('error').hidden=!message; $('error').textContent=message; } async function api(path, options={}) { const credential=token; const response=await fetch(path,{...options,headers:{...options.headers,Authorization:`Bearer ${credential}`},cache:'no-store'}); const data=await response.json(); if(!response.ok) { if(response.status===401 && credential===token) logout(); throw new Error(data.code || `HTTP ${response.status}`); } return data; } function progress(value, label) { const outer=text('div','','progress'); outer.setAttribute('role','progressbar'); outer.setAttribute('aria-label',label); outer.setAttribute('aria-valuemin','0'); outer.setAttribute('aria-valuemax','100'); outer.setAttribute('aria-valuenow',String(value)); const inner=text('div','','progress-fill'); inner.style.width=`${value}%`; outer.append(inner); return outer; } function logout() { session++;token=''; clearTimeout(timer); $('token').value=''; $('content').hidden=true; $('login').hidden=false; $('logout').hidden=true; $('connection').textContent='Nicht verbunden'; document.body.classList.remove('connected'); $('updated').textContent='Warte auf Verbindung'; overview=null;projects=[];submission=null; $('submit-form').reset();$('receipt').textContent='';$('new-files').hidden=false;$('return-files').hidden=true; ['workers','case-rows','bars','legend','project','project-filter'].forEach(id=>$(id).replaceChildren()); ['active-count','token-count','correction-count','finished-count'].forEach(id=>$(id).textContent='—'); $('pie').getContext('2d').clearRect(0,0,360,360); } function renderWorkers(workers, project) { const root=$('workers'); root.replaceChildren(); const visible=workers.filter(w=>!project || (w.case && w.case.project_id===project)); $('worker-count').textContent=`${visible.length} online`; if(!visible.length)root.append(text('div','Kein aktiver Worker für diese Auswahl.','empty')); visible.forEach(worker=>{ const row=text('article','','worker'); const head=text('div','','worker-top'); head.append(document.createElement('i'),text('span',worker.name)); row.append(head); if(worker.case) { const c=worker.case, line=text('div','','worker-case'); line.append(text('span',`${c.case_id} · ${c.title}`),text('span',`${c.progress} %`)); row.append(line,progress(c.progress,`Fortschritt ${c.case_id}`)); } else row.append(text('p','Bereit für den nächsten Auftrag.','hint')); root.append(row); }); } function sumBuckets(cases) { // Per-project aggregation comes from server-filtered individual usage summaries. return cases.reduce((map,c)=>{ (c.usage || []).forEach(b=>{const a=map.get(b.id)||{...b,input_tokens:0,output_tokens:0,calls:0,unknown_usage:0}; ['input_tokens','output_tokens','calls','unknown_usage'].forEach(k=>a[k]+=b[k]||0);map.set(b.id,a);});return map; },new Map()); } function renderPie(buckets) { const canvas=$('pie'), ctx=canvas.getContext('2d'), size=360; ctx.clearRect(0,0,size,size); const total=buckets.reduce((n,b)=>n+b.input_tokens+b.output_tokens,0); const legend=$('legend'); legend.replaceChildren(); let angle=-Math.PI/2; if(!total) {ctx.beginPath();ctx.arc(180,180,140,0,Math.PI*2);ctx.fillStyle='#edf0f7';ctx.fill();} buckets.forEach((b,i)=>{ const count=b.input_tokens+b.output_tokens; if(b.id==='unknown'&&!count&&!b.calls)return; const color=colors[i%colors.length]; if(total&&count) {const end=angle+count/total*Math.PI*2; ctx.beginPath();ctx.moveTo(180,180);ctx.arc(180,180,140,angle,end);ctx.closePath();ctx.fillStyle=color;ctx.fill();angle=end;} const item=document.createElement('li'), swatch=text('span','','swatch');swatch.style.background=color; const name=text('span',b.label), val=text('b',total?`${(count/total*100).toFixed(1).replace('.',',')} %`:'—'); item.title=`${b.label}: ${number(count)} Tokens, ${b.calls} Aufrufe${b.unknown_usage?' · Messwerte unvollständig':''}`; item.append(swatch,name,val);legend.append(item); }); canvas.setAttribute('aria-label',total?'Tokenanteile: '+buckets.filter(b=>b.input_tokens+b.output_tokens).map(b=>`${b.label}: ${number(b.input_tokens+b.output_tokens)}`).join(', '):'Noch keine gemessene Tokennutzung'); } function renderBars(buckets) { const root=$('bars');root.replaceChildren(); const max=Math.max(1,...buckets.map(b=>b.input_tokens+b.output_tokens)); buckets.filter(b=>b.calls).forEach(b=>{ const row=text('div','',`bar-row ${b.id}`), label=text('div','','bar-label'); label.append(text('span',b.label),text('span',number(b.input_tokens+b.output_tokens))); const track=text('div','','bar-track'), input=text('div','','bar-input'), output=text('div','','bar-output'); input.style.width=`${b.input_tokens/max*100}%`;output.style.width=`${b.output_tokens/max*100}%`; row.title=`Eingabe: ${number(b.input_tokens)} · Ausgabe inkl. Denken: ${number(b.output_tokens)}`; track.append(input,output);row.append(label,track);root.append(row); }); if(!root.children.length)root.append(text('div','Noch keine gemessene Tokennutzung.','empty')); } function render() { if(!overview)return; const project=$('project-filter').value; const cases=overview.cases.filter(c=>!project || c.project_id===project), active=cases.filter(c=>!finished.has(c.state)); const buckets=project?[...sumBuckets(cases).values()]:overview.usage.buckets; const total=buckets.reduce((n,b)=>n+b.input_tokens+b.output_tokens,0), corrections=buckets.find(b=>b.id==='correction'); const corrected=corrections?corrections.input_tokens+corrections.output_tokens:0; $('active-count').textContent=number(active.length);$('finished-count').textContent=number(cases.length-active.length); $('token-count').textContent=number(total);$('correction-count').textContent=total?`${(corrected/total*100).toFixed(1).replace('.',',')} %`:'—'; renderWorkers(overview.workers,project);renderPie(buckets);renderBars(buckets); const missing=buckets.reduce((n,b)=>n+b.unknown_usage,0), unassigned=buckets.find(b=>b.id==='unknown'); $('coverage').textContent=`${buckets.reduce((n,b)=>n+b.calls,0)} erfasste Aufrufe. ${missing?`${missing} Aufrufe mit fehlenden Tokenmesswerten; die Diagramme zeigen nur bekannte Werte.`:'Alle erfassten Aufrufe mit vollständigen Eingabe-/Ausgabewerten.'}${unassigned?.calls?` ${unassigned.calls} ältere Aufrufe ohne sichere Korrekturzuordnung.`:''}`; const root=$('case-rows');root.replaceChildren(); active.forEach(c=>{const row=document.createElement('tr'), title=document.createElement('td'); title.append(text('strong',c.case_id),text('small',c.title));row.append(title,text('td',c.project_id),text('td',names[c.state]||c.state),text('td',String(c.analysis_revision))); const cell=document.createElement('td');cell.append(progress(c.progress,`Fortschritt ${c.case_id}`));row.append(cell);root.append(row);}); if(!active.length){const row=document.createElement('tr'),cell=text('td','Keine offenen Fälle.','empty');cell.colSpan=5;row.append(cell);root.append(row);} } async function refresh() { clearTimeout(timer); if(!token)return;const generation=session; try {const response=await api('/v1/overview');if(generation!==session)return;overview=response;render();$('updated').textContent=`Aktualisiert ${new Date().toLocaleTimeString('de-DE')}`; error(overview.read_errors?'Einige Akten konnten nicht gelesen werden. Die Verbrauchsdaten können unvollständig sein.':''); } catch(e) {if(generation===session)error(`Aktualisierung fehlgeschlagen: ${e.message}`);} if(token && generation===session)timer=setTimeout(refresh,4000); } $('login-form').addEventListener('submit',async e=>{ e.preventDefault();const generation=++session;token=$('token').value.trim();error(); try {const response=await api('/v1/profiles');if(generation!==session)return;projects=response.projects; $('project').replaceChildren();$('project-filter').replaceChildren(new Option('Alle freigegebenen Projekte','')); projects.forEach(p=>{$('project').add(new Option(p.project_id,p.project_id));$('project-filter').add(new Option(p.project_id,p.project_id));}); $('login').hidden=true;$('content').hidden=false;$('logout').hidden=false;$('token').value=''; $('connection').textContent='Verbunden';document.body.classList.add('connected');await refresh(); } catch(e) {if(generation===session){token='';error(`Verbindung fehlgeschlagen: ${e.message}`);}else if(!token)error('Verbindung fehlgeschlagen: API-Zugangsschlüssel prüfen.');} }); $('logout').addEventListener('click',logout);$('project-filter').addEventListener('change',render); $('kind').addEventListener('change',()=>{$('new-files').hidden=$('kind').value!=='new_case';$('return-files').hidden=$('kind').value!=='case_return';}); $('submit-form').addEventListener('submit',async e=>{ e.preventDefault();if(sending)return;error(); const project=projects.find(p=>p.project_id===$('project').value), kind=$('kind').value; if(!project)return; const observations=$('observations').value.split('\n').map(v=>v.trim()).filter(Boolean); if(!observations.length)return error('Bitte mindestens eine Beobachtung angeben.'); const metadata={kind,project_id:project.project_id,profile_id:project.profile_id,case_id:$('case-id').value.trim()}; const form=new FormData(), chosen=[]; function append(role, files){for(const file of files){form.append(role,file);chosen.push([role,file.name,file.size,file.lastModified]);}} if(kind==='new_case') {if(!$('wowdiag').files.length)return error('Bitte das wowdiag-Archiv auswählen.'); metadata.report={title:$('title').value.trim(),observations};append('wowdiag',$('wowdiag').files);append('wowsrc',$('wowsrc').files); } else {if(!$('case-package').files.length)return error('Bitte die abgeschlossene ZIP-Akte auswählen.'); metadata.feedback={outcome:$('outcome').value,summary:$('title').value.trim(),observations};append('case_package',$('case-package').files);} append('attachments',$('attachments').files);form.append('metadata',JSON.stringify(metadata)); const signature=JSON.stringify([metadata,chosen]); if(!submission || submission.signature!==signature)submission={signature,key:crypto.randomUUID()}; const generation=session;sending=true;$('send').disabled=true;$('receipt').textContent='Auftrag wird übertragen …'; try {const receipt=await api('/v1/submissions',{method:'POST',headers:{'Idempotency-Key':submission.key},body:form}); if(generation!==session)return; $('receipt').textContent=`Angenommen: ${receipt.case_id} · ${receipt.status} · Auftrags-ID ${receipt.submission_id}`;await refresh(); } catch(e) {if(generation===session){$('receipt').textContent='';error(`Übergabe fehlgeschlagen: ${e.message}. Bei unveränderten Eingaben wird derselbe Idempotenzschlüssel wiederverwendet.`);}} finally{sending=false;$('send').disabled=false;} });