Files
glpi-ai-agent/internal/web/templates/diagnostics.html
groot 31fa84f243
All checks were successful
release-tag / release-image (push) Successful in 1m34s
Bugfix GLPI-KB-Category
2026-08-05 15:33:56 +02:00

90 lines
39 KiB
HTML
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.
<!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(auto-fit,minmax(180px,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}.flow{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px}.flow-step{border:1px solid var(--line);background:#091522;border-radius:11px;padding:12px}.flow-step .n{font-size:11px;color:var(--blue);font-weight:800;letter-spacing:.08em;text-transform:uppercase}.flow-step .h{font-weight:720;margin-top:5px}.flow-step .d{font-size:11px;color:var(--muted);line-height:1.5;margin-top:5px}.select{border:1px solid var(--line);background:#091523;color:var(--text);border-radius:10px;padding:10px 11px}.analysis-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:10px}.analysis-card{border:1px solid var(--line);background:#091522;border-radius:11px;padding:12px;cursor:pointer}.analysis-card:hover{border-color:#4771a3}.analysis-card .name{font-weight:720;margin:5px 0}.analysis-card .meta{font-size:11px;color:var(--muted);line-height:1.5}.analysis-detail{margin-top:12px;border-top:1px solid var(--line);padding-top:14px}.hidden{display:none!important}
@media(max-width:1050px){.flow{grid-template-columns:1fr}.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>
<section class="panel section"><div class="panel-head"><div><div class="panel-title">Mehrstufiger Analyseablauf</div><div class="panel-sub">Kategorie → Priorität → Uptime-Kuma-Zuordnung → normale KB-Antwort nur bei Bedarf</div></div></div><div class="panel-body"><div class="flow" id="analysisFlow"></div></div></section>
<section class="panel section"><div class="panel-head"><div><div class="panel-title">Eigenständige Analyseläufe</div><div class="panel-sub">Jede KI-Funktion besitzt eine eigene ID, Eingabe, Entscheidung, Policy-Prüfung und Aktion</div></div></div><div class="panel-body"><div class="analysis-grid" id="analysisRuns"></div><div class="analysis-detail hidden" id="analysisDetail"></div></div></section>
<div class="grid2 section">
<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">Prioritätsentscheidung</div><div class="panel-sub">Aktuell → KI-Empfehlung → deterministisch erlaubter Zielwert</div></div><div class="chips" id="priorityBadges"></div></div><div class="panel-body"><div id="prioritySummary" class="warning" style="margin-top:0"></div><div id="priorityRules"></div></div></section>
</div>
<section class="panel section"><div class="panel-head"><div><div class="panel-title">Status-Template-Regelwerk</div><div class="panel-sub">Relevanz × KI-Confidence; der Nachrichtentext ist vollständig vordefiniert</div></div></div><div class="panel-body" id="statusRules"></div></section>
<section class="panel section"><div class="panel-head"><div><div class="panel-title">Auto-Reply-Regelwerk</div><div class="panel-sub">Normale Knowledge-Antwort; wird bei freigegebenem Status-Template übersprungen</div></div></div><div class="panel-body" id="replyRules"></div></section>
<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">Uptime-Kuma-Kandidaten für Statusantwort</div><div class="panel-sub">Die KI darf nur einen Eintrag auswählen; sie erzeugt keinen Benutzertext</div></div></div><div class="panel-body"><div id="statusCandidateInfo" class="muted" style="font-size:11px;margin-bottom:10px"></div><div style="overflow:auto"><table class="candidate-table"><thead><tr><th>ID</th><th>Dienst / Eintrag</th><th>Typ</th><th>Status</th><th>Relevanz</th><th>KI-Confidence</th><th>Final</th><th>Ergebnis</th></tr></thead><tbody id="statusCandidateRows"></tbody></table></div><div style="margin-top:12px"><div class="label">Vordefinierter, gerenderter Nachrichtentext</div><div class="raw" id="statusRenderedText"></div></div></div></section>
<div class="grid2 section">
<section class="panel"><div class="panel-head"><div><div class="panel-title">KBs für die Kategorieanalyse</div><div class="panel-sub">Nur Quellen aus KNOWLEDGE_CATEGORY_SOURCES; Antwortfelder werden nicht übertragen</div></div></div><div class="panel-body"><div id="categoryCandidateInfo" 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>Status</th></tr></thead><tbody id="categoryCandidateRows"></tbody></table></div></div></section>
<section class="panel"><div class="panel-head"><div><div class="panel-title">KBs für die Antwortanalyse</div><div class="panel-sub">Nach der Kategorieentscheidung neu gerankt; nur diese IDs darf die Antwort-KI wählen</div></div></div><div class="panel-body"><div id="replyCandidateInfo" 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>Kategorie</th><th>Titel</th><th>Status</th></tr></thead><tbody id="replyCandidateRows"></tbody></table></div></div></section>
</div>
<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"><select class="select" id="kbPurpose"><option value="reply">Antwortanalyse</option><option value="category">Kategorieanalyse</option></select><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">Getrennte KI-Ergebnisse & Policy-Code</div><div class="panel-sub">Status-KI bewertet nur die Zuordnung und schreibt niemals den Followup-Text</div></div></div><div class="panel-body"><div class="grid2"><div><div class="label">Begründung Kategorieanalyse</div><div class="raw" id="categoryAIReason"></div></div><div><div class="label">Begründung Prioritätsanalyse</div><div class="raw" id="priorityAIReason"></div></div></div><div class="grid2" style="margin-top:12px"><div><div class="label">Begründung Statuszuordnung</div><div class="raw" id="statusAIReason"></div></div><div><div class="label">Begründung normale Antwortanalyse</div><div class="raw" id="replyAIReason"></div></div></div><div class="grid2" style="margin-top:12px"><div><div class="label">Policy</div><div class="raw" id="policyReason"></div></div><div><div class="label">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.trigger,r.category_decision,r.priority_decision,r.reply_decision,r.policy_reason,(r.analyses||[]).map(a=>a.analysis_type).join(' ')].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.trigger||'legacy')} · ${esc(r.outcome)}</span></div><div style="margin-top:7px">${statusChip(r.category_decision||'',r.category_changed?'good':'info')} ${r.priority_decision?statusChip(r.priority_decision,r.priority_changed?'good':r.priority_would_change?'warn':'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 skipText(code){return({existing_followup:'Ticket besitzt bereits ein Followup',auto_reply_disabled:'AUTO_REPLY ist deaktiviert',context_incomplete:'Kontextabfrage war unvollständig',no_status_candidates:'Keine aktive Störung oder Wartung vorhanden',status_reply_disabled:'Statusantworten sind deaktiviert',status_ai_failed:'Ollama-Statuszuordnung ist fehlgeschlagen',status_reply_selected:'Vordefiniertes Status-Template wurde ausgewählt',no_reply_knowledge_candidates:'Keine Antwort-KB erreichte die Kandidatenauswahl',reply_ai_failed:'Ollama-Antwortanalyse ist fehlgeschlagen'})[code]||code||'nicht ausgeführt'}
function priorityDecisionText(code){return({priority_disabled:'Prioritätsanalyse deaktiviert',priority_ai_failed:'Prioritätsanalyse fehlgeschlagen',priority_ai_unavailable:'Prioritätsanalyse nicht verfügbar',priority_invalid_recommendation:'Ungültige Prioritätsempfehlung',priority_current_unknown:'Aktuelle GLPI-Priorität unbekannt',priority_decrease_blocked:'Herabstufung durch Policy gesperrt',priority_no_change_insufficient_information:'Keine Änderung Angaben reichen für eine Höherstufung nicht aus',priority_already_matches:'Keine Änderung aktuelle Priorität passt',priority_insufficient_information:'Erhöhung gesperrt Angaben unzureichend',priority_confidence_below_threshold:'Erhöhung gesperrt Confidence zu niedrig',priority_reason_not_allowed:'Erhöhung gesperrt kein freigegebener Grund',priority_accepted:'Erhöhung durch Policy freigegeben',priority_accepted_shadow:'Erhöhung im Shadow Mode vorgeschlagen',priority_accepted_dry_run:'Erhöhung im DRY RUN simuliert',priority_prewrite_recheck_failed:'Erhöhung gesperrt erneute Ticketprüfung fehlgeschlagen',priority_ticket_changed_before_write:'Erhöhung verworfen Ticket wurde inzwischen geändert',priority_write_unavailable:'GLPI-Schreibfunktion nicht verfügbar',priority_write_failed:'Prioritätsänderung fehlgeschlagen',priority_written:'Priorität in GLPI geändert'})[code]||code||'Kein Ergebnis'}
function priorityScopeText(v){return({single_user:'einzelne Person',multiple_users:'mehrere Personen',site:'Standort',organization:'gesamte Organisation',unknown:'unklar'})[v]||v||''}
function priorityCriticalityText(v){return({low:'niedrig',normal:'normal',high:'hoch',immediate:'sofort',unknown:'unklar'})[v]||v||''}
function candidateTable(rows,purpose){const category=purpose==='category';return (rows||[]).map((c,i)=>`<tr class="candidate-row" data-kb="${esc(c.id)}" data-purpose="${purpose}"><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(category?c.semantic_score:c.category_score)}</td><td>${pct(c.title_score)}</td><td>${c.sent_to_ai?statusChip(category?'an Kategorie-KI':'an Antwort-KI','good'):statusChip(selectionText(c.selection_reason),'warn')}</td></tr>`).join('')||`<tr><td colspan="6" class="empty">${category?'Keine Kategorie-KBs im Audit.':'Keine Antwort-KBs im Audit.'}</td></tr>`}
function statusCandidateTable(rows){return (rows||[]).map(c=>`<tr><td>${esc(c.id)}</td><td><strong>${esc(c.name)}</strong></td><td>${esc(c.kind||'')}</td><td>${statusChip(c.status||'',c.status==='maintenance'?'info':c.status==='down'?'bad':'warn')}</td><td>${pct(c.relevance)}</td><td>${c.ai_selected?pct(c.ai_confidence):''}</td><td>${c.ai_selected?pct(c.final_score):''}</td><td>${c.ai_selected?statusChip(c.decision||'ausgewählt',c.decision?.includes('accepted')?'good':'warn'):statusChip('nicht ausgewählt','info')}</td></tr>`).join('')||'<tr><td colspan="8" class="empty">Keine aktiven Uptime-Kuma-Kandidaten.</td></tr>'}
function analysisLabel(type){return({category:'Kategorie',priority:'Priorität',status_match:'Störungszuordnung',reply_selection:'Antwortauswahl',escalation:'Eskalation',routing:'Routing',information_quality:'Informationsqualität',security_triage:'Security-Triage',duplicate_detection:'Dublettenprüfung'})[type]||type||'Analyse'}
function analysisKind(a){return a.outcome==='error'?'bad':a.outcome==='skipped'?'warn':a.action?.executed?'good':'info'}
function renderAnalyses(r){const rows=r.analyses||[];$('#analysisRuns').innerHTML=rows.length?rows.map(a=>`<div class="analysis-card" data-analysis="${esc(a.analysis_id)}"><div class="chips">${statusChip(analysisLabel(a.analysis_type),'info')}${statusChip(a.outcome||'',analysisKind(a))}${a.action?.executed?statusChip('Aktion ausgeführt','good'):a.action?.proposed?statusChip(a.action.dry_run?'Aktion simuliert':'Aktion vorgeschlagen','warn'):''}</div><div class="name">${esc(a.analysis_id)}</div><div class="meta">${esc(a.prompt_version||'ohne Prompt-Version')} · ${Number(a.duration_ms||0).toLocaleString('de-DE')} ms<br>${esc(a.explanation||a.error||'Keine Begründung gespeichert.')}</div></div>`).join(''):'<div class="empty">Historischer Lauf ohne generische AnalysisRun-Datensätze.</div>';$('#analysisDetail').classList.add('hidden');document.querySelectorAll('[data-analysis]').forEach(x=>x.onclick=()=>showAnalysis(x.dataset.analysis))}
function actionStepsHTML(action){const steps=action?.steps||[];if(!steps.length)return '<div class="empty">Keine einzelnen Aktionsschritte.</div>';return steps.map((x,i)=>`<div class="rule ${x.error&&x.executed?'warn':x.error?'bad':x.executed?'good':x.proposed?'warn':'info'}"><div class="rule-icon">${x.error?'×':x.executed?'✓':x.dry_run?'◌':'•'}</div><div class="rule-body"><div class="rule-label">${esc(x.step||`Schritt ${i+1}`)}${x.target?` · ${esc(x.target)}`:''}</div><div class="rule-detail">${esc(x.result||'')}${x.error?` · ${esc(x.error)}`:''}</div></div><div class="rule-values"><strong>${x.executed?'ausgeführt':x.dry_run&&x.proposed?'simuliert':x.proposed?'vorgeschlagen':'nicht ausgeführt'}</strong><span>${esc(x.before||'')}${esc(x.after||'')}</span></div></div>`).join('')}
async function showAnalysis(id){const box=$('#analysisDetail');box.classList.remove('hidden');box.innerHTML='<div class="empty">Lade Analyselauf …</div>';try{const a=await api(`/api/diagnostics/analysis/${encodeURIComponent(id)}`);const action=a.action||{};box.innerHTML=`<div style="display:flex;justify-content:space-between;gap:12px;align-items:flex-start"><div><div class="eyebrow">${esc(analysisLabel(a.analysis_type))}</div><h2 style="margin:4px 0">${esc(a.analysis_id)}</h2><div class="muted">Parent ${esc(a.parent_run_id)} · Trigger ${esc(a.trigger)} · ${esc(a.model||'deterministisch')} · ${esc(a.prompt_version||'')}</div></div><div class="chips">${statusChip(a.outcome||'',analysisKind(a))}${statusChip(`Confidence ${pct(a.confidence)}`,'info')}${action.type?statusChip(action.type,action.executed?'good':action.proposed?'warn':'info'):''}</div></div><div class="grid2" style="margin-top:14px"><div><div class="label">Begründung / Grundcodes</div><div class="raw">${esc(a.explanation||'')}\n\n${esc((a.reason_codes||[]).join(', ')||'keine')}</div></div><div><div class="label">Aktionsplan</div>${actionStepsHTML(action)}<div class="raw" style="margin-top:10px">${esc(JSON.stringify(action,null,2))}</div></div></div><div style="margin-top:14px"><div class="panel-title">Policy-Prüfungen</div>${(a.checks||[]).length?(a.checks||[]).map(ruleHTML).join(''):'<div class="empty">Keine Regelchecks.</div>'}</div><div style="margin-top:14px"><div class="panel-title">Ollama-Routing und Failover</div><div class="raw">${esc(JSON.stringify(a.provider??{provider:'keine Provider-Diagnose gespeichert'},null,2))}</div></div><div class="grid2" style="margin-top:14px"><div><div class="label">Historischer Input-Snapshot · SHA-256 ${esc(a.input_hash||'')}</div><div class="raw">${esc(JSON.stringify(a.input_snapshot??{},null,2))}</div></div><div><div class="label">Strukturierte Entscheidung</div><div class="raw">${esc(JSON.stringify(a.decision??{},null,2))}</div></div></div>`}catch(e){box.innerHTML=`<div class="warning">${esc(e.message)}</div>`}}
function renderRun(){
const r=current,legacy=!r.category_analysis_executed&&!r.reply_analysis_executed&&!r.category_knowledge_candidates;
$('#ticketTitle').textContent=`#${r.ticket_id} ${r.ticket_name||''}`;$('#runMeta').textContent=`Run ${r.run_id} · Trigger ${r.trigger||'legacy'} · ${date(r.finished_at)} · ${r.outcome}${r.caused_by_run_id?` · Ursache ${r.caused_by_run_id}`:''}`;
$('#heroChips').innerHTML=[statusChip(r.dry_run?'DRY RUN':'LIVE',r.dry_run?'warn':'good'),statusChip(r.category_decision||'Kategorie ','info'),statusChip(r.priority_decision||'Priorität ',r.priority_changed?'good':r.priority_would_change?'warn':'info'),statusChip(r.reply_decision||'Reply ',r.reply_written?'good':r.reply_proposed?'info':'warn')].join('');
const basis=r.reply_basis_category_name||r.category_before_name||(r.reply_basis_category_id?`Kategorie #${r.reply_basis_category_id}`:'');
const statusSummary=r.status_reply_selected?`${r.status_reply_type==='maintenance'?'Wartung':'Störung'} · ${pct(r.status_reply_final_score)}`:(r.status_analysis_executed?'kein Status-Template':`übersprungen`);const priorityValue=r.priority_analysis_executed?`#${r.priority_before||'?'} → #${r.ai_recommended_priority||'?'}${r.priority_proposed&&r.priority_proposed!==r.ai_recommended_priority?` (Policy #${r.priority_proposed})`:''}`:'nicht ausgeführt';const cards=[['Kategorie-Ergebnis',r.ai_recommended_category_name?`${r.ai_recommended_category_name} · ${pct(r.ai_category_confidence)}`:'keine Empfehlung',`Schwellwert ${pct(r.category_threshold)}`],['Prioritäts-Ergebnis',priorityValue,priorityDecisionText(r.priority_decision)],['Statusanalyse',statusSummary,r.status_reply_decision||skipText(r.status_analysis_skip_reason)],['Antwortbasis',basis,r.reply_basis_category_id?`Kategorie #${r.reply_basis_category_id}`:''],['Antwort-Ergebnis',r.status_reply_selected?'vordefiniertes Template':(r.ai_reply_recommended?`KB-Auswahl · ${pct(r.ai_reply_confidence)}`:'keine Antwortempfehlung'),r.reply_decision||'']];
$('#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('');
const catState=r.category_analysis_executed?`Ausgeführt${r.category_analysis_duration_ms?` · ${r.category_analysis_duration_ms} ms`:''}`:(legacy?'Historischer gemeinsamer Ollama-Aufruf':'Nicht ausgeführt');const priorityState=r.priority_analysis_executed?`Ausgeführt${r.priority_analysis_duration_ms?` · ${r.priority_analysis_duration_ms} ms`:''}`:`Nicht ausgeführt`;
const statusState=r.status_analysis_skip_reason==='status_ai_failed'?`Fehlgeschlagen${r.status_analysis_duration_ms?` · ${r.status_analysis_duration_ms} ms`:''}`:r.status_analysis_executed?`Ausgeführt${r.status_analysis_duration_ms?` · ${r.status_analysis_duration_ms} ms`:''}`:`Übersprungen · ${skipText(r.status_analysis_skip_reason)}`;const replyState=r.reply_analysis_skip_reason==='reply_ai_failed'?`Fehlgeschlagen${r.reply_analysis_duration_ms?` · ${r.reply_analysis_duration_ms} ms`:''}`:r.reply_analysis_executed?`Ausgeführt${r.reply_analysis_duration_ms?` · ${r.reply_analysis_duration_ms} ms`:''}`:`Übersprungen · ${skipText(r.reply_analysis_skip_reason)}`;
$('#analysisFlow').innerHTML=`<div class="flow-step"><div class="n">Stufe 1</div><div class="h">Kategorieanalyse</div><div class="d">${esc(catState)}. Ergebnis: ${esc(r.ai_recommended_category_name||'keine Kategorie')} (${pct(r.ai_category_confidence)}).</div></div><div class="flow-step"><div class="n">Stufe 2</div><div class="h">Prioritätsanalyse</div><div class="d">${esc(priorityState)}. Aktuell #${esc(r.priority_before||'')}, KI #${esc(r.ai_recommended_priority||'')}, Policy-Ziel #${esc(r.priority_proposed||r.priority_before||'')}. Scope ${esc(priorityScopeText(r.priority_affected_scope))}, Impact #${esc(r.ai_recommended_impact||'')}, Dringlichkeit #${esc(r.ai_recommended_urgency||'')}. Ergebnis: ${esc(priorityDecisionText(r.priority_decision))}.</div></div><div class="flow-step"><div class="n">Stufe 3</div><div class="h">Uptime-Kuma-Zuordnung</div><div class="d">${esc(statusState)}. Die KI wählt nur einen Kandidaten und erzeugt keinen Nachrichtentext. Ergebnis: ${r.status_reply_selected?`${esc(r.status_reply_candidate_name)} · ${pct(r.status_reply_final_score)}`:'kein freigegebenes Template'}.</div></div><div class="flow-step"><div class="n">Stufe 4</div><div class="h">Normale Antwortanalyse</div><div class="d">${esc(replyState)}. Antwort-KBs werden anhand von ${esc(basis)} gerankt. Ergebnis: ${r.status_reply_selected?'durch Status-Template ersetzt':(r.ai_knowledge_id?`KB ${esc(r.ai_knowledge_id)} mit ${pct(r.ai_reply_confidence)}`:'keine Antwortempfehlung')}.</div></div>`;
renderAnalyses(r);
renderRules('#categoryRules',r.category_checks);renderRules('#priorityRules',r.priority_checks);renderRules('#statusRules',r.status_checks);renderRules('#replyRules',r.reply_checks);renderRules('#executionRules',r.execution_checks);
const priorityMode=r.priority_changed?'in GLPI geschrieben':r.priority_would_change?(r.dry_run?'im DRY RUN simuliert':'im Shadow Mode nur vorgeschlagen'):'keine Änderung freigegeben';const priorityFacts=r.priority_analysis_executed?`Scope: ${priorityScopeText(r.priority_affected_scope)} · Impact: #${r.ai_recommended_impact||''} · Dringlichkeit: #${r.ai_recommended_urgency||''} · Zeitkritik: ${priorityCriticalityText(r.priority_time_criticality)}`:'';$('#prioritySummary').innerHTML=`<strong>${r.priority_analysis_executed?`Aktuell #${esc(r.priority_before||'')} → KI #${esc(r.ai_recommended_priority||'')} → Policy #${esc(r.priority_proposed||r.priority_before||'')}`:'Prioritätsanalyse nicht ausgeführt'}</strong><br>${esc(priorityDecisionText(r.priority_decision))} · ${esc(priorityMode)}${priorityFacts?`<br>${esc(priorityFacts)}`:''}${(r.priority_reason_codes||[]).length?`<br>Gründe: ${esc(r.priority_reason_codes.join(', '))}`:''}`;$('#priorityBadges').innerHTML=[statusChip(`Confidence ${pct(r.ai_priority_confidence)}`,'info'),r.priority_affected_scope?statusChip(priorityScopeText(r.priority_affected_scope),'info'):'',r.priority_would_change?statusChip(r.priority_changed?'geändert':'würde ändern',r.priority_changed?'good':'warn'):statusChip('keine Änderung','info')].join('');
$('#statusCandidateInfo').textContent=`Ausgewählt: ${r.status_reply_candidate_name||''} · Relevanz ${pct(r.status_reply_relevance)} (min. ${pct(r.status_reply_min_relevance)}) · KI ${pct(r.status_reply_ai_confidence)} (min. ${pct(r.status_reply_min_ai_confidence)}) · Final ${pct(r.status_reply_final_score)} (min. ${pct(r.status_reply_min_final_score)})`;$('#statusCandidateRows').innerHTML=statusCandidateTable(r.status_candidates||[]);$('#statusRenderedText').textContent=r.status_reply_rendered_text||'';
$('#categoryCandidateInfo').textContent=`An Kategorie-KI: ${r.category_knowledge_llm_candidates||0} · Cutoff ${pct(r.category_knowledge_candidate_cutoff)} · Max. Abstand ${pct(r.knowledge_candidate_max_gap)} · Audit Top K ${r.knowledge_audit_top_k||0}`;
$('#replyCandidateInfo').textContent=`An Antwort-KI: ${r.knowledge_llm_candidates||0} · Cutoff ${pct(r.knowledge_candidate_cutoff)} · Basis: ${basis} · Audit Top K ${r.knowledge_audit_top_k||0}`;
$('#categoryCandidateRows').innerHTML=candidateTable(r.category_knowledge_candidates||[],'category');
$('#replyCandidateRows').innerHTML=candidateTable(r.reply_knowledge_candidates||r.knowledge_candidates||[],'reply');
document.querySelectorAll('[data-kb][data-purpose]').forEach(x=>x.onclick=()=>{const purpose=x.dataset.purpose;$('#kbPurpose').value=purpose;diagnoseKB(x.dataset.kb,purpose)});
$('#categoryAIReason').textContent=r.category_ai_reason||(legacy?r.ai_reason:'')||'';$('#priorityAIReason').textContent=r.priority_ai_reason||'';$('#statusAIReason').textContent=r.status_ai_reason||'';$('#replyAIReason').textContent=r.reply_ai_reason||(legacy?'Im historischen Lauf gemeinsam mit der Kategorie erzeugt.':'');$('#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',source_not_allowed_for_purpose:'Quelle für diese Analyse nicht freigegeben'})[code]||code||''}
async function diagnoseKB(id,purpose=$('#kbPurpose').value){if(!current)return;$('#kbPurpose').value=purpose;$('#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)}&purpose=${encodeURIComponent(purpose)}`);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]];const purposeLabel=d.purpose==='category'?'Kategorieanalyse':'Antwortanalyse';const evidence=d.purpose==='reply'?`${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.document.auto_reply_decision?statusChip(d.document.auto_reply_decision,d.document.auto_reply?'good':'warn'):''}`:'';$('#kbDiagnostic').innerHTML=`${warn}<div style="display:flex;justify-content:space-between;gap:12px;align-items:flex-start"><div><div class="eyebrow">KB Inspector · ${esc(purposeLabel)}</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">${d.retrieval_rank?statusChip(`Rang #${d.retrieval_rank}`,'info'):statusChip('nicht in Quellenliste','warn')}${d.sent_to_ai?statusChip(`an ${purposeLabel}-KI gesendet`,'good'):statusChip(selectionText(d.selection_reason),'warn')}${d.ai_selected?statusChip('von Antwort-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')}${evidence}${(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>${d.document.auto_reply_detail?`<div style="margin-top:13px"><div class="label">Auto-Reply-Freigabe</div><div class="excerpt">${esc(d.document.auto_reply_detail)}</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.auto_reply_decision?` · ${esc(x.auto_reply_decision)}`:''}${(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,$('#kbPurpose').value)})}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>