(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 curvePath(x1, y1, x2, y2) { const cx1 = x1 + (x2 - x1) * 0.35; const cy1 = y1; const cx2 = x1 + (x2 - x1) * 0.72; const cy2 = y2; return `M ${x1} ${y1} C ${cx1} ${cy1}, ${cx2} ${cy2}, ${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 : 1000; const height = vb && vb.height ? vb.height : 620; const cx = width / 2; const cy = height / 2; const radius = Math.min(width, height) * 0.35; const souls = (data.nodes || []).filter((n) => (n.agent_key || '').toLowerCase() !== 'herman'); const edgesBySource = {}; (data.edges || []).forEach((e) => { edgesBySource[e.source] = e; }); souls.forEach((node, i) => { const a = (Math.PI * 2 * i) / Math.max(1, souls.length) - Math.PI / 2; node._x = cx + Math.cos(a) * radius; node._y = cy + Math.sin(a) * radius; }); const edgesLayer = make('g'); const nodesLayer = make('g'); svg.appendChild(edgesLayer); svg.appendChild(nodesLayer); souls.forEach((node) => { const pathDef = curvePath(node._x, node._y, cx, cy); const edge = make('path', { d: pathDef, class: 'mesh-edge' }); edgesLayer.appendChild(edge); if (edgesBySource[node.agent_key]) { const pulse = make('path', { d: pathDef, class: 'mesh-pulse' }); pulse.style.animationDuration = `${Math.max(1.2, 3.5 - Math.min(2.2, edgesBySource[node.agent_key].weight / 8))}s`; edgesLayer.appendChild(pulse); } }); const herman = make('g', { class: 'mesh-node mesh-herman' }); herman.appendChild(make('circle', { class: 'ring', cx, cy, r: 62 })); herman.appendChild(make('circle', { class: 'main', cx, cy, r: 46 })); const crown = make('text', { x: cx, y: cy - 2, 'font-size': 24, 'text-anchor': 'middle' }); crown.textContent = '๐Ÿ‘‘'; herman.appendChild(crown); const label = make('text', { x: cx, y: cy + 22 }); label.textContent = 'Herman ยท Co-CEO'; herman.appendChild(label); nodesLayer.appendChild(herman); souls.forEach((node) => { const group = make('g', { class: `mesh-node ${node.health || 'idle'}` }); group.appendChild(make('circle', { cx: node._x, cy: node._y, r: 24 })); 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 + 42, 'font-size': 10, opacity: 0.82 }); role.textContent = node.role_title || node.agent_key; group.appendChild(role); group.addEventListener('click', () => { if (window.Cockpit && Cockpit.toast) { Cockpit.toast(`${node.display_name || node.agent_key}: ${node.health || 'idle'}`, 'success'); } }); 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 }; })();