90 lines
39 KiB
HTML
90 lines
39 KiB
HTML
<!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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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>
|