176 lines
75 KiB
HTML
176 lines
75 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 · 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">0–1</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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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>
|