Files
glpi-ai-agent/internal/web/templates/diagnostics.html
T
jbergner 7ec10c490b
release-tag / release-image (push) Successful in 1m34s
Diagnose impllementiert
2026-07-29 22:19:43 +02:00

55 lines
20 KiB
HTML
Raw 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.
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="dark">
<title>GLPI AI Agent · Entscheidungsdiagnose</title>
<style>
:root{color-scheme:dark;--bg:#07101b;--panel:#0e1a2a;--panel2:#122238;--line:#243a55;--text:#eef6ff;--muted:#91a6be;--blue:#66a0ff;--green:#44d69b;--amber:#f3bd62;--red:#ff7878;--violet:#9b83ff;font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 70% -10%,rgba(90,130,255,.16),transparent 35%),var(--bg);color:var(--text)}button,input{font:inherit}.shell{min-height:100vh;display:grid;grid-template-rows:auto 1fr}.top{display:flex;align-items:center;justify-content:space-between;gap:18px;padding:18px 24px;border-bottom:1px solid var(--line);background:rgba(7,16,27,.92);position:sticky;top:0;z-index:9;backdrop-filter:blur(14px)}.brand{display:flex;align-items:center;gap:12px}.mark{width:38px;height:38px;border-radius:12px;background:linear-gradient(135deg,#4f82f4,#835be8);display:grid;place-items:center;font-weight:800}.title{font-weight:780}.sub{font-size:12px;color:var(--muted);margin-top:2px}.btn{border:1px solid var(--line);background:var(--panel2);color:var(--text);padding:8px 11px;border-radius:9px;cursor:pointer;text-decoration:none}.btn:hover{border-color:#3d6088}.layout{display:grid;grid-template-columns:330px minmax(0,1fr);min-height:0}.sidebar{border-right:1px solid var(--line);background:rgba(9,19,32,.72);min-height:calc(100vh - 75px);padding:16px;position:sticky;top:75px;height:calc(100vh - 75px);overflow:auto}.search{width:100%;border:1px solid var(--line);background:#091523;color:var(--text);border-radius:10px;padding:10px 11px;outline:none}.search:focus{border-color:var(--blue);box-shadow:0 0 0 3px rgba(102,160,255,.12)}.runlist{display:grid;gap:8px;margin-top:13px}.run{border:1px solid var(--line);background:rgba(14,26,42,.8);border-radius:11px;padding:11px;cursor:pointer}.run:hover,.run.active{border-color:#4771a3;background:#12243b}.run-title{font-weight:680;font-size:13px}.run-meta{color:var(--muted);font-size:11px;margin-top:5px;display:flex;justify-content:space-between;gap:10px}.main{padding:24px;max-width:1580px;width:100%;margin:0 auto}.hero{display:flex;justify-content:space-between;gap:18px;align-items:flex-start;margin-bottom:18px}.eyebrow{font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--blue);font-weight:750}.hero h1{font-size:28px;margin:4px 0 6px;letter-spacing:-.035em}.muted{color:var(--muted)}.chips{display:flex;gap:7px;flex-wrap:wrap}.chip{display:inline-flex;gap:6px;align-items:center;padding:5px 9px;border:1px solid var(--line);border-radius:999px;color:#c8d6e6;font-size:11px;background:rgba(18,34,56,.8)}.chip.good{color:#a7f0d1;border-color:rgba(68,214,155,.35);background:rgba(68,214,155,.08)}.chip.bad{color:#ffb7b7;border-color:rgba(255,120,120,.35);background:rgba(255,120,120,.08)}.chip.warn{color:#f8d99d;border-color:rgba(243,189,98,.35);background:rgba(243,189,98,.08)}.chip.info{color:#c8d7ff;border-color:rgba(102,160,255,.35);background:rgba(102,160,255,.08)}.summary{display:grid;grid-template-columns:repeat(4,minmax(160px,1fr));gap:12px;margin-bottom:16px}.card,.panel{border:1px solid var(--line);background:linear-gradient(180deg,rgba(16,30,49,.96),rgba(11,22,37,.96));border-radius:14px}.card{padding:14px}.label{font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}.value{font-size:21px;font-weight:760;margin-top:8px}.foot{font-size:11px;color:var(--muted);margin-top:4px}.grid2{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:13px}.panel-head{padding:14px 16px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:center;gap:12px}.panel-title{font-size:14px;font-weight:720}.panel-sub{font-size:11px;color:var(--muted);margin-top:2px}.panel-body{padding:14px 16px}.rule{display:grid;grid-template-columns:26px minmax(0,1fr) auto;gap:10px;padding:10px 0;border-bottom:1px solid rgba(36,58,85,.65);align-items:start}.rule:last-child{border-bottom:0}.dot{width:20px;height:20px;border-radius:50%;display:grid;place-items:center;font-size:11px;font-weight:800;margin-top:1px}.dot.pass{background:rgba(68,214,155,.14);color:var(--green)}.dot.fail{background:rgba(255,120,120,.14);color:var(--red)}.dot.warn{background:rgba(243,189,98,.13);color:var(--amber)}.dot.info{background:rgba(102,160,255,.13);color:var(--blue)}.dot.na{background:rgba(145,166,190,.12);color:var(--muted)}.rule-name{font-size:12px;font-weight:650}.rule-detail{font-size:11px;color:var(--muted);line-height:1.5;margin-top:3px}.rule-values{text-align:right;font-size:10px;color:var(--muted);min-width:135px}.rule-values strong{display:block;color:#d9e5f4;font-size:11px}.section{margin-top:14px}.kbtoolbar{display:flex;gap:9px;align-items:center}.kbtoolbar .search{max-width:520px}.results{position:absolute;background:#0c1828;border:1px solid var(--line);border-radius:10px;box-shadow:0 20px 45px rgba(0,0,0,.35);width:min(620px,calc(100vw - 40px));max-height:330px;overflow:auto;z-index:20;margin-top:4px}.result{padding:10px 11px;border-bottom:1px solid var(--line);cursor:pointer}.result:hover{background:#13243a}.result:last-child{border-bottom:0}.scoregrid{display:grid;grid-template-columns:repeat(5,minmax(100px,1fr));gap:8px;margin:11px 0}.scorebox{border:1px solid var(--line);border-radius:10px;padding:9px;background:#091522}.scorebox .n{font-weight:750;font-size:17px;margin-top:4px}.bar{height:6px;background:#07101b;border:1px solid rgba(36,58,85,.65);border-radius:999px;overflow:hidden;margin-top:7px}.bar i{display:block;height:100%;background:linear-gradient(90deg,#557ff0,#64c7e9)}.kbmeta{display:flex;gap:6px;flex-wrap:wrap;margin:8px 0 12px}.excerpt{border:1px solid var(--line);background:#08131f;border-radius:9px;padding:10px;font-size:11px;line-height:1.5;color:#bacadc;white-space:pre-wrap}.candidate-table{width:100%;border-collapse:collapse}.candidate-table th,.candidate-table td{font-size:11px;text-align:left;padding:9px;border-bottom:1px solid var(--line)}.candidate-table th{color:var(--muted);text-transform:uppercase;letter-spacing:.05em}.candidate-row{cursor:pointer}.candidate-row:hover{background:rgba(102,160,255,.05)}.empty{padding:32px;text-align:center;color:var(--muted)}.warning{border-left:3px solid var(--amber);padding:10px 12px;border-radius:8px;background:rgba(243,189,98,.08);font-size:11px;line-height:1.55;color:#edd7ab;margin:10px 0}.raw{font:11px ui-monospace,SFMono-Regular,Menlo,monospace;white-space:pre-wrap;background:#06101b;border:1px solid var(--line);border-radius:9px;padding:10px;max-height:260px;overflow:auto;color:#b8c9da}.hidden{display:none!important}
@media(max-width:1050px){.layout{grid-template-columns:1fr}.sidebar{position:static;height:auto;min-height:0;border-right:0;border-bottom:1px solid var(--line)}.runlist{grid-template-columns:repeat(auto-fill,minmax(230px,1fr));max-height:300px;overflow:auto}.summary{grid-template-columns:repeat(2,1fr)}.grid2{grid-template-columns:1fr}}@media(max-width:620px){.top,.main{padding:14px}.summary{grid-template-columns:1fr}.scoregrid{grid-template-columns:repeat(2,1fr)}.hero{flex-direction:column}.rule{grid-template-columns:24px 1fr}.rule-values{grid-column:2;text-align:left}}
</style>
</head>
<body>
<div class="shell">
<header class="top"><div class="brand"><div class="mark">AI</div><div><div class="title">Entscheidungsdiagnose</div><div class="sub">Regeln, Retrieval und Auto-Reply nachvollziehbar prüfen</div></div></div><div style="display:flex;gap:8px"><a class="btn" href="/">← Control Center</a><button class="btn" id="refreshBtn">Aktualisieren</button></div></header>
<div class="layout">
<aside class="sidebar"><input class="search" id="runSearch" placeholder="Ticket, Run-ID, Entscheidung …"><div class="runlist" id="runList"><div class="empty">Lade Läufe …</div></div></aside>
<main class="main">
<div id="emptyState" class="empty"><h2>Ticketlauf auswählen</h2><p>Links einen Lauf wählen, um alle Entscheidungsregeln und Knowledge-Kandidaten zu prüfen.</p></div>
<div id="content" class="hidden">
<div class="hero"><div><div class="eyebrow">Policy Replay</div><h1 id="ticketTitle">–</h1><div class="muted" id="runMeta">–</div></div><div class="chips" id="heroChips"></div></div>
<div class="summary" id="summary"></div>
<div class="grid2">
<section class="panel"><div class="panel-head"><div><div class="panel-title">Kategorieentscheidung</div><div class="panel-sub">Deterministische Gates vor einer GLPI-Änderung</div></div></div><div class="panel-body" id="categoryRules"></div></section>
<section class="panel"><div class="panel-head"><div><div class="panel-title">Auto-Reply-Regelwerk</div><div class="panel-sub">Alle Bedingungen, auch nach dem ersten Blocker</div></div></div><div class="panel-body" id="replyRules"></div></section>
</div>
<section class="panel section"><div class="panel-head"><div><div class="panel-title">Ausführung & Race-Protection</div><div class="panel-sub">Followups, Dry-Run und Schreibschutz</div></div></div><div class="panel-body" id="executionRules"></div></section>
<section class="panel section"><div class="panel-head"><div><div class="panel-title">Knowledge-Kandidaten des Laufs</div><div class="panel-sub">Warum ein Artikel an die KI ging – oder nur im Audit blieb</div></div></div><div class="panel-body"><div id="candidateInfo" class="muted" style="font-size:11px;margin-bottom:10px"></div><div style="overflow:auto"><table class="candidate-table"><thead><tr><th>Rang</th><th>Artikel</th><th>Retrieval</th><th>Semantik</th><th>Titel</th><th>Lexikalisch</th><th>Status</th></tr></thead><tbody id="candidateRows"></tbody></table></div></div></section>
<section class="panel section"><div class="panel-head"><div><div class="panel-title">Beliebige KB prüfen</div><div class="panel-sub">Auch Artikel untersuchen, die nicht in den Audit-Top-K lagen</div></div></div><div class="panel-body"><div class="kbtoolbar"><div style="position:relative;flex:1"><input class="search" id="kbSearch" placeholder="KB-ID, Titel, Keyword oder externe Kategorie …"><div id="kbResults" class="results hidden"></div></div></div><div id="kbDiagnostic" class="empty">Knowledge-Artikel suchen oder oben einen Kandidaten anklicken.</div></div></section>
<section class="panel section"><div class="panel-head"><div><div class="panel-title">KI-Begründung & Policy-Code</div><div class="panel-sub">Freitext ist nur Erklärung; maßgeblich sind die Regeln oben</div></div></div><div class="panel-body"><div class="grid2"><div><div class="label">KI-Begründung</div><div class="raw" id="aiReason"></div></div><div><div class="label">Policy</div><div class="raw" id="policyReason"></div><div class="label" style="margin-top:10px">Fehler</div><div class="raw" id="errorText"></div></div></div></div></section>
</div>
</main></div></div>
<script>
'use strict';
const $=q=>document.querySelector(q), esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const pct=v=>`${(Number(v||0)*100).toLocaleString('de-DE',{maximumFractionDigits:1})} %`;const date=v=>v?new Date(v).toLocaleString('de-DE'):'–';
let runs=[],current=null,kbTimer=null;
async function api(u){const r=await fetch(u,{headers:{'X-Requested-With':'GLPI-AI-Agent'}});if(!r.ok)throw new Error((await r.text()).trim()||`HTTP ${r.status}`);return r.json()}
function statusChip(text,kind=''){return `<span class="chip ${kind}">${esc(text)}</span>`}
function icon(s){return s==='pass'?'✓':s==='fail'?'×':s==='warn'?'!':s==='info'?'i':'–'}
function ruleHTML(r){return `<div class="rule"><div class="dot ${esc(r.status||'na')}">${icon(r.status)}</div><div><div class="rule-name">${esc(r.label||r.code)}</div>${r.detail?`<div class="rule-detail">${esc(r.detail)}</div>`:''}${r.blocking?'<div class="rule-detail" style="color:var(--red)">Blockierende Bedingung</div>':''}</div><div class="rule-values"><strong>${esc(r.actual||'–')}</strong>${r.expected?`Soll: ${esc(r.expected)}`:''}</div></div>`}
function renderRules(el,rows){$(el).innerHTML=rows?.length?rows.map(ruleHTML).join(''):'<div class="empty">Für diesen älteren Lauf wurden noch keine strukturierten Regelchecks gespeichert.</div>'}
function filterRuns(){const q=$('#runSearch').value.trim().toLowerCase();const rows=runs.filter(r=>!q||[r.ticket_id,r.ticket_name,r.run_id,r.category_decision,r.reply_decision,r.policy_reason].join(' ').toLowerCase().includes(q));$('#runList').innerHTML=rows.map(r=>`<div class="run ${current?.run_id===r.run_id?'active':''}" data-run="${esc(r.run_id)}"><div class="run-title">#${r.ticket_id} ${esc(r.ticket_name||'')}</div><div class="run-meta"><span>${esc(date(r.finished_at))}</span><span>${esc(r.outcome)}</span></div><div style="margin-top:7px">${statusChip(r.category_decision||'–',r.category_changed?'good':'info')} ${statusChip(r.reply_decision||'–',r.reply_written?'good':r.reply_proposed?'info':'warn')}</div></div>`).join('')||'<div class="empty">Keine Läufe.</div>';document.querySelectorAll('[data-run]').forEach(x=>x.onclick=()=>loadRun(x.dataset.run))}
async function loadRuns(){runs=await api('/api/runs?limit=200');filterRuns();if(!current&&runs.length)loadRun(runs[0].run_id)}
async function loadRun(id){current=await api(`/api/diagnostics/run/${encodeURIComponent(id)}`);$('#emptyState').classList.add('hidden');$('#content').classList.remove('hidden');filterRuns();renderRun()}
function renderRun(){const r=current;$('#ticketTitle').textContent=`#${r.ticket_id} ${r.ticket_name||''}`;$('#runMeta').textContent=`Run ${r.run_id} · ${date(r.finished_at)} · ${r.outcome}`;$('#heroChips').innerHTML=[statusChip(r.dry_run?'DRY RUN':'LIVE',r.dry_run?'warn':'good'),statusChip(r.category_decision||'Kategorie –','info'),statusChip(r.reply_decision||'Reply –',r.reply_written?'good':r.reply_proposed?'info':'warn')].join('');
const cards=[['Kategorie',r.ai_recommended_category_name?`${r.ai_recommended_category_name} · ${pct(r.ai_category_confidence)}`:'keine Empfehlung',`Schwellwert ${pct(r.category_threshold)}`],['Knowledge',r.knowledge_top_title||'kein Treffer',r.knowledge_top_id?`Retrieval ${pct(r.knowledge_score)} · Evidenz ${pct(r.knowledge_evidence_score)}`:'–'],['Auto-Reply',r.ai_reply_recommended?`KI: Ja · ${pct(r.ai_reply_confidence)}`:'KI: Nein',r.reply_decision||'–'],['Kontext',`C:${r.context_changes||0} I:${r.context_incidents||0} U:${r.context_issues||0} D:${r.context_devices||0}`,(r.context_warnings||[]).length?`${r.context_warnings.length} Warnungen`:'vollständig']];$('#summary').innerHTML=cards.map(c=>`<div class="card"><div class="label">${esc(c[0])}</div><div class="value">${esc(c[1])}</div><div class="foot">${esc(c[2])}</div></div>`).join('');
renderRules('#categoryRules',r.category_checks);renderRules('#replyRules',r.reply_checks);renderRules('#executionRules',r.execution_checks);
$('#candidateInfo').textContent=`An KI: ${r.knowledge_llm_candidates||0} · Cutoff ${pct(r.knowledge_candidate_cutoff)} · Max. Abstand ${pct(r.knowledge_candidate_max_gap)} · Audit Top K ${r.knowledge_audit_top_k||0}`;
const cs=r.knowledge_candidates||[];$('#candidateRows').innerHTML=cs.map((c,i)=>`<tr class="candidate-row" data-kb="${esc(c.id)}"><td>${c.retrieval_rank||i+1}</td><td><strong>${esc(c.title)}</strong><div class="muted">${esc(c.id)} · ${esc(c.source)}</div></td><td>${pct(c.score)}</td><td>${pct(c.semantic_score)}</td><td>${pct(c.title_score)}</td><td>${pct(c.lexical_score)}</td><td>${c.sent_to_ai?statusChip('an KI','good'):statusChip(c.selection_reason||'nur Audit','warn')}</td></tr>`).join('')||'<tr><td colspan="7" class="empty">Keine Kandidaten im Audit.</td></tr>';document.querySelectorAll('[data-kb]').forEach(x=>x.onclick=()=>diagnoseKB(x.dataset.kb));$('#aiReason').textContent=r.ai_reason||r.reason||'–';$('#policyReason').textContent=r.policy_reason||'–';$('#errorText').textContent=r.error||'–';$('#kbDiagnostic').innerHTML='<div class="empty">Knowledge-Artikel suchen oder einen Kandidaten anklicken.</div>'}
function selectionText(code){return({sent_to_ai:'An die KI gesendet',below_retrieval_floor:'Unter Retrieval-Floor',outside_candidate_gap:'Außerhalb des dynamischen Abstands',max_candidates_reached:'Maximale Kandidatenzahl erreicht',not_selected:'Nicht ausgewählt'})[code]||code||'–'}
async function diagnoseKB(id){if(!current)return;$('#kbDiagnostic').innerHTML='<div class="empty">Berechne Diagnose …</div>';try{const d=await api(`/api/diagnostics/run/${encodeURIComponent(current.run_id)}/knowledge?knowledge_id=${encodeURIComponent(id)}`);const warn=d.current_ticket_changed?'<div class="warning">Das Ticket wurde seit diesem historischen Lauf verändert. Die KB-Bewertung unten ist eine aktuelle Neu-Berechnung gegen den jetzigen Ticketstand/Index; die gespeicherten Laufregeln oben bleiben historisch verbindlich.</div>':'';const scores=[['Retrieval',d.retrieval_score],['Semantik',d.semantic_score],['Titel',d.title_score],['Lexikalisch',d.lexical_score],['Kategorie',d.category_score]];$('#kbDiagnostic').innerHTML=`${warn}<div style="display:flex;justify-content:space-between;gap:12px;align-items:flex-start"><div><div class="eyebrow">KB Inspector</div><h2 style="margin:4px 0">${esc(d.title)}</h2><div class="muted">${esc(d.knowledge_id)} · ${esc(d.source)}</div></div><div class="chips">${statusChip(`Rang #${d.retrieval_rank}`,'info')}${d.sent_to_ai?statusChip('an KI gesendet','good'):statusChip(selectionText(d.selection_reason),'warn')}${d.ai_selected?statusChip('von KI gewählt','good'):''}</div></div><div class="scoregrid">${scores.map(x=>`<div class="scorebox"><div class="label">${esc(x[0])}</div><div class="n">${pct(x[1])}</div><div class="bar"><i style="width:${Math.max(0,Math.min(100,Number(x[1]||0)*100))}%"></i></div></div>`).join('')}</div><div class="kbmeta">${statusChip(`Cutoff ${pct(d.candidate_cutoff)}`,'info')}${statusChip(`Evidenz ${pct(d.evidence_score)}`,d.evidence_score>=d.required_score?'good':'warn')}${statusChip(`erforderlich ${pct(d.required_score)}`,'info')}${d.document.auto_reply?statusChip('auto_reply=true','good'):statusChip('auto_reply=false','bad')}${(d.unmapped_categories||[]).length?statusChip(`ungemappt: ${d.unmapped_categories.join(', ')}`,'bad'):''}</div><div class="grid2"><div><div class="label">Passender Ticketabschnitt</div><div class="excerpt">${esc(d.best_query_excerpt||'–')}</div></div><div><div class="label">Passender KB-Abschnitt</div><div class="excerpt">${esc(d.best_chunk_excerpt||'–')}</div></div></div><div style="margin-top:13px"><div class="panel-title">Regelprüfung für diesen Artikel</div>${(d.checks||[]).map(ruleHTML).join('')}</div>`}catch(e){$('#kbDiagnostic').innerHTML=`<div class="warning">${esc(e.message)}</div>`}}
async function searchKB(){const q=$('#kbSearch').value.trim();if(q.length<2){$('#kbResults').classList.add('hidden');return}try{const rows=await api(`/api/diagnostics/knowledge?q=${encodeURIComponent(q)}&limit=30`);$('#kbResults').innerHTML=rows.map(x=>`<div class="result" data-search-kb="${esc(x.id)}"><strong>${esc(x.title)}</strong><div class="muted" style="font-size:11px;margin-top:3px">${esc(x.id)} · ${esc(x.source)} · auto_reply=${x.auto_reply?'true':'false'}${(x.unmapped_categories||[]).length?` · ungemappt: ${esc(x.unmapped_categories.join(', '))}`:''}</div></div>`).join('')||'<div class="empty">Keine Treffer.</div>';$('#kbResults').classList.remove('hidden');document.querySelectorAll('[data-search-kb]').forEach(x=>x.onclick=()=>{$('#kbResults').classList.add('hidden');$('#kbSearch').value=x.dataset.searchKb;diagnoseKB(x.dataset.searchKb)})}catch(e){$('#kbResults').innerHTML=`<div class="warning">${esc(e.message)}</div>`;$('#kbResults').classList.remove('hidden')}}
$('#runSearch').addEventListener('input',filterRuns);$('#kbSearch').addEventListener('input',()=>{clearTimeout(kbTimer);kbTimer=setTimeout(searchKB,220)});$('#refreshBtn').onclick=()=>loadRuns();document.addEventListener('click',e=>{if(!e.target.closest('#kbSearch')&&!e.target.closest('#kbResults'))$('#kbResults').classList.add('hidden')});loadRuns().catch(e=>{$('#runList').innerHTML=`<div class="warning">${esc(e.message)}</div>`});
</script>
</body></html>