(function () { const NS = 'http://www.w3.org/2000/svg'; function make(tag, attrs = {}) { const el = document.createElementNS(NS, tag); Object.entries(attrs).forEach(([k, v]) => el.setAttribute(k, String(v))); return el; } function clear(el) { while (el.firstChild) el.removeChild(el.firstChild); } function linePath(x1, y1, x2, y2) { const midY = y1 + (y2 - y1) * 0.45; return `M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`; } async function loadMeshData() { const r = await fetch('/api/agents/mesh'); if (!r.ok) throw new Error('Mesh API unavailable'); return r.json(); } function drawMesh(svg, data) { clear(svg); const vb = svg.viewBox.baseVal; const width = vb && vb.width ? vb.width : 1100; const height = vb && vb.height ? vb.height : 640; const cx = width / 2; const hermanY = 280; const execY = 75; const agentY = 520; const souls = (data.nodes || []).filter((n) => { const key = (n.agent_key || '').toLowerCase(); return key && key !== 'herman'; }); const edgesBySource = {}; (data.edges || []).forEach((e) => { edgesBySource[e.source] = e; }); const agentCount = Math.max(1, souls.length); const pad = 70; const span = width - pad * 2; souls.forEach((node, i) => { node._x = pad + (span * i) / Math.max(1, agentCount - 1 || 1); if (agentCount === 1) node._x = cx; node._y = agentY; }); const ceo = { x: cx - 180, y: execY, label: 'CEO', sub: 'Aissa · beslissingen' }; const cto = { x: cx + 180, y: execY, label: 'CTO', sub: 'Platform · techniek' }; const edgesLayer = make('g'); const nodesLayer = make('g'); svg.appendChild(edgesLayer); svg.appendChild(nodesLayer); function addEdge(x1, y1, x2, y2, pulse) { const d = linePath(x1, y1, x2, y2); edgesLayer.appendChild(make('path', { d, class: 'mesh-edge' })); if (pulse) { const p = make('path', { d, class: 'mesh-pulse' }); p.style.animationDuration = pulse + 's'; edgesLayer.appendChild(p); } } souls.forEach((node) => { const weight = edgesBySource[node.agent_key]?.weight || 1; addEdge(node._x, node._y - 26, cx, hermanY + 48, Math.max(1.2, 3.2 - Math.min(2, weight / 10))); }); addEdge(cx, hermanY - 48, ceo.x, ceo.y + 28, 2.2); addEdge(cx, hermanY - 48, cto.x, cto.y + 28, 2.4); function drawExec(node, cls) { const g = make('g', { class: `mesh-node mesh-exec ${cls}` }); g.appendChild(make('circle', { cx: node.x, cy: node.y, r: 34, class: 'main' })); g.appendChild(make('circle', { cx: node.x, cy: node.y, r: 42, class: 'ring' })); g.appendChild(make('text', { x: node.x, y: node.y - 2, 'font-size': 13 })); g.lastChild.textContent = node.label; const sub = make('text', { x: node.x, y: node.y + 14, 'font-size': 9, opacity: 0.85 }); sub.textContent = node.sub; g.appendChild(sub); nodesLayer.appendChild(g); } drawExec(ceo, 'mesh-ceo'); drawExec(cto, 'mesh-cto'); const herman = make('g', { class: 'mesh-node mesh-herman' }); herman.appendChild(make('circle', { class: 'ring', cx, cy: hermanY, r: 58 })); herman.appendChild(make('circle', { class: 'main', cx, cy: hermanY, r: 44 })); const crown = make('text', { x: cx, y: hermanY - 4, 'font-size': 22, 'text-anchor': 'middle' }); crown.textContent = '👑'; herman.appendChild(crown); const hLabel = make('text', { x: cx, y: hermanY + 18 }); hLabel.textContent = 'Herman · Co-CEO'; herman.appendChild(hLabel); const hSub = make('text', { x: cx, y: hermanY + 34, 'font-size': 10, opacity: 0.85 }); hSub.textContent = 'Takenverdeler'; herman.appendChild(hSub); nodesLayer.appendChild(herman); souls.forEach((node) => { const key = (node.agent_key || '').toLowerCase(); const group = make('g', { class: `mesh-node mesh-agent ${node.health || 'idle'}` }); group.appendChild(make('circle', { cx: node._x, cy: node._y, r: 22 })); const shortName = (node.display_name || node.agent_key || '?').split(' ')[0]; const t = make('text', { x: node._x, y: node._y + 4 }); t.textContent = shortName; group.appendChild(t); const role = make('text', { x: node._x, y: node._y + 38, 'font-size': 9, opacity: 0.82 }); role.textContent = (node.role_title || key).split('·')[0].trim().slice(0, 18); group.appendChild(role); const status = make('text', { x: node._x, y: node._y + 52, 'font-size': 8, class: 'mesh-status' }); status.textContent = (node.health || 'idle').toUpperCase(); group.appendChild(status); group.addEventListener('click', () => { if (window.Cockpit && Cockpit.toast) { Cockpit.toast(`${node.display_name || key}: ${node.health || 'idle'}`, 'info'); } }); nodesLayer.appendChild(group); }); } async function mount(targetId) { const svg = document.getElementById(targetId); if (!svg) return; try { const data = await loadMeshData(); drawMesh(svg, data); } catch (e) { clear(svg); const msg = make('text', { x: 32, y: 40, fill: '#ef4444', 'font-size': 14 }); msg.textContent = 'Mesh kon niet geladen worden'; svg.appendChild(msg); } } window.AgentsMesh = { mount }; })();