[object Object]

← 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 →