2026-06-09 00:41:27 +00:00
|
|
|
(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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 10:41:13 +00:00
|
|
|
function linePath(x1, y1, x2, y2) {
|
|
|
|
|
const midY = y1 + (y2 - y1) * 0.45;
|
|
|
|
|
return `M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`;
|
2026-06-09 00:41:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
2026-06-09 10:41:13 +00:00
|
|
|
const width = vb && vb.width ? vb.width : 1100;
|
|
|
|
|
const height = vb && vb.height ? vb.height : 640;
|
|
|
|
|
|
2026-06-09 00:41:27 +00:00
|
|
|
const cx = width / 2;
|
2026-06-09 10:41:13 +00:00
|
|
|
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';
|
|
|
|
|
});
|
2026-06-09 00:41:27 +00:00
|
|
|
|
|
|
|
|
const edgesBySource = {};
|
|
|
|
|
(data.edges || []).forEach((e) => { edgesBySource[e.source] = e; });
|
|
|
|
|
|
2026-06-09 10:41:13 +00:00
|
|
|
const agentCount = Math.max(1, souls.length);
|
|
|
|
|
const pad = 70;
|
|
|
|
|
const span = width - pad * 2;
|
2026-06-09 00:41:27 +00:00
|
|
|
souls.forEach((node, i) => {
|
2026-06-09 10:41:13 +00:00
|
|
|
node._x = pad + (span * i) / Math.max(1, agentCount - 1 || 1);
|
|
|
|
|
if (agentCount === 1) node._x = cx;
|
|
|
|
|
node._y = agentY;
|
2026-06-09 00:41:27 +00:00
|
|
|
});
|
|
|
|
|
|
2026-06-09 10:41:13 +00:00
|
|
|
const ceo = { x: cx - 180, y: execY, label: 'CEO', sub: 'Aissa · beslissingen' };
|
|
|
|
|
const cto = { x: cx + 180, y: execY, label: 'CTO', sub: 'Platform · techniek' };
|
|
|
|
|
|
2026-06-09 00:41:27 +00:00
|
|
|
const edgesLayer = make('g');
|
|
|
|
|
const nodesLayer = make('g');
|
|
|
|
|
svg.appendChild(edgesLayer);
|
|
|
|
|
svg.appendChild(nodesLayer);
|
|
|
|
|
|
2026-06-09 10:41:13 +00:00
|
|
|
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);
|
2026-06-09 00:41:27 +00:00
|
|
|
}
|
2026-06-09 10:41:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)));
|
2026-06-09 00:41:27 +00:00
|
|
|
});
|
|
|
|
|
|
2026-06-09 10:41:13 +00:00
|
|
|
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');
|
|
|
|
|
|
2026-06-09 00:41:27 +00:00
|
|
|
const herman = make('g', { class: 'mesh-node mesh-herman' });
|
2026-06-09 10:41:13 +00:00
|
|
|
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' });
|
2026-06-09 00:41:27 +00:00
|
|
|
crown.textContent = '👑';
|
|
|
|
|
herman.appendChild(crown);
|
2026-06-09 10:41:13 +00:00
|
|
|
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);
|
2026-06-09 00:41:27 +00:00
|
|
|
nodesLayer.appendChild(herman);
|
|
|
|
|
|
|
|
|
|
souls.forEach((node) => {
|
2026-06-09 10:41:13 +00:00
|
|
|
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 }));
|
2026-06-09 00:41:27 +00:00
|
|
|
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);
|
2026-06-09 10:41:13 +00:00
|
|
|
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);
|
2026-06-09 00:41:27 +00:00
|
|
|
group.appendChild(role);
|
2026-06-09 10:41:13 +00:00
|
|
|
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);
|
2026-06-09 00:41:27 +00:00
|
|
|
group.addEventListener('click', () => {
|
|
|
|
|
if (window.Cockpit && Cockpit.toast) {
|
2026-06-09 10:41:13 +00:00
|
|
|
Cockpit.toast(`${node.display_name || key}: ${node.health || 'idle'}`, 'info');
|
2026-06-09 00:41:27 +00:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
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 };
|
|
|
|
|
})();
|