Files
glpi-ai-agent/internal/web/templates/dashboard.html
2026-08-05 19:13:45 +02:00

176 lines
75 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 · Control Center</title>
<style>
:root{
color-scheme:dark;
--bg:#08101c;--bg2:#0b1422;--panel:#101b2b;--panel2:#142238;--panel3:#182940;
--border:#263952;--border2:#314965;--text:#eef5ff;--muted:#8fa3bc;--muted2:#b4c2d2;
--accent:#5b8cff;--accent2:#7c5cff;--green:#41d695;--amber:#f4bd5f;--red:#ff7474;--cyan:#56d6e7;
--shadow:0 18px 45px rgba(0,0,0,.25);--radius:16px;--radius-sm:10px;
font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
}
*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:radial-gradient(circle at 80% -20%,rgba(91,140,255,.16),transparent 35%),linear-gradient(180deg,var(--bg),#07101a 70%);color:var(--text);min-height:100vh}
button,input,textarea,select{font:inherit}button{color:inherit}.app{display:grid;grid-template-columns:250px minmax(0,1fr);min-height:100vh}.sidebar{position:sticky;top:0;height:100vh;padding:24px 16px;border-right:1px solid var(--border);background:rgba(8,16,28,.88);backdrop-filter:blur(18px);z-index:10}.brand{display:flex;gap:12px;align-items:center;padding:0 8px 24px}.logo{width:42px;height:42px;border-radius:13px;display:grid;place-items:center;background:linear-gradient(135deg,var(--accent),var(--accent2));box-shadow:0 10px 28px rgba(91,140,255,.3);font-weight:800}.brand-title{font-weight:760;letter-spacing:-.02em}.brand-sub{font-size:12px;color:var(--muted);margin-top:2px}.nav{display:grid;gap:5px}.nav-btn{border:0;background:transparent;border-radius:10px;padding:10px 12px;display:flex;align-items:center;gap:10px;text-align:left;color:var(--muted2);cursor:pointer}.nav-btn:hover{background:var(--panel2);color:var(--text)}.nav-btn.active{background:linear-gradient(90deg,rgba(91,140,255,.18),rgba(91,140,255,.05));color:#dce8ff;box-shadow:inset 3px 0 var(--accent)}.nav-icon{width:20px;text-align:center}.side-bottom{position:absolute;bottom:20px;left:16px;right:16px}.modebox{border:1px solid var(--border);border-radius:12px;padding:12px;background:var(--panel);font-size:12px;color:var(--muted)}.content{min-width:0;padding:28px 32px 50px;max-width:1780px;width:100%;margin:0 auto}.topbar{display:flex;justify-content:space-between;gap:24px;align-items:flex-start;margin-bottom:24px}.eyebrow{font-size:12px;letter-spacing:.12em;text-transform:uppercase;color:var(--accent);font-weight:700}.page-title{font-size:30px;line-height:1.05;margin:5px 0 7px;letter-spacing:-.035em}.page-sub{color:var(--muted);max-width:760px}.toolbar{display:flex;gap:9px;align-items:center;flex-wrap:wrap;justify-content:flex-end}.status-dot{width:8px;height:8px;border-radius:50%;display:inline-block;background:var(--muted)}.status-dot.ok{background:var(--green);box-shadow:0 0 0 4px rgba(65,214,149,.1)}.status-dot.bad{background:var(--red);box-shadow:0 0 0 4px rgba(255,116,116,.1)}.chip,.badge{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--border);background:rgba(16,27,43,.8);border-radius:999px;padding:6px 10px;font-size:12px;color:var(--muted2)}.badge.good{border-color:rgba(65,214,149,.25);background:rgba(65,214,149,.09);color:#9aefc8}.badge.warn{border-color:rgba(244,189,95,.28);background:rgba(244,189,95,.1);color:#f7d89d}.badge.bad{border-color:rgba(255,116,116,.28);background:rgba(255,116,116,.1);color:#ffb5b5}.badge.info{border-color:rgba(91,140,255,.3);background:rgba(91,140,255,.11);color:#bdd0ff}.btn{border:1px solid var(--border2);background:var(--panel2);border-radius:10px;padding:9px 12px;cursor:pointer;transition:.15s;display:inline-flex;align-items:center;justify-content:center;gap:7px}.btn:hover{transform:translateY(-1px);border-color:#456588;background:var(--panel3)}.btn.primary{background:linear-gradient(135deg,#3f75ef,#6c52e7);border-color:transparent;box-shadow:0 8px 24px rgba(64,113,239,.22)}.btn.danger{border-color:rgba(255,116,116,.35);background:rgba(126,35,44,.35);color:#ffb9b9}.btn.ghost{background:transparent}.btn.small{padding:6px 9px;font-size:12px}.btn:disabled{opacity:.45;cursor:not-allowed;transform:none}.view{display:none}.view.active{display:block}.grid{display:grid;gap:14px}.stats{grid-template-columns:repeat(6,minmax(140px,1fr));margin-bottom:20px}.stat{background:linear-gradient(180deg,rgba(20,34,56,.96),rgba(15,26,43,.96));border:1px solid var(--border);border-radius:var(--radius);padding:16px;min-height:112px;box-shadow:0 8px 22px rgba(0,0,0,.12)}.stat-top{display:flex;justify-content:space-between;align-items:center;gap:8px}.stat-label{font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em}.stat-value{font-size:27px;font-weight:760;margin-top:13px;letter-spacing:-.03em}.stat-foot{font-size:12px;color:var(--muted);margin-top:5px}.panel{background:linear-gradient(180deg,rgba(16,27,43,.98),rgba(13,23,38,.98));border:1px solid var(--border);border-radius:var(--radius);box-shadow:0 9px 28px rgba(0,0,0,.12)}.panel-head{display:flex;justify-content:space-between;gap:15px;align-items:center;padding:17px 18px;border-bottom:1px solid var(--border)}.panel-title{font-size:15px;font-weight:700}.panel-sub{font-size:12px;color:var(--muted);margin-top:2px}.panel-body{padding:18px}.cols-2{grid-template-columns:minmax(0,1.35fr) minmax(320px,.65fr)}.cols-eq{grid-template-columns:repeat(2,minmax(0,1fr))}.health-row{display:flex;align-items:center;justify-content:space-between;padding:11px 0;border-bottom:1px solid rgba(49,73,101,.55)}.health-row:last-child{border:0}.health-name{display:flex;align-items:center;gap:9px}.health-meta{font-size:12px;color:var(--muted)}.notice-list{display:grid;gap:9px}.notice{border:1px solid var(--border);border-left:3px solid var(--accent);border-radius:10px;padding:11px 12px;background:rgba(20,34,56,.6);font-size:13px;line-height:1.45}.notice.warn{border-left-color:var(--amber)}.notice.bad{border-left-color:var(--red)}.notice.good{border-left-color:var(--green)}.section-title{font-size:20px;margin:0 0 6px;letter-spacing:-.02em}.section-sub{color:var(--muted);margin-bottom:18px;font-size:13px}.filterbar{display:flex;gap:9px;flex-wrap:wrap;margin-bottom:13px}.search{position:relative;min-width:240px;flex:1}.search input{padding-left:34px}.search:before{content:"⌕";position:absolute;left:12px;top:9px;color:var(--muted)}input,textarea,select{width:100%;background:#0b1625;border:1px solid var(--border2);border-radius:10px;color:var(--text);padding:9px 10px;outline:none}input:focus,textarea:focus,select:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(91,140,255,.12)}textarea{resize:vertical;min-height:130px;line-height:1.5}.table-wrap{overflow:auto;border-radius:var(--radius);border:1px solid var(--border)}table{width:100%;border-collapse:collapse;background:rgba(12,22,36,.75)}th{position:sticky;top:0;background:#101d30;z-index:1;text-align:left;color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.055em;padding:11px 12px;border-bottom:1px solid var(--border)}td{padding:12px;border-bottom:1px solid rgba(38,57,82,.7);vertical-align:top;font-size:13px}tr:last-child td{border-bottom:0}.click-row{cursor:pointer}.click-row:hover td{background:rgba(91,140,255,.045)}.ticket-title{font-weight:650;max-width:360px}.small{font-size:12px}.muted{color:var(--muted)}.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.nowrap{white-space:nowrap}.decision-mini{min-width:220px}.decision-line{display:flex;gap:7px;align-items:center;flex-wrap:wrap;margin-bottom:4px}.score{font-weight:700}.score.good{color:var(--green)}.score.warn{color:var(--amber)}.score.bad{color:var(--red)}.progress{height:7px;border-radius:999px;background:#0a1421;overflow:hidden;border:1px solid rgba(49,73,101,.6)}.progress>i{display:block;height:100%;border-radius:999px;background:linear-gradient(90deg,var(--accent),var(--cyan));width:0}.progress.good>i{background:linear-gradient(90deg,#2ebd7c,#63e0ae)}.progress.warn>i{background:linear-gradient(90deg,#d99337,#f2c36e)}.progress.bad>i{background:linear-gradient(90deg,#d95a65,#ff8585)}.score-row{display:grid;grid-template-columns:145px 1fr 54px;gap:10px;align-items:center;margin:8px 0;font-size:12px}.score-row .value{text-align:right;font-variant-numeric:tabular-nums}.kb-layout{display:grid;grid-template-columns:minmax(0,1fr);gap:14px}.kb-card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px}.kb-card{border:1px solid var(--border);border-radius:14px;padding:14px;background:rgba(16,29,47,.72);display:flex;flex-direction:column;min-height:190px}.kb-card:hover{border-color:#38577b}.kb-card-title{font-weight:700;font-size:15px}.kb-meta{display:flex;flex-wrap:wrap;gap:6px;margin:9px 0}.kb-snippet{color:var(--muted2);font-size:12px;line-height:1.5;flex:1}.kb-actions{display:flex;gap:7px;margin-top:12px}.config-groups{grid-template-columns:repeat(auto-fit,minmax(320px,1fr))}.config-card{padding:0}.config-list{padding:6px 16px 14px}.config-row{display:grid;grid-template-columns:minmax(125px,.8fr) minmax(130px,1.2fr);gap:12px;padding:9px 0;border-bottom:1px solid rgba(38,57,82,.55);font-size:12px}.config-row:last-child{border:0}.config-key{color:var(--muted)}.config-val{overflow-wrap:anywhere;text-align:right}.drawer-backdrop{display:none;position:fixed;inset:0;background:rgba(1,7,14,.62);backdrop-filter:blur(3px);z-index:40}.drawer-backdrop.show{display:block}.drawer{position:fixed;z-index:41;right:0;top:0;height:100vh;width:min(720px,94vw);background:#0d1929;border-left:1px solid var(--border2);box-shadow:-20px 0 55px rgba(0,0,0,.32);transform:translateX(102%);transition:transform .22s ease;overflow:auto}.drawer.show{transform:translateX(0)}.drawer-head{position:sticky;top:0;z-index:3;background:rgba(13,25,41,.96);backdrop-filter:blur(12px);border-bottom:1px solid var(--border);padding:19px 21px;display:flex;justify-content:space-between;gap:16px}.drawer-body{padding:20px}.detail-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.detail-box{border:1px solid var(--border);border-radius:12px;padding:13px;background:rgba(20,34,56,.58)}.detail-box.full{grid-column:1/-1}.detail-heading{font-size:11px;text-transform:uppercase;letter-spacing:.07em;color:var(--muted);margin-bottom:8px}.reason-text{white-space:pre-wrap;color:var(--muted2);line-height:1.55;font-size:13px}.candidate{padding:12px;border:1px solid var(--border);border-radius:11px;margin-top:9px;background:#0c1726}.candidate:first-child{border-color:rgba(91,140,255,.5)}.candidate-title{display:flex;justify-content:space-between;gap:12px;align-items:flex-start}.excerpt{background:#081320;border:1px solid rgba(38,57,82,.7);padding:9px;border-radius:8px;font-size:12px;color:var(--muted2);line-height:1.45;margin-top:8px;white-space:pre-wrap}.context-item{border-bottom:1px solid var(--border);padding:9px 0}.context-item:last-child{border:0}.context-kind{text-transform:uppercase;font-size:10px;color:var(--accent);letter-spacing:.08em}.modal-backdrop{display:none;position:fixed;inset:0;background:rgba(1,7,14,.7);z-index:50;align-items:flex-start;justify-content:center;padding:4vh 18px;overflow:auto}.modal-backdrop.show{display:flex}.modal{width:min(980px,100%);background:#0e1a2a;border:1px solid var(--border2);border-radius:18px;box-shadow:var(--shadow);overflow:hidden}.modal-head{padding:18px 20px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;gap:15px;align-items:center}.modal-body{padding:20px}.form-grid{display:grid;grid-template-columns:repeat(12,1fr);gap:13px}.field{grid-column:span 4}.field.half{grid-column:span 6}.field.wide{grid-column:1/-1}.field label{display:flex;justify-content:space-between;gap:8px;font-size:12px;color:var(--muted2);margin:0 0 6px}.field-hint{color:var(--muted);font-size:11px}.checkline{display:flex;align-items:center;gap:9px;padding:11px;border:1px solid var(--border);border-radius:10px;background:#0b1625}.checkline input{width:auto}.category-picker{border:1px solid var(--border2);border-radius:10px;background:#0b1625;overflow:hidden}.category-picker input{border:0;border-bottom:1px solid var(--border);border-radius:0;box-shadow:none}.category-list{max-height:180px;overflow:auto;padding:6px}.category-item{display:flex;gap:8px;align-items:flex-start;padding:7px;border-radius:7px;font-size:12px}.category-item:hover{background:var(--panel2)}.category-item input{width:auto;margin-top:2px}.modal-foot{display:flex;justify-content:space-between;gap:10px;padding:15px 20px;border-top:1px solid var(--border);background:#0b1726}.form-message{display:none;padding:10px 12px;margin-bottom:13px;border-radius:10px;font-size:13px}.form-message.show{display:block}.form-message.error{background:rgba(150,45,54,.25);border:1px solid rgba(255,116,116,.35);color:#ffc1c1}.form-message.success{background:rgba(35,125,85,.2);border:1px solid rgba(65,214,149,.3);color:#b6f5d7}.toast-stack{position:fixed;right:22px;bottom:22px;z-index:90;display:grid;gap:9px;max-width:420px}.toast{background:#132238;border:1px solid var(--border2);box-shadow:var(--shadow);padding:11px 13px;border-radius:11px;font-size:13px}.toast.good{border-left:3px solid var(--green)}.toast.error{border-left:3px solid var(--red)}.empty{padding:32px;text-align:center;color:var(--muted)}.loading{opacity:.6;pointer-events:none}.kbd{border:1px solid var(--border2);border-bottom-width:2px;border-radius:5px;padding:1px 5px;font-size:10px;color:var(--muted)}.raw{max-height:340px;overflow:auto;white-space:pre-wrap;font-size:11px;color:#b9c8d8;background:#07111e;padding:12px;border-radius:9px;border:1px solid var(--border)}
@media(max-width:1350px){.stats{grid-template-columns:repeat(3,1fr)}.cols-2{grid-template-columns:1fr}.app{grid-template-columns:210px minmax(0,1fr)}}
@media(max-width:900px){.app{display:block}.sidebar{height:auto;position:sticky;padding:10px 14px;border-right:0;border-bottom:1px solid var(--border);display:flex;align-items:center}.brand{padding:0;margin-right:12px}.brand-sub,.side-bottom{display:none}.logo{width:34px;height:34px}.nav{display:flex;overflow:auto}.nav-btn{white-space:nowrap}.nav-icon{display:none}.content{padding:20px 16px 42px}.topbar{flex-direction:column}.toolbar{justify-content:flex-start}.detail-grid{grid-template-columns:1fr}.detail-box.full{grid-column:auto}.field,.field.half{grid-column:1/-1}}
@media(max-width:620px){.stats{grid-template-columns:repeat(2,1fr)}.stat{min-height:96px}.stat-value{font-size:23px}.page-title{font-size:26px}.hide-mobile{display:none}.score-row{grid-template-columns:110px 1fr 48px}.kb-card-grid{grid-template-columns:1fr}}
</style>
</head>
<body>
<div class="app">
<aside class="sidebar">
<div class="brand"><div class="logo">AI</div><div><div class="brand-title">GLPI Agent</div><div class="brand-sub">Control Center</div></div></div>
<nav class="nav" id="nav">
<button class="nav-btn active" data-view="overview"><span class="nav-icon"></span>Übersicht</button>
<button class="nav-btn" data-view="runs"><span class="nav-icon"></span>Verarbeitungen</button>
<button class="nav-btn" data-view="knowledge"><span class="nav-icon"></span>Knowledge Base</button>
<button class="nav-btn" data-view="learning"><span class="nav-icon"></span>Lernen</button>
<button class="nav-btn" data-view="settings"><span class="nav-icon"></span>Konfiguration</button>
<a class="nav-btn" href="/category-mappings" style="text-decoration:none"><span class="nav-icon"></span>Kategorie-Mapping</a>
<a class="nav-btn" href="/diagnostics" style="text-decoration:none"><span class="nav-icon"></span>Diagnose</a>
</nav>
<div class="side-bottom"><div class="modebox" id="sideMode">Status wird geladen…</div></div>
</aside>
<main class="content">
<header class="topbar">
<div><div class="eyebrow">Operations & Diagnose</div><h1 class="page-title" id="pageTitle">Übersicht</h1><div class="page-sub" id="pageSub">Gesundheit, Verarbeitung und die wichtigsten Stellschrauben auf einen Blick.</div></div>
<div class="toolbar"><span class="chip" id="glpiChip"><span class="status-dot"></span>GLPI</span><span class="chip" id="ollamaChip"><span class="status-dot"></span>Ollama</span><span class="chip" id="knowledgeChip"><span class="status-dot"></span>Knowledge</span><span class="chip"><span id="lastRefresh"></span></span><button class="btn small" id="refreshBtn">Jetzt aktualisieren</button></div>
</header>
<section class="view active" id="view-overview">
<div class="grid stats" id="overviewStats"></div>
<div class="grid cols-2">
<div class="panel"><div class="panel-head"><div><div class="panel-title">Letzte Entscheidungen</div><div class="panel-sub">Klick auf einen Lauf öffnet die vollständige Diagnose.</div></div><button class="btn small" data-goto="runs">Alle anzeigen</button></div><div id="recentRuns" class="panel-body"></div></div>
<div class="panel"><div class="panel-head"><div><div class="panel-title">Betriebsdiagnose</div><div class="panel-sub">Hinweise aus der effektiven Konfiguration.</div></div></div><div class="panel-body"><div id="diagnosticNotices" class="notice-list"></div><div class="notice" style="margin-top:10px"><strong>Ticket manuell neu analysieren</strong><div class="muted small" style="margin:4px 0 9px">Erzeugt einen separaten Lauf auch dann, wenn diese Ticketversion bereits verarbeitet wurde. Im LIVE-Modus gelten die konfigurierten Auto-Aktionen.</div><div style="display:flex;gap:8px;align-items:center"><input id="reprocessTicketID" type="number" min="1" step="1" placeholder="Ticket-ID" style="max-width:150px"><button class="btn small" id="reprocessTicketBtn">Neu analysieren</button></div></div></div></div>
</div>
<div class="grid cols-eq" style="margin-top:14px">
<div class="panel"><div class="panel-head"><div><div class="panel-title">Integrationen</div><div class="panel-sub">Aktueller Zustand der Kontextquellen.</div></div></div><div class="panel-body" id="integrationHealth"></div></div>
<div class="panel"><div class="panel-head"><div><div class="panel-title">Scoring-Profil</div><div class="panel-sub">Aktive Gewichte und Schwellwerte des RAG.</div></div></div><div class="panel-body" id="scoringOverview"></div></div>
</div>
</section>
<section class="view" id="view-runs">
<h2 class="section-title">Verarbeitungen</h2><div class="section-sub">Jeder Lauf enthält KI-Empfehlung, Policy-Entscheidung, KB-Ranking und Kontext. Klick auf eine Zeile für Details.</div>
<div class="filterbar"><div class="search"><input id="runSearch" placeholder="Ticket, Grund, Knowledge-ID suchen …"></div><select id="runOutcome" style="max-width:180px"><option value="">Alle Ergebnisse</option><option value="processed">processed</option><option value="skipped">skipped</option><option value="error">error</option></select><select id="runLimit" style="max-width:150px"><option>25</option><option selected>50</option><option>100</option><option>200</option></select></div>
<div class="table-wrap"><table><thead><tr><th>Zeit / Ticket</th><th>Ergebnis</th><th>Kategorie</th><th>Antwort / KB</th><th class="hide-mobile">Kontext</th><th></th></tr></thead><tbody id="runsTable"></tbody></table></div>
</section>
<section class="view" id="view-knowledge">
<div style="display:flex;justify-content:space-between;gap:16px;align-items:flex-start;flex-wrap:wrap"><div><h2 class="section-title">Knowledge Base</h2><div class="section-sub">Interne Artikel verwalten und synchronisierte Quellen kontrollieren.</div></div><button class="btn primary" id="newKbBtn"> Neuer interner Artikel</button></div>
<div class="grid stats" id="kbStats" style="grid-template-columns:repeat(4,minmax(150px,1fr))"></div>
<div class="filterbar"><div class="search"><input id="kbSearch" placeholder="ID, Titel, Inhalt oder Keyword suchen …"></div><select id="kbSourceFilter" style="max-width:210px"><option value="">Alle Quellen</option></select><select id="kbManageFilter" style="max-width:210px"><option value="">Alle Artikel</option><option value="managed">Web-verwaltet</option><option value="readonly">Read-only</option><option value="autoreply">Auto-Reply erlaubt</option></select></div>
<div id="kbGrid" class="kb-card-grid"></div>
</section>
<section class="view" id="view-learning">
<h2 class="section-title">Bestätigtes Lernen</h2><div class="section-sub">Nur menschlich bestätigte oder korrigierte Zuordnungen werden als Beispiele an die KI weitergegeben.</div>
<div class="grid stats" id="learningStats" style="grid-template-columns:repeat(3,minmax(150px,1fr))"></div>
<div class="filterbar"><div class="search"><input id="learningSearch" placeholder="Ticket oder Kategorie suchen …"></div></div>
<div class="table-wrap"><table><thead><tr><th>Ticket / Beispiel</th><th>Kategorie</th><th>Typ</th><th>Zeit</th><th></th></tr></thead><tbody id="learningTable"></tbody></table></div>
</section>
<section class="view" id="view-settings">
<h2 class="section-title">Effektive Konfiguration</h2><div class="section-sub">Read-only Ansicht der wirksamen Werte. Secrets werden bewusst nicht angezeigt. Änderungen erfolgen weiterhin über ENV/Deployment.</div>
<div class="grid config-groups" id="configGroups"></div>
</section>
</main>
</div>
<div class="drawer-backdrop" id="runBackdrop"></div><aside class="drawer" id="runDrawer"><div class="drawer-head"><div><div class="eyebrow">Laufdiagnose</div><div class="panel-title" id="runDrawerTitle"></div></div><button class="btn ghost" id="closeRunDrawer"></button></div><div class="drawer-body" id="runDrawerBody"></div></aside>
<div class="modal-backdrop" id="kbModalBackdrop"><div class="modal" role="dialog" aria-modal="true" aria-labelledby="kbModalTitle"><div class="modal-head"><div><div class="eyebrow" id="kbModalEyebrow">Interne Knowledge Base</div><div class="panel-title" id="kbModalTitle">Neuen Artikel anlegen</div></div><button class="btn ghost" id="closeKbModal"></button></div><form id="kbForm"><div class="modal-body"><div id="kbFormMessage" class="form-message"></div><div class="form-grid">
<div class="field"><label>ID <span class="field-hint">unveränderlich nach Anlage</span></label><input id="kbId" required maxlength="100" placeholder="KB-AD-001"></div>
<div class="field half"><label>Titel</label><input id="kbTitle" required maxlength="240" placeholder="Benutzerkonto gesperrt"></div>
<div class="field"><label>Quelle</label><select id="kbSource"></select></div>
<div class="field"><label>Sprache</label><input id="kbLanguage" value="de-DE"></div><div class="field"><label>Stil</label><select id="kbStyle"><option value="formal">formal</option><option value="neutral">neutral</option><option value="informal">informal</option></select></div><div class="field"><label>Min. Evidenz-Score <span class="field-hint">01</span></label><input id="kbScore" type="number" min="0" max="1" step="0.01" value="0.70"></div>
<div class="field half"><label>GLPI-Kategorien <span class="field-hint">leer = alle</span></label><div class="category-picker"><input id="kbCategorySearch" placeholder="Kategorien filtern …"><div id="kbCategoryList" class="category-list"></div></div></div>
<div class="field half"><label>Keywords <span class="field-hint">Komma-getrennt</span></label><textarea id="kbKeywords" style="min-height:180px" placeholder="Konto gesperrt, Login, Passwort, Active Directory"></textarea></div>
<div class="field wide"><label>Source URI <span class="field-hint">optional</span></label><input id="kbUri" placeholder="kb://identity/account-locked"></div>
<div class="field wide"><label>Wissen / Diagnosekontext <span class="field-hint" id="kbTextCount">0 Zeichen</span></label><textarea id="kbText" required style="min-height:210px" placeholder="Beschreiben Sie Symptome, Ursachen, Voraussetzungen und Diagnosehinweise …"></textarea></div>
<div class="field wide"><label>Freigegebener Antworttext <span class="field-hint" id="kbAnswerCount">0 Zeichen</span></label><textarea id="kbAnswer" style="min-height:170px" placeholder="Nur der fachliche Antworttext. Anrede und Signatur ergänzt der Agent."></textarea></div>
<div class="field wide"><label>Automatisierung</label><div class="checkline"><input id="kbAutoReply" type="checkbox"><div><strong>Für Auto-Reply grundsätzlich freigeben</strong><div class="field-hint">Die globale Source-, Score-, Confidence- und Ticket-Policy gilt weiterhin.</div></div></div></div>
</div></div><div class="modal-foot"><div class="muted small" id="kbEditState">Neuer Artikel</div><div style="display:flex;gap:8px"><button type="button" class="btn" id="cancelKbBtn">Abbrechen</button><button type="submit" class="btn primary" id="saveKbBtn">Speichern</button></div></div></form></div></div>
<div class="modal-backdrop" id="learnBackdrop"><div class="modal" style="width:min(620px,100%)"><div class="modal-head"><div><div class="eyebrow">Human-in-the-loop</div><div class="panel-title">Kategorie bestätigen / korrigieren</div></div><button class="btn ghost" id="closeLearnModal"></button></div><div class="modal-body"><div id="learnTicket" class="reason-text" style="margin-bottom:14px"></div><div class="field wide"><label>Korrekte GLPI-Kategorie</label><select id="learnCategory"></select></div></div><div class="modal-foot"><span class="muted small">Dieses Beispiel fließt künftig in ähnliche Entscheidungen ein.</span><div style="display:flex;gap:8px"><button class="btn" id="cancelLearnBtn">Abbrechen</button><button class="btn primary" id="saveLearnBtn">Bestätigen</button></div></div></div></div>
<div class="toast-stack" id="toasts"></div>
<script>
'use strict';
const $=s=>document.querySelector(s), $$=s=>[...document.querySelectorAll(s)];
const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const pct=v=>`${Math.round(Number(v||0)*100)} %`; const fmtNum=v=>new Intl.NumberFormat('de-DE').format(Number(v||0));
const fmtDate=v=>{if(!v)return '';const d=new Date(v);return Number.isNaN(d.getTime())?'':d.toLocaleString('de-DE')};
const clamp=v=>Math.max(0,Math.min(1,Number(v||0))); const scoreClass=v=>Number(v)>=.8?'good':Number(v)>=.6?'warn':'bad';
let statusData={},runsData=[],categories=[],kbDocs=[],learningRows=[],currentRun=null,currentKbId='',currentLearnRun='',kbCategorySelection=new Set();
const viewMeta={overview:['Übersicht','Gesundheit, Verarbeitung und die wichtigsten Stellschrauben auf einen Blick.'],runs:['Verarbeitungen','Audit-Trail mit KI-, Policy-, RAG- und Kontextdetails.'],knowledge:['Knowledge Base','Interne und synchronisierte Wissensquellen verwalten und kontrollieren.'],learning:['Bestätigtes Lernen','Menschlich bestätigte Beispiele, die die Kategorisierung schrittweise verbessern.'],settings:['Effektive Konfiguration','Alle nicht-geheimen Laufzeitwerte zum Tuning und Troubleshooting.']};
function toast(text,kind='good'){const n=document.createElement('div');n.className=`toast ${kind}`;n.textContent=text;$('#toasts').appendChild(n);setTimeout(()=>n.remove(),5000)}
async function api(url,opt={}){const headers={...(opt.headers||{}),'X-Requested-With':'GLPI-AI-Agent'};if(opt.body)headers['Content-Type']='application/json';const r=await fetch(url,{...opt,headers});if(!r.ok){const t=(await r.text()).trim();throw new Error(t||`HTTP ${r.status}`)}if(r.status===204)return null;return r.json()}
function setView(view){if(!viewMeta[view])view='overview';$$('.view').forEach(x=>x.classList.toggle('active',x.id===`view-${view}`));$$('.nav-btn').forEach(x=>x.classList.toggle('active',x.dataset.view===view));$('#pageTitle').textContent=viewMeta[view][0];$('#pageSub').textContent=viewMeta[view][1];history.replaceState(null,'',`#${view}`)}
function badge(text,kind=''){return `<span class="badge ${kind}">${esc(text)}</span>`}
function progress(label,value){const c=scoreClass(value);return `<div class="score-row"><span>${esc(label)}</span><div class="progress ${c}"><i style="width:${Math.round(clamp(value)*100)}%"></i></div><span class="value score ${c}">${esc(pct(value))}</span></div>`}
function outcomeBadge(x){if(x==='error')return badge('Fehler','bad');if(x==='skipped')return badge('Übersprungen','warn');return badge('Verarbeitet','good')}
function policyLabel(code){const map={category_written:['Geändert','good'],category_accepted_dry_run:['Würde ändern','info'],category_accepted:['Freigegeben','good'],category_already_correct:['Bereits korrekt','good'],category_confidence_below_threshold:['Unter Schwellwert','warn'],category_no_recommendation:['Keine Empfehlung','warn'],category_auto_disabled:['Auto-Kategorie aus','warn'],category_unknown:['Kategorie unbekannt','bad'],category_ticket_changed_before_write:['Ticket geändert','warn'],category_write_failed:['Schreibfehler','bad'],reply_written:['Gesendet','good'],reply_accepted_dry_run:['Würde senden','info'],reply_accepted:['Freigegeben','good'],reply_auto_disabled:['Auto-Reply aus','warn'],reply_model_not_recommended:['KI empfiehlt keine Antwort','warn'],reply_confidence_below_threshold:['Confidence zu niedrig','warn'],reply_knowledge_score_below_threshold:['KB-Score zu niedrig (alt)','warn'],reply_knowledge_retrieval_below_floor:['Retrieval zu schwach','warn'],reply_knowledge_evidence_below_threshold:['Evidenz zu niedrig','warn'],reply_no_knowledge_candidates:['Keine KB-Treffer','warn'],reply_no_knowledge_selected:['Keine KB gewählt','warn'],reply_knowledge_not_found:['KB nicht gefunden','bad'],reply_source_not_allowed:['Quelle gesperrt','warn'],reply_source_not_allowed_for_auto_reply:['Quelle nicht für Auto-Reply','warn'],reply_language_mismatch:['Sprache passt nicht','warn'],reply_style_mismatch:['Stil passt nicht','warn'],reply_knowledge_auto_reply_disabled:['Artikel nicht freigegeben','warn'],reply_knowledge_auto_reply_not_approved:['Artikel nicht freigegeben','warn'],reply_knowledge_answer_empty:['Antworttext fehlt','warn'],reply_category_not_allowed:['Kategorie nicht freigegeben','warn'],reply_context_incomplete:['Kontext unvollständig','warn'],reply_relevant_incident:['Störung/Incident erkannt','warn'],reply_status_incident_accepted:['Störungstext freigegeben','good'],reply_status_maintenance_accepted:['Wartungstext freigegeben','good'],status_reply_relevance_below_threshold:['Status-Relevanz zu niedrig','warn'],status_reply_confidence_below_threshold:['Status-Confidence zu niedrig','warn'],status_reply_final_score_below_threshold:['Status-Score zu niedrig','warn'],reply_existing_followup:['Bereits beantwortet','good'],reply_followup_appeared_before_write:['Antwort hinzugekommen','warn'],reply_ticket_changed_before_write:['Ticket geändert','warn'],reply_write_failed:['Schreibfehler','bad']};return map[code]||[code||'Keine Aktion','']}
function configNotice(text,kind=''){return `<div class="notice ${kind}">${text}</div>`}
function renderStatusChrome(){const g=!!statusData.glpi_ok,o=!!statusData.ollama_ok,k=!!statusData.knowledge_ready,ks=statusData.knowledge_init_state||'waiting',total=Number(statusData.ollama_node_count||0),healthy=Number(statusData.ollama_healthy_nodes||0);$('#glpiChip').innerHTML=`<span class="status-dot ${g?'ok':'bad'}"></span>GLPI ${g?'OK':'Fehler'}`;$('#ollamaChip').innerHTML=`<span class="status-dot ${o?'ok':'bad'}"></span>Ollama ${total?`${healthy}/${total}`:(o?'OK':'Fehler')}`;$('#knowledgeChip').innerHTML=`<span class="status-dot ${k?'ok':ks==='error'?'bad':''}"></span>Knowledge ${k?'bereit':ks==='error'?'Fehler':'lädt'}`;$('#sideMode').innerHTML=`${statusData.dry_run?badge('DRY RUN','warn'):badge('LIVE','good')} ${statusData.auto_reply?badge('Auto-Reply','good'):badge('Auto-Reply aus','warn')} ${statusData.priority_enabled?badge(statusData.auto_priority?'Auto-Priorität':'Priorität Shadow',statusData.auto_priority?'good':'info'):badge('Priorität aus','warn')} ${statusData.escalation_enabled?badge(statusData.auto_escalation?'Auto-Eskalation':'Eskalation Shadow',statusData.auto_escalation?'good':'info'):badge('Eskalation aus','warn')}<div style="margin-top:8px">${esc(statusData.ollama_model||'')} · Pool ${esc(statusData.ollama_routing_mode||'')} · ${esc(statusData.communication_language||'')} / ${esc(statusData.communication_style||'')}</div>`;$('#lastRefresh').textContent=new Date().toLocaleTimeString('de-DE')}
function renderOverview(){const stats=[['Verarbeitet',fmtNum(statusData.processed),'seit Start'],['Fehler',fmtNum(statusData.errors),statusData.errors?'prüfen':'keine'],['Queue',fmtNum(statusData.queue_depth),`von ${fmtNum(statusData.queue_size)}`],['Knowledge',fmtNum(statusData.knowledge_docs),`${fmtNum(statusData.glpi_kb_documents)} aus GLPI`],['KI-Triage',`${fmtNum(statusData.priority_recommendations)} / ${fmtNum(statusData.escalation_runs)}`,'Priorität / Eskalationsläufe'],['Auto-Aktionen',`${fmtNum(statusData.category_changes)} / ${fmtNum(statusData.priority_changes)} / ${fmtNum(statusData.replies)} / ${fmtNum(statusData.escalations)}`,'Kat. / Prio / Reply / Esk.']];$('#overviewStats').innerHTML=stats.map(x=>`<div class="stat"><div class="stat-label">${esc(x[0])}</div><div class="stat-value">${esc(x[1])}</div><div class="stat-foot">${esc(x[2])}</div></div>`).join('');
const recent=runsData.slice(0,6);$('#recentRuns').innerHTML=recent.length?recent.map(x=>{const score=x.knowledge_score?` · KB ${pct(x.knowledge_score)}`:'';return `<div class="health-row click-row" data-run-id="${esc(x.run_id)}"><div><div class="ticket-title">#${esc(x.ticket_id)} ${esc(x.ticket_name||'')}</div><div class="health-meta">${esc(fmtDate(x.finished_at))}${esc(score)}</div></div>${outcomeBadge(x.outcome)}</div>`}).join(''):'<div class="empty">Noch keine Verarbeitungen.</div>';
const notices=[];const pollAt=statusData.last_poll?fmtDate(statusData.last_poll):'';if(statusData.poll_last_error){notices.push(configNotice(`<strong>Letzter GLPI-Poll fehlgeschlagen.</strong> ${esc(statusData.poll_last_error)}`,'bad'))}else if(!statusData.last_poll){notices.push(configNotice('<strong>Noch kein GLPI-Ticket-Poll abgeschlossen.</strong> Die Ticketverarbeitung wurde noch nicht aktiv oder der erste Abruf läuft.','warn'))}else if(Number(statusData.poll_last_fetched||0)===0){notices.push(configNotice(`<strong>GLPI-Poll aktiv, aber ohne Treffer.</strong> Letzter Abruf ${esc(pollAt)} · 0 Tickets. Prüfen Sie insbesondere GLPI_TICKET_FILTER und die API-Berechtigung auf /Assistance/Ticket.`,'warn'))}else if(Number(statusData.poll_last_unseen||0)===0){notices.push(configNotice(`<strong>GLPI-Poll funktioniert.</strong> ${fmtNum(statusData.poll_last_fetched)} Ticket(s) abgerufen, davon ${fmtNum(statusData.poll_last_seen)} bereits in derselben Version verarbeitet; deshalb 0 neue Queue-Einträge. Dauerhaft bekannte Ticketversionen: ${fmtNum(statusData.processed_version_count||0)}.`,'good'))}else{notices.push(configNotice(`<strong>GLPI-Poll funktioniert.</strong> ${fmtNum(statusData.poll_last_fetched)} abgerufen · ${fmtNum(statusData.poll_last_unseen)} neu · ${fmtNum(statusData.poll_last_enqueued)} eingereiht · ${fmtNum(statusData.poll_last_rejected)} abgewiesen.`,'good'))}if(!statusData.knowledge_ready&&statusData.knowledge_init_state!=='error'){const total=Number(statusData.knowledge_init_total_files||0),done=Number(statusData.knowledge_init_processed_files||0),idx=Number(statusData.knowledge_init_indexed_docs||0),docs=Number(statusData.knowledge_init_loaded_docs||0);notices.push(configNotice(`<strong>Knowledge-Index wird aufgebaut.</strong> Phase: ${esc(statusData.knowledge_init_phase||'')} · Dateien ${fmtNum(done)} / ${fmtNum(total)} · Dokumente ${fmtNum(docs)} · indexiert ${fmtNum(idx)}. Ticketverarbeitung bleibt bis zur Bereitschaft pausiert.`,'warn'))}if(statusData.knowledge_snapshot_loaded)notices.push(configNotice(`<strong>Persistenter Knowledge-Index geladen.</strong> ${fmtNum(statusData.knowledge_docs)} Artikel waren sofort verfügbar; Quelldateien werden inkrementell im Hintergrund geprüft.`,'good'));if(statusData.knowledge_last_scan_error)notices.push(configNotice(`<strong>Letzter inkrementeller Knowledge-Scan fehlgeschlagen.</strong> Der vorherige Index bleibt aktiv. ${esc(statusData.knowledge_last_scan_error)}`,'warn'));if(statusData.knowledge_init_state==='error')notices.push(configNotice(`<strong>Knowledge-Initialisierung fehlgeschlagen.</strong> ${esc(statusData.knowledge_init_error||'Kein Fehlertext verfügbar.')}`,'bad'));if(statusData.dry_run)notices.push(configNotice('<strong>Dry Run aktiv.</strong> Änderungen und Antworten werden nur simuliert.','warn'));if(!statusData.auto_reply)notices.push(configNotice('<strong>Auto-Reply global deaktiviert.</strong> KB-Treffer werden bewertet, aber nicht gesendet.','warn'));if(statusData.priority_enabled&&!statusData.auto_priority)notices.push(configNotice('<strong>Prioritätsanalyse im Shadow Mode.</strong> Empfehlungen werden als eigener KI-Lauf protokolliert, GLPI bleibt unverändert.'));if(statusData.auto_priority)notices.push(configNotice(`<strong>Automatische Priorisierung aktiv.</strong> Erhöhungen sind auf ${esc(statusData.priority_max_increase)} Stufe(n) je Lauf begrenzt.${statusData.dry_run?' Dry Run verhindert Schreibzugriffe.':''}`,statusData.dry_run?'warn':'bad'));if(statusData.escalation_enabled&&!statusData.auto_escalation)notices.push(configNotice(`<strong>Eskalationsanalyse im Shadow Mode.</strong> Zeittrigger erzeugen eigene Diagnose-Läufe. Verfügbare Aktionen: ${esc((statusData.escalation_allowed_actions||[]).join(', ')||'keine')}.`));if(statusData.auto_escalation)notices.push(configNotice(`<strong>Automatische Eskalation aktiv.</strong> Der Scheduler darf freigegebene Aktionen bis Stufe ${esc(statusData.escalation_max_level)} ausführen.${statusData.dry_run?' Dry Run verhindert Schreibzugriffe.':''}`,statusData.dry_run?'warn':'bad'));if(statusData.knowledge_min_score>=.8)notices.push(configNotice(`<strong>Hoher Evidenz-Schwellwert:</strong> ${pct(statusData.knowledge_min_score)}. Dieser gilt erst nach der KI-Auswahl.`,'warn'));if(statusData.knowledge_retrieval_floor>=.5)notices.push(configNotice(`<strong>Hoher Retrieval-Floor:</strong> ${pct(statusData.knowledge_retrieval_floor)}. Kurze Tickets könnten bereits vor dem Evidenz-Reranking blockiert werden.`,'warn'));if(statusData.glpi_kb_enabled&&!statusData.glpi_kb_ok)notices.push(configNotice(`<strong>GLPI-KB-Sync gestört.</strong> ${esc(statusData.glpi_kb_last_error||'Kein Fehlertext verfügbar.')}`,'bad'));if(statusData.knowledge_unmapped_category_files>0)notices.push(configNotice(`<strong>${esc(statusData.knowledge_unmapped_category_files)} KB-Datei(en) mit nicht zugeordneten Fremdkategorien.</strong> Diese Artikel bleiben suchbar, Auto-Reply ist dafür fail-closed deaktiviert. Nicht zugeordnet: ${esc((statusData.knowledge_unmapped_categories||[]).join(', ')||'')}`,'warn'));if(statusData.knowledge_ignored_files>0)notices.push(configNotice(`<strong>${esc(statusData.knowledge_ignored_files)} KB-Datei(en) durch Kompatibilitäts-/Ignore-Regeln übersprungen.</strong>`,'warn'));if(statusData.rag_enabled&&!statusData.knowledge_docs)notices.push(configNotice('<strong>RAG aktiv, aber keine Knowledge-Dokumente geladen.</strong>','bad'));if(statusData.context_fail_closed)notices.push(configNotice('Kontextquellen arbeiten <strong>fail-closed</strong>: Fehler können Auto-Replies blockieren.'));const ollamaNodes=statusData.ollama_nodes||[];const badNodes=ollamaNodes.filter(n=>!n.healthy||!n.compatible);if(badNodes.length)notices.push(configNotice(`<strong>${badNodes.length} Ollama-Node(s) nicht nutzbar.</strong> ${badNodes.map(n=>`${esc(n.name)}: ${esc(n.last_error||(!n.compatible?'Modelldigest abweichend':'nicht erreichbar'))}`).join(' · ')}`,'bad'));else if(ollamaNodes.length>1)notices.push(configNotice(`<strong>Ollama-Pool aktiv.</strong> ${fmtNum(statusData.ollama_healthy_nodes)} von ${fmtNum(statusData.ollama_node_count)} Nodes kompatibel · Routing ${esc(statusData.ollama_routing_mode)} · Failover ${statusData.ollama_failover_enabled?'aktiv':'aus'}.`,'good'));if(!notices.length)notices.push(configNotice('Keine auffälligen Konfigurationshinweise erkannt.','good'));$('#diagnosticNotices').innerHTML=notices.join('');
const kTotal=Number(statusData.knowledge_init_loaded_docs||0),kIndexed=Number(statusData.knowledge_init_indexed_docs||0),kPct=kTotal?Math.round((kIndexed/kTotal)*100):0;const nodeHealth=(statusData.ollama_nodes||[]).map(n=>[`Ollama · ${n.name}`,!!(n.healthy&&n.compatible),`${n.inflight||0}/${n.max_inflight||0} aktiv · Ø ${Math.round(Number(n.average_duration_ms||0))} ms${n.last_error?` · ${n.last_error}`:''}`]);const health=[['Lokale Knowledge Base',!!statusData.knowledge_ready,statusData.knowledge_ready?`${fmtNum(statusData.knowledge_docs)} Artikel bereit`:`${esc(statusData.knowledge_init_phase||'waiting')} · ${fmtNum(kIndexed)}/${fmtNum(kTotal)} indexiert (${kPct}%)`],['GLPI API',statusData.glpi_ok,statusData.glpi_api_version||''],...nodeHealth,['GLPI Knowledge Base',!statusData.glpi_kb_enabled||statusData.glpi_kb_ok,statusData.glpi_kb_enabled?`${statusData.glpi_kb_documents||0} Artikel · Sync ${fmtDate(statusData.glpi_kb_last_sync)}`:'deaktiviert'],['Uptime Kuma',true,statusData.uptime_kuma_enabled?`aktiv · ${statusData.uptime_kuma_mode}`:'deaktiviert'],['Change Calendar',true,statusData.change_calendar_enabled?'aktiv':'deaktiviert'],['Major Incidents',true,statusData.major_incidents_enabled?'aktiv':'deaktiviert'],['Benutzer ↔ Geräte',true,statusData.user_device_context_enabled?'aktiv':'deaktiviert']];$('#integrationHealth').innerHTML=health.map(x=>`<div class="health-row"><div class="health-name"><span class="status-dot ${x[1]?'ok':'bad'}"></span><div>${esc(x[0])}<div class="health-meta">${esc(x[2])}</div></div></div>${x[1]?badge('OK','good'):badge('Fehler','bad')}</div>`).join('');
$('#scoringOverview').innerHTML=`${progress('Retrieval: Semantik',statusData.knowledge_weight_semantic)}${progress('Retrieval: Titel',statusData.knowledge_weight_title)}${progress('Retrieval: Lexikalisch',statusData.knowledge_weight_lexical)}${progress('Retrieval: Keywords',statusData.knowledge_weight_keywords)}${progress('Retrieval: Kategorie/Lernen',statusData.knowledge_weight_category)}<div style="margin-top:14px" class="health-row"><span>Retrieval-Floor</span><strong>${esc(pct(statusData.knowledge_retrieval_floor))}</strong></div><div class="health-row"><span>Finaler Evidenz-Schwellwert</span><strong>${esc(pct(statusData.knowledge_min_score))}</strong></div><div class="health-row"><span>Evidenz: Retrieval / KI / Kategorie</span><strong>${esc(pct(statusData.knowledge_evidence_weight_retrieval))} / ${esc(pct(statusData.knowledge_evidence_weight_ai))} / ${esc(pct(statusData.knowledge_evidence_weight_category))}</strong></div><div class="health-row"><span>Kategorie-Confidence</span><strong>${esc(pct(statusData.category_confidence))}</strong></div><div class="health-row"><span>Reply-Confidence</span><strong>${esc(pct(statusData.reply_confidence))}</strong></div>`}
function categoryMini(x){if(!x.ai_recommended_category_id)return `<div class="decision-mini">${badge('Keine Empfehlung','warn')}<div class="muted small">KI-Sicherheit ${pct(x.ai_category_confidence)}</div></div>`;const p=policyLabel(x.category_decision);return `<div class="decision-mini"><div class="decision-line"><strong>${esc(x.ai_recommended_category_name||`#${x.ai_recommended_category_id}`)}</strong> <span class="score ${scoreClass(x.ai_category_confidence)}">${esc(pct(x.ai_category_confidence))}</span></div>${badge(p[0],p[1])}<div class="muted small">Schwellwert ${esc(pct(x.category_threshold))}</div></div>`}
function replyMini(x){const p=policyLabel(x.reply_decision);return `<div class="decision-mini"><div class="decision-line">${x.ai_reply_recommended?'<strong>KI: Antwort</strong>':'<span class="muted">KI: keine Antwort</span>'} <span class="score ${scoreClass(x.ai_reply_confidence)}">${esc(pct(x.ai_reply_confidence))}</span></div>${badge(p[0],p[1])}${x.knowledge_top_id?`<div class="muted small">${esc(x.ai_knowledge_id||x.knowledge_top_id)} · Retrieval ${esc(pct(x.knowledge_score))}${x.knowledge_evidence_score?` · Evidenz ${esc(pct(x.knowledge_evidence_score))} / ${esc(pct(x.knowledge_threshold))}`:''}</div>`:'<div class="muted small">Keine Knowledge-Treffer</div>'}</div>`}
function filteredRuns(){const q=$('#runSearch').value.trim().toLowerCase(),out=$('#runOutcome').value;return runsData.filter(x=>(!out||x.outcome===out)&&(!q||[x.ticket_id,x.ticket_name,x.ai_reason,x.policy_reason,x.knowledge_top_id,x.knowledge_top_title,x.error].join(' ').toLowerCase().includes(q)))}
function renderRuns(){const rows=filteredRuns();$('#runsTable').innerHTML=rows.length?rows.map(x=>`<tr class="click-row" data-run-id="${esc(x.run_id)}"><td><div class="ticket-title">#${esc(x.ticket_id)} ${esc(x.ticket_name||'')}</div><div class="muted small nowrap">${esc(fmtDate(x.finished_at))}</div></td><td>${outcomeBadge(x.outcome)}${x.dry_run?'<div class="small muted" style="margin-top:5px">Dry Run</div>':''}</td><td>${categoryMini(x)}</td><td>${replyMini(x)}</td><td class="hide-mobile"><div class="small">C:${esc(x.context_changes||0)} · I:${esc(x.context_incidents||0)} · U:${esc(x.context_issues||0)} · D:${esc(x.context_devices||0)}</div>${(x.context_warnings||[]).length?badge(`${x.context_warnings.length} Warnung(en)`,'warn'):''}</td><td></td></tr>`).join(''):'<tr><td colspan="6" class="empty">Keine passenden Läufe.</td></tr>'}
function contextKindLabel(k){return ({change:'Change',incident:'Major Incident',uptime:'Uptime Kuma',device:'Gerät'})[k]||k}
function renderRunDrawer(x){currentRun=x;$('#runDrawerTitle').textContent=`#${x.ticket_id} ${x.ticket_name||''}`;const catP=policyLabel(x.category_decision),repP=policyLabel(x.reply_decision);const candidates=(x.knowledge_candidates||[]).map((c,i)=>`<div class="candidate"><div class="candidate-title"><div><strong>${i+1}. ${esc(c.title)}</strong><div class="muted small">${esc(c.id)} · ${esc(c.source)} ${c.auto_reply?'· Auto-Reply freigegeben':''} · ${c.sent_to_ai?'<span class="badge info">an KI gesendet</span>':'<span class="badge">nur Audit</span>'}</div></div><span class="score ${scoreClass(c.score)}">Retrieval ${esc(pct(c.score))}</span></div>${progress('Semantik (raw)',c.semantic_score)}${progress('Titel',c.title_score)}${progress('Lexikalisch',c.lexical_score)}${progress('Keywords',c.keyword_score)}${progress('Kategorie/Lernen',c.category_score)}<div class="muted small">Evidenz-Schwelle: ${esc(pct(c.required_score))} · Chunks ${esc(c.query_chunk_count||0)} × ${esc(c.document_chunk_count||0)}</div>${c.best_query_excerpt?`<div class="excerpt"><strong>Ticket:</strong> ${esc(c.best_query_excerpt)}</div>`:''}${c.best_chunk_excerpt?`<div class="excerpt"><strong>KB:</strong> ${esc(c.best_chunk_excerpt)}</div>`:''}</div>`).join('')||'<div class="empty">Keine Knowledge-Kandidaten im Audit gespeichert.</div>';
const contexts=(x.context_details||[]).map(c=>`<div class="context-item"><div class="context-kind">${esc(contextKindLabel(c.kind))}</div><strong>${esc(c.name||`#${c.id}`)}</strong>${c.relevance?` <span class="score ${scoreClass(c.relevance)}">${esc(pct(c.relevance))}</span>`:''}${c.status?` ${badge(c.status)}`:''}${c.detail?`<div class="muted small" style="margin-top:3px">${esc(c.detail)}</div>`:''}</div>`).join('')||'<div class="muted small">Keine Kontextdetails gespeichert.</div>';
$('#runDrawerBody').innerHTML=`<div class="detail-grid"><div class="detail-box"><div class="detail-heading">Ergebnis</div>${outcomeBadge(x.outcome)} ${x.dry_run?badge('DRY RUN','warn'):badge('LIVE','good')}<div class="muted small" style="margin-top:8px">${esc(fmtDate(x.finished_at))}</div></div><div class="detail-box"><div class="detail-heading">Quelle</div><div class="mono small">${esc(x.source_version||'')}</div></div>
<div class="detail-box"><div class="detail-heading">Kategorie</div><div><strong>Aktuell:</strong> ${esc(x.category_before_name||'Nicht gesetzt')} (#${esc(x.category_before||0)})</div><div style="margin-top:5px"><strong>KI:</strong> ${x.ai_recommended_category_id?`${esc(x.ai_recommended_category_name||'')} (#${esc(x.ai_recommended_category_id)}) · ${esc(pct(x.ai_category_confidence))}`:'keine Empfehlung'}</div><div style="margin-top:8px">${badge(catP[0],catP[1])} <span class="muted small">Schwellwert ${esc(pct(x.category_threshold))}</span></div></div>
<div class="detail-box"><div class="detail-heading">Antwort</div><div><strong>KI:</strong> ${x.ai_reply_recommended?'Ja':'Nein'} · ${esc(pct(x.ai_reply_confidence))}</div><div style="margin-top:5px"><strong>KB:</strong> ${esc(x.ai_knowledge_id||x.knowledge_top_id||'keine')}</div><div style="margin-top:8px">${badge(repP[0],repP[1])} <span class="muted small">Schwellwert ${esc(pct(x.reply_threshold))}</span></div></div>
<div class="detail-box full"><div class="detail-heading">Top-Knowledge-Kandidat</div>${x.knowledge_top_id?`${progress('Retrieval / Ranking',x.knowledge_score)}${x.knowledge_evidence_score?progress('Finale Evidenz',x.knowledge_evidence_score):''}${progress('Semantik (raw)',x.knowledge_semantic_score)}${progress('Titel',x.knowledge_title_score)}${progress('Lexikalisch',x.knowledge_lexical_score)}${progress('Keywords',x.knowledge_keyword_score)}${progress('Kategorie/Lernen',x.knowledge_category_score)}<div class="muted small">${esc(x.knowledge_top_title)} · ${esc(x.knowledge_top_id)} · Retrieval-Floor ${esc(pct(x.knowledge_retrieval_floor||0))} · Evidenz erforderlich ${esc(pct(x.knowledge_threshold))}${x.knowledge_category_aligned?' · Kategorie exakt zugeordnet':''}</div>`:'<div class="muted">Kein Treffer.</div>'}</div>
<div class="detail-box full"><div class="detail-heading">KI-Begründung</div><div class="reason-text">${esc(x.ai_reason||x.reason||'')}</div></div><div class="detail-box full"><div class="detail-heading">Policy</div><div class="reason-text">${esc(x.policy_reason||'')}</div>${x.error?`<div class="reason-text" style="color:#ffb5b5;margin-top:9px"><strong>Fehler:</strong> ${esc(x.error)}</div>`:''}</div>
<div class="detail-box full"><div class="detail-heading">Knowledge-Ranking</div><div class="muted small" style="margin-bottom:10px">An KI gesendet: ${esc(x.knowledge_llm_candidates||0)} · Kandidaten-Cutoff ${esc(pct(x.knowledge_candidate_cutoff||0))} · Max. Abstand ${esc(pct(x.knowledge_candidate_max_gap||0))} · Audit Top K ${esc(x.knowledge_audit_top_k||0)}</div>${candidates}</div><div class="detail-box full"><div class="detail-heading">Kontextquellen</div>${contexts}${(x.context_warnings||[]).length?`<div class="notice warn" style="margin-top:10px">${x.context_warnings.map(esc).join('<br>')}</div>`:''}</div>
<div class="detail-box full"><div class="detail-heading">Lernen</div>${categories.length?`<button class="btn small primary" data-learn-run="${esc(x.run_id)}" data-learn-cat="${esc(x.ai_recommended_category_id||0)}">Kategorie bestätigen / korrigieren</button>`:'<span class="muted">Kategorien nicht geladen.</span>'}</div>
<div class="detail-box full"><details><summary class="small muted" style="cursor:pointer">Audit-JSON anzeigen</summary><pre class="raw">${esc(JSON.stringify(x,null,2))}</pre></details></div></div>`;openRunDrawer()}
function openRunDrawer(){ $('#runBackdrop').classList.add('show');$('#runDrawer').classList.add('show') } function closeRunDrawer(){ $('#runBackdrop').classList.remove('show');$('#runDrawer').classList.remove('show');currentRun=null }
function kbStatsData(){const managed=kbDocs.filter(x=>x.managed).length,glpi=kbDocs.filter(x=>x.source==='glpi-kb').length,auto=kbDocs.filter(x=>x.auto_reply).length;return [['Gesamt',kbDocs.length,'geladene Artikel'],['Web-verwaltet',managed,'editierbar'],['GLPI-KB',glpi,'read-only synchronisiert'],['Auto-Reply',auto,'grundsätzlich freigegeben']]}
function filteredKB(){const q=$('#kbSearch').value.trim().toLowerCase(),src=$('#kbSourceFilter').value,mode=$('#kbManageFilter').value;return kbDocs.filter(d=>(!src||d.source===src)&&(!mode||(mode==='managed'&&d.managed)||(mode==='readonly'&&!d.managed)||(mode==='autoreply'&&d.auto_reply))&&(!q||[d.id,d.title,d.text,d.answer,(d.keywords||[]).join(' '),(d.categories||[]).join(' ')].join(' ').toLowerCase().includes(q)))}
function renderKB(){const st=kbStatsData();$('#kbStats').innerHTML=st.map(x=>`<div class="stat"><div class="stat-label">${esc(x[0])}</div><div class="stat-value">${fmtNum(x[1])}</div><div class="stat-foot">${esc(x[2])}</div></div>`).join('');const docs=filteredKB();$('#kbGrid').innerHTML=docs.length?docs.map(d=>`<article class="kb-card"><div style="display:flex;justify-content:space-between;gap:10px"><div><div class="kb-card-title">${esc(d.title)}</div><div class="muted small mono">${esc(d.id)}</div></div>${d.managed?badge('Web','info'):badge(d.origin==='glpi-kb'?'GLPI':'Read-only')}</div><div class="kb-meta">${badge(d.source||'')}${d.answer_html?badge('Rich Text','good'):badge('Plaintext')}${d.auto_reply?badge('Auto-Reply','good'):badge('kein Auto-Reply','warn')}${badge(`Min ${pct(d.min_score)}`)}</div><div class="kb-snippet">${esc((d.text||'').slice(0,260))}${(d.text||'').length>260?'…':''}</div><div class="muted small" style="margin-top:8px">Kategorien: ${esc((d.categories||[]).join(', ')||'alle')}${(d.external_categories||[]).length?` · Extern: ${esc(d.external_categories.join(', '))}`:''}${(d.unmapped_external_categories||[]).length?` · ⚠ nicht zugeordnet: ${esc(d.unmapped_external_categories.join(', '))}`:''}${(d.source_category_ids||[]).length?` · GLPI-KB: ${esc(d.source_category_ids.join(', '))}`:''}</div><div class="kb-actions">${d.managed?`<button class="btn small" data-kb-edit="${esc(d.id)}">Bearbeiten</button><button class="btn small danger" data-kb-delete="${esc(d.id)}">Löschen</button>`:`<span class="muted small">${d.origin==='glpi-kb'?'wird aus GLPI synchronisiert':'über Datei/Git verwalten'}</span>`}</div></article>`).join(''):'<div class="empty">Keine passenden Knowledge-Einträge.</div>'}
function renderLearning(){const q=$('#learningSearch').value.trim().toLowerCase(),rows=learningRows.filter(x=>!q||[x.ticket_id,x.subject,x.text,x.category_name,x.category_id].join(' ').toLowerCase().includes(q));const corrections=learningRows.filter(x=>x.correction).length;$('#learningStats').innerHTML=[['Gesamt',learningRows.length,'bestätigte Beispiele'],['Korrekturen',corrections,'KI lag anders'],['Bestätigungen',learningRows.length-corrections,'KI wurde bestätigt']].map(x=>`<div class="stat"><div class="stat-label">${esc(x[0])}</div><div class="stat-value">${fmtNum(x[1])}</div><div class="stat-foot">${esc(x[2])}</div></div>`).join('');$('#learningTable').innerHTML=rows.length?rows.map(x=>`<tr><td><div class="ticket-title">#${esc(x.ticket_id)} ${esc(x.subject)}</div><div class="muted small">${esc((x.text||'').slice(0,220))}</div></td><td><strong>${esc(x.category_name)}</strong> (#${esc(x.category_id)})${x.ai_recommended_category_id?`<div class="muted small">KI: #${esc(x.ai_recommended_category_id)} · ${esc(pct(x.ai_confidence))}</div>`:''}</td><td>${x.correction?badge('Korrektur','warn'):badge('Bestätigung','good')}</td><td class="nowrap">${esc(fmtDate(x.created_at))}</td><td><button class="btn small danger" data-learning-delete="${esc(x.id)}">Löschen</button></td></tr>`).join(''):'<tr><td colspan="5" class="empty">Keine Lernbeispiele.</td></tr>'}
function configCard(title,subtitle,rows){return `<div class="panel config-card"><div class="panel-head"><div><div class="panel-title">${esc(title)}</div><div class="panel-sub">${esc(subtitle)}</div></div></div><div class="config-list">${rows.map(([k,v])=>`<div class="config-row"><div class="config-key">${esc(k)}</div><div class="config-val">${v}</div></div>`).join('')}</div></div>`}
function val(v){if(typeof v==='boolean')return v?badge('aktiv','good'):badge('aus','warn');if(Array.isArray(v))return esc(v.length?v.join(', '):'');return esc(v??'')}
function renderConfig(){const s=statusData;const groups=[configCard('Agent & GLPI','Polling, Worker und Schreibmodus',[['Dry Run',val(s.dry_run)],['Auto-Kategorie',val(s.auto_category)],['Auto-Reply',val(s.auto_reply)],['Worker',val(s.workers)],['Queue-Größe',val(s.queue_size)],['API-Version',val(s.glpi_api_version)],['Poll-Intervall',val(s.glpi_poll_interval)],['Poll-Limit',val(s.glpi_poll_limit)],['Letzter Poll',val(fmtDate(s.last_poll))],['Poll: abgerufen / bekannt / neu / Queue',val(`${s.poll_last_fetched||0} / ${s.poll_last_seen||0} / ${s.poll_last_unseen||0} / ${s.poll_last_enqueued||0}`)],['Poll-Fehler',val(s.poll_last_error||'')],['Bekannte Ticketversionen',val(s.processed_version_count||0)],['Ticket-Filter gesetzt',val(s.glpi_ticket_filter_configured)],['Erlaubte Status',val(s.glpi_allowed_status_ids)],['GLPI-Timeout',val(s.glpi_timeout)]]),configCard('Priorität & Eskalation','Separate KI-Läufe mit deterministischen Schreibregeln',[['Prioritätsanalyse',val(s.priority_enabled)],['Auto-Priorität',val(s.auto_priority)],['Prioritäts-Confidence',`<strong>${pct(s.priority_confidence)}</strong>`],['Prioritäts-Timeout',val(s.priority_analysis_timeout)],['Max. Erhöhung/Lauf',val(s.priority_max_increase)],['Erlaubte Prioritätsgründe',val(s.priority_allowed_reason_codes)],['Eskalationsanalyse',val(s.escalation_enabled)],['Auto-Eskalation',val(s.auto_escalation)],['Scan-Intervall',val(s.escalation_scan_interval)],['Mindestalter',val(s.escalation_min_age)],['Mindest-Inaktivität',val(s.escalation_min_inactivity)],['KI-Zeitbudget',val(s.escalation_analysis_timeout)],['Eskalations-Confidence',`<strong>${pct(s.escalation_confidence)}</strong>`],['Max. Eskalationsstufe',val(s.escalation_max_level)],['SLA-Risikofenster',val(s.escalation_sla_risk_window)],['Service Owner ab Stufe',val(s.escalation_service_owner_min_level)],['Management-Review ab Stufe',val(s.escalation_manager_review_min_level)],['Major-Incident-Relevanz',pct(s.escalation_major_incident_min_relevance)],['Erlaubte Eskalationsgründe',val(s.escalation_allowed_reason_codes)],['Erlaubte Eskalationsaktionen',val(s.escalation_allowed_actions)],['Second-Level-Gruppe',val(s.escalation_second_level_group_id||'')],['Security-Gruppe',val(s.escalation_security_group_id||'')],['Service Owner Gruppe / Benutzer',val(`${s.escalation_service_owner_group_id||''} / ${s.escalation_service_owner_user_id||''}`)],['Management Gruppe / Benutzer',val(`${s.escalation_manager_review_group_id||''} / ${s.escalation_manager_review_user_id||''}`)],['Private Eskalationsnotizen',val(s.escalation_add_private_followup)],['Webhook konfiguriert',val(s.escalation_webhook_configured)],['Webhook-Timeout',val(s.escalation_webhook_timeout)],['Unsicheres Webhook-HTTP',val(s.escalation_webhook_allow_insecure_http)],['GLPI Gruppen-/Benutzerfeld',val(`${s.glpi_escalation_group_patch_field||''} / ${s.glpi_escalation_user_patch_field||''}`)],['Major-Incident-Linkadapter',val(s.glpi_escalation_itil_link_configured)],['GLPI-Eskalationsfilter gesetzt',val(s.glpi_escalation_filter_configured)],['Kandidatenlimit',val(s.glpi_escalation_limit)]]),configCard('Ollama-Pool','Modelle, Routing, Health und Failover',[['Chat-Modell',val(s.ollama_model)],['Embedding-Modell',val(s.ollama_embedding_model)],['Embedding-Profil',val(s.knowledge_embedding_profile)],['Nodes gesund / gesamt',val(`${s.ollama_healthy_nodes||0} / ${s.ollama_node_count||0}`)],['Routing',val(s.ollama_routing_mode)],['Max. parallel je Node',val(s.ollama_node_max_inflight)],['Health-Intervall',val(s.ollama_node_health_interval)],['Fehler-Cooldown',val(s.ollama_node_failure_cooldown)],['Request-Timeout je Node',val(s.ollama_node_request_timeout)],['Failover',val(s.ollama_failover_enabled)],['Max. Versuche',val(s.ollama_failover_attempts)],['Gleicher Modelldigest Pflicht',val(s.ollama_require_same_model_digest)],['Embedding-Modell je Node Pflicht',val(s.ollama_require_embedding_model)],['Gesamt-Timeout',val(s.ollama_timeout)],['Num Predict',val(s.ollama_num_predict)],['Keep Alive',val(s.ollama_keep_alive)],['Thinking',val(s.ollama_think)],['JSON-Retries',val(s.ollama_json_retries)],['Node-Details',val((s.ollama_nodes||[]).map(n=>`${n.name}: ${n.healthy&&n.compatible?'OK':'Fehler'} · ${n.requests||0} Requests · ${n.failures||0} Fehler`).join(' | ')||'')]]),configCard('Knowledge / RAG','Retrieval, Chunking und Ranking',[['RAG',val(s.rag_enabled)],['Index-Modus',val(s.knowledge_index_mode)],['Snapshot geladen',val(s.knowledge_snapshot_loaded)],['Snapshot gespeichert',val(fmtDate(s.knowledge_snapshot_saved_at))],['Letzter Delta-Scan',val(fmtDate(s.knowledge_last_scan_at))],['Letzter Scanfehler',val(s.knowledge_last_scan_error||'')],['Geänderte Dateien',val(s.knowledge_changed_files)],['Gelöschte Dateien',val(s.knowledge_deleted_files)],['Wiederverwendete Vektoren',val(s.knowledge_reused_files)],['Embedding-Batch',val(s.knowledge_embed_batch_size)],['Scan-Intervall',val(s.knowledge_index_scan_interval)],['Knowledge bereit',val(s.knowledge_ready)],['Startup-Status',val(s.knowledge_init_state)],['Startup-Phase',val(s.knowledge_init_phase)],['Dateien verarbeitet',val(`${s.knowledge_init_processed_files||0} / ${s.knowledge_init_total_files||0}`)],['Dokumente geladen',val(s.knowledge_init_loaded_docs)],['Dokumente indexiert',val(s.knowledge_init_indexed_docs)],['Embedding-Cache-Treffer',val(s.knowledge_init_cache_hits)],['Offene Embeddings',val(s.knowledge_init_pending_embeddings)],['Startup-Fehler',val(s.knowledge_init_error||'')],['Max. Kandidaten an KI',val(s.knowledge_top_k)],['Audit Top K',val(s.knowledge_audit_top_k)],['Max. Abstand zum Top-Treffer',pct(s.knowledge_candidate_max_gap)],['Finaler Evidenz-Schwellwert',`<strong>${pct(s.knowledge_min_score)}</strong>`],['Retrieval-Floor',`<strong>${pct(s.knowledge_retrieval_floor)}</strong>`],['Evidenzgewicht Retrieval',pct(s.knowledge_evidence_weight_retrieval)],['Evidenzgewicht KI',pct(s.knowledge_evidence_weight_ai)],['Evidenzgewicht Kategorie',pct(s.knowledge_evidence_weight_category)],['Retrieval: Semantik',pct(s.knowledge_weight_semantic)],['Retrieval: Titel',pct(s.knowledge_weight_title)],['Retrieval: Lexikalisch',pct(s.knowledge_weight_lexical)],['Retrieval: Keywords',pct(s.knowledge_weight_keywords)],['Retrieval: Kategorie/Lernen',pct(s.knowledge_weight_category)],['Chunk-Wörter',val(s.knowledge_chunk_words)],['Overlap-Wörter',val(s.knowledge_chunk_overlap_words)],['Max. KB-Chunks',val(s.knowledge_max_chunks_per_doc)],['Max. Ticket-Chunks',val(s.knowledge_max_query_chunks)],['Antwort-/Retrieval-Quellen',val(s.knowledge_allowed_sources)],['Kategorisierungsquellen',val(s.knowledge_category_sources)],['Auto-Reply-Quellen',val(s.knowledge_auto_reply_sources)],['Fremdkategorie-Modus',val(s.knowledge_category_mode)],['Kategorie-Mapping',val(s.knowledge_category_map_configured?'konfiguriert':'')],['Ignore-Globs',val(s.knowledge_ignore_globs)],['Ignorierte Dateien',val(s.knowledge_ignored_files)],['KBs mit ungemappten Kategorien',val(s.knowledge_unmapped_category_files)],['Ungemappte Kategorien',val(s.knowledge_unmapped_categories)]]),configCard('GLPI Knowledge Base','Synchronisation der GLPI-Wissensdatenbank',[['Aktiv',val(s.glpi_kb_enabled)],['Sync OK',val(s.glpi_kb_ok)],['Dokumente',val(s.glpi_kb_documents)],['Letzter Sync',val(fmtDate(s.glpi_kb_last_sync))],['Intervall',val(s.glpi_kb_sync_interval)],['Pfad',val(s.glpi_kb_path)],['Filter gesetzt',val(s.glpi_kb_filter_configured)],['Limit',val(s.glpi_kb_limit)],['Auto-Reply',val(s.glpi_kb_auto_reply)],['Auto-Reply freigegeben / blockiert',val(`${s.glpi_kb_auto_reply_approved||0} / ${s.glpi_kb_auto_reply_blocked||0}`)],['Auto-Reply-Entscheidungen',val(s.glpi_kb_auto_reply_decisions)],['Auto-Reply-KB-Kategorien',val(s.glpi_kb_auto_reply_category_ids)],['Kategorielose Artikel zulassen',val(s.glpi_kb_auto_reply_allow_uncategorized)],['Freigegebene kategorielose Artikel-IDs',val(s.glpi_kb_auto_reply_uncategorized_article_ids)],['Letzter Fehler',val(s.glpi_kb_last_error||'')]]),configCard('Policy & Kommunikation','Entscheidungsschwellen und Sprache',[['Kategorie-Confidence',`<strong>${pct(s.category_confidence)}</strong>`],['Reply-Confidence',`<strong>${pct(s.reply_confidence)}</strong>`],['Sprache',val(s.communication_language)],['Stil',val(s.communication_style)],['KI-Kennzeichnung',val(s.ai_content_label_enabled)],['KB-Webeditor',val(s.knowledge_edit_enabled)],['Lernen',val(s.learning_enabled)],['Max. Lernbeispiele',val(s.learning_max_examples)],['Beispiele/Kategorie',val(s.learning_examples_per_category)]]),configCard('Kontextquellen','Störungen, Changes, Incidents und Geräte',[['Kontext aktiv',val(s.context_enabled)],['Timeout',val(s.context_timeout)],['Relevanz-Minimum',pct(s.context_relevance_min_score)],['Fail-closed',val(s.context_fail_closed)],['Incident blockiert normalen Reply',val(s.context_incident_block)],['Vordefinierte Statusantwort',val(s.context_status_reply_enabled)],['Status: Relevanz-Minimum',pct(s.context_status_reply_min_relevance)],['Status: KI-Minimum',pct(s.context_status_reply_min_ai_confidence)],['Status: Final-Minimum',pct(s.context_status_reply_min_final_score)],['Störungstext konfiguriert',val(s.context_incident_reply_text_configured)],['Wartungstext konfiguriert',val(s.context_maintenance_reply_text_configured)],['Change Calendar',val(s.change_calendar_enabled)],['Lookback',val(s.change_lookback)],['Lookahead',val(s.change_lookahead)],['Major Incidents',val(s.major_incidents_enabled)],['Benutzer-Geräte',val(s.user_device_context_enabled)],['Uptime Kuma',val(s.uptime_kuma_enabled)],['Uptime-Modus',val(s.uptime_kuma_mode)]] )];$('#configGroups').innerHTML=groups.join('')}
function renderSourceOptions(){const filterOld=$('#kbSourceFilter').value,sourceOld=$('#kbSource').value;const sources=[...new Set(kbDocs.map(x=>x.source).filter(Boolean))].sort();$('#kbSourceFilter').innerHTML='<option value="">Alle Quellen</option>'+sources.map(x=>`<option value="${esc(x)}">${esc(x)}</option>`).join('');if([...$('#kbSourceFilter').options].some(o=>o.value===filterOld))$('#kbSourceFilter').value=filterOld;const allowed=statusData.knowledge_allowed_sources||[];$('#kbSource').innerHTML=allowed.map(x=>`<option value="${esc(x)}">${esc(x)}</option>`).join('');if([...$('#kbSource').options].some(o=>o.value===sourceOld))$('#kbSource').value=sourceOld;else if([...$('#kbSource').options].some(o=>o.value==='internal-kb'))$('#kbSource').value='internal-kb'}
function renderCategoryPicker(filter=''){const q=filter.toLowerCase();$('#kbCategoryList').innerHTML=categories.filter(c=>!q||(c.completename||c.name||'').toLowerCase().includes(q)).map(c=>`<label class="category-item"><input type="checkbox" value="${Number(c.id)}" ${kbCategorySelection.has(Number(c.id))?'checked':''}><span>${esc(c.completename||c.name)} <span class="muted">#${Number(c.id)}</span></span></label>`).join('')||'<div class="muted small" style="padding:8px">Keine Kategorie gefunden.</div>'}
function clearKbForm(){currentKbId='';kbCategorySelection=new Set();$('#kbForm').reset();$('#kbId').disabled=false;$('#kbId').value='';$('#kbLanguage').value=statusData.communication_language||'de-DE';$('#kbStyle').value=statusData.communication_style||'formal';$('#kbScore').value=Number(statusData.knowledge_min_score||.70).toFixed(2);if([...$('#kbSource').options].some(x=>x.value==='internal-kb'))$('#kbSource').value='internal-kb';$('#kbModalTitle').textContent='Neuen Artikel anlegen';$('#kbModalEyebrow').textContent='Interne Knowledge Base';$('#kbEditState').textContent='Neuer Artikel';$('#kbFormMessage').className='form-message';$('#kbCategorySearch').value='';renderCategoryPicker();updateCounts()}
function openKbModal(){if(!statusData.knowledge_edit_enabled){toast('KB-Webeditor ist deaktiviert.','error');return}$('#kbModalBackdrop').classList.add('show');setTimeout(()=>$('#kbTitle').focus(),30)} function closeKbModal(){if($('#saveKbBtn').disabled)return;$('#kbModalBackdrop').classList.remove('show')}
async function editKB(id){try{const data=await api(`/api/knowledge/${encodeURIComponent(id)}`);if(!data.managed){toast('Dieser Artikel ist read-only.','error');return}const d=data.document;currentKbId=id;kbCategorySelection=new Set((d.categories||[]).map(Number));$('#kbId').value=d.id||id;$('#kbId').disabled=true;$('#kbTitle').value=d.title||'';$('#kbSource').value=d.source||'internal-kb';$('#kbLanguage').value=d.language||statusData.communication_language||'de-DE';$('#kbStyle').value=d.communication_style||statusData.communication_style||'formal';$('#kbScore').value=Number(d.min_score??statusData.knowledge_min_score??.70).toFixed(2);$('#kbKeywords').value=(d.keywords||[]).join(', ');$('#kbUri').value=d.source_uri||'';$('#kbText').value=d.text||'';$('#kbAnswer').value=d.answer||'';$('#kbAutoReply').checked=!!d.auto_reply;$('#kbModalTitle').textContent=`Artikel bearbeiten · ${id}`;$('#kbModalEyebrow').textContent='Web-verwaltete Knowledge Base';$('#kbEditState').textContent='ID ist im Bearbeitungsmodus gesperrt.';$('#kbFormMessage').className='form-message';$('#kbCategorySearch').value='';renderCategoryPicker();updateCounts();openKbModal()}catch(e){toast(e.message,'error')}}
function updateCounts(){const t=$('#kbText').value||'',a=$('#kbAnswer').value||'';$('#kbTextCount').textContent=`${fmtNum(t.length)} Zeichen`;$('#kbAnswerCount').textContent=`${fmtNum(a.length)} Zeichen`}
function kbPayload(){return {id:$('#kbId').value.trim(),title:$('#kbTitle').value.trim(),source:$('#kbSource').value.trim(),language:$('#kbLanguage').value.trim(),communication_style:$('#kbStyle').value,text:$('#kbText').value.trim(),answer:$('#kbAnswer').value.trim(),auto_reply:$('#kbAutoReply').checked,min_score:Number($('#kbScore').value||0),categories:[...kbCategorySelection].sort((a,b)=>a-b),keywords:$('#kbKeywords').value.split(',').map(x=>x.trim()).filter(Boolean),source_uri:$('#kbUri').value.trim()}}
async function saveKB(e){e.preventDefault();const b=$('#saveKbBtn'),msg=$('#kbFormMessage');b.disabled=true;b.textContent='Speichere …';msg.className='form-message';try{const payload=kbPayload();if(!payload.id||!payload.title||!payload.text)throw new Error('ID, Titel und Diagnosekontext sind Pflichtfelder.');if(payload.min_score<0||payload.min_score>1)throw new Error('Der Mindestscore muss zwischen 0 und 1 liegen.');const url=currentKbId?`/api/knowledge/${encodeURIComponent(currentKbId)}`:'/api/knowledge';await api(url,{method:currentKbId?'PUT':'POST',body:JSON.stringify(payload)});msg.textContent=currentKbId?'Artikel erfolgreich aktualisiert.':'Artikel erfolgreich angelegt.';msg.className='form-message show success';toast(msg.textContent);await loadKnowledge();setTimeout(closeKbModal,350)}catch(e){msg.textContent=e.message;msg.className='form-message show error'}finally{b.disabled=false;b.textContent='Speichern'}}
async function deleteKB(id){if(!confirm(`Knowledge-Eintrag ${id} wirklich löschen?`))return;try{await api(`/api/knowledge/${encodeURIComponent(id)}`,{method:'DELETE'});toast('Knowledge-Eintrag gelöscht.');await loadKnowledge()}catch(e){toast(e.message,'error')}}
function openLearn(runID,preferred){currentLearnRun=runID;const r=runsData.find(x=>x.run_id===runID);$('#learnTicket').textContent=r?`#${r.ticket_id} ${r.ticket_name}\n\nKI: ${r.ai_recommended_category_name||'keine Kategorie'} · ${pct(r.ai_category_confidence)}`:runID;$('#learnCategory').innerHTML=categories.map(c=>`<option value="${Number(c.id)}">${esc(c.completename||c.name)} (#${Number(c.id)})</option>`).join('');if(preferred>0)$('#learnCategory').value=String(preferred);$('#learnBackdrop').classList.add('show')}
function closeLearn(){$('#learnBackdrop').classList.remove('show');currentLearnRun=''}
async function saveLearning(){const b=$('#saveLearnBtn');b.disabled=true;try{await api('/api/learning',{method:'POST',body:JSON.stringify({run_id:currentLearnRun,category_id:Number($('#learnCategory').value)})});toast('Kategorie als Lernbeispiel gespeichert.');closeLearn();await loadLearning();await loadRuns()}catch(e){toast(e.message,'error')}finally{b.disabled=false}}
async function deleteLearning(id){if(!confirm('Lernbeispiel wirklich löschen?'))return;try{await api(`/api/learning/${encodeURIComponent(id)}`,{method:'DELETE'});toast('Lernbeispiel gelöscht.');await loadLearning()}catch(e){toast(e.message,'error')}}
async function reprocessTicket(){const id=Number($('#reprocessTicketID').value||0);if(!Number.isInteger(id)||id<=0){toast('Bitte eine gültige Ticket-ID eingeben.','error');return}const live=!statusData.dry_run;if(live&&!confirm(`Ticket #${id} im LIVE-Modus neu analysieren? Konfigurierte Auto-Aktionen können ausgeführt werden.`))return;const b=$('#reprocessTicketBtn');b.disabled=true;try{await api(`/api/tickets/${id}/reprocess`,{method:'POST',body:'{}'});toast(`Ticket #${id} wurde zur Neuanalyse eingereiht.`);setTimeout(()=>refreshAll(),800)}catch(e){toast(e.message,'error')}finally{b.disabled=false}}
async function loadStatus(){statusData=await api('/api/status');renderStatusChrome();renderOverview();renderConfig();renderSourceOptions()}
async function loadRuns(){const limit=Number($('#runLimit').value||50);runsData=await api(`/api/runs?limit=${limit}`);renderRuns();renderOverview()}
async function loadKnowledge(){kbDocs=await api('/api/knowledge');renderSourceOptions();renderKB();renderOverview()}
async function loadLearning(){learningRows=await api('/api/learning');renderLearning();renderOverview()}
async function loadCategories(){categories=await api('/api/categories');renderCategoryPicker($('#kbCategorySearch').value||'')}
async function refreshAll(showToast=false){$('#refreshBtn').disabled=true;try{await Promise.all([loadStatus(),loadRuns(),loadKnowledge(),loadLearning(),loadCategories()]);renderSourceOptions();renderOverview();renderRuns();renderKB();renderLearning();renderConfig();if(showToast)toast('Dashboard aktualisiert.')}catch(e){toast(e.message,'error')}finally{$('#refreshBtn').disabled=false;$('#lastRefresh').textContent=new Date().toLocaleTimeString('de-DE')}}
$('#nav').addEventListener('click',e=>{const b=e.target.closest('[data-view]');if(b)setView(b.dataset.view)});document.addEventListener('click',e=>{const g=e.target.closest('[data-goto]');if(g)setView(g.dataset.goto);const row=e.target.closest('[data-run-id]');if(row){const x=runsData.find(r=>r.run_id===row.dataset.runId);if(x)renderRunDrawer(x)}const ed=e.target.closest('[data-kb-edit]');if(ed)editKB(ed.dataset.kbEdit);const del=e.target.closest('[data-kb-delete]');if(del)deleteKB(del.dataset.kbDelete);const ld=e.target.closest('[data-learning-delete]');if(ld)deleteLearning(ld.dataset.learningDelete);const lr=e.target.closest('[data-learn-run]');if(lr)openLearn(lr.dataset.learnRun,Number(lr.dataset.learnCat||0))});
$('#refreshBtn').addEventListener('click',()=>refreshAll(true));$('#reprocessTicketBtn').addEventListener('click',reprocessTicket);$('#reprocessTicketID').addEventListener('keydown',e=>{if(e.key==='Enter')reprocessTicket()});$('#runSearch').addEventListener('input',renderRuns);$('#runOutcome').addEventListener('change',renderRuns);$('#runLimit').addEventListener('change',loadRuns);$('#kbSearch').addEventListener('input',renderKB);$('#kbSourceFilter').addEventListener('change',renderKB);$('#kbManageFilter').addEventListener('change',renderKB);$('#learningSearch').addEventListener('input',renderLearning);$('#newKbBtn').addEventListener('click',()=>{clearKbForm();openKbModal()});$('#closeKbModal').addEventListener('click',closeKbModal);$('#cancelKbBtn').addEventListener('click',closeKbModal);$('#kbModalBackdrop').addEventListener('click',e=>{if(e.target===e.currentTarget)closeKbModal()});$('#kbForm').addEventListener('submit',saveKB);$('#kbCategorySearch').addEventListener('input',e=>renderCategoryPicker(e.target.value));$('#kbCategoryList').addEventListener('change',e=>{if(e.target.type==='checkbox'){const id=Number(e.target.value);e.target.checked?kbCategorySelection.add(id):kbCategorySelection.delete(id)}});$('#kbText').addEventListener('input',updateCounts);$('#kbAnswer').addEventListener('input',updateCounts);$('#runBackdrop').addEventListener('click',closeRunDrawer);$('#closeRunDrawer').addEventListener('click',closeRunDrawer);$('#closeLearnModal').addEventListener('click',closeLearn);$('#cancelLearnBtn').addEventListener('click',closeLearn);$('#saveLearnBtn').addEventListener('click',saveLearning);$('#learnBackdrop').addEventListener('click',e=>{if(e.target===e.currentTarget)closeLearn()});document.addEventListener('keydown',e=>{if(e.key==='Escape'){closeRunDrawer();closeKbModal();closeLearn()}});
setView(location.hash.replace('#','')||'overview');refreshAll();setInterval(async()=>{try{await Promise.all([loadStatus(),loadRuns()])}catch{}},10000);
</script>
</body>
</html>