Files
VianoBugAnalysis/src/viano_bug_analysis/web/case-detail.js
T

117 lines
14 KiB
JavaScript

'use strict';
let selectedCase=null, detailLoading=false, detailHash=null;
const words={observed_behavior:'Beobachtetes Verhalten',expected_behavior:'Erwartetes Verhalten',tester_assumptions:'Testerannahmen',confirmed_technical_facts:'Bestätigte technische Fakten',behavior_gap:'Abweichung',first_known_divergence:'Erste bekannte Abweichung',failure_stage:'Fehlerstelle',contradictions:'Widersprüche',unknowns:'Offene Fragen',relevant_entities:'Relevante Entitäten',investigation_direction:'Untersuchungsrichtung',interpretation_summary:'Einordnung',described_behavior:'Beschriebenes Verhalten',provisional_behavior_gap:'Vorläufige Abweichung',mechanism:'Vermuteter Mechanismus',rationale:'Begründung',expected_evidence:'Erwartete Belege',falsifying_evidence:'Mögliche Gegenbelege',open_questions:'Offene Fragen',text:'Aussage',scope:'Geltungsbereich',reason:'Begründung',status:'Status',confidence:'Confidence',selected_hypothesis:'Gewählte Hypothese',root_cause:'Fehlerursache',evaluations:'Bewertungen der Hypothesen',remaining_uncertainty:'Verbleibende Unsicherheit',recommended_human_actions:'Empfohlene menschliche Maßnahmen',decision_basis:'Entscheidungsgrundlage',causal_claim:'Kausale Aussage',supporting_evidence:'Stützende Belege',counter_evidence:'Gegenbelege',scope_limits:'Geltungsgrenzen',title:'Aufgabe',instructions:'Anweisungen',expected_results:'Erwartetes Ergebnis',problem_summary:'Problem',required_change:'Erforderliche Änderung',implementation_intent:'Umsetzungsziel',affected_scope:'Betroffener Bereich',implementation_guidance:'Hinweise zur Umsetzung',validation_requirements:'Validierung',constraints:'Vorgaben',preserve:'Beibehalten',forbidden_changes:'Unzulässige Änderungen',do_not_fix_by:'Zu vermeidende Lösungswege',missing_information:'Fehlende Informationen',placeholders:'Platzhalter',diagnostic_caveats:'Diagnostische Einschränkungen',coverage:'Abdeckung',kind:'Art',operation:'Werkzeug',duplicate_of:'Bereits erfasster Beleg',hypothesis_id:'Hypothese',resolved:'Beantwortet',author:'Beobachter',patch_id:'Patch',tested_build:'Getesteter Build',summary:'Zusammenfassung',observations:'Beobachtungen',outcome:'Rückmeldung',entity_id:'Entität',required:'Erforderlich',description:'Beschreibung',value:'Wert',column:'Spalte',table:'Tabelle',source_ref:'Quelle',evidence_refs:'Belegreferenzen',source_ids:'Quellen',interpretation_ids:'Interpretationsreferenzen'};
const values={unassessed:'Unbewertet',weak:'Schwach',plausible:'Plausibel',supported:'Gestützt',strongly_supported:'Stark gestützt',weakened:'Geschwächt',confirmed:'Bestätigt',inconclusive:'Nicht abschließend geklärt',assigned:'Auftrag vorhanden',none:'Kein Auftrag',ready:'Bereit',ready_with_placeholders:'Vorlage mit Platzhaltern',refuted:'Verworfen',finished:'Abgeschlossen',open:'Offen',investigation_finished:'Untersuchung abgeschlossen',patch_failed:'Patch behebt den Fehler nicht',validation_failed:'Validierung fehlgeschlagen',new_evidence:'Neue Belege',review_feedback:'Review-Rückmeldung'};
function friendly(value){return typeof value==='string'?(values[value]||value):value===true?'Ja':value===false?'Nein':String(value);}
Object.assign(words,{completeness:'Vollständigkeit',statements:'Aussagen',observed_at:'Beobachtet am',received_at:'Eingegangen am',revision:'Revision',effect:'Wirkung',support:'Dafür',against:'Dagegen'});
Object.assign(values,{missing:'Nicht erfasst',complete:'Vollständig',partial:'Teilweise erfasst'});
function semantic(value,depth=0){
if(value===null||value===undefined)return text('p','Nicht angegeben.','hint');
if(Array.isArray(value)){
if(!value.length)return text('p','Keine Angaben.','hint');
const list=text('div','','semantic-list');value.forEach(v=>{const item=text('div','','semantic-item');item.append(semantic(v,depth+1));list.append(item);});return list;
}
if(typeof value!=='object')return text('p',friendly(value),'semantic-text');
if(typeof value.text==='string'){
const block=text('div','','statement');block.append(text('p',value.text,'semantic-text'));
for(const field of ['scope','author','observed_at'])if(value[field])block.append(text('small',`${words[field]||field}: ${value[field]}`));
for(const field of ['evidence_refs','source_ids'])if(value[field]?.length)block.append(text('div',value[field].join(' · '),'reference-list'));
return block;
}
const dl=text('dl','','semantic-fields');Object.entries(value).forEach(([key,child])=>{
if(key==='provenance'||key==='origin')return;
dl.append(text('dt',words[key]||key.replaceAll('_',' ')));const dd=document.createElement('dd');dd.append(semantic(child,depth+1));dl.append(dd);
});return dl;
}
function panel(title,content,expanded=true){
const card=text('details','','panel detail-panel');card.open=expanded;card.append(text('summary',title));
const body=text('div','','detail-body');body.append(content);card.append(body);return card;
}
function renderArchive(){
if(!overview)return;
const project=$('result-project').value,search=$('result-search').value.trim().toLocaleLowerCase('de-DE');
const list=overview.cases.filter(c=>finished.has(c.state)&&(!project||project===c.project_id)&&(!search||`${c.case_id} ${c.title}`.toLocaleLowerCase('de-DE').includes(search))).sort((a,b)=>b.updated_at.localeCompare(a.updated_at));
const body=$('result-rows');body.replaceChildren();
list.forEach(c=>{const row=document.createElement('tr'),name=document.createElement('td'),link=text('a',c.case_id,'case-link');link.href=`#case/${c.key}`;
name.append(link,text('small',c.title));row.append(name,text('td',c.project_id),text('td',({OUTPUT_READY:'Ergebnis bereit',HANDED_OFF:'Übergeben',FAILED_TO_ANALYZE:'Menschliches Review'})[c.state]),text('td',String(c.analysis_revision)),text('td',new Date(c.updated_at).toLocaleString('de-DE')));body.append(row);});
if(!list.length){const row=document.createElement('tr'),cell=text('td','Keine abgeschlossenen Fälle für diese Auswahl.','empty');cell.colSpan=5;row.append(cell);body.append(row);}
}
function clearDetails(){selectedCase=null;detailHash=null;detailLoading=false;$('result-search').value='';}
async function downloadCase(data,button){
const generation=session,credential=token;button.disabled=true;error();
try{const response=await fetch(`/v1/cases/${data.key}/package`,{headers:{Authorization:`Bearer ${credential}`},cache:'no-store'});
if(!response.ok){const issue=await response.json();if(response.status===401&&credential===token)logout();throw new Error(issue.code||`HTTP ${response.status}`);}
const blob=await response.blob();if(generation!==session)return;
const url=URL.createObjectURL(blob),link=document.createElement('a');link.href=url;link.download=`${data.case_id.replace(/[^A-Za-z0-9_.-]/g,'_')}-Fallakte.zip`;document.body.append(link);link.click();link.remove();setTimeout(()=>URL.revokeObjectURL(url),30000);
}catch(e){if(generation===session)error(`Download fehlgeschlagen: ${e.message}`);}finally{button.disabled=false;}
}
function renderDetail(data){
const root=$('case-detail');root.replaceChildren();
const head=text('section','','panel detail-header'),top=text('div','','section-head'),info=document.createElement('div');
info.append(text('div',`${data.project_id} · ${data.case_id}`,'eyebrow'));
const title=data.reports.find(r=>r.title)?.title||data.case_id;info.append(text('h2',title));
info.append(text('p',`${names[data.state]||({OUTPUT_READY:'Ergebnis bereit',HANDED_OFF:'Übergeben',FAILED_TO_ANALYZE:'Menschliches Review'})[data.state]||data.state} · Revision ${data.revision}`));
const controls=text('div','','detail-controls'),label=text('label','Analyse-Revision'),select=document.createElement('select');select.id='revision-select';select.setAttribute('aria-label','Analyse-Revision');
data.revisions.forEach(r=>select.add(new Option(`Revision ${r.revision}${r.revision===data.current_revision?' · aktuell':''}`,r.revision)));
select.value=String(data.revision);select.addEventListener('change',()=>location.hash=`#case/${data.key}/revision/${select.value}`);label.append(select);controls.append(label);
const download=text('button','Fallakte herunterladen');download.type='button';download.addEventListener('click',()=>downloadCase(data,download));controls.append(download);
top.append(info,controls);head.append(top,text('p','Der Download enthält die vollständige aktuelle Akte einschließlich aller erhaltenen Revisionen.','hint'));
if(data.revision!==data.current_revision)head.append(text('p','Historische Ansicht: Ergebnisse anderer Revisionen werden nicht beigemischt.','history-note'));
if(data.truncated)head.append(text('p','Die Anzeige ist gekürzt. Die vollständigen Inhalte bleiben in der herunterladbaren Fallakte erhalten.','history-note'));
root.append(head);
const actions=text('div','','next-actions');
if(data.reason)actions.append(text('p',`Sperr-/Reviewgrund: ${data.reason}`));
if(data.worker.code)actions.append(text('p',`Workerhinweis: ${data.worker.code}`));
data.actions.forEach(a=>actions.append(text('p',a.text)));
if(typeof addRetryControl==='function')addRetryControl(data,actions);
root.append(panel('Zustand und nächste Schritte',actions));
root.append(panel('Testerbeobachtungen',semantic({observations:data.observations,tester_assumptions:data.tester_assumptions,expected_behavior:data.expected_behavior})));
root.append(panel('Caseinterpretation',Object.keys(data.interpretation).length?semantic(data.interpretation):text('p','Für diese Revision liegt noch keine akzeptierte Interpretation vor.','hint'),false));
const hypotheses=text('div','','hypothesis-list');
data.hypotheses.forEach(h=>{const card=text('article','','hypothesis-card'),line=text('div','','hypothesis-heading');line.append(text('h3',`${h.id} · ${h.title}`),text('span',`Confidence: ${friendly(h.confidence)}`,'badge'));
card.append(line,text('p',`Status: ${friendly(h.disposition)} · Untersuchungsschritte: ${h.iterations}`,'hint'),semantic(h.proposal));
const evidence=text('div','','evidence-columns');for(const [key,title] of [['support','Befunde dafür'],['against','Befunde dagegen']]){const column=document.createElement('div');column.append(text('h4',title),semantic(h[key]));evidence.append(column);}card.append(evidence,text('p','Diese Befunde sind KI-Bewertungen der referenzierten Belege, keine zusätzlichen Messungen.','hint'));
if(Object.keys(h.review).length)card.append(panel('Letzte kausale Bewertung',semantic(h.review),false));
if(h.uncertainties.length)card.append(panel('Offene Unsicherheiten',semantic(h.uncertainties),false));
if(h.evidence_ids.length)card.append(text('p',`Belege: ${h.evidence_ids.join(' · ')}`,'reference-list'));
hypotheses.append(card);});
if(!data.hypotheses.length)hypotheses.append(text('p','Noch keine akzeptierten Hypothesen in dieser Revision.','hint'));
root.append(panel('Hypothesen und Bewertung',hypotheses));
root.append(panel('Belegverzeichnis',data.evidence.length?semantic(data.evidence):text('p','Noch keine erfassten Belege.','hint'),false));
root.append(panel('Diagnose',data.diagnosis?semantic(data.diagnosis):text('p','Noch keine finale Diagnose in dieser Revision.','hint')));
if(Object.keys(data.review).length)root.append(panel('Menschliches Review',semantic(data.review)));
const coding=document.createElement('div');
if(!Object.keys(data.coding).length)coding.append(text('p','Für diese Revision wurde noch kein Codingauftragsdokument erstellt.','hint'));
else{coding.append(text('p',data.coding.assignment==='none'?'Kein Codingauftrag vorhanden.':'Codingauftrag vorhanden.','assignment-status'));coding.append(semantic(data.coding.request||{}));if(data.coding.diagnostic_caveats)coding.append(panel('Diagnostische Einschränkungen',semantic(data.coding.diagnostic_caveats),false));}
root.append(panel('Codingauftrag',coding));
const human=document.createElement('div');
if(data.human&&['none','assigned'].includes(data.human.status)){human.append(text('p',data.human.status==='none'?'Kein Human-Dev-Auftrag vorhanden.':'Human-Dev-Auftrag vorhanden.','assignment-status'),semantic(data.human));}
else if(data.human_text)human.append(text('pre',data.human_text,'human-document'));
else human.append(text('p','Für diese Revision liegt noch kein Human-Dev-Auftragsdokument vor.','hint'));
root.append(panel('Human-Dev-Auftrag',human));
root.append(panel('Patch-Rückmeldungen und neue Beobachtungen',data.feedback.length?semantic(data.feedback):text('p','Bis zu dieser Revision liegen keine über die Übergabe-API eingereichten Rückmeldungen vor.','hint'),false));
}
async function loadDetail(hash){
const match=hash.match(/^#case\/([a-f0-9]{64}\.[a-f0-9]{64})(?:\/revision\/([0-9]+))?$/);if(!match||!token)return;
const generation=session;detailLoading=true;detailHash=hash;
if(!selectedCase||selectedCase.key!==match[1]||String(selectedCase.revision)!==match[2])$('case-detail').replaceChildren(text('p','Fallakte wird geladen …','panel'));
try{const data=await api(`/v1/cases/${match[1]}${match[2]!==undefined?'?revision='+match[2]:''}`);
if(generation!==session||location.hash!==hash)return;selectedCase=data;renderDetail(data);
}catch(e){if(generation===session&&location.hash===hash){$('case-detail').replaceChildren(text('p',`Fallakte nicht verfügbar: ${e.message}`,'panel'));}}
finally{if(generation===session&&detailHash===hash)detailLoading=false;}
}
function syncDetail(){
if(!token||!location.hash.startsWith('#case/'))return;
const current=overview?.cases.find(c=>c.key===selectedCase?.key);
if(!detailLoading&&(!selectedCase||detailHash!==location.hash||(!location.hash.includes('/revision/')&&current?.package_generation!==selectedCase.package_generation)))loadDetail(location.hash);
}
function route(){
const detail=location.hash.startsWith('#case/'),results=location.hash==='#results',submissionPage=location.hash==='#submit';
$('overview-page').hidden=detail||results||submissionPage;$('results').hidden=!results;$('case-detail').hidden=!detail;$('submit').hidden=detail||results;
document.querySelector('header h1').textContent=detail?'Die Fallakte.':results?'Abgeschlossene Fälle.':submissionPage?'Auftrag übergeben.':'Analyse im Blick.';
document.querySelectorAll('nav a').forEach(a=>a.classList.toggle('selected',detail?a.hash==='#cases':a.hash===(results?'#results':submissionPage?'#submit':location.hash==='#cases'?'#cases':'#overview')));
if(detail)loadDetail(location.hash);if(results)renderArchive();
}
$('result-project').addEventListener('change',renderArchive);$('result-search').addEventListener('input',renderArchive);
window.addEventListener('hashchange',route);route();