// === Variable Dependency Graph (pure SVG radial layout — no external dep) ===
// Renders variable nodes on a circle with curved edges. Fully interactive:
// hover shows a detailed tooltip + highlights the node's neighbourhood,
// click selects the node and populates the SELECTED_NODE detail panel.
// Pure SVG guarantees rendering even if the D3 CDN is unreachable.
function renderVariableGraph() {
const container = document.getElementById('variableGraphContainer');
if (!container) return;
const rels = DATA.relationships || [];
if (!rels.length) {
container.innerHTML = '<div class="text-outline text-xs px-2 py-4">No relationship data.</div>';
return;
}
// Build node metadata: in/out degree, type, pointer, cycle flag.
const nodeMeta = {};
rels.forEach(r => {
const s = r.source_var_name || r.source_ptr || '?';
const t = r.target_var_name || r.target_ptr || '?';
if (!nodeMeta[s]) nodeMeta[s] = { name: s, type: r.type_name || '?', ptr: r.source_ptr || '', inDeg: 0, outDeg: 0, cycle: false };
if (!nodeMeta[t]) nodeMeta[t] = { name: t, type: r.type_name || '?', ptr: r.target_ptr || '', inDeg: 0, outDeg: 0, cycle: false };
nodeMeta[s].outDeg++;
nodeMeta[t].inDeg++;
if (r.is_part_of_cycle) { nodeMeta[s].cycle = true; nodeMeta[t].cycle = true; }
});
// Cap visible nodes for readability; keep highest-degree ones.
const nodeNames = Object.keys(nodeMeta)
.sort((a, b) => (nodeMeta[b].inDeg + nodeMeta[b].outDeg) - (nodeMeta[a].inDeg + nodeMeta[a].outDeg))
.slice(0, 36);
const visSet = {};
nodeNames.forEach(n => { visSet[n] = true; });
const W = container.clientWidth || 640;
const H = 380;
const cx = W / 2, cy = H / 2;
const radius = Math.min(W, H) * 0.38;
const pos = {};
nodeNames.forEach((n, i) => {
const angle = (i / nodeNames.length) * Math.PI * 2 - Math.PI / 2;
pos[n] = { x: cx + Math.cos(angle) * radius, y: cy + Math.sin(angle) * radius };
});
// Edges between visible nodes only.
const edges = rels.filter(r => {
const s = r.source_var_name || r.source_ptr || '?';
const t = r.target_var_name || r.target_ptr || '?';
return visSet[s] && visSet[t] && s !== t;
}).slice(0, 140);
let html = '<svg width="100%" height="' + H + '" viewBox="0 0 ' + W + ' ' + H + '" id="varGraphSvg">';
html += '<defs><marker id="vgArrow" viewBox="0 -5 10 10" refX="10" refY="0" markerWidth="5" markerHeight="5" orient="auto"><path d="M0,-5L10,0,L0,5" fill="#948e9c"/></marker></defs>';
// Edges as quadratic curves for a cleaner look than straight lines.
edges.forEach(e => {
const s = e.source_var_name || e.source_ptr || '?';
const t = e.target_var_name || e.target_ptr || '?';
const sp = pos[s], tp = pos[t];
if (!sp || !tp) return;
const mx = (sp.x + tp.x) / 2, my = (sp.y + tp.y) / 2;
const dx = tp.x - sp.x, dy = tp.y - sp.y;
const ctrlX = mx - dy * 0.12, ctrlY = my + dx * 0.12;
const color = e.color || '#494551';
const sw = Math.max(0.8, (e.strength || 0.5) * 2.2).toFixed(2);
html += '<path class="vg-edge" data-source="' + s + '" data-target="' + t + '" d="M' + sp.x + ',' + sp.y + ' Q' + ctrlX + ',' + ctrlY + ' ' + tp.x + ',' + tp.y + '" stroke="' + color + '" stroke-width="' + sw + '" fill="none" opacity="0.45" marker-end="url(#vgArrow)" style="cursor:pointer"/>';
});
// Nodes — radius scales with degree; cycle nodes get a red ring.
nodeNames.forEach(n => {
const p = pos[n];
const m = nodeMeta[n];
const deg = m.inDeg + m.outDeg;
const r = Math.max(4, Math.min(11, 4 + deg * 0.45));
const ring = m.cycle ? '#ef4444' : 'var(--accent-amber)';
html += '<g class="vg-node" data-name="' + n + '" data-type="' + m.type + '" data-ptr="' + m.ptr + '" data-in="' + m.inDeg + '" data-out="' + m.outDeg + '" data-cycle="' + (m.cycle ? 1 : 0) + '" style="cursor:pointer">';
html += '<circle cx="' + p.x + '" cy="' + p.y + '" r="' + r + '" fill="var(--primary)" stroke="' + ring + '" stroke-width="1.5"/>';
const label = n.length > 14 ? n.slice(0, 14) + '…' : n;
html += '<text x="' + p.x + '" y="' + (p.y - r - 4) + '" fill="#cbc4d2" font-family="JetBrains Mono" font-size="9" text-anchor="middle">' + label + '</text>';
html += '</g>';
});
html += '</svg>';
container.innerHTML = html;
const svgEl = container.querySelector('#varGraphSvg');
// Highlight a node and its incident edges; dim the rest.
function highlightNode(name) {
svgEl.querySelectorAll('.vg-edge').forEach(e => {
const active = e.getAttribute('data-source') === name || e.getAttribute('data-target') === name;
e.setAttribute('opacity', active ? '0.95' : '0.10');
});
svgEl.querySelectorAll('.vg-node').forEach(g => {
g.style.opacity = (g.getAttribute('data-name') === name) ? '1' : '0.30';
});
}
function clearHighlight() {
svgEl.querySelectorAll('.vg-edge').forEach(e => { e.setAttribute('opacity', '0.45'); });
svgEl.querySelectorAll('.vg-node').forEach(g => { g.style.opacity = '1'; });
}
// Node hover + click — full detail tooltip and SELECTED_NODE panel sync.
svgEl.querySelectorAll('.vg-node').forEach(g => {
const name = g.getAttribute('data-name');
const type = g.getAttribute('data-type');
const ptr = g.getAttribute('data-ptr');
const inDeg = g.getAttribute('data-in');
const outDeg = g.getAttribute('data-out');
const isCycle = g.getAttribute('data-cycle') === '1';
g.addEventListener('mouseenter', function(ev) {
showChartTooltip(ev, 'VARIABLE_NODE', [
['name', name], ['type', type], ['ptr', ptr || '—'],
['in_degree', inDeg], ['out_degree', outDeg],
['total_rels', (parseInt(inDeg) || 0) + (parseInt(outDeg) || 0)],
['cycle', isCycle ? 'YES' : 'no']
]);
highlightNode(name);
});
g.addEventListener('mousemove', moveChartTooltip);
g.addEventListener('mouseleave', function() { hideChartTooltip(); clearHighlight(); });
g.addEventListener('click', function() {
const detail = document.getElementById('detailNodeName');
const status = document.getElementById('detailNodeStatus');
const typeEl = document.getElementById('detailNodeType');
const cycleEl = document.getElementById('detailNodeCycle');
const execEl = document.getElementById('detailNodeExecTime');
const upEl = document.getElementById('detailNodeUpstream');
const traceEl = document.getElementById('detailNodeTrace');
const uuidEl = document.getElementById('detailNodeUuid');
if (detail) detail.textContent = name;
if (status) status.textContent = 'in:' + inDeg + ' · out:' + outDeg;
if (typeEl) typeEl.textContent = 'TYPE: ' + (type || '—');
if (cycleEl) {
cycleEl.textContent = isCycle ? '⚠ PART OF CYCLE (leak risk)' : '✓ not in cycle';
cycleEl.className = 'font-data-mono text-[10px] mt-1 ' + (isCycle ? 'text-error' : 'text-success');
}
if (execEl) execEl.textContent = ptr ? ptr : '—';
if (upEl) upEl.textContent = (parseInt(inDeg) || 0) + (parseInt(outDeg) || 0) + ' total';
if (uuidEl) uuidEl.textContent = 'PTR: ' + (ptr || '—');
// Build a synthetic trace from incident relationships.
if (traceEl) {
const incidents = rels.filter(r => (r.source_var_name || r.source_ptr) === name || (r.target_var_name || r.target_ptr) === name).slice(0, 8);
if (incidents.length) {
traceEl.innerHTML = incidents.map(r => {
const s = r.source_var_name || r.source_ptr || '?';
const t = r.target_var_name || r.target_ptr || '?';
const cyc = r.is_part_of_cycle ? ' ⚠' : '';
return '<div class="text-on-surface-variant exec-trace-item" data-source="' + s + '" data-target="' + t + '" style="cursor:pointer">' + s + ' → ' + t + ' · ' + (r.relationship_type || 'link') + ' · ' + (r.strength || 0).toFixed(2) + cyc + '</div>';
}).join('');
// Bind hover + click to each freshly-created trace row so the
// detail panel's exec trace is itself interactive.
traceEl.querySelectorAll('.exec-trace-item').forEach((item, i) => {
const r = incidents[i] || {};
const sName = item.getAttribute('data-source');
const tName = item.getAttribute('data-target');
item.classList.add('chart-interactive');
item.addEventListener('mousemove', function(ev) {
showChartTooltip(ev, 'EXEC_TRACE', [
['from', sName],
['to', tName],
['type', r.relationship_type || 'link'],
['strength', (r.strength || 0).toFixed(2)],
['type_name', r.type_name || '—'],
['cycle', r.is_part_of_cycle ? 'YES ⚠' : 'no'],
['container_src', r.is_container_source ? 'yes' : 'no'],
['container_tgt', r.is_container_target ? 'yes' : 'no']
]);
});
item.addEventListener('mouseleave', hideChartTooltip);
item.addEventListener('click', function() {
drillKpi('exec_trace', 'RELATIONSHIP', sName + ' → ' + tName + ' · ' + (r.relationship_type || 'link') + ' · ' + (r.strength || 0).toFixed(2));
});
});
} else {
traceEl.innerHTML = '<div class="text-outline">No incident relationships.</div>';
}
}
drillKpi('var_node', 'VARIABLE_NODE', name + ' · ' + type + ' · in ' + inDeg + ' / out ' + outDeg + (isCycle ? ' · CYCLE' : ''));
});
});
// Edge hover + click — look up the originating relationship for full detail.
svgEl.querySelectorAll('.vg-edge').forEach(e => {
const s = e.getAttribute('data-source');
const t = e.getAttribute('data-target');
const rel = rels.find(r => (r.source_var_name || r.source_ptr) === s && (r.target_var_name || r.target_ptr) === t) || {};
e.addEventListener('mouseenter', function(ev) {
showChartTooltip(ev, 'RELATIONSHIP', [
['from', s], ['to', t],
['type', rel.relationship_type || 'link'],
['strength', (rel.strength != null ? rel.strength.toFixed(2) : '?')],
['type_name', rel.type_name || '—'],
['cycle', rel.is_part_of_cycle ? 'yes' : 'no']
]);
});
e.addEventListener('mousemove', moveChartTooltip);
e.addEventListener('mouseleave', hideChartTooltip);
e.addEventListener('click', function() {
drillKpi('var_edge', 'RELATIONSHIP', s + ' → ' + t + ' · ' + (rel.relationship_type || 'link'));
});
});
}
registerThemeAware('variableGraph', renderVariableGraph);
renderVariableGraph();
// === Task Topology DAG (SVG) ===
(function renderTaskTopology() {
const canvas = document.getElementById('taskTopologyCanvas');
if (!canvas) return;
const nodes = DATA.task_topology_nodes || [];
const edges = DATA.task_topology_edges || [];
if (nodes.length === 0) {
canvas.innerHTML = '<div class="text-outline text-xs px-4 py-8">No topology data.</div>';
return;
}
const W = canvas.clientWidth || 600;
const H = 410;
let html = '<svg width="100%" height="' + H + '" viewBox="0 0 ' + W + ' ' + H + '">';
// Build lookup map by task_id for edge resolution
const nodeMap = {};
nodes.forEach(n => { nodeMap[n.task_id] = n; });
// Draw edges
edges.forEach(edge => {
const src = nodeMap[edge.source];
const tgt = nodeMap[edge.target];
if (!src || !tgt) return;
const x1 = (src.x_pct / 100) * W;
const y1 = (src.y_pct / 100) * H;
const x2 = (tgt.x_pct / 100) * W;
const y2 = (tgt.y_pct / 100) * H;
const edgeStroke = edge.is_active === true ? 'var(--accent-amber)' : '#494551';
html += '<line x1="' + x1 + '" y1="' + y1 + '" x2="' + x2 + '" y2="' + y2 + '" style="stroke: ' + edgeStroke + ';cursor:pointer" stroke-width="1.5" opacity="0.5" class="topo-edge" data-active="' + (edge.is_active == null ? 'Unknown' : edge.is_active ? 'yes' : 'no') + '"/>';
});
// Draw nodes
nodes.forEach(node => {
const x = (node.x_pct / 100) * W;
const y = (node.y_pct / 100) * H;
// RUNNING → semantic green; POLLING → theme accent; COMPLETED → theme primary; else muted.
const fillVar = node.status === 'RUNNING' ? '#10B981' : node.status === 'POLLING' ? 'var(--accent-amber)' : node.status === 'COMPLETED' ? 'var(--primary)' : '#948e9c';
html += '<circle cx="' + x + '" cy="' + y + '" r="8" style="fill: ' + fillVar + '" opacity="0.85" class="topo-node chart-interactive" data-id="' + node.task_id + '" data-name="' + node.name + '" data-status="' + node.status + '" data-duration="' + (node.duration_ms == null ? 'Unknown' : node.duration_ms + 'ms') + '"/>';
html += '<text x="' + x + '" y="' + (y + 18) + '" fill="#cbc4d2" font-family="JetBrains Mono" font-size="9" text-anchor="middle" paint-order="stroke" stroke="#141218" stroke-width="3">' + (node.name || '').slice(0, 18) + '</text>';
});
html += '</svg>';
canvas.innerHTML = html;
canvas.querySelectorAll('.topo-node').forEach(n => {
const nm = n.getAttribute('data-name');
const id = n.getAttribute('data-id');
const st = n.getAttribute('data-status');
const dur = n.getAttribute('data-duration');
n.addEventListener('mousemove', function(ev) {
showChartTooltip(ev, 'TASK_NODE', [['name', nm], ['task_id', id], ['status', st], ['duration', dur]]);
});
n.addEventListener('mouseleave', hideChartTooltip);
n.addEventListener('click', function() {
drillKpi('topo_node', 'TASK_NODE', nm + ' [' + id + '] — ' + st + ' (' + dur + ')');
});
});
canvas.querySelectorAll('.topo-edge').forEach(e => {
const act = e.getAttribute('data-active');
e.addEventListener('mousemove', function(ev) {
showChartTooltip(ev, 'TASK_EDGE', [['active', act]]);
});
e.addEventListener('mouseleave', hideChartTooltip);
});
})();
// === FFI Thread Execution Timeline (HTML segments) — inline styles for CSS var colors ===
(function renderFfiThreadTimeline() {
const container = document.getElementById('ffiThreadTimeline');
if (!container) return;
const rows = DATA.thread_timeline || [];
if (rows.length === 0) {
container.innerHTML = '<div class="text-outline text-xs">No timeline data.</div>';
return;
}
let html = '';
rows.forEach((row, ri) => {
html += '<div class="flex items-center gap-3">';
html += '<span class="font-data-mono text-xs text-accent-amber w-32 truncate">' + (row.thread_name || '?') + '</span>';
html += '<div class="flex-1 h-5 bg-surface-container-low rounded overflow-hidden relative">';
(row.segments || []).forEach((seg, si) => {
const w = Math.max(1, seg.width_pct);
// Use inline style background so CSS variables like var(--primary) resolve correctly
html += '<div class="absolute h-full opacity-70 chart-interactive" style="left:' + seg.start_pct + '%;width:' + w + '%;background:' + (seg.color || 'var(--primary)') + '" title="' + (row.thread_name || '') + ' seg #' + si + ' · ' + seg.width_pct + '%"></div>';
});
html += '</div></div>';
});
container.innerHTML = html;
// Add click handlers to segments
container.querySelectorAll('.chart-interactive').forEach((seg, i) => {
seg.addEventListener('click', function() {
drillKpi('timeline_seg', 'TIMELINE_SEG', this.getAttribute('title'));
});
});
})();