Files
glpi-neuroforge-mega/services/control/index.html
jbergner 1decb831d6
All checks were successful
release-tag / release-image (push) Successful in 3m42s
1.4.5
2026-08-27 06:06:00 +02:00

51 lines
19 KiB
HTML
Raw 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">
<title>GLPI NeuroForge Control Center · Unified Graph</title>
<style>
:root{color-scheme:dark;font-family:Inter,system-ui,sans-serif;background:#0a0f16;color:#e7eef8;--card:#111a25;--line:#26364b;--muted:#91a2b7;--accent:#83bcff;--good:#81e6a8;--bad:#ff9caa;--warn:#ffd17a}*{box-sizing:border-box}body{margin:0;background:#0a0f16;color:#e7eef8}.shell{max-width:1540px;margin:0 auto;padding:24px 20px 40px}h1{font-size:29px;margin:0 0 4px}h2{margin:0;font-size:18px}p{color:var(--muted)}.top{display:flex;align-items:flex-start;justify-content:space-between;gap:20px}.badge{border:1px solid var(--line);border-radius:999px;padding:7px 11px;font-size:12px;color:#c8d8eb;background:#101925}.bar,.grid{display:grid;gap:12px}.bar{grid-template-columns:repeat(auto-fit,minmax(145px,1fr));margin:20px 0}.grid{grid-template-columns:repeat(4,minmax(0,1fr))}.card{background:var(--card);border:1px solid var(--line);border-radius:13px;padding:15px}.label{font-size:11px;text-transform:uppercase;color:#8295ad;letter-spacing:.08em}.value{font-size:18px;font-weight:700;margin-top:6px}.ok{color:var(--good)}.bad{color:var(--bad)}.warn{color:var(--warn)}.muted{color:var(--muted)}.service h2{margin-bottom:7px}.service a,.link{color:#8fc8ff}.detail{white-space:pre-wrap;font:11px ui-monospace,monospace;background:#090e14;padding:8px;border-radius:8px;max-height:135px;overflow:auto}.graphCard{margin-top:16px;padding:0;overflow:hidden}.graphHead{padding:14px;border-bottom:1px solid var(--line);display:flex;flex-wrap:wrap;align-items:center;gap:8px;background:#0f1721}.graphHead select,.graphHead input,.graphHead button{background:#0a111a;color:#dde9f8;border:1px solid #304158;border-radius:8px;padding:8px 10px;font:inherit}.graphHead input{min-width:190px}.graphHead button{cursor:pointer}.graphHead button.active{border-color:#78b7ff;background:#152b45}.graphMeta{margin-left:auto;color:var(--muted);font-size:12px}.graphBody{display:grid;grid-template-columns:minmax(0,1fr) 330px;min-height:650px}.canvasWrap{position:relative;background:radial-gradient(circle at 50% 45%,#101b29 0,#091019 58%,#070b11 100%);overflow:hidden}.canvasWrap canvas{width:100%;height:650px;display:block;touch-action:none}.floating{position:absolute;left:12px;top:12px;max-width:min(620px,80%);display:flex;flex-wrap:wrap;gap:6px;pointer-events:none}.chip{background:#0b1420dd;border:1px solid #2a3d55;border-radius:999px;padding:5px 8px;font-size:11px;color:#bbcadb}.hint{position:absolute;left:12px;bottom:10px;color:#7f91a7;font-size:11px;background:#091019cc;padding:5px 8px;border-radius:7px}.inspector{border-left:1px solid var(--line);background:#0d151f;padding:15px;overflow:auto;max-height:650px}.inspector h3{margin:0 0 8px;font-size:17px}.kv{display:grid;grid-template-columns:105px 1fr;gap:5px 8px;font-size:12px;margin-top:10px}.kv b{color:#8396ae;font-weight:500}.metaBox{font:11px ui-monospace,monospace;white-space:pre-wrap;word-break:break-word;background:#080d13;border-radius:8px;padding:9px;margin-top:12px;max-height:330px;overflow:auto}.empty{color:#8799af;padding:22px;text-align:center}.footer{margin-top:18px;font-size:12px}.sectionTitle{display:flex;justify-content:space-between;align-items:center;margin:22px 0 10px}.sectionTitle span{font-size:12px;color:var(--muted)}@media(max-width:1050px){.grid{grid-template-columns:repeat(2,1fr)}.graphBody{grid-template-columns:1fr}.inspector{border-left:0;border-top:1px solid var(--line);max-height:none}.canvasWrap canvas{height:560px}}@media(max-width:650px){.shell{padding:18px 12px}.grid,.bar{grid-template-columns:1fr}.top{display:block}.graphHead>*{flex:1}.graphMeta{width:100%;margin:0}.canvasWrap canvas{height:480px}}
</style></head><body><div class="shell">
<div class="top"><div><h1>GLPI × NeuroForge Control Center</h1><p>Read-only Operations-, Evidence-, Learning-, Research- und Engineering-Graph. Schreibrechte bleiben in den spezialisierten Komponenten.</p></div><div class="badge">Unified Graph Explorer · v1.4</div></div>
<div class="bar"><div class="card"><div class="label">Vector Backend</div><div id="mode" class="value"></div></div><div class="card"><div class="label">Controlled Learning</div><div id="controlled" class="value"></div></div><div class="card"><div class="label">Outcome Retrieval</div><div id="outcomeRetrieval" class="value"></div></div><div class="card"><div class="label">Research / SearXNG</div><div id="research" class="value"></div></div><div class="card"><div class="label">Autonomy</div><div id="autonomy" class="value"></div></div><div class="card"><div class="label">Control Plane</div><div class="value">Read-only</div></div></div>
<div class="sectionTitle"><h2>Service Status</h2><span>10-Sekunden-Refresh · optionale Engineering-Komponenten beeinflussen Readiness nicht</span></div><div id="services" class="grid"></div>
<div class="card graphCard"><div class="graphHead">
<select id="scope"><option value="runtime">Runtime & Trust</option><option value="ticket">Ticket Evidence</option><option value="learning">Learning Lineage</option><option value="research">Research Provenance</option><option value="brain">NeuroForge Brain</option><option value="engineering">Engineering Graph</option><option value="impact">Change Impact</option></select>
<select id="runSelect" hidden><option value="">Run auswählen…</option></select>
<input id="search" type="search" placeholder="Knoten suchen / Engineering-Query">
<select id="kindFilter"><option value="">alle Knotentypen</option></select>
<select id="edgeFilter"><option value="">alle Kantentypen</option></select>
<select id="budget"><option value="250">250 Nodes</option><option value="450">450 Nodes</option><option value="650" selected>650 Nodes</option><option value="1000">1000 Nodes</option></select>
<button id="mode2d" class="active">2D</button><button id="mode3d">3D</button><button id="fit">⌂ Fit</button><button id="reload">↻ Reload</button>
<div id="graphMeta" class="graphMeta"></div></div>
<div class="graphBody"><div class="canvasWrap"><canvas id="graph"></canvas><div id="chips" class="floating"></div><div class="hint">2D: Drag=Pan · Wheel=Zoom · 3D: Drag=Rotate · Klick=Inspector · Doppelklick=Nachbarschaft fokussieren</div></div><aside class="inspector"><div id="inspect"><div class="empty">Knoten auswählen, um Provenance, Status, Score und Beziehungen zu sehen.</div></div></aside></div></div>
<div class="footer muted">Engineering Graph: reproduzierbarer Go-AST/Compose-Snapshot. Codebase Memory MCP kann optional separat für tiefere MCP/Cypher/3D-Analyse betrieben und verlinkt werden; es ist keine Produktionsabhängigkeit.</div>
</div>
<script>
const $=id=>document.getElementById(id), esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
let config={}, graph={nodes:[],edges:[],scope:'',title:''}, viewNodes=[], viewEdges=[], positions=new Map(), selected='', renderMode='2d', cam={x:0,y:0,z:1,yaw:0.6,pitch:-0.25}, drag={on:false,x:0,y:0,moved:false}, neighborhood=null;
const canvas=$('graph'),ctx=canvas.getContext('2d');
const palette={ticket:'#7eb6ff',run:'#9a8cff',knowledge:'#70d5ae',validated_outcome:'#ffd176',human_outcome:'#ffb878',memory:'#c294ff',memory_semantic:'#9c8cff',memory_episodic:'#70d4bb',memory_working:'#d19aff',memory_procedural:'#ffc56c',policy_check:'#ff8e9d',analysis:'#82b8ff',model_node:'#83d7ff',reply:'#67e5ad',category:'#ffc96d',source:'#69c7ff',query:'#b892ff',claim:'#ffd16c',research_goal:'#9d8dff',research_run:'#74b8ff',service:'#69d0b4',external:'#ffb477',engineering:'#a6b7ca',component:'#79b7ff',package:'#9d8fff',file:'#72d2c0',function:'#e1b56d',route:'#ff8ca2',model_runtime:'#7ed7ff',research_runtime:'#79cca0'};
function nodeColor(n){if(n.status==='problem'||n.status==='error'||n.status==='fail'||n.status==='failed')return '#ff7f92';if(n.status==='superseded'||n.status==='archived')return '#65758a';if(n.status==='corrected'||n.status==='corroborated')return '#ffd16d';return palette[n.kind]||palette[n.group]||'#9ab0c8'}
function resize(){let r=canvas.getBoundingClientRect(),d=devicePixelRatio||1;canvas.width=Math.round(r.width*d);canvas.height=Math.round(r.height*d);ctx.setTransform(d,0,0,d,0,0);draw()}addEventListener('resize',resize);
async function jget(url){let r=await fetch(url,{cache:'no-store'});if(!r.ok)throw new Error(await r.text()||r.statusText);return r.json()}
async function loadStatus(){try{const [c,r]=await Promise.all([jget('/api/config'),jget('/api/status')]);config=c;mode.textContent=c.vector_backend;controlled.textContent=c.controlled_learning==='true'?'ON':'OFF';outcomeRetrieval.textContent=(c.outcome_retrieval==='true'?'ON':'OFF')+' · K='+c.outcome_retrieval_search_k+' · min '+c.outcome_retrieval_min_similarity;research.textContent=(c.research_enabled==='true'?'ON':'OFF')+' / '+(c.searxng_enabled==='true'?'SearXNG':'kein SearXNG')+' / '+(c.kb_staging_bridge==='true'?'Staging':'kein Staging');autonomy.textContent=c.autonomy_enabled==='true'?'ON':'OFF';services.innerHTML=(r.services||[]).map(s=>`<div class="card service"><h2>${esc(s.name)}${s.optional?' <span class="muted">(optional)</span>':''}</h2><div class="${s.ok?'ok':s.optional?'warn':'bad'}"><b>${s.ok?'ONLINE':s.optional?'OPTIONAL/OFFLINE':'PROBLEM'}</b> · HTTP ${s.status||''} · ${s.latency_ms} ms</div>${s.public_url?`<p><a href="${esc(s.public_url)}" target="_blank" rel="noopener">Oberfläche öffnen</a></p>`:''}<div class="detail">${esc(JSON.stringify(s.detail||s.error||{},null,2))}</div></div>`).join('')}catch(e){services.innerHTML=`<div class="card bad">Status nicht verfügbar: ${esc(e.message)}</div>`}}
async function loadRuns(){try{let xs=await jget('/api/graph/runs?limit=80');runSelect.innerHTML='<option value="">Run auswählen…</option>'+xs.map(x=>`<option value="${esc(x.run_id)}">#${x.ticket_id} · ${esc(x.ticket_name||'')} · ${esc(x.outcome||'')}</option>`).join('')}catch(e){runSelect.innerHTML='<option value="">Run-Liste nicht verfügbar</option>'}}
function endpoint(){let s=scope.value,b=budget.value,q=search.value.trim();if(s==='runtime')return '/api/graph/runtime';if(s==='ticket')return runSelect.value?'/api/graph/ticket?run_id='+encodeURIComponent(runSelect.value):'';if(s==='learning')return '/api/graph/learning?limit='+Math.min(500,Number(b)||180);if(s==='research')return '/api/graph/research?runs=8&max_events='+Math.min(800,Number(b)||320);if(s==='brain')return '/api/graph/brain?max_nodes='+Math.min(700,Number(b)||320);if(s==='engineering')return '/api/graph/engineering?max_nodes='+encodeURIComponent(b)+(q?'&q='+encodeURIComponent(q):'');if(s==='impact')return q?'/api/graph/impact?max_nodes='+encodeURIComponent(b)+'&depth=2&q='+encodeURIComponent(q):'';return ''}
async function loadGraph(){let u=endpoint();if(!u){graph={nodes:[],edges:[],title:'Run auswählen'};applyGraph();return}graphMeta.textContent='lädt…';try{graph=await jget(u);selected='';neighborhood=null;applyGraph()}catch(e){graph={nodes:[],edges:[],title:'Fehler',meta:{error:e.message}};applyGraph();graphMeta.textContent='Fehler: '+e.message}}
function applyGraph(){populateFilters();layout();filterGraph();fitView();draw();graphMeta.textContent=`${graph.title||graph.scope||''} · ${viewNodes.length}/${(graph.nodes||[]).length} Nodes · ${viewEdges.length}/${(graph.edges||[]).length} Edges`;chips.innerHTML=summaryChips().map(x=>`<span class="chip">${esc(x)}</span>`).join('');inspect.innerHTML='<div class="empty">Knoten auswählen, um Details und Beziehungen zu sehen.</div>'}
function populateFilters(){let oldK=kindFilter.value,oldE=edgeFilter.value,k=[...new Set((graph.nodes||[]).map(n=>n.kind))].sort(),e=[...new Set((graph.edges||[]).map(x=>x.kind))].sort();kindFilter.innerHTML='<option value="">alle Knotentypen</option>'+k.map(x=>`<option>${esc(x)}</option>`).join('');edgeFilter.innerHTML='<option value="">alle Kantentypen</option>'+e.map(x=>`<option>${esc(x)}</option>`).join('');if(k.includes(oldK))kindFilter.value=oldK;if(e.includes(oldE))edgeFilter.value=oldE}
function filterGraph(){let k=kindFilter.value,e=edgeFilter.value,q=search.value.trim().toLowerCase(),ids=new Set();viewNodes=(graph.nodes||[]).filter(n=>(!k||n.kind===k)&&(!q||(graph.scope==='engineering'||graph.scope==='impact')||(`${n.label} ${n.id} ${JSON.stringify(n.meta||{})}`).toLowerCase().includes(q)));if(neighborhood){viewNodes=viewNodes.filter(n=>neighborhood.has(n.id))}viewNodes.forEach(n=>ids.add(n.id));viewEdges=(graph.edges||[]).filter(x=>ids.has(x.from)&&ids.has(x.to)&&(!e||x.kind===e));graphMeta.textContent=`${graph.title||graph.scope||''} · ${viewNodes.length}/${(graph.nodes||[]).length} Nodes · ${viewEdges.length}/${(graph.edges||[]).length} Edges`;draw()}
function summaryChips(){let out=[],c={};if(graph.meta?.risk)out.push(`risk: ${graph.meta.risk}`);if(graph.meta?.affected_components?.length)out.push(`components: ${graph.meta.affected_components.length}`);for(let n of viewNodes)c[n.kind]=(c[n.kind]||0)+1;return out.concat(Object.entries(c).sort((a,b)=>b[1]-a[1]).slice(0,9).map(([k,v])=>`${k}: ${v}`))}
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)/4294967295}
function layout(){positions.clear();let ns=graph.nodes||[],communities=[...new Set(ns.map(n=>n.community||n.group||n.kind||'other'))].sort(),cols=Math.ceil(Math.sqrt(communities.length)),centers={};communities.forEach((c,i)=>{let col=i%cols,row=Math.floor(i/cols);centers[c]={x:(col-(cols-1)/2)*360,y:(row-(Math.ceil(communities.length/cols)-1)/2)*300,z:(hash(c)-.5)*420}});let groups={};ns.forEach(n=>(groups[n.community||n.group||n.kind||'other']??=[]).push(n));for(let [c,xs] of Object.entries(groups)){let cc=centers[c]||{x:0,y:0,z:0},rings=Math.ceil(Math.sqrt(xs.length));xs.forEach((n,i)=>{let a=hash(n.id+'a')*Math.PI*2,r=30+Math.sqrt(i+1)*18,z=cc.z+(hash(n.id+'z')-.5)*180;positions.set(n.id,{x:cc.x+Math.cos(a)*r,y:cc.y+Math.sin(a)*r,z,n})})}}
function project(p){let r=canvas.getBoundingClientRect();if(renderMode==='2d')return{x:(p.x+cam.x)*cam.z+r.width/2,y:(p.y+cam.y)*cam.z+r.height/2,s:cam.z,depth:0};let cy=Math.cos(cam.yaw),sy=Math.sin(cam.yaw),cp=Math.cos(cam.pitch),sp=Math.sin(cam.pitch),x=p.x*cy-p.z*sy,z1=p.x*sy+p.z*cy,y=p.y*cp-z1*sp,z=p.y*sp+z1*cp,depth=900+z,scale=cam.z*700/Math.max(180,depth);return{x:r.width/2+(x+cam.x)*scale,y:r.height/2+(y+cam.y)*scale,s:scale,depth:z}}
function draw(){let r=canvas.getBoundingClientRect();ctx.clearRect(0,0,r.width,r.height);if(!viewNodes.length)return;let visible=new Set(viewNodes.map(n=>n.id)),edges=viewEdges;if(edges.length>3200&&cam.z<1.25)edges=edges.filter((_,i)=>i%Math.ceil(edges.length/2500)===0);ctx.lineCap='round';for(let e of edges){let a=positions.get(e.from),b=positions.get(e.to);if(!a||!b||!visible.has(e.from)||!visible.has(e.to))continue;let A=project(a),B=project(b),neg=e.status==='error'||e.status==='fail'||e.kind==='supersedes';ctx.strokeStyle=neg?'rgba(255,115,135,.30)':'rgba(120,158,205,.20)';ctx.lineWidth=Math.min(3,.55+Math.abs(Number(e.weight||0))*.85);ctx.beginPath();ctx.moveTo(A.x,A.y);ctx.lineTo(B.x,B.y);ctx.stroke()}let nodes=viewNodes.slice();if(renderMode==='3d')nodes.sort((a,b)=>project(positions.get(a)).depth-project(positions.get(b)).depth);for(let n of nodes){let p=positions.get(n.id),s=project(p),sel=n.id===selected,rad=Math.max(2.3,Math.min(9,(sel?7:4.2)*Math.sqrt(Math.max(.35,s.s))));if(s.x<-20||s.y<-20||s.x>r.width+20||s.y>r.height+20)continue;ctx.fillStyle=nodeColor(n);ctx.globalAlpha=(n.status==='superseded'||n.status==='archived')?.35:.92;ctx.beginPath();ctx.arc(s.x,s.y,rad,0,Math.PI*2);ctx.fill();ctx.globalAlpha=1;if(sel){ctx.strokeStyle='#fff';ctx.lineWidth=2;ctx.stroke()}if(sel||(cam.z>1.35&&viewNodes.length<500&&rad>3.5)){ctx.font='11px system-ui';ctx.fillStyle='#dbe8f7';ctx.fillText((n.label||n.id).slice(0,38),s.x+rad+4,s.y+4)}}}
function fitView(){if(!viewNodes.length){cam={x:0,y:0,z:1,yaw:.6,pitch:-.25};return}let ps=viewNodes.map(n=>positions.get(n.id)).filter(Boolean),xs=ps.map(p=>p.x),ys=ps.map(p=>p.y),w=Math.max(...xs)-Math.min(...xs)+180,h=Math.max(...ys)-Math.min(...ys)+180,r=canvas.getBoundingClientRect();cam.x=-(Math.max(...xs)+Math.min(...xs))/2;cam.y=-(Math.max(...ys)+Math.min(...ys))/2;cam.z=Math.max(.16,Math.min(1.35,Math.min(r.width/w,r.height/h)));if(renderMode==='3d')cam.z=Math.max(.45,cam.z*1.6)}
function pick(x,y){let best=null,bd=14;for(let n of viewNodes){let p=positions.get(n.id);if(!p)continue;let s=project(p),d=Math.hypot(s.x-x,s.y-y);if(d<bd){best=n;bd=d}}return best}
function showNode(n){selected=n.id;let related=viewEdges.filter(e=>e.from===n.id||e.to===n.id).slice(0,80),status=n.status||'';inspect.innerHTML=`<h3>${esc(n.label||n.id)}</h3><div class="${status==='problem'||status==='error'||status==='fail'?'bad':'muted'}">${esc(n.kind)} · ${esc(status)}</div><div class="kv"><b>ID</b><span>${esc(n.id)}</span><b>Community</b><span>${esc(n.community||'')}</span><b>Group</b><span>${esc(n.group||'')}</span><b>Score</b><span>${Number(n.score||0).toFixed(4)}</span><b>Relations</b><span>${related.length}</span></div><div class="metaBox">${esc(JSON.stringify(n.meta||{},null,2))}</div><div class="metaBox">${esc(related.map(e=>`${e.kind}: ${e.from===n.id?'→ '+e.to:'← '+e.from}${e.label?' · '+e.label:''}`).join('\n')||'keine sichtbaren Beziehungen')}</div>`;draw()}
canvas.addEventListener('pointerdown',e=>{drag={on:true,x:e.clientX,y:e.clientY,moved:false};canvas.setPointerCapture(e.pointerId)});canvas.addEventListener('pointermove',e=>{if(!drag.on)return;let dx=e.clientX-drag.x,dy=e.clientY-drag.y;if(Math.hypot(dx,dy)>2)drag.moved=true;if(renderMode==='3d'){cam.yaw+=dx*.006;cam.pitch=Math.max(-1.2,Math.min(1.2,cam.pitch+dy*.005))}else{cam.x+=dx/cam.z;cam.y+=dy/cam.z}drag.x=e.clientX;drag.y=e.clientY;draw()});canvas.addEventListener('pointerup',e=>{drag.on=false;if(drag.moved)return;let r=canvas.getBoundingClientRect(),n=pick(e.clientX-r.left,e.clientY-r.top);if(n)showNode(n)});canvas.addEventListener('dblclick',e=>{let r=canvas.getBoundingClientRect(),n=pick(e.clientX-r.left,e.clientY-r.top);if(!n)return;let set=new Set([n.id]);for(let edge of graph.edges||[]){if(edge.from===n.id)set.add(edge.to);if(edge.to===n.id)set.add(edge.from)}neighborhood=set;filterGraph();fitView();draw()});canvas.addEventListener('wheel',e=>{e.preventDefault();cam.z=Math.max(.08,Math.min(5,cam.z*Math.exp(-e.deltaY*.0011)));draw()},{passive:false});
mode2d.onclick=()=>{renderMode='2d';mode2d.classList.add('active');mode3d.classList.remove('active');fitView();draw()};mode3d.onclick=()=>{renderMode='3d';mode3d.classList.add('active');mode2d.classList.remove('active');fitView();draw()};fit.onclick=()=>{neighborhood=null;filterGraph();fitView();draw()};reload.onclick=loadGraph;scope.onchange=async()=>{runSelect.hidden=scope.value!=='ticket';search.placeholder=(scope.value==='engineering'||scope.value==='impact')?'Engineering Query / Datei / Symbol':'Knoten suchen';if(scope.value==='ticket'&&!runSelect.options.length)await loadRuns();await loadGraph()};runSelect.onchange=loadGraph;kindFilter.onchange=filterGraph;edgeFilter.onchange=filterGraph;budget.onchange=loadGraph;let stimer;search.oninput=()=>{clearTimeout(stimer);stimer=setTimeout(()=>(scope.value==='engineering'||scope.value==='impact')?loadGraph():filterGraph(),300)};
(async()=>{await loadStatus();await loadRuns();await loadGraph();resize()})();setInterval(loadStatus,10000);
</script></body></html>