Files
jbergner 9030c2efb7
All checks were successful
release-tag / release-image (push) Successful in 6m17s
1.5.3
2026-08-27 10:43:07 +02:00

242 lines
67 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,viewport-fit=cover">
<title>NeuroForge · Knowledge OS</title>
<style>
:root{
color-scheme:dark;--bg:#080b12;--panel:#101622;--panel2:#151d2c;--line:#26334a;--muted:#91a0b7;--text:#eef3fb;
--accent:#7aa7ff;--accent2:#65d6b5;--warn:#f7c36d;--bad:#ff7c8d;--shadow:0 18px 48px rgba(0,0,0,.28);
font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif
}
*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 15% 0,#10192b 0,#080b12 36rem);color:var(--text)}
button,input,textarea,select{font:inherit}button{cursor:pointer}.app{min-height:100vh;display:grid;grid-template-columns:238px minmax(0,1fr)}
.sidebar{position:sticky;top:0;height:100vh;padding:22px 16px;border-right:1px solid var(--line);background:rgba(8,11,18,.9);backdrop-filter:blur(16px);z-index:20}
.brand{display:flex;align-items:center;gap:12px;padding:4px 8px 24px}.mark{width:36px;height:36px;border-radius:12px;background:linear-gradient(145deg,var(--accent),#8d7cff);box-shadow:0 8px 24px #637cff45}.brand b{font-size:20px}.brand small{display:block;color:var(--muted);margin-top:2px}
.nav{display:grid;gap:5px}.nav button{display:flex;align-items:center;gap:10px;width:100%;border:0;background:transparent;color:var(--muted);padding:10px 12px;border-radius:11px;text-align:left}.nav button:hover{background:#151e2e;color:var(--text)}.nav button.active{background:#1b2840;color:#fff;box-shadow:inset 3px 0 var(--accent)}.nav .ico{width:20px;text-align:center}
.sidebarFoot{position:absolute;left:16px;right:16px;bottom:18px}.statusChip{display:flex;align-items:center;gap:8px;padding:10px 12px;border:1px solid var(--line);border-radius:12px;background:#0e1420;color:var(--muted);font-size:13px}.dot{width:8px;height:8px;border-radius:50%;background:var(--muted)}.dot.ok{background:var(--accent2);box-shadow:0 0 12px #65d6b566}.dot.bad{background:var(--bad)}
.main{min-width:0;padding:26px 30px 60px}.topbar{display:flex;justify-content:space-between;gap:16px;align-items:center;margin-bottom:22px}.topbar h1{font-size:25px;margin:0}.topbar p{margin:5px 0 0;color:var(--muted);font-size:14px}.actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.btn{border:1px solid transparent;border-radius:10px;padding:9px 13px;font-weight:650;background:var(--accent);color:#07101e}.btn:hover{filter:brightness(1.06)}.btn.secondary{background:#172133;border-color:#2d3c55;color:var(--text)}.btn.ghost{background:transparent;border-color:var(--line);color:var(--muted)}.btn.danger{background:#3a1821;border-color:#65303e;color:#ffb8c2}.btn:disabled{opacity:.45;cursor:not-allowed}
.view{display:none}.view.active{display:block}.grid{display:grid;gap:14px}.metrics{grid-template-columns:repeat(4,minmax(0,1fr));margin-bottom:14px}.metric{border:1px solid var(--line);background:linear-gradient(160deg,#131b2a,#0e141f);border-radius:16px;padding:17px;box-shadow:var(--shadow)}.metric .label{color:var(--muted);font-size:12px;text-transform:uppercase;letter-spacing:.06em}.metric .value{font-size:28px;font-weight:750;margin-top:5px}.metric .sub{font-size:12px;color:var(--muted);margin-top:4px}
.card{border:1px solid var(--line);background:rgba(16,22,34,.94);border-radius:16px;padding:18px;box-shadow:var(--shadow)}.card h2{font-size:17px;margin:0 0 14px}.card h3{font-size:14px;margin:18px 0 9px}.two{grid-template-columns:minmax(0,1.35fr) minmax(300px,.65fr)}.three{grid-template-columns:repeat(3,minmax(0,1fr))}.split{display:grid;grid-template-columns:minmax(0,1.7fr) minmax(320px,.72fr);gap:14px}
label{display:block;color:var(--muted);font-size:12px;margin-bottom:5px}.field{margin-bottom:12px}.control{width:100%;border:1px solid #30405b;background:#0b111c;color:var(--text);border-radius:10px;padding:10px 11px;outline:none}.control:focus{border-color:var(--accent);box-shadow:0 0 0 3px #7aa7ff18}textarea.control{min-height:120px;resize:vertical}.mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.row{display:flex;gap:10px;align-items:center}.row>*{min-width:0}.grow{flex:1}.checks{display:flex;gap:14px;flex-wrap:wrap}.checks label{display:flex;align-items:center;gap:7px;color:var(--text);font-size:13px;margin:0}.checks input{accent-color:var(--accent)}
.pill{display:inline-flex;align-items:center;gap:5px;padding:4px 8px;border-radius:99px;background:#1a2639;color:#b9c8dd;font-size:11px;border:1px solid #2c3b55}.pill.good{background:#102b25;border-color:#225445;color:#8de0c5}.pill.warn{background:#2b2414;border-color:#5b4923;color:#f5cf85}.pill.bad{background:#321821;border-color:#66303e;color:#ffacb8}.muted{color:var(--muted)}.small{font-size:12px}.error{color:#ffadb8}.good{color:#83dec1}.warn{color:#f7ce84}.pre{white-space:pre-wrap;word-break:break-word;background:#090e17;border:1px solid #202c40;border-radius:10px;padding:11px;max-height:360px;overflow:auto}.scroll{max-height:590px;overflow:auto}.list{display:grid;gap:8px}.item{border:1px solid #26344a;background:#0d1420;border-radius:12px;padding:12px}.item.click{cursor:pointer}.item.click:hover{border-color:#3c5377;background:#111b2b}.itemTitle{display:flex;justify-content:space-between;gap:10px;align-items:flex-start}.itemTitle b{font-size:13px}.item p{margin:6px 0;color:#ccd7e7;font-size:13px;line-height:1.42}.meta{display:flex;gap:6px;flex-wrap:wrap;color:var(--muted);font-size:11px}
.progress{height:7px;border-radius:99px;background:#1a2536;overflow:hidden}.progress span{display:block;height:100%;background:linear-gradient(90deg,var(--accent),var(--accent2));border-radius:inherit}
.pipeline{display:grid;grid-template-columns:repeat(7,minmax(120px,1fr));gap:8px}.stage{position:relative;padding:12px;border:1px solid var(--line);border-radius:12px;background:#0d1420}.stage b{display:block;font-size:12px}.stage small{display:block;color:var(--muted);margin-top:4px}.stage:not(:last-child):after{content:"→";position:absolute;right:-8px;top:28px;color:var(--accent);z-index:2}
.graphShell{position:relative;border:1px solid var(--line);border-radius:16px;background:radial-gradient(circle at center,#121d30,#090d15 72%);overflow:hidden;min-height:620px}.graphShell canvas{width:100%;height:620px;display:block;touch-action:none}.graphHud{position:absolute;left:12px;top:12px;display:flex;gap:7px;flex-wrap:wrap;pointer-events:none}.graphHud .pill{background:#0b111ddd;backdrop-filter:blur(8px)}.graphTools{position:absolute;right:12px;top:12px;display:flex;gap:6px}.graphTools button{width:34px;height:34px;border-radius:10px;padding:0}.graphHint{position:absolute;left:12px;bottom:10px;color:#8697b1;font-size:11px;background:#080d16c7;padding:5px 8px;border-radius:8px}.legend{display:flex;gap:10px;flex-wrap:wrap;margin:9px 0 0}.legend span{font-size:11px;color:var(--muted)}.sw{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px}
.drop{border:1px dashed #405575;border-radius:14px;padding:25px;text-align:center;background:#0b121e;transition:.15s}.drop.drag{border-color:var(--accent);background:#111d31}.drop input{max-width:100%}.sourceGrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.sourceIcon{width:34px;height:34px;display:grid;place-items:center;border-radius:10px;background:#1a2639;color:var(--accent)}
.goalGrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.goal{border:1px solid var(--line);border-radius:14px;padding:15px;background:#0d1420}.goalHead{display:flex;justify-content:space-between;gap:12px}.goal h3{margin:0 0 4px;font-size:15px}.goal .progress{margin:12px 0 7px}.goalActions{display:flex;gap:6px;flex-wrap:wrap;margin-top:10px}
.researchLive{display:none;margin-top:14px;overflow:hidden}.researchLive.show{display:block}.liveHead{display:flex;justify-content:space-between;gap:14px;align-items:flex-start}.liveTitle{display:flex;gap:10px;align-items:center}.livePulse{width:10px;height:10px;border-radius:50%;background:var(--muted);flex:0 0 auto;margin-top:5px}.livePulse.running{background:var(--accent2);box-shadow:0 0 0 0 rgba(101,214,181,.5);animation:researchPulse 1.6s infinite}.livePulse.warn{background:var(--warn)}.livePulse.error{background:var(--bad)}@keyframes researchPulse{0%{box-shadow:0 0 0 0 rgba(101,214,181,.45)}70%{box-shadow:0 0 0 9px rgba(101,214,181,0)}100%{box-shadow:0 0 0 0 rgba(101,214,181,0)}}
.liveStats{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:8px;margin:14px 0}.liveStat{border:1px solid #24334a;border-radius:11px;background:#0b121e;padding:10px}.liveStat b{font-size:19px;display:block}.liveStat span{font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted)}.queryChips{display:flex;gap:6px;flex-wrap:wrap;margin:8px 0 14px}.queryChip{font-size:11px;padding:6px 8px;border:1px solid #30435f;border-radius:9px;background:#0d1726;color:#c8d7ec;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.eventBoard{display:grid;grid-template-columns:repeat(auto-fit,minmax(245px,1fr));gap:9px}.eventLane{border:1px solid #223149;border-radius:13px;background:#0a101a;min-height:180px;overflow:hidden}.eventLaneHead{display:flex;align-items:center;justify-content:space-between;padding:10px 11px;border-bottom:1px solid #223149;background:#0f1724;position:sticky;top:0;z-index:1}.eventLaneHead b{font-size:12px}.eventLaneBody{padding:7px;display:grid;gap:6px;max-height:360px;overflow:auto}.researchEvent{border:1px solid #25354c;border-radius:9px;background:#0d1521;padding:8px 9px;animation:eventIn .18s ease-out}.researchEvent.running{border-color:#2e6959}.researchEvent.error{border-color:#65303e}.researchEvent.warn,.researchEvent.skipped{border-color:#624e26}.researchEvent .eventTop{display:flex;justify-content:space-between;gap:8px;font-size:10px;color:var(--muted)}.researchEvent b{font-size:11px;display:block;margin:4px 0}.researchEvent p{font-size:11px;line-height:1.35;margin:3px 0;color:#c9d5e6}.researchEvent a{color:var(--accent);font-size:10px;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.researchEvent .preview{border-left:2px solid #304866;padding-left:6px;color:#aebdd1}.emptyLane{color:#687990;font-size:11px;padding:10px}@keyframes eventIn{from{transform:translateY(3px);opacity:.45}to{transform:none;opacity:1}}
.researchHistory{margin-top:12px;display:flex;gap:7px;overflow:auto;padding-bottom:2px}.historyRun{min-width:205px;border:1px solid #26354d;border-radius:10px;background:#0c1420;padding:9px}.historyRun b{font-size:11px}.historyRun .meta{margin-top:5px}
.tableWrap{overflow:auto}table{width:100%;border-collapse:collapse;font-size:12px}th,td{text-align:left;padding:9px;border-bottom:1px solid #223047;vertical-align:top}th{color:var(--muted);font-weight:600}.kv{display:grid;grid-template-columns:130px 1fr;gap:7px 12px;font-size:12px}.kv div:nth-child(odd){color:var(--muted)}
.toast{position:fixed;right:20px;bottom:20px;max-width:440px;padding:12px 14px;border:1px solid #334766;border-radius:12px;background:#111b2b;color:#eef3fb;box-shadow:var(--shadow);z-index:80;display:none}.toast.show{display:block}.toast.bad{border-color:#673241;background:#28131a}
.mobileNav{display:none}
@media(max-width:1100px){.metrics{grid-template-columns:repeat(2,1fr)}.liveStats{grid-template-columns:repeat(3,1fr)}.pipeline{grid-template-columns:repeat(4,1fr)}.pipeline .stage:nth-child(4):after{display:none}.split,.two{grid-template-columns:1fr}.graphShell canvas{height:560px}.graphShell{min-height:560px}}
@media(max-width:760px){.app{display:block}.liveStats{grid-template-columns:repeat(2,1fr)}.eventLaneBody{max-height:280px}.sidebar{display:none}.main{padding:18px 14px 90px}.topbar{align-items:flex-start}.topbar h1{font-size:21px}.topbar .actions .btn.ghost{display:none}.metrics{grid-template-columns:1fr 1fr}.metric .value{font-size:23px}.three,.goalGrid,.sourceGrid{grid-template-columns:1fr}.pipeline{grid-template-columns:1fr 1fr}.pipeline .stage:nth-child(even):after{display:none}.pipeline .stage:nth-child(4):after{display:none}.graphShell,.graphShell canvas{min-height:500px;height:500px}.mobileNav{position:fixed;display:flex;left:8px;right:8px;bottom:8px;z-index:60;background:#0c121ddd;border:1px solid #29384f;border-radius:15px;padding:6px;backdrop-filter:blur(18px);overflow-x:auto}.mobileNav button{flex:1;min-width:64px;border:0;background:transparent;color:var(--muted);padding:8px 6px;border-radius:9px;font-size:10px}.mobileNav button.active{background:#1b2840;color:#fff}.kv{grid-template-columns:100px 1fr}}
@media(max-width:430px){.metrics{grid-template-columns:1fr}.pipeline{grid-template-columns:1fr}.stage:after{display:none!important}.row.stack{flex-direction:column;align-items:stretch}}
.goalPaused{opacity:.78;border-style:dashed}.pill.warn{color:#ffd27d;border-color:rgba(255,210,125,.35);background:rgba(255,210,125,.08)}</style>
</head>
<body>
<div class="app">
<aside class="sidebar">
<div class="brand"><div class="mark"></div><div><b>NeuroForge</b><small>Knowledge OS · v0.8</small></div></div>
<nav class="nav" id="nav">
<button class="active" data-view="dashboard"><span class="ico"></span>Übersicht</button>
<button data-view="knowledge"><span class="ico"></span>Wissensraum</button>
<button data-view="sources"><span class="ico"></span>Quellen & Import</button>
<button data-view="research"><span class="ico"></span>Research</button>
<button data-view="goals"><span class="ico"></span>Ziele & Autonomie</button>
<button data-view="chat"><span class="ico"></span>Chat</button>
<button data-view="models"><span class="ico"></span>Modelle</button>
<button data-view="ops"><span class="ico"></span>Betrieb</button>
</nav>
<div class="sidebarFoot"><div class="statusChip"><span class="dot" id="sideDot"></span><span id="sideStatus">nicht verbunden</span></div></div>
</aside>
<main class="main">
<header class="topbar"><div><h1 id="pageTitle">Übersicht</h1><p id="pageSub">Was das System weiß, lernt und gerade tut.</p></div><div class="actions"><button class="btn ghost" id="refreshBtn">Aktualisieren</button><button class="btn secondary" id="tokenBtn">Admin-Token</button></div></header>
<section class="view active" id="view-dashboard">
<div class="grid metrics">
<div class="metric"><div class="label">Memories</div><div class="value" id="mMem"></div><div class="sub" id="mTypes">Wissensknoten</div></div>
<div class="metric"><div class="label">Synapsen</div><div class="value" id="mSyn"></div><div class="sub">gewichtete Beziehungen</div></div>
<div class="metric"><div class="label">Quellen</div><div class="value" id="mSources"></div><div class="sub">Dokumente · Text · Web</div></div>
<div class="metric"><div class="label">Aktive Ziele</div><div class="value" id="mGoals"></div><div class="sub" id="mNextGoal">autonomer Lernplan</div></div>
</div>
<div class="grid two">
<div class="card"><h2>Lernpfad</h2><div class="pipeline">
<div class="stage"><b>1 · Quelle</b><small>Chat, Text, Dokument, Web</small></div>
<div class="stage"><b>2 · Evidence</b><small>Extrahieren, chunken, deduplizieren</small></div>
<div class="stage"><b>3 · Embedding</b><small>stabiler semantischer Raum</small></div>
<div class="stage"><b>4 · Recall</b><small>HNSW + PQ + Synapsen</small></div>
<div class="stage"><b>5 · Reason</b><small>Actor/Critic mit Evidenz</small></div>
<div class="stage"><b>6 · Learn</b><small>Reward + Provenance + Konflikte</small></div>
<div class="stage"><b>7 · Consolidate</b><small>Episoden → Langzeitwissen</small></div>
</div><p class="muted small">Web-Ergebnisse und Dokumente werden als quellengebundene Evidence-Memories gespeichert. Erst der normale Recall-/Konsolidierungspfad macht daraus stärkeres semantisches Wissen.</p></div>
<div class="card"><h2>Systemzustand</h2><div id="systemState" class="list"></div></div>
</div>
<div class="grid two" style="margin-top:14px"><div class="card"><h2>Letzte Lernereignisse</h2><div class="list scroll" id="dashEvents"></div></div><div class="card"><h2>Aktive Ziele</h2><div class="list" id="dashGoals"></div></div></div>
</section>
<section class="view" id="view-knowledge">
<div class="row stack" style="margin-bottom:12px"><div class="grow"><input id="knowledgeQuery" class="control" placeholder="Wissen semantisch durchsuchen …"></div><button class="btn" id="knowledgeSearchBtn">Suchen</button><button class="btn secondary" id="graphReloadBtn">Graph neu laden</button></div>
<div class="split">
<div><div class="graphShell"><canvas id="knowledgeCanvas"></canvas><div class="graphHud"><span class="pill" id="lodBadge">LOD · Übersicht</span><span class="pill" id="graphCount">0 Knoten</span><span class="pill" id="graphZoom">100%</span></div><div class="graphTools"><button class="btn secondary" id="zoomInBtn">+</button><button class="btn secondary" id="zoomOutBtn"></button><button class="btn secondary" id="fitBtn"></button></div><div class="graphHint">Mausrad/Pinch: Zoom · Ziehen: Pan · Klick: Memory öffnen · LOD blendet Details progressiv ein</div></div><div class="legend"><span><i class="sw" style="background:#7aa7ff"></i>semantic</span><span><i class="sw" style="background:#65d6b5"></i>episodic</span><span><i class="sw" style="background:#f7c36d"></i>procedural</span><span><i class="sw" style="background:#bd8cff"></i>working/evidence</span></div></div>
<div class="card"><h2>Inspektor</h2><div id="knowledgeInspector" class="scroll"><p class="muted">Klicke einen Knoten oder suche nach einem Begriff. Hier siehst du Herkunft, Confidence, Reward, Verknüpfungen und Lernhistorie.</p></div></div>
</div>
<div class="card" style="margin-top:14px"><h2>Recall-Erklärung</h2><div id="knowledgeHits" class="list"></div></div>
</section>
<section class="view" id="view-sources">
<div class="grid two">
<div class="card"><h2>Dokument importieren</h2><div class="drop" id="dropZone"><p><b>Datei hier ablegen</b> oder auswählen</p><p class="muted small">TXT · Markdown · HTML · JSON · CSV/TSV · DOCX · PDF*</p><input id="docFile" type="file" class="control" accept=".txt,.md,.markdown,.html,.htm,.json,.csv,.tsv,.docx,.pdf,text/*,application/json,application/pdf"></div><div class="row stack" style="margin-top:12px"><div class="grow"><label>Titel</label><input id="docTitle" class="control" placeholder="optional"></div><div class="grow"><label>Tags (Komma)</label><input id="docTags" class="control" placeholder="nvidia,cuda"></div><div style="width:110px"><label>Trust 01</label><input id="docTrust" class="control" type="number" min="0" max="1" step="0.05" value="1"></div></div><button class="btn" id="uploadBtn">Dokument lernen</button><p class="muted small">* PDF wird serverseitig über <span class="mono">pdftotext</span> extrahiert; das Release-Containerimage enthält das Tool.</p></div>
<div class="card"><h2>Text einspeisen</h2><div class="field"><label>Titel / Kontext</label><input id="textTitle" class="control" placeholder="z. B. Interne Notiz: NVIDIA Infrastruktur"></div><div class="field"><label>Text</label><textarea id="textInput" class="control" placeholder="Hier Wissen, Notizen, Protokolle oder kopierten Dokumenttext einfügen …"></textarea></div><div class="row stack"><div class="grow"><label>Quelle/URI (optional)</label><input id="textURI" class="control" placeholder="https://… oder interne://…"></div><div style="width:110px"><label>Trust 01</label><input id="textTrust" class="control" type="number" min="0" max="1" step="0.05" value="1"></div></div><button class="btn" id="textLearnBtn">Text lernen</button></div>
</div>
<div class="card" style="margin-top:14px"><div class="itemTitle"><h2>Quellenbibliothek</h2><button class="btn ghost" id="sourcesReloadBtn">Neu laden</button></div><div id="sourcesList" class="sourceGrid"></div></div>
</section>
<section class="view" id="view-research">
<div class="grid two">
<div class="card"><h2>SearXNG Research</h2><div class="row stack"><div class="grow"><label>Suchanfrage</label><input id="researchQuery" class="control" placeholder="z. B. NVIDIA Blackwell Architektur offizielle Dokumentation"></div><div style="width:130px"><label>Max Ergebnisse</label><input id="researchMax" class="control" type="number" min="1" max="50" value="8"></div></div><div class="checks"><label><input type="checkbox" id="researchLearn"> Ergebnisse lernen</label><label><input type="checkbox" id="researchFetch" checked> Seiten abrufen</label></div><div class="row" style="margin-top:12px"><button class="btn" id="researchBtn">Suchen</button><button class="btn secondary" id="researchTestBtn">Verbindung testen</button></div><div id="researchResults" class="list" style="margin-top:14px"></div></div>
<div class="card"><h2>Research-Konfiguration</h2><div class="checks"><label><input type="checkbox" id="rcEnabled"> Research aktiv</label><label><input type="checkbox" id="rcSearx"> SearXNG aktiv</label><label><input type="checkbox" id="rcGoal"> Goal-Research</label></div><div class="field" style="margin-top:12px"><label>SearXNG Base URL</label><input id="rcBase" class="control mono" placeholder="http://searxng:8080"></div><div class="row stack"><div class="grow"><label>Sprache</label><input id="rcLang" class="control" value="de-DE"></div><div class="grow"><label>Kategorien</label><input id="rcCats" class="control" value="general,science,it"></div></div><div class="row stack"><div class="grow"><label>Queries / Goal-Cycle</label><input id="rcQueries" class="control" type="number" min="1" max="20"></div><div class="grow"><label>Seiten / Cycle</label><input id="rcPages" class="control" type="number" min="0" max="50"></div><div class="grow"><label>Goal-Intervall (min)</label><input id="rcInterval" class="control" type="number" min="1"></div></div><div class="field"><label>Authorization-Header (optional, wird nicht zurückgelesen)</label><input id="rcAuth" class="control mono" type="password" placeholder="Bearer …"></div><div class="checks"><label><input type="checkbox" id="rcFetch"> Webseiten nachladen</label><label><input type="checkbox" id="rcAuto"> Autonomie aktiv</label><label><input type="checkbox" id="rcImmediate"> neue Goals sofort einplanen</label></div><button class="btn" id="researchSaveBtn" style="margin-top:12px">Research speichern</button><p class="muted small">Private Ziel-URLs sind für den Web-Fetch standardmäßig blockiert (SSRF-Schutz). Die lokale SearXNG-Adresse ist davon nicht betroffen, weil sie nur als Such-API angesprochen wird. PDF/DOCX/TXT/MD/CSV/JSON-Dateitreffer werden automatisch über die Dokument-Pipeline extrahiert, gechunkt und als quellengebundene Evidence verarbeitet.</p></div>
</div>
</section>
<section class="view" id="view-goals">
<div class="grid two"><div class="card"><h2>Neues Ziel</h2><div class="field"><label>Titel</label><input id="goalTitle" class="control" placeholder="NVIDIA"></div><div class="field"><label>Beschreibung</label><textarea id="goalDesc" class="control" placeholder="Welche Wissenslücke soll das System autonom schließen?"></textarea></div><div class="field"><label>Messbares Ziel</label><input id="goalTarget" class="control" placeholder="z. B. 100 hochwertige, quellengebundene Wissenseinträge"></div><div class="row stack"><div class="grow"><label>Priorität 1100</label><input id="goalPriority" class="control" type="number" min="1" max="100" value="50"></div><div class="grow"><label>Intervall (Minuten)</label><input id="goalInterval" class="control" type="number" min="1" value="10"></div></div><button class="btn" id="goalCreateBtn">Ziel anlegen</button></div><div class="card"><h2>So arbeitet ein Goal</h2><div class="pre">FÄLLIG
SEARCH PLAN (Actor)
SEARXNG → Web Evidence
Chunk + Embed + Dedup
Recall vorhandenes + neues Wissen
Predict → Evaluate → Learn
NextCycleAt / Backoff</div><p class="muted small">Ein neues Ziel wird bei aktivierter Autonomie sofort fällig. Danach besitzt jedes Goal sein eigenes Intervall und Fehler-Backoff.</p></div></div>
<div class="card" style="margin-top:14px"><div class="itemTitle"><h2>Ziele</h2><button class="btn secondary" id="runAutonomyBtn">Alle fälligen jetzt</button></div><div class="goalGrid" id="goalsList"></div></div>
<div class="card researchLive" id="goalResearchLive">
<div class="liveHead"><div class="liveTitle"><span class="livePulse" id="livePulse"></span><div><h2 style="margin:0" id="liveGoalTitle">Live Research</h2><div class="muted small" id="liveRunMeta">Noch kein Research-Lauf ausgewählt.</div></div></div><div class="row"><button class="btn ghost" id="liveRefreshBtn">Aktualisieren</button><button class="btn ghost" id="liveCloseBtn">Schließen</button></div></div>
<div class="liveStats"><div class="liveStat"><b id="lsResults">0</b><span>Treffer</span></div><div class="liveStat"><b id="lsDownloads">0</b><span>Downloads</span></div><div class="liveStat"><b id="lsClaims">0</b><span>Claims</span></div><div class="liveStat"><b id="lsNew">0</b><span>Neu gelernt</span></div><div class="liveStat"><b id="lsDup">0</b><span>Duplikate</span></div><div class="liveStat"><b id="lsConfirm">0</b><span>Bestätigt</span></div></div>
<div><label>Aktuelle / geplante Suchqueries</label><div class="queryChips" id="liveQueries"><span class="muted small"></span></div></div>
<div class="eventBoard">
<div class="eventLane"><div class="eventLaneHead"><b>⌕ Queries</b><span class="pill" id="lcQueries">0</span></div><div class="eventLaneBody" id="laneQueries"></div></div>
<div class="eventLane"><div class="eventLaneHead"><b>↗ Gefundene URLs</b><span class="pill" id="lcResults">0</span></div><div class="eventLaneBody" id="laneResults"></div></div>
<div class="eventLane"><div class="eventLaneHead"><b>⇣ Downloads / Quellen</b><span class="pill" id="lcDownloads">0</span></div><div class="eventLaneBody" id="laneDownloads"></div></div>
<div class="eventLane"><div class="eventLaneHead"><b>◈ Claims / Evidenz</b><span class="pill" id="lcClaims">0</span></div><div class="eventLaneBody" id="laneClaims"></div></div>
<div class="eventLane"><div class="eventLaneHead"><b>✓ Duplikate / Bestätigung</b><span class="pill" id="lcDedupe">0</span></div><div class="eventLaneBody" id="laneDedupe"></div></div>
<div class="eventLane"><div class="eventLaneHead"><b>! Verworfen / Fehler</b><span class="pill" id="lcRejected">0</span></div><div class="eventLaneBody" id="laneRejected"></div></div>
</div>
<h3>Letzte Research-Läufe</h3><div class="researchHistory" id="researchHistory"></div>
</div>
</section>
<section class="view" id="view-chat">
<div class="card"><h2>Chat gegen das lernende System</h2><div class="row stack"><div class="grow"><label>Session</label><input id="chatSession" class="control" value="web-demo"></div><div class="grow"><label>Provider</label><select id="chatProvider" class="control"><option value="">Auto</option><option value="ollama">Ollama</option><option value="openai">OpenAI</option></select></div></div><div class="field"><label>Nachricht</label><textarea id="chatPrompt" class="control" style="min-height:180px" placeholder="Frage stellen …"></textarea></div><div class="row"><button class="btn" id="chatBtn">Senden & lernen</button><button class="btn danger" id="chatAbortBtn" disabled>Abbrechen</button><span class="muted small" id="chatElapsed"></span></div><div id="chatOutput" class="pre" style="margin-top:14px;min-height:80px"></div></div>
</section>
<section class="view" id="view-models">
<div class="grid two"><div class="card"><h2>Modelle & Routing</h2><p class="muted small">Hier siehst du den aktiven Routing-Block. Für feine Änderungen kannst du den JSON-Editor verwenden.</p><div id="modelCards" class="list"></div><button class="btn secondary" id="modelHealthBtn" style="margin-top:12px">Ollama-Verbindungen prüfen</button><div id="modelHealth" class="pre" style="margin-top:10px"></div></div><div class="card"><h2>Routing JSON</h2><textarea id="modelJSON" class="control mono" style="min-height:410px"></textarea><button class="btn" id="modelSaveBtn">Routing speichern</button></div></div>
</section>
<section class="view" id="view-ops">
<div class="grid three"><div class="card"><h2>Index</h2><div id="opsIndex" class="list"></div><div class="row"><button class="btn secondary" id="pqBuildBtn">PQ rebuild</button><button class="btn ghost" id="indexMergeBtn">HNSW merge</button></div></div><div class="card"><h2>Storage</h2><div id="opsStorage" class="list"></div><div class="row"><button class="btn secondary" id="checkpointBtn">Checkpoint</button><button class="btn ghost" id="compactBtn">Kompaktieren</button></div></div><div class="card"><h2>Observability</h2><div id="opsObs" class="list"></div><div class="field"><label>Prometheus</label><div class="pre mono">GET /metrics\nAuthorization: Bearer &lt;METRICS_TOKEN&gt;</div></div></div></div>
<div class="card" style="margin-top:14px"><h2>Gesamtkonfiguration</h2><textarea id="configJSON" class="control mono" style="min-height:420px"></textarea><button class="btn" id="configSaveBtn">Konfiguration speichern</button></div>
</section>
</main>
</div>
<nav class="mobileNav" id="mobileNav"><button class="active" data-view="dashboard">Home</button><button data-view="knowledge">Graph</button><button data-view="sources">Import</button><button data-view="research">Web</button><button data-view="goals">Goals</button><button data-view="chat">Chat</button><button data-view="ops">Ops</button></nav>
<div class="toast" id="toast"></div>
<script>
'use strict';
const $=id=>document.getElementById(id), $$=q=>[...document.querySelectorAll(q)];
let admin=sessionStorage.getItem('neuroforge_admin_token')||'', currentView='dashboard', chatAbort=null, graphData={nodes:[],edges:[]}, graphPos=new Map(), selectedMemory='', graphLayoutReady=false, researchWatch={goalId:'',goalTitle:'',runId:'',after:0,events:[],run:null,timer:null};
const viewMeta={dashboard:['Übersicht','Was das System weiß, lernt und gerade tut.'],knowledge:['Wissensraum','Interaktiver Graph mit Level-of-Detail und echter Recall-Erklärung.'],sources:['Quellen & Import','Dokumente und Text werden zu nachvollziehbarer Evidence.'],research:['Research','SearXNG suchen, Webseiten abrufen und Quellen kontrolliert lernen.'],goals:['Ziele & Autonomie','Per-Goal Scheduler, Research und Fehler-Backoff.'],chat:['Chat','Interaktiv mit Recall, Lernen und Abbruch.'],models:['Modelle & Routing','Actor, Embeddings und Ollama-Nodes.'],ops:['Betrieb','Index, Storage, Observability und Rohkonfiguration.']};
function toast(msg,bad=false){let t=$('toast');t.textContent=msg;t.className='toast show'+(bad?' bad':'');clearTimeout(t._timer);t._timer=setTimeout(()=>t.className='toast',4500)}
function esc(s){return String(s??'').replace(/[&<>"']/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m]))}
function safeHref(raw){try{let u=new URL(String(raw||''),location.origin);return (u.protocol==='http:'||u.protocol==='https:')?u.href:'#'}catch{return '#'}}
function fmtN(n){return Number(n||0).toLocaleString('de-DE')}function fmtBytes(n){n=Number(n||0);if(n<1024)return n+' B';if(n<1048576)return(n/1024).toFixed(1)+' KiB';if(n<1073741824)return(n/1048576).toFixed(1)+' MiB';return(n/1073741824).toFixed(2)+' GiB'}
function normalizeStatus(s){s=s&&typeof s==='object'?s:{};let st=s.stats&&typeof s.stats==='object'?s.stats:{};return {...s,...st}}
function when(d){if(!d)return'';let x=new Date(d),ms=x-Date.now(),a=Math.abs(ms),m=Math.round(a/60000);if(a<60000)return ms>0?'gleich':'gerade';if(m<60)return(ms>0?'in ':'vor ')+m+' min';let h=Math.round(m/60);return(ms>0?'in ':'vor ')+h+' h'}
function validHeaderToken(value){return typeof value==='string'&&/^[\x21-\x7E]+$/.test(value)}
function tokenHeaders(json=true){if(!admin)throw new Error('Admin-Token fehlt.');if(!validHeaderToken(admin))throw new Error('Admin-Token enthält Leerzeichen/Unicode. Bitte echten ASCII-Token neu eingeben.');let h={'X-Admin-Token':admin};if(json)h['Content-Type']='application/json';return h}
async function req(path,opt={}){let o={...opt,headers:{...(opt.headers||{}),...tokenHeaders(opt.json!==false)}};let r=await fetch(path,o),txt=await r.text();let data=null;try{data=txt?JSON.parse(txt):null}catch{data=txt}if(!r.ok)throw new Error((data&&data.error)||data||`HTTP ${r.status}`);return data}
async function setToken(){let v=prompt('Admin-Token',admin||'');if(v===null)return;v=v.trim();if(!v)return;admin=v;sessionStorage.setItem('neuroforge_admin_token',v);await refreshAll()}
function switchView(v){currentView=v;$$('.view').forEach(x=>x.classList.toggle('active',x.id==='view-'+v));$$('[data-view]').forEach(x=>x.classList.toggle('active',x.dataset.view===v));$('pageTitle').textContent=viewMeta[v][0];$('pageSub').textContent=viewMeta[v][1];if(v!=='goals')stopResearchPolling();if(v==='knowledge')loadKnowledgeGraph();if(v==='sources')loadSources();if(v==='research')loadResearchConfig();if(v==='goals'){loadGoals();if(researchWatch.goalId)startResearchPolling()}if(v==='models')loadModels();if(v==='ops')loadOps()}
$$('[data-view]').forEach(b=>b.addEventListener('click',()=>switchView(b.dataset.view)));$('tokenBtn').addEventListener('click',setToken);$('refreshBtn').addEventListener('click',refreshAll);
async function refreshAll(){if(!admin){$('sideStatus').textContent='Admin-Token setzen';return}try{let [raw,k,g,src]=await Promise.all([req('/admin/api/status'),req('/admin/api/knowledge/summary'),req('/api/v1/goals'),req('/api/v1/sources?limit=8')]),s=normalizeStatus(raw);src=Array.isArray(src)?src:[];g=Array.isArray(g)?g:[];$('sideDot').className='dot ok';$('sideStatus').textContent='bereit · rev '+fmtN(s.revision);$('mMem').textContent=fmtN(s.memories);$('mSyn').textContent=fmtN(s.synapses);$('mSources').textContent=fmtN(k?.sources||src.length);let active=g.filter(x=>x.status==='active');$('mGoals').textContent=active.length;$('mNextGoal').textContent=active.length?('nächster '+when(active.map(x=>x.next_cycle_at).filter(Boolean).sort()[0])):'keine aktiven Ziele';$('mTypes').textContent=Object.entries(k?.by_type||{}).map(([a,b])=>a+' '+b).join(' · ');renderSystemState(s);renderEvents(k?.recent_events||[],'dashEvents');renderDashboardGoals(active.slice(0,5));if(currentView==='ops')loadOps()}catch(e){$('sideDot').className='dot bad';$('sideStatus').textContent='Fehler';toast(e.message,true)}}
function renderSystemState(s){let o=s.observability||{},m=s.maintenance||{};$('systemState').innerHTML=[['Index',`${s.index_mode||o.index_mode||''} · HNSW ${fmtN(s.hnsw_nodes)} · PQ ${fmtN(s.disk_pq_items)}`],['Jobs',fmtN(s.pending_jobs)+' ausstehend'],['Cluster',s.cluster?.enabled?(s.cluster.role+' · term '+s.cluster.term):'deaktiviert'],['Letzter Autonomie-Lauf',m.last_autonomy_run?new Date(m.last_autonomy_run).toLocaleString():''],['Letzter Fehler',m.last_error||'keiner']].map(([a,b])=>`<div class="item"><div class="itemTitle"><b>${esc(a)}</b><span class="muted small">${esc(b)}</span></div></div>`).join('')}
function renderEvents(xs,id){xs=Array.isArray(xs)?xs:[];$(id).innerHTML=xs.length?xs.map(e=>`<div class="item"><div class="itemTitle"><b>${esc(e.type)}</b><span class="pill">${esc(e.actor||'system')}</span></div><p>${esc(e.summary||'')}</p><div class="meta"><span>${new Date(e.created_at).toLocaleString()}</span>${e.reason?`<span>${esc(e.reason)}</span>`:''}</div></div>`).join(''):'<p class="muted">Noch keine Lernereignisse.</p>'}
function renderDashboardGoals(xs){$('dashGoals').innerHTML=xs.length?xs.map(g=>`<div class="item"><div class="itemTitle"><b>${esc(g.title)}</b><span class="pill ${g.auto_run?'good':''}">${g.auto_run?'AUTO':'MANUELL'}</span></div><div class="progress" style="margin:9px 0"><span style="width:${Math.round((g.progress||0)*100)}%"></span></div><div class="meta"><span>${Math.round((g.progress||0)*100)}%</span><span>${g.research_enabled?'Research an':'Research aus'}</span><span>${g.next_cycle_at?when(g.next_cycle_at):'nicht geplant'}</span></div></div>`).join(''):'<p class="muted">Keine aktiven Ziele.</p>'}
// ---- Knowledge graph with client-side LOD ----
const canvas=$('knowledgeCanvas'),ctx=canvas.getContext('2d'),cam={x:0,y:0,z:1},pointer={down:false,x:0,y:0,moved:false},typeColors={semantic:'#7aa7ff',episodic:'#65d6b5',procedural:'#f7c36d',working:'#bd8cff'};
function hash(s){let h=2166136261;for(let i=0;i<s.length;i++){h^=s.charCodeAt(i);h=Math.imul(h,16777619)}return h>>>0}function rand01(s){return(hash(s)%100000)/100000}
function layoutGraph(){graphPos.clear();let types=[...new Set(graphData.nodes.map(n=>n.memory_type||'other'))],centers={};types.forEach((t,i)=>{let a=i/types.length*Math.PI*2;centers[t]={x:Math.cos(a)*260,y:Math.sin(a)*210}});for(let n of graphData.nodes){let c=centers[n.memory_type||'other']||{x:0,y:0},a=rand01(n.id)*Math.PI*2,r=40+rand01(n.id+'r')*170;graphPos.set(n.id,{x:c.x+Math.cos(a)*r,y:c.y+Math.sin(a)*r,vx:0,vy:0,n})}for(let it=0;it<110;it++){for(let e of graphData.edges){let a=graphPos.get(e.a),b=graphPos.get(e.b);if(!a||!b)continue;let dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1,target=65+70/(1+Math.abs(Number(e.weight||0))),f=(d-target)*.0035,fx=dx/d*f,fy=dy/d*f;a.vx+=fx;a.vy+=fy;b.vx-=fx;b.vy-=fy}for(let p of graphPos.values()){let c=centers[p.n.memory_type||'other']||{x:0,y:0};p.vx+=(c.x-p.x)*.0007;p.vy+=(c.y-p.y)*.0007;p.vx*=.86;p.vy*=.86;p.x+=p.vx;p.y+=p.vy}}graphLayoutReady=true;fitGraph();drawGraph()}
function resizeCanvas(){let r=canvas.getBoundingClientRect(),dpr=devicePixelRatio||1;canvas.width=Math.max(1,Math.floor(r.width*dpr));canvas.height=Math.max(1,Math.floor(r.height*dpr));ctx.setTransform(dpr,0,0,dpr,0,0);drawGraph()}new ResizeObserver(resizeCanvas).observe(canvas);
function worldToScreen(p){let r=canvas.getBoundingClientRect();return{x:r.width/2+cam.x+p.x*cam.z,y:r.height/2+cam.y+p.y*cam.z}}function screenToWorld(x,y){let r=canvas.getBoundingClientRect();return{x:(x-r.width/2-cam.x)/cam.z,y:(y-r.height/2-cam.y)/cam.z}}
function lod(){return cam.z<.62?0:cam.z<1.45?1:2}function drawGraph(){let r=canvas.getBoundingClientRect();ctx.clearRect(0,0,r.width,r.height);let L=lod();$('lodBadge').textContent='LOD · '+(['Übersicht','Netz','Detail'][L]);$('graphZoom').textContent=Math.round(cam.z*100)+'%';if(!graphLayoutReady)return;if(L===0){drawClusters();return}let edges=graphData.edges.slice(0,L===1?Math.min(graphData.edges.length,graphData.nodes.length*2):graphData.edges.length);for(let e of edges){let a=graphPos.get(e.a),b=graphPos.get(e.b);if(!a||!b)continue;let A=worldToScreen(a),B=worldToScreen(b),w=Math.abs(Number(e.weight||0));ctx.strokeStyle=Number(e.weight||0)<0?'rgba(255,124,141,.28)':'rgba(117,153,207,'+(L===2?.32:.18)+')';ctx.lineWidth=Math.min(4,.45+w*.8);ctx.beginPath();ctx.moveTo(A.x,A.y);ctx.lineTo(B.x,B.y);ctx.stroke()}for(let p of graphPos.values()){let s=worldToScreen(p),sel=p.n.id===selectedMemory,rr=(sel?8:5)*(L===2?1.15:1);ctx.fillStyle=typeColors[p.n.memory_type]||'#bd8cff';ctx.beginPath();ctx.arc(s.x,s.y,rr,0,Math.PI*2);ctx.fill();if(sel){ctx.strokeStyle='#fff';ctx.lineWidth=2;ctx.stroke()}let important=(p.n.salience||0)>1.15||(p.n.access_count||0)>4;if(sel||(L===2&&important)){ctx.fillStyle='#dce7f7';ctx.font='11px system-ui';let txt=(p.n.truth_key||p.n.text||p.n.kind||p.n.id).slice(0,34);ctx.fillText(txt,s.x+10,s.y+4)}}}
function drawClusters(){let groups={};for(let p of graphPos.values()){let t=p.n.memory_type||'other';(groups[t]??=[]).push(p)}for(let[t,xs]of Object.entries(groups)){let x=xs.reduce((a,p)=>a+p.x,0)/xs.length,y=xs.reduce((a,p)=>a+p.y,0)/xs.length,s=worldToScreen({x,y}),rad=Math.min(58,20+Math.sqrt(xs.length)*6);ctx.fillStyle=(typeColors[t]||'#bd8cff')+'44';ctx.strokeStyle=typeColors[t]||'#bd8cff';ctx.lineWidth=2;ctx.beginPath();ctx.arc(s.x,s.y,rad,0,Math.PI*2);ctx.fill();ctx.stroke();ctx.fillStyle='#eef3fb';ctx.font='600 13px system-ui';ctx.textAlign='center';ctx.fillText(t,s.x,s.y-2);ctx.fillStyle='#a9b7cb';ctx.font='11px system-ui';ctx.fillText(xs.length+' nodes',s.x,s.y+15)}ctx.textAlign='left'}
function fitGraph(){if(!graphPos.size)return;let xs=[...graphPos.values()].map(p=>p.x),ys=[...graphPos.values()].map(p=>p.y),w=Math.max(...xs)-Math.min(...xs)+140,h=Math.max(...ys)-Math.min(...ys)+140,r=canvas.getBoundingClientRect();cam.z=Math.max(.25,Math.min(1.15,Math.min(r.width/w,r.height/h)));cam.x=cam.y=0;drawGraph()}
canvas.addEventListener('wheel',e=>{e.preventDefault();let before=screenToWorld(e.offsetX,e.offsetY),factor=Math.exp(-e.deltaY*.0012);cam.z=Math.max(.2,Math.min(5,cam.z*factor));let after=worldToScreen(before);cam.x+=e.offsetX-after.x;cam.y+=e.offsetY-after.y;drawGraph()},{passive:false});canvas.addEventListener('pointerdown',e=>{pointer.down=true;pointer.x=e.clientX;pointer.y=e.clientY;pointer.moved=false;canvas.setPointerCapture(e.pointerId)});canvas.addEventListener('pointermove',e=>{if(!pointer.down)return;let dx=e.clientX-pointer.x,dy=e.clientY-pointer.y;if(Math.hypot(dx,dy)>2)pointer.moved=true;cam.x+=dx;cam.y+=dy;pointer.x=e.clientX;pointer.y=e.clientY;drawGraph()});canvas.addEventListener('pointerup',e=>{pointer.down=false;if(pointer.moved)return;let rect=canvas.getBoundingClientRect(),x=e.clientX-rect.left,y=e.clientY-rect.top;if(lod()===0){cam.z=.8;drawGraph();return}let best=null,bd=15;for(let p of graphPos.values()){let s=worldToScreen(p),d=Math.hypot(s.x-x,s.y-y);if(d<bd){best=p;bd=d}}if(best)openMemory(best.n.id)});$('zoomInBtn').addEventListener('click',()=>{cam.z=Math.min(5,cam.z*1.3);drawGraph()});$('zoomOutBtn').addEventListener('click',()=>{cam.z=Math.max(.2,cam.z/1.3);drawGraph()});$('fitBtn').addEventListener('click',fitGraph);
async function loadKnowledgeGraph(center=''){try{let d=await req('/admin/api/knowledge/graph?depth=3&max_nodes=600'+(center?'&center='+encodeURIComponent(center):''));graphData={...(d||{}),nodes:Array.isArray(d?.nodes)?d.nodes:[],edges:Array.isArray(d?.edges)?d.edges:[]};let tn=Number(d?.total_memories??graphData.nodes.length),te=Number(d?.total_synapses??graphData.edges.length),ns=fmtN(graphData.nodes.length)+(tn>graphData.nodes.length?' / '+fmtN(tn):''),es=fmtN(graphData.edges.length)+(te>graphData.edges.length?' / '+fmtN(te):'');$('graphCount').textContent=ns+' Knoten · '+es+' Kanten';if(tn>0&&graphData.nodes.length===0)toast('Memory-Katalog enthält '+fmtN(tn)+' Einträge, aber der Graph konnte keine kanonischen IDs auflösen.',true);layoutGraph()}catch(e){graphData={nodes:[],edges:[]};graphLayoutReady=false;$('graphCount').textContent='0 Knoten · 0 Kanten';drawGraph();toast(e.message,true)}}$('graphReloadBtn').addEventListener('click',()=>loadKnowledgeGraph(selectedMemory));
$('knowledgeSearchBtn').addEventListener('click',searchKnowledge);$('knowledgeQuery').addEventListener('keydown',e=>{if(e.key==='Enter')searchKnowledge()});async function searchKnowledge(){let q=$('knowledgeQuery').value.trim();if(!q)return;try{let d=await req('/admin/api/knowledge/search',{method:'POST',body:JSON.stringify({text:q,k:16})});$('knowledgeHits').innerHTML=(d.hits||[]).map(h=>`<div class="item click" data-mid="${esc(h.memory.id)}"><div class="itemTitle"><b>${esc(h.memory.text.slice(0,120))}</b><span class="pill">${Number(h.score||0).toFixed(3)}</span></div><div class="meta"><span>${esc(h.candidate_source||'')}</span><span>sim ${Number(h.similarity||0).toFixed(3)}</span><span>base ${Number(h.base_score||0).toFixed(3)}</span><span>graph +${Number(h.graph_boost||0).toFixed(3)}</span></div></div>`).join('')||'<p class="muted">Keine Treffer.</p>';$$('#knowledgeHits [data-mid]').forEach(x=>x.addEventListener('click',()=>openMemory(x.dataset.mid)));if(d.hits?.[0]){selectedMemory=d.hits[0].memory.id;await loadKnowledgeGraph(selectedMemory);await openMemory(selectedMemory,false)}}catch(e){toast(e.message,true)}}
async function openMemory(id,recenter=true){selectedMemory=id;try{let d=await req('/admin/api/knowledge/memory/'+encodeURIComponent(id)),m=d.memory||{},p=m.provenance||{};$('knowledgeInspector').innerHTML=`<div class="meta"><span class="pill">${esc(m.memory_type)}</span><span class="pill">${esc(m.kind)}</span><span class="pill ${m.status==='active'?'good':m.status==='conflicted'?'warn':''}">${esc(m.status||'active')}</span></div><h3>${esc(m.truth_key||p.source_title||m.id)}</h3><div style="white-space:pre-wrap;line-height:1.5;font-size:13px">${esc(m.text)}</div><h3>Qualität</h3><div class="kv"><div>Confidence</div><div>${Number(m.confidence||0).toFixed(3)}</div><div>Salience</div><div>${Number(m.salience||0).toFixed(3)}</div><div>Reward</div><div>${Number(m.reward||0).toFixed(3)}</div><div>Evidenzquellen</div><div>${fmtN(m.evidence_count||m.evidence_source_ids?.length||0)}</div><div>Aufrufe</div><div>${fmtN(m.access_count)}</div></div><h3>Provenance</h3><div class="kv"><div>Quelle</div><div>${esc(p.source||'legacy/unknown')}</div><div>Source-ID</div><div class="mono">${esc(p.source_id||'')}</div><div>Titel</div><div>${esc(p.source_title||'')}</div><div>URI</div><div>${p.source_uri?`<a href="${esc(safeHref(p.source_uri))}" target="_blank" rel="noopener" style="color:var(--accent)">${esc(p.source_uri)}</a>`:''}</div><div>Chunk</div><div>${p.chunk_index||''} / ${p.chunk_count||''}</div><div>Embedding</div><div>${esc([p.embedding_model,p.embedding_node_id].filter(Boolean).join(' · ')||'')}</div><div>Generierung</div><div>${esc([p.generation_model,p.generation_node_id].filter(Boolean).join(' · ')||'')}</div></div><h3>Stärkste Synapsen</h3>${(d.edges||[]).slice(0,12).map(e=>`<div class="item click" data-mid="${esc(e.a===id?e.b:e.a)}"><div class="itemTitle"><span class="mono small">${esc(e.a===id?e.b:e.a)}</span><span class="pill">w ${Number(e.weight).toFixed(2)}</span></div></div>`).join('')||'<p class="muted">Keine Synapsen.</p>'}<h3>Timeline</h3>${(d.events||[]).map(e=>`<div class="item"><b>${esc(e.type)}</b><p>${esc(e.summary)}</p><div class="meta">${new Date(e.created_at).toLocaleString()}</div></div>`).join('')||'<p class="muted">Keine Events.</p>'}`;$$('#knowledgeInspector [data-mid]').forEach(x=>x.addEventListener('click',()=>openMemory(x.dataset.mid)));drawGraph();if(recenter)loadKnowledgeGraph(id)}catch(e){toast(e.message,true)}}
// ---- Sources & ingestion ----
async function loadSources(){try{let xs=await req('/api/v1/sources?limit=100');xs=Array.isArray(xs)?xs:[];$('sourcesList').innerHTML=xs.length?xs.map(s=>`<div class="item"><div class="itemTitle"><div class="row"><span class="sourceIcon">${s.type==='document'?'▤':s.type==='web'?'⌁':'T'}</span><div><b>${esc(s.title)}</b><div class="meta"><span>${esc(s.type)}</span><span>${s.chunk_count||0} chunks</span><span>${fmtBytes(s.bytes)}</span></div></div></div><span class="pill ${s.status==='ready'?'good':s.status==='error'?'bad':'warn'}">${esc(s.status)}</span></div>${s.uri?`<p class="mono small">${esc(s.uri)}</p>`:''}${s.error?`<p class="error small">${esc(s.error)}</p>`:''}</div>`).join(''):'<p class="muted">Noch keine externen Quellen.</p>';$('mSources').textContent=fmtN(xs.length)}catch(e){toast(e.message,true)}}$('sourcesReloadBtn').addEventListener('click',loadSources);
const dz=$('dropZone'),fi=$('docFile');['dragenter','dragover'].forEach(n=>dz.addEventListener(n,e=>{e.preventDefault();dz.classList.add('drag')}));['dragleave','drop'].forEach(n=>dz.addEventListener(n,e=>{e.preventDefault();dz.classList.remove('drag')}));dz.addEventListener('drop',e=>{if(e.dataTransfer.files.length)fi.files=e.dataTransfer.files});$('uploadBtn').addEventListener('click',async()=>{let f=fi.files[0];if(!f)return toast('Bitte eine Datei auswählen.',true);let fd=new FormData();fd.append('file',f);fd.append('title',$('docTitle').value);fd.append('tags',$('docTags').value);fd.append('trust',$('docTrust').value);try{let r=await fetch('/api/v1/ingest/document',{method:'POST',headers:tokenHeaders(false),body:fd}),txt=await r.text(),d;try{d=JSON.parse(txt)}catch{d=txt}if(!r.ok)throw new Error(d.error||d);toast(`Dokument gelernt: ${d.chunks} Chunks, ${d.duplicates} Duplikate`);fi.value='';loadSources();refreshAll()}catch(e){toast(e.message,true)}});
$('textLearnBtn').addEventListener('click',async()=>{try{let d=await req('/api/v1/ingest/text',{method:'POST',body:JSON.stringify({title:$('textTitle').value||'Manual text',text:$('textInput').value,source_uri:$('textURI').value,trust:Number($('textTrust').value||1),source_type:'text',memory_type:'semantic'})});toast(`Text gelernt: ${d.chunks} Chunks`);$('textInput').value='';loadSources();refreshAll()}catch(e){toast(e.message,true)}});
// ---- Research ----
async function loadResearchConfig(){try{let d=await req('/admin/api/research'),r=d.research||{},s=r.searxng||{},w=r.web_fetch||{},g=r.goal||{},a=d.autonomy||{};$('rcEnabled').checked=!!r.enabled;$('rcSearx').checked=!!s.enabled;$('rcGoal').checked=!!g.enabled;$('rcBase').value=s.base_url||'';$('rcLang').value=s.language||'de-DE';$('rcCats').value=s.categories||'general';$('rcQueries').value=g.max_queries_per_cycle||2;$('rcPages').value=g.max_pages_per_cycle??4;$('rcFetch').checked=!!w.enabled;$('rcAuto').checked=!!a.enabled;$('rcImmediate').checked=!!a.run_on_goal_create;$('rcInterval').value=a.default_goal_interval_minutes||10;$('rcAuth').placeholder=d.searxng_auth_configured?'konfiguriert leer lassen zum Behalten':'Bearer …'}catch(e){toast(e.message,true)}}
$('researchSaveBtn').addEventListener('click',async()=>{try{let current=await req('/admin/api/research'),r=current.research||{};r.enabled=$('rcEnabled').checked;r.searxng={...(r.searxng||{}),enabled:$('rcSearx').checked,base_url:$('rcBase').value.trim(),language:$('rcLang').value.trim(),categories:$('rcCats').value.trim()};r.web_fetch={...(r.web_fetch||{}),enabled:$('rcFetch').checked};r.goal={...(r.goal||{}),enabled:$('rcGoal').checked,max_queries_per_cycle:Number($('rcQueries').value||2),max_pages_per_cycle:Number($('rcPages').value||0)};let body={research:r,ingestion:current.ingestion,autonomy:{enabled:$('rcAuto').checked,run_on_goal_create:$('rcImmediate').checked,default_goal_interval_minutes:Number($('rcInterval').value||10)}};if($('rcAuth').value)body.searxng_auth_header=$('rcAuth').value;await req('/admin/api/research',{method:'PUT',body:JSON.stringify(body)});$('rcAuth').value='';toast('Research-Konfiguration gespeichert.')}catch(e){toast(e.message,true)}});
$('researchBtn').addEventListener('click',()=>runResearch(false));$('researchTestBtn').addEventListener('click',()=>runResearch(true));
function researchKind(x){let file=!!(x.filename||x.mimetype||String(x.template||'').toLowerCase().includes('file')||/\.(pdf|docx|txt|md|markdown|csv|tsv|json|ya?ml)(?:$|[?#])/i.test(x.url||''));return file?'DOKUMENT':'WEB'}
async function runResearch(test){let q=$('researchQuery').value.trim()||(test?'NVIDIA CUDA':'');if(!q)return toast('Suchanfrage fehlt.',true);$('researchResults').innerHTML='<p class="muted">SearXNG sucht …</p>';try{let d=test?await req('/admin/api/research/test',{method:'POST',body:JSON.stringify({query:q})}):await req('/api/v1/research',{method:'POST',body:JSON.stringify({query:q,learn:$('researchLearn').checked,fetch_pages:$('researchFetch').checked,max_results:Number($('researchMax').value||8)})});$('researchResults').innerHTML=(d.results||[]).map(x=>`<div class="item"><div class="itemTitle"><b>${esc(x.title||x.filename||x.url)}</b><span class="pill ${researchKind(x)==='DOKUMENT'?'good':''}">${researchKind(x)}</span></div><p>${esc(x.content||x.abstract||'')}</p><div class="meta"><span>${esc(x.mimetype||x.engine||((x.engines||[])[0]||'web'))}</span>${x.filename?`<span>${esc(x.filename)}</span>`:''}${x.size?`<span>${esc(x.size)}</span>`:''}<a href="${esc(safeHref(x.url))}" target="_blank" rel="noopener" style="color:var(--accent)">${esc(x.url)}</a></div></div>`).join('')||'<p class="muted">Keine Ergebnisse.</p>';if(d.ingested)toast(`${d.ingested} Evidence-Memories gelernt${d.documents_ingested?` · ${d.documents_ingested} Dokument(e) verarbeitet`:''}.`);if(d.errors?.length)toast(d.errors[0],true);if(d.ingested)loadSources()}catch(e){$('researchResults').innerHTML='<p class="error">'+esc(e.message)+'</p>';toast(e.message,true)}}
// ---- Goals + live research ----
function stopResearchPolling(){if(researchWatch.timer){clearInterval(researchWatch.timer);researchWatch.timer=null}}
function startResearchPolling(){stopResearchPolling();if(!researchWatch.goalId||currentView!=='goals')return;pollGoalResearch();researchWatch.timer=setInterval(pollGoalResearch,1200)}
function researchLane(ev){
if(['plan.started','plan.completed','plan.fallback','query.planned','search.started','search.completed'].includes(ev.type))return'queries';
if(ev.type==='search.result')return'results';
if(['download.started','download.completed','source.processing','source.ingested','source.duplicate'].includes(ev.type))return'downloads';
if(['claim.extracted','evidence.learned'].includes(ev.type))return'claims';
if(['evidence.duplicate','evidence.corroborated'].includes(ev.type))return'dedupe';
if(ev.status==='error'||ev.status==='skipped'||['source.rejected','evidence.skipped','evidence.error','search.error'].includes(ev.type))return'rejected';
return'queries';
}
function eventLabel(ev){return ({'query.planned':'Query','search.started':'SearXNG','search.completed':'SearXNG','search.result':'Treffer','download.started':'Download','download.completed':'Geladen','source.processing':'Extraktion','source.ingested':'Quelle','source.duplicate':'Quelle bekannt','claim.extracted':'Claim-Kandidat','evidence.learned':'Neu gelernt','evidence.duplicate':'Duplikat','evidence.corroborated':'Bestätigt','source.rejected':'Verworfen','evidence.skipped':'Verworfen','evidence.error':'Fehler','plan.fallback':'Fallback','run.started':'Start','run.finished':'Ende'}[ev.type]||ev.type)}
function liveEventCard(ev,terminals){let status=ev.status||'ok';if(ev.type==='download.started'&&terminals.has(ev.url))status='ok';let link=ev.url?`<a href="${esc(safeHref(ev.url))}" target="_blank" rel="noopener">${esc(ev.url)}</a>`:'';let preview=ev.preview?`<p class="preview">${esc(ev.preview)}</p>`:'';let score=ev.similarity?` · sim ${Number(ev.similarity).toFixed(3)}`:ev.score?` · score ${Number(ev.score).toFixed(3)}`:'';return `<div class="researchEvent ${esc(status)}"><div class="eventTop"><span>${esc(eventLabel(ev))}${score}</span><span>${new Date(ev.created_at).toLocaleTimeString()}</span></div><b>${esc(ev.title||ev.query||ev.message||'Event')}</b>${ev.message&&ev.message!==(ev.title||ev.query)?`<p>${esc(ev.message)}</p>`:''}${preview}${link}</div>`}
function renderResearchLive(){let run=researchWatch.run,events=researchWatch.events;if(!run){$('liveRunMeta').textContent='Für dieses Ziel existiert noch kein Research-Lauf.';$('livePulse').className='livePulse';return}
let st=run.stats||{},running=run.status==='running';$('livePulse').className='livePulse '+(running?'running':run.status==='completed_with_errors'?'warn':run.status==='failed'||run.status==='cancelled'?'error':'');let elapsed=run.started_at?Math.max(0,Math.round(((run.completed_at?new Date(run.completed_at):new Date())-new Date(run.started_at))/1000)):0;$('liveRunMeta').textContent=`${run.status} · Run ${run.id} · ${elapsed}s · letztes Event #${run.last_seq||0}${run.last_error?' · '+run.last_error:''}`;
$('lsResults').textContent=fmtN(st.results);$('lsDownloads').textContent=fmtN(st.downloads_completed);$('lsClaims').textContent=fmtN(st.claims);$('lsNew').textContent=fmtN(st.new_evidence);$('lsDup').textContent=fmtN(st.duplicates);$('lsConfirm').textContent=fmtN(st.corroborations);$('liveQueries').innerHTML=(run.queries||[]).map(q=>`<span class="queryChip" title="${esc(q)}">${esc(q)}</span>`).join('')||'<span class="muted small">Noch keine Query geplant.</span>';
let terminals=new Set(events.filter(e=>['download.completed','source.rejected'].includes(e.type)&&e.url).map(e=>e.url));let groups={queries:[],results:[],downloads:[],claims:[],dedupe:[],rejected:[]};for(let ev of events){let lane=researchLane(ev);(groups[lane]||groups.queries).push(ev)}
let ids={queries:'laneQueries',results:'laneResults',downloads:'laneDownloads',claims:'laneClaims',dedupe:'laneDedupe',rejected:'laneRejected'},counts={queries:'lcQueries',results:'lcResults',downloads:'lcDownloads',claims:'lcClaims',dedupe:'lcDedupe',rejected:'lcRejected'};for(let k of Object.keys(ids)){let xs=groups[k].slice(-40).reverse();$(counts[k]).textContent=fmtN(groups[k].length);$(ids[k]).innerHTML=xs.length?xs.map(e=>liveEventCard(e,terminals)).join(''):'<div class="emptyLane">Noch keine Events.</div>'}
}
async function loadResearchHistory(){if(!researchWatch.goalId)return;try{let xs=await req('/api/v1/goals/'+encodeURIComponent(researchWatch.goalId)+'/research/history?limit=8');$('researchHistory').innerHTML=xs.length?xs.map(r=>`<div class="historyRun"><div class="itemTitle"><b>${new Date(r.started_at).toLocaleString()}</b><span class="pill ${r.status==='completed'?'good':r.status==='running'?'warn':r.status==='failed'?'bad':''}">${esc(r.status)}</span></div><div class="meta"><span>${r.stats?.results||0} Treffer</span><span>${r.stats?.new_evidence||0} neu</span><span>${r.stats?.corroborations||0} bestätigt</span></div></div>`).join(''):'<span class="muted small">Noch keine Research-Läufe.</span>'}catch(e){console.warn(e)}}
async function pollGoalResearch(){if(!researchWatch.goalId||currentView!=='goals')return;try{let q=`?after=${researchWatch.after}`+(researchWatch.runId?`&run_id=${encodeURIComponent(researchWatch.runId)}`:'');let d=await req('/api/v1/goals/'+encodeURIComponent(researchWatch.goalId)+'/research/live'+q);if(!d.run){researchWatch.run=null;researchWatch.events=[];researchWatch.after=0;renderResearchLive();return}if(d.reset||researchWatch.runId!==d.run.id){researchWatch.runId=d.run.id;researchWatch.after=0;researchWatch.events=[]}researchWatch.run=d.run;if(d.events?.length){researchWatch.events.push(...d.events);if(researchWatch.events.length>500)researchWatch.events=researchWatch.events.slice(-500);researchWatch.after=Math.max(researchWatch.after,...d.events.map(e=>Number(e.seq||0)))}renderResearchLive();if(d.run.status!=='running')loadResearchHistory()}catch(e){$('liveRunMeta').textContent='Live-Research konnte nicht geladen werden: '+e.message;$('livePulse').className='livePulse error'}}
function openGoalResearch(id,title){researchWatch.goalId=id;researchWatch.goalTitle=title||id;researchWatch.runId='';researchWatch.after=0;researchWatch.events=[];researchWatch.run=null;$('goalResearchLive').classList.add('show');$('liveGoalTitle').textContent='Live Research · '+researchWatch.goalTitle;renderResearchLive();loadResearchHistory();startResearchPolling();setTimeout(()=>$('goalResearchLive').scrollIntoView({behavior:'smooth',block:'start'}),20)}
$('liveCloseBtn').addEventListener('click',()=>{$('goalResearchLive').classList.remove('show');stopResearchPolling()});$('liveRefreshBtn').addEventListener('click',()=>{researchWatch.after=0;researchWatch.events=[];researchWatch.runId='';pollGoalResearch();loadResearchHistory()});
async function loadGoals(){try{let xs=await req('/api/v1/goals');xs=Array.isArray(xs)?xs:[];$('goalsList').innerHTML=xs.length?xs.map(g=>{let active=g.status==='active',paused=g.status==='paused';return `<div class="goal ${paused?'goalPaused':''}"><div class="goalHead"><div><h3>${esc(g.title)}</h3><div class="meta"><span>Priorität ${g.priority}</span><span>${g.research_enabled?'⌕ Research':'ohne Research'}</span><span>${g.auto_run?'AUTO':'MANUELL'}</span></div></div><span class="pill ${active?'good':paused?'warn':''}">${paused?'PAUSIERT':esc(g.status)}</span></div><p class="muted small">${esc(g.description||'')}</p><div class="progress"><span style="width:${Math.round((g.progress||0)*100)}%"></span></div><div class="meta"><span>${Math.round((g.progress||0)*100)}%</span><span>${esc(g.progress_reason||'Fortschritt noch nicht messbar')}</span><span>Intervall ${g.interval_minutes||''} min</span><span>${paused?'Scheduler angehalten':('Nächster: '+(g.next_cycle_at?when(g.next_cycle_at):''))}</span></div>${g.research_enabled?`<div class="meta"><span>${g.research_evidence||0} Evidenzen</span><span>${g.research_sources||0} Quellen</span><span>${g.research_corroborations||0} bestätigt</span>${g.last_staging_draft_id?`<span class="pill good">Staging ${esc(g.last_staging_draft_id)}</span>`:''}</div>`:''}${g.next_action?`<p><b>Nächste Aktion:</b> ${esc(g.next_action)}</p>`:''}${g.last_staging_error?`<p class="error small"><b>Staging:</b> ${esc(g.last_staging_error)}</p>`:''}${g.last_error?`<p class="error small">${esc(g.last_error)}</p>`:''}<div class="goalActions">${active?`<button class="btn secondary" data-goal-action="cycle" data-goal-id="${esc(g.id)}" data-goal-title="${esc(g.title)}">Jetzt lernen</button><button class="btn ghost" data-goal-action="pause" data-goal-id="${esc(g.id)}">Pausieren</button>`:''}${paused?`<button class="btn secondary" data-goal-action="resume" data-goal-id="${esc(g.id)}">Fortsetzen</button>`:''}${g.research_enabled?`<button class="btn ghost" data-research-live="${esc(g.id)}" data-goal-title="${esc(g.title)}">Live Research</button>`:''}<button class="btn ghost" data-focus="${esc(g.id)}">Graph</button><button class="btn danger" data-goal-action="delete" data-goal-id="${esc(g.id)}" data-goal-title="${esc(g.title)}">Löschen</button></div></div>`}).join(''):'<p class="muted">Noch keine Ziele.</p>';$$('[data-goal-action]').forEach(b=>b.addEventListener('click',()=>goalAction(b)));$$('[data-research-live]').forEach(b=>b.addEventListener('click',()=>openGoalResearch(b.dataset.researchLive,b.dataset.goalTitle)));$$('[data-focus]').forEach(b=>b.addEventListener('click',()=>{switchView('knowledge');$('knowledgeQuery').value='goal:'+b.dataset.focus;searchKnowledge()}))}catch(e){toast(e.message,true)}}
async function goalAction(b){let id=b.dataset.goalId,action=b.dataset.goalAction;if(action==='delete'&&!confirm(`Ziel „${b.dataset.goalTitle||id}“ wirklich löschen? Bereits gelerntes Wissen bleibt erhalten.`))return;if(action==='cycle')openGoalResearch(id,b.dataset.goalTitle||id);let old=b.textContent;try{b.disabled=true;b.textContent=action==='cycle'?'läuft …':action==='delete'?'lösche …':'speichere …';let path='/api/v1/goals/'+encodeURIComponent(id)+(action==='cycle'?'/cycle':action==='pause'?'/pause':action==='resume'?'/resume':'');let d=await req(path,{method:action==='delete'?'DELETE':'POST',body:action==='delete'?undefined:'{}'});if(action==='cycle')toast(`Zyklus fertig · Evaluation ${Number(d.evaluation).toFixed(2)} · ${d.sources_ingested||0} Quellen`);else if(action==='pause')toast('Ziel pausiert. Der Scheduler verarbeitet es nicht weiter.');else if(action==='resume')toast('Ziel wieder aktiv. Nächster Lauf wurde eingeplant.');else{toast('Ziel gelöscht. Bereits gelerntes Wissen bleibt erhalten.');if(researchWatch.goalId===id){$('goalResearchLive').classList.remove('show');stopResearchPolling();researchWatch.goalId=''}}await loadGoals();refreshAll()}catch(e){toast(e.message,true)}finally{b.disabled=false;b.textContent=old}}
$('goalCreateBtn').addEventListener('click',async()=>{let title=$('goalTitle').value.trim();if(!title)return toast('Titel fehlt.',true);let b=$('goalCreateBtn'),old=b.textContent;try{b.disabled=true;b.textContent='lege an …';let g=await req('/api/v1/goals',{method:'POST',body:JSON.stringify({title,description:$('goalDesc').value,target:$('goalTarget').value,priority:Number($('goalPriority').value||50),interval_minutes:Number($('goalInterval').value||10),status:'active'})});toast(`Ziel ${g.title} angelegt erster Lauf ist eingeplant.`);$('goalTitle').value='';$('goalDesc').value='';await loadGoals();refreshAll()}catch(e){toast(e.message,true)}finally{b.disabled=false;b.textContent=old}});$('runAutonomyBtn').addEventListener('click',async()=>{try{let d=await req('/admin/api/autonomy',{method:'POST',body:'{}'});toast(`${d.cycles?.length||0} fällige Zyklen ausgeführt.`);loadGoals()}catch(e){toast(e.message,true)}});
// ---- Chat ----
$('chatBtn').addEventListener('click',async()=>{let input=document.getElementById('chatPrompt').value.trim();if(!input)return toast('Nachricht fehlt.',true);chatAbort=new AbortController();$('chatAbortBtn').disabled=false;$('chatBtn').disabled=true;let start=Date.now(),timer=setInterval(()=>$('chatElapsed').textContent=Math.round((Date.now()-start)/1000)+' s',1000);$('chatOutput').textContent='Ollama / NeuroForge arbeitet …';try{let r=await fetch('/api/v1/chat',{method:'POST',headers:tokenHeaders(),signal:chatAbort.signal,body:JSON.stringify({session_id:$('chatSession').value,input,provider:$('chatProvider').value})}),txt=await r.text(),d;try{d=JSON.parse(txt)}catch{d=txt}if(!r.ok)throw new Error(d.error||d);$('chatOutput').textContent=(d.answer||'')+'\n\n— '+(d.provider||'')+' · '+(d.model||'')+' · Recall '+(d.recalled?.length||0)+' · Reward '+Number(d.auto_reward||0).toFixed(3);refreshAll()}catch(e){$('chatOutput').textContent=e.name==='AbortError'?'Abgebrochen.':e.message}finally{clearInterval(timer);$('chatElapsed').textContent='';$('chatAbortBtn').disabled=true;$('chatBtn').disabled=false;chatAbort=null}});$('chatAbortBtn').addEventListener('click',()=>chatAbort?.abort());
// ---- Models ----
async function loadModels(){try{let d=await req('/admin/api/model-routing');$('modelJSON').value=JSON.stringify(d,null,2);let nodes=d.ollama||[];$('modelCards').innerHTML=nodes.map(n=>`<div class="item"><div class="itemTitle"><b>${esc(n.name||n.id)}</b><span class="pill ${n.enabled?'good':''}">${n.enabled?'aktiv':'aus'}</span></div><p class="mono small">${esc(n.base_url)}</p><div class="meta"><span>Chat: ${esc(n.chat_model)}</span><span>Embed: ${esc(n.embedding_model)}</span><span>ctx ${n.num_ctx||'default'}</span><span>think ${esc(n.think||'off')}</span></div></div>`).join('')}catch(e){toast(e.message,true)}}$('modelSaveBtn').addEventListener('click',async()=>{try{let d=JSON.parse($('modelJSON').value);await req('/admin/api/model-routing',{method:'PUT',body:JSON.stringify(d)});toast('Modelle & Routing gespeichert.');loadModels()}catch(e){toast(e.message,true)}});$('modelHealthBtn').addEventListener('click',async()=>{try{$('modelHealth').textContent='prüfe …';let d=await req('/admin/api/provider-health',{method:'POST',body:'{}'});$('modelHealth').textContent=JSON.stringify(d,null,2)}catch(e){$('modelHealth').textContent=e.message}});
// ---- Operations ----
async function loadOps(){try{let [raw,st,cfg]=await Promise.all([req('/admin/api/status'),req('/admin/api/storage'),req('/admin/api/config')]),s=normalizeStatus(raw);$('opsIndex').innerHTML=`<div class="item">HNSW <b>${fmtN(s.hnsw_nodes)}</b></div><div class="item">Disk-PQ <b>${fmtN(s.disk_pq_items)}</b></div><div class="item">Mode <b>${esc(s.index_mode||'')}</b></div>`;$('opsStorage').innerHTML=`<div class="item">Revision <b>${fmtN(s.revision)}</b></div><div class="item">Segmente <b>${fmtN(st.segments?.files||st.memory_segments?.files||0)}</b></div><div class="item">Disk <b>${fmtBytes(st.segments?.bytes||st.memory_segments?.bytes||0)}</b></div>`;$('opsObs').innerHTML=`<div class="item">Pending jobs <b>${fmtN(s.pending_jobs)}</b></div><div class="item">Remote shards <b>${fmtN(s.remote_shards)}</b></div><div class="item">Letzter Fehler <span class="small">${esc(s.maintenance?.last_error||'keiner')}</span></div>`;$('configJSON').value=JSON.stringify(cfg,null,2)}catch(e){toast(e.message,true)}}$('configSaveBtn').addEventListener('click',async()=>{try{let c=JSON.parse($('configJSON').value);await req('/admin/api/config',{method:'PUT',body:JSON.stringify(c)});toast('Konfiguration gespeichert.')}catch(e){toast(e.message,true)}});$('checkpointBtn').addEventListener('click',()=>opPost('/admin/api/checkpoint','Checkpoint erstellt'));$('compactBtn').addEventListener('click',()=>opPost('/admin/api/storage/compact','Segmente kompaktiert'));$('pqBuildBtn').addEventListener('click',()=>opPost('/admin/api/index/disk/rebuild','PQ-Index neu gebaut'));$('indexMergeBtn').addEventListener('click',()=>opPost('/admin/api/index/merge','Index gemerged'));async function opPost(path,msg){try{await req(path,{method:'POST',body:'{}'});toast(msg);loadOps()}catch(e){toast(e.message,true)}}
async function boot(){if(!admin){await setToken();if(!admin)return}await refreshAll();setInterval(()=>{if(admin&&['dashboard','goals'].includes(currentView))refreshAll()},15000)}boot();
</script>
</body></html>