← back to Exo
show ips on dashboard
e60681963f45f0d65af923ce3e7fa2873a56400f · 2025-11-06 11:18:07 -0800 · Alex Cheema
Files touched
Diff
commit e60681963f45f0d65af923ce3e7fa2873a56400f
Author: Alex Cheema <41707476+AlexCheema@users.noreply.github.com>
Date: Thu Nov 6 11:18:07 2025 -0800
show ips on dashboard
---
dashboard/index.html | 316 ++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 311 insertions(+), 5 deletions(-)
diff --git a/dashboard/index.html b/dashboard/index.html
index 24c6132f..715fdb54 100644
--- a/dashboard/index.html
+++ b/dashboard/index.html
@@ -165,6 +165,20 @@
}
}
+ .edge-label {
+ font-size: 10px;
+ fill: var(--exo-light-gray);
+ text-anchor: middle;
+ pointer-events: none;
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+ opacity: 0.95;
+ }
+
+ .edge-label-bg {
+ fill: var(--exo-dark-gray);
+ opacity: 0.85;
+ }
+
.node-info-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
@@ -363,6 +377,16 @@
margin-bottom: 12px;
border-left: 4px solid var(--exo-yellow);
transition: background-color 0.2s ease;
+ position: relative;
+ }
+
+ .instance-color-indicator {
+ position: absolute;
+ top: 15px;
+ left: -4px;
+ width: 4px;
+ height: calc(100% - 30px);
+ border-radius: 0 2px 2px 0;
}
.instance-item:hover {
@@ -1006,6 +1030,25 @@
}
setRgbVar('exo-yellow', getComputedStyle(document.documentElement).getPropertyValue('--exo-yellow').trim());
+ // Generate a consistent color for an instance ID using a simple hash
+ function generateInstanceColor(instanceId) {
+ if (!instanceId) return '#888888';
+
+ // Simple hash function
+ let hash = 0;
+ for (let i = 0; i < instanceId.length; i++) {
+ hash = instanceId.charCodeAt(i) + ((hash << 5) - hash);
+ }
+
+ // Convert to HSL for better color distribution
+ // Use high saturation and medium lightness for vibrant, distinguishable colors
+ const hue = Math.abs(hash % 360);
+ const saturation = 65 + (Math.abs(hash >> 8) % 20); // 65-85%
+ const lightness = 55 + (Math.abs(hash >> 16) % 15); // 55-70%
+
+ return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
+ }
+
const topologyGraphContainer = document.getElementById('topologyGraphContainer');
const lastUpdatedElement = document.getElementById('lastUpdated');
const nodeDetailPanel = document.getElementById('nodeDetailPanel');
@@ -1027,6 +1070,8 @@
const USE_MOCK_DATA = false; // <<< FLAG TO TOGGLE MOCK DATA
let currentlySelectedNodeId = null; // To store the ID of the currently selected node
let nodeIdToFriendlyName = {}; // Map nodeId -> friendly name for download sections
+ let instanceIdToColor = {}; // Map instanceId -> color for visual coding
+ let connectionToInstances = {}; // Map "nodeA|nodeB" -> [instanceIds] using that connection
const API_ENDPOINT = window.location.origin + window.location.pathname.replace(/\/$/, "") + '/state';
const REFRESH_INTERVAL = 1000; // 1 second
@@ -1449,6 +1494,33 @@
return;
}
+ // Build maps for instance colors and connection usage
+ instanceIdToColor = {};
+ connectionToInstances = {};
+
+ instancesArray.forEach(instance => {
+ const instanceId = instance.instanceId;
+ instanceIdToColor[instanceId] = generateInstanceColor(instanceId);
+
+ // Determine which nodes this instance uses
+ const nodeToRunner = instance.shardAssignments?.nodeToRunner || {};
+ const nodesUsed = Object.keys(nodeToRunner);
+
+ // For each pair of nodes, record that this instance uses that connection
+ for (let i = 0; i < nodesUsed.length; i++) {
+ for (let j = i + 1; j < nodesUsed.length; j++) {
+ const nodeA = nodesUsed[i];
+ const nodeB = nodesUsed[j];
+ const key = nodeA < nodeB ? `${nodeA}|${nodeB}` : `${nodeB}|${nodeA}`;
+
+ if (!connectionToInstances[key]) {
+ connectionToInstances[key] = [];
+ }
+ connectionToInstances[key].push(instanceId);
+ }
+ }
+ });
+
const instancesHTML = instancesArray.map(instance => {
const modelId = instance.shardAssignments?.modelId || 'Unknown Model';
const truncatedInstanceId = instance.instanceId.length > 8
@@ -1586,8 +1658,14 @@
}
const shardCount = Object.keys(runnerToShard).length;
+
+ // Use the instance's color for the indicator
+ const instanceColor = instanceIdToColor[instance.instanceId] || 'var(--exo-yellow)';
+ const borderStyle = `background-color: ${instanceColor};`;
+
return `
<div class="instance-item">
+ <div class="instance-color-indicator" style="${borderStyle}"></div>
<div class="instance-header">
<div class="instance-info">
<span class="instance-id">${truncatedInstanceId}</span>
@@ -1658,6 +1736,78 @@
}
}
+ // Helper function to create edge labels with optional colored indicators for instances
+ function createEdgeLabel(labelLines, labelX, labelY, parentGroup, instanceColors = []) {
+ if (!labelLines || labelLines.length === 0) return;
+
+ const colorStripWidth = 3; // Narrow strip width
+ const colorStripHeight = 12; // Taller for visibility
+ const colorStripSpacing = 1.5; // Small gap between strips
+ const paddingBetweenStripsAndText = 8; // Space between strips and text
+ const hasColorBoxes = instanceColors.length > 0;
+
+ // Create color indicator strips if colors are provided
+ let totalColorBoxWidth = 0;
+ if (hasColorBoxes) {
+ totalColorBoxWidth = instanceColors.length * (colorStripWidth + colorStripSpacing) - colorStripSpacing;
+ const stripsStartX = labelX - totalColorBoxWidth - paddingBetweenStripsAndText - 30; // Move 30px further left
+
+ instanceColors.forEach((color, idx) => {
+ const colorStrip = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ // Position strips well to the left of the text
+ const stripX = stripsStartX + idx * (colorStripWidth + colorStripSpacing);
+ colorStrip.setAttribute('x', stripX);
+ colorStrip.setAttribute('y', labelY - colorStripHeight / 2);
+ colorStrip.setAttribute('width', colorStripWidth);
+ colorStrip.setAttribute('height', colorStripHeight);
+ colorStrip.setAttribute('fill', color);
+ colorStrip.setAttribute('stroke', 'var(--exo-light-gray)');
+ colorStrip.setAttribute('stroke-width', '0.5');
+ colorStrip.setAttribute('rx', 1);
+ parentGroup.appendChild(colorStrip);
+ });
+ }
+
+ // Create text element
+ const labelText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ labelText.setAttribute('class', 'edge-label');
+ labelText.setAttribute('x', labelX);
+ labelText.setAttribute('y', labelY);
+
+ // Add background for better readability
+ const labelBg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ labelBg.setAttribute('class', 'edge-label-bg');
+
+ // Add each line as a tspan
+ labelLines.forEach((line, idx) => {
+ const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
+ tspan.setAttribute('x', labelX);
+ tspan.setAttribute('dy', idx === 0 ? '0' : '1.1em');
+ tspan.textContent = line;
+ labelText.appendChild(tspan);
+ });
+
+ // Add text first to get bounding box, then add background
+ parentGroup.appendChild(labelText);
+
+ // Get text bounding box and create background rect
+ try {
+ const bbox = labelText.getBBox();
+ const padding = 3;
+ const extraLeft = hasColorBoxes ? totalColorBoxWidth : 0;
+
+ // Background should cover text area only, strips are separate
+ labelBg.setAttribute('x', bbox.x - padding);
+ labelBg.setAttribute('y', bbox.y - padding);
+ labelBg.setAttribute('width', bbox.width + 2 * padding);
+ labelBg.setAttribute('height', bbox.height + 2 * padding);
+ labelBg.setAttribute('rx', 2);
+ parentGroup.insertBefore(labelBg, labelText);
+ } catch (e) {
+ console.error('Failed to get bbox for label:', e);
+ }
+ }
+
function renderNodes(topologyData) {
if (!topologyGraphContainer) return;
topologyGraphContainer.innerHTML = ''; // Clear previous SVG content
@@ -1730,13 +1880,16 @@
const arrowsGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
arrowsGroup.setAttribute('class', 'arrows-group');
arrowsGroup.setAttribute('style', 'pointer-events: none;');
+ const edgeLabelsGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ edgeLabelsGroup.setAttribute('class', 'edge-labels-group');
+ edgeLabelsGroup.setAttribute('style', 'pointer-events: none;');
// Build quick lookup for node positions
const positionById = {};
nodesWithPositions.forEach(n => { positionById[n.id] = { x: n.x, y: n.y }; });
// Group directed edges into undirected pairs to support single line with two arrows
- const pairMap = new Map(); // key: "a|b" with a<b, value: { a, b, aToB, bToA }
+ const pairMap = new Map(); // key: "a|b" with a<b, value: { a, b, aToB, bToA, aToBEdges, bToAEdges }
edgesData.forEach(edge => {
if (!edge || !edge.source || !edge.target) return;
if (!positionById[edge.source] || !positionById[edge.target]) return;
@@ -1744,8 +1897,14 @@
const a = edge.source < edge.target ? edge.source : edge.target;
const b = edge.source < edge.target ? edge.target : edge.source;
const key = `${a}|${b}`;
- const entry = pairMap.get(key) || { a, b, aToB: false, bToA: false };
- if (edge.source === a && edge.target === b) entry.aToB = true; else entry.bToA = true;
+ const entry = pairMap.get(key) || { a, b, aToB: false, bToA: false, aToBEdges: [], bToAEdges: [] };
+ if (edge.source === a && edge.target === b) {
+ entry.aToB = true;
+ entry.aToBEdges.push(edge); // Store all A->B edges
+ } else {
+ entry.bToA = true;
+ entry.bToAEdges.push(edge); // Store all B->A edges
+ }
pairMap.set(key, entry);
});
@@ -1799,6 +1958,52 @@
arrowSeg.setAttribute('fill', 'none');
arrowSeg.setAttribute('marker-end', 'url(#arrowhead)');
arrowsGroup.appendChild(arrowSeg);
+
+ // Add label for A->B direction (show all connections)
+ if (entry.aToBEdges && entry.aToBEdges.length > 0) {
+ // Count occurrences of each IP/interface combination
+ const connectionCounts = new Map();
+
+ entry.aToBEdges.forEach(edgeData => {
+ if (edgeData.sendBackIp) {
+ let ipLabel = edgeData.sendBackIp;
+ if (edgeData.sendBackInterface) {
+ ipLabel = `${edgeData.sendBackInterface}: ${ipLabel}`;
+ }
+ connectionCounts.set(ipLabel, (connectionCounts.get(ipLabel) || 0) + 1);
+ }
+ });
+
+ // Build label lines with counts for duplicates
+ const labelLines = [];
+ connectionCounts.forEach((count, ipLabel) => {
+ if (count > 1) {
+ labelLines.push(`${ipLabel} (${count})`);
+ } else {
+ labelLines.push(ipLabel);
+ }
+ });
+
+ if (labelLines.length > 0) {
+ // Position label before the A->B arrow (toward A side, away from arrow tip)
+ // Move further back from center along the line toward A
+ const labelPosX = mx - ux * (tipOffset * 2.5);
+ const labelPosY = my - uy * (tipOffset * 2.5);
+ // Offset perpendicular to the line (to the side)
+ const perpX = -uy;
+ const perpY = ux;
+ const labelOffset = 25; // Increased offset to be clearly beside the line
+ const labelX = labelPosX + perpX * labelOffset;
+ const labelY = labelPosY + perpY * labelOffset;
+
+ // Get colors for instances using this connection
+ const connectionKey = `${entry.a}|${entry.b}`;
+ const instancesUsingConnection = connectionToInstances[connectionKey] || [];
+ const instanceColors = instancesUsingConnection.map(id => instanceIdToColor[id]).filter(c => c);
+
+ createEdgeLabel(labelLines, labelX, labelY, edgeLabelsGroup, instanceColors);
+ }
+ }
}
if (entry.bToA) {
@@ -1818,6 +2023,52 @@
arrowSeg.setAttribute('fill', 'none');
arrowSeg.setAttribute('marker-end', 'url(#arrowhead)');
arrowsGroup.appendChild(arrowSeg);
+
+ // Add label for B->A direction (show all connections)
+ if (entry.bToAEdges && entry.bToAEdges.length > 0) {
+ // Count occurrences of each IP/interface combination
+ const connectionCounts = new Map();
+
+ entry.bToAEdges.forEach(edgeData => {
+ if (edgeData.sendBackIp) {
+ let ipLabel = edgeData.sendBackIp;
+ if (edgeData.sendBackInterface) {
+ ipLabel = `${edgeData.sendBackInterface}: ${ipLabel}`;
+ }
+ connectionCounts.set(ipLabel, (connectionCounts.get(ipLabel) || 0) + 1);
+ }
+ });
+
+ // Build label lines with counts for duplicates
+ const labelLines = [];
+ connectionCounts.forEach((count, ipLabel) => {
+ if (count > 1) {
+ labelLines.push(`${ipLabel} (${count})`);
+ } else {
+ labelLines.push(ipLabel);
+ }
+ });
+
+ if (labelLines.length > 0) {
+ // Position label before the B->A arrow (toward B side, away from arrow tip)
+ // Move further back from center along the line toward B
+ const labelPosX = mx + ux * (tipOffset * 2.5);
+ const labelPosY = my + uy * (tipOffset * 2.5);
+ // Offset perpendicular to the line (to the side)
+ const perpX = -uy;
+ const perpY = ux;
+ const labelOffset = 25; // Increased offset to be clearly beside the line
+ const labelX = labelPosX + perpX * labelOffset;
+ const labelY = labelPosY + perpY * labelOffset;
+
+ // Get colors for instances using this connection
+ const connectionKey = `${entry.a}|${entry.b}`;
+ const instancesUsingConnection = connectionToInstances[connectionKey] || [];
+ const instanceColors = instancesUsingConnection.map(id => instanceIdToColor[id]).filter(c => c);
+
+ createEdgeLabel(labelLines, labelX, labelY, edgeLabelsGroup, instanceColors);
+ }
+ }
}
});
// Create group for nodes
@@ -2327,8 +2578,9 @@
nodesGroup.appendChild(nodeG);
});
- // Draw order: lines at the very back, then nodes, then mid-line arrows on top
+ // Draw order: lines at the very back, then edge labels, then nodes, then mid-line arrows on top
topologyGraphContainer.appendChild(linksGroup);
+ topologyGraphContainer.appendChild(edgeLabelsGroup);
topologyGraphContainer.appendChild(nodesGroup);
topologyGraphContainer.appendChild(arrowsGroup);
}
@@ -2676,7 +2928,55 @@
if (!src || !dst) return;
if (!resultNodes[src] || !resultNodes[dst]) return; // only draw edges between known nodes
if (src === dst) return; // skip self loops for now
- resultEdges.push({ source: src, target: dst });
+
+ // Extract address information from connection
+ const sendBackMultiaddr = conn.sendBackMultiaddr ?? conn.send_back_multiaddr;
+
+ // Extract IP from sendBackMultiaddr object
+ // It might have properties like 'multiaddr' or be serialized differently
+ let sendBackAddrString = null;
+ if (sendBackMultiaddr) {
+ // Try different possible field names
+ sendBackAddrString = sendBackMultiaddr.multiaddr ??
+ sendBackMultiaddr.address ??
+ sendBackMultiaddr.addr ??
+ (typeof sendBackMultiaddr === 'string' ? sendBackMultiaddr : null);
+
+ // If it's still an object, try to convert to string
+ if (!sendBackAddrString && typeof sendBackMultiaddr === 'object') {
+ sendBackAddrString = sendBackMultiaddr.toString?.() ?? JSON.stringify(sendBackMultiaddr);
+ }
+ }
+
+ // Extract IP from the multiaddr string
+ const sendBackIp = _extractIpFromMultiaddr(sendBackAddrString);
+
+ // Try to map IP to interface name on destination node
+ let sendBackInterface = null;
+
+ if (sendBackIp && resultNodes[dst]) {
+ const dstNode = nodesToProcess[dst];
+ if (dstNode) {
+ const netIfacesSnake = dstNode.network_interfaces;
+ const netIfacesCamel = dstNode.networkInterfaces;
+ const interfaces = Array.isArray(netIfacesSnake) ? netIfacesSnake : (Array.isArray(netIfacesCamel) ? netIfacesCamel : []);
+ const matchingIface = interfaces.find(intf => {
+ const ip = intf.ip_address ?? intf.ipAddress;
+ return ip === sendBackIp;
+ });
+ if (matchingIface) {
+ sendBackInterface = matchingIface.name ?? matchingIface.interface_name ?? matchingIface.interfaceName;
+ }
+ }
+ }
+
+ resultEdges.push({
+ source: src,
+ target: dst,
+ sendBackIp: sendBackIp,
+ sendBackInterface: sendBackInterface,
+ multiaddr: sendBackAddrString
+ });
});
return { nodes: resultNodes, edges: resultEdges };
@@ -2791,6 +3091,12 @@
};
function updateMockData() {
+ // Build name map for mock nodes
+ nodeIdToFriendlyName = {};
+ for (const nodeId in mockData) {
+ nodeIdToFriendlyName[nodeId] = mockData[nodeId].friendly_name || nodeId;
+ }
+
for (const nodeId in mockData) {
const node = mockData[nodeId];
node.last_addr_update = (Date.now() / 1000) - (Math.random() * 10);
← 0bb621b6 Add mlx nn stubs
·
back to Exo
·
set max_transmit_size on gossipsub to 1MB. Fixes large messa 19e90572 →