Some checks failed
release-tag / Resolve release metadata (push) Successful in 30s
release-tag / Build knowledge (push) Failing after 4m51s
release-tag / Build control (push) Failing after 5m0s
release-tag / Build agent (push) Failing after 5m0s
release-tag / Build agent-data-init (push) Failing after 5m5s
release-tag / Build neuroforge-worker (push) Failing after 5m7s
release-tag / Build neuroforge (push) Failing after 5m9s
51 lines
19 KiB
HTML
51 lines
19 KiB
HTML
<!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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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');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>
|