From ae1ced57b919919aeef8c2775d67b0cddf02edad Mon Sep 17 00:00:00 2001 From: Sudo-Ivan Date: Mon, 9 Mar 2026 14:06:23 -0500 Subject: [PATCH] Add discovered interfaces feature to NetworkVisualiser component --- .../network-visualiser/NetworkVisualiser.vue | 153 ++++++++++++++---- 1 file changed, 122 insertions(+), 31 deletions(-) diff --git a/meshchatx/src/frontend/components/network-visualiser/NetworkVisualiser.vue b/meshchatx/src/frontend/components/network-visualiser/NetworkVisualiser.vue index a306ca8..19685b0 100644 --- a/meshchatx/src/frontend/components/network-visualiser/NetworkVisualiser.vue +++ b/meshchatx/src/frontend/components/network-visualiser/NetworkVisualiser.vue @@ -252,6 +252,13 @@
Multi-Hop +
+
+
+ Discovered ({{ discoveredInterfaces.length }}) +
@@ -291,6 +298,8 @@ export default { totalBatches: 0, interfaces: [], + discoveredInterfaces: [], + discoveredActive: [], pathTable: [], announces: {}, conversations: {}, @@ -405,6 +414,17 @@ export default { console.error("Failed to fetch interface stats", e); } }, + async getDiscoveredInterfaces() { + try { + const response = await window.axios.get(`/api/v1/reticulum/discovered-interfaces`, { + signal: this.abortController.signal, + }); + this.discoveredInterfaces = response.data?.interfaces ?? []; + this.discoveredActive = response.data?.active ?? []; + } catch (e) { + if (window.axios.isCancel(e)) return; + } + }, async getPathTableBatch() { this.pathTable = []; try { @@ -894,6 +914,12 @@ export default { nodes: { borderWidth: 3, borderWidthSelected: 6, + color: { + border: "#3b82f6", + background: isDarkMode ? "#1e40af" : "#eff6ff", + highlight: { border: "#3b82f6", background: isDarkMode ? "#2563eb" : "#dbeafe" }, + hover: { border: "#3b82f6", background: isDarkMode ? "#2563eb" : "#dbeafe" }, + }, font: { face: "Inter, system-ui, sans-serif", strokeWidth: 4, @@ -1020,26 +1046,40 @@ export default { }); this.nodes.update(updates); }, + nodeColor(border, background) { + return { + border, + background, + highlight: { border, background }, + hover: { border, background }, + }; + }, getNodeLODProps(node, lod) { const isDarkMode = document.documentElement.classList.contains("dark"); const fontColor = isDarkMode ? "#ffffff" : "#000000"; + const blueBorder = "#3b82f6"; + const blueBg = isDarkMode ? "#1e40af" : "#eff6ff"; if (lod === "low") { + const isInterface = node.group === "interface"; + const baseColor = isInterface && node.color + ? node.color + : this.nodeColor(blueBorder, blueBg); return { id: node.id, shape: "dot", size: node.id === "me" ? 15 : 10, - font: { size: 0 }, // hide labels + font: { size: 0 }, + color: baseColor, }; } else if (lod === "medium") { return { id: node.id, shape: node._originalShape || "circularImage", size: node._originalSize || (node.id === "me" ? 50 : 25), - font: { size: 0 }, // hide labels + font: { size: 0 }, }; } else { - // high return { id: node.id, shape: node._originalShape || "circularImage", @@ -1053,7 +1093,7 @@ export default { this.currentBatch = 0; this.totalBatches = 0; - await Promise.all([this.getConfig(), this.getInterfaceStats(), this.getConversations()]); + await Promise.all([this.getConfig(), this.getInterfaceStats(), this.getConversations(), this.getDiscoveredInterfaces()]); if (this.abortController.signal.aborted) return; this.loadingStatus = "Fetching network data..."; @@ -1093,7 +1133,7 @@ export default { image: this.reticulumLogoPath, label: meLabel, title: `Local Node: ${meLabel}\nIdentity: ${this.config?.identity_hash ?? "Unknown"}`, - color: { border: "#3b82f6", background: isDarkMode ? "#1e40af" : "#eff6ff" }, + color: this.nodeColor("#3b82f6", isDarkMode ? "#1e40af" : "#eff6ff"), font: { color: fontColor, size: 16, bold: true }, x: 0, y: 0, @@ -1134,10 +1174,10 @@ export default { image: entry.status ? "/assets/images/network-visualiser/interface_connected.png" : "/assets/images/network-visualiser/interface_disconnected.png", - color: { - border: entry.status ? "#10b981" : "#ef4444", - background: isDarkMode ? "#064e3b" : "#ecfdf5", - }, + color: this.nodeColor( + entry.status ? "#10b981" : "#ef4444", + isDarkMode ? "#064e3b" : "#ecfdf5" + ), font: { color: fontColor, size: 12, bold: true }, x: initialX, y: initialY, @@ -1163,6 +1203,67 @@ export default { if (interfaceNodes.length > 0) this.nodes.update(interfaceNodes); if (interfaceEdges.length > 0) this.edges.update(interfaceEdges); + const discoveredNodes = []; + const discoveredEdges = []; + for (const disc of this.discoveredInterfaces) { + const discId = `discovered~${disc.discovery_hash || disc.name}`; + const discLabel = disc.name || disc.reachable_on || "Unknown"; + if (!matchesSearch(discLabel) && !matchesSearch(disc.reachable_on) && !matchesSearch(disc.transport_id)) { + continue; + } + + const isConnected = this.discoveredActive.some((a) => { + const aHost = a.target_host || a.remote || a.listen_ip; + const aPort = a.target_port || a.listen_port; + return aHost && aPort && disc.reachable_on === aHost && String(disc.port) === String(aPort); + }); + + const angle = Math.random() * 2 * Math.PI; + const dist = 800 + Math.random() * 200; + let discNode = { + id: discId, + group: "discovered", + label: discLabel, + title: `Discovered: ${discLabel}\nType: ${disc.type || "Unknown"}\nHops: ${disc.hops ?? "?"}\nStatus: ${isConnected ? "Connected" : disc.status || "Available"}${disc.reachable_on ? `\nAddress: ${disc.reachable_on}:${disc.port}` : ""}`, + size: 25, + _originalSize: 25, + shape: "circularImage", + _originalShape: "circularImage", + image: isConnected + ? "/assets/images/network-visualiser/interface_connected.png" + : "/assets/images/network-visualiser/interface_disconnected.png", + color: this.nodeColor( + isConnected ? "#06b6d4" : "#64748b", + isDarkMode + ? isConnected ? "#164e63" : "#1e293b" + : isConnected ? "#ecfeff" : "#f1f5f9" + ), + font: { color: fontColor, size: 10 }, + x: Math.cos(angle) * dist, + y: Math.sin(angle) * dist, + }; + discNode = { ...discNode, ...this.getNodeLODProps(discNode, this.currentLOD) }; + discoveredNodes.push(discNode); + processedNodeIds.add(discId); + + const edgeId = `me~${discId}`; + discoveredEdges.push({ + id: edgeId, + from: "me", + to: discId, + color: { + color: isDarkMode ? "#155e75" : "#06b6d4", + opacity: 0.4, + }, + width: 1, + dashes: true, + hidden: this.enableOrbit, + }); + processedEdgeIds.add(edgeId); + } + if (discoveredNodes.length > 0) this.nodes.update(discoveredNodes); + if (discoveredEdges.length > 0) this.edges.update(discoveredEdges); + await this.$nextTick(); if (this.abortController.signal.aborted) return; @@ -1259,17 +1360,12 @@ export default { ? "/assets/images/network-visualiser/user_1hop.png" : "/assets/images/network-visualiser/user.png"; } - node.color = { - border: entry.hops === 1 ? "#10b981" : "#3b82f6", - background: - entry.hops === 1 - ? isDarkMode - ? "#064e3b" - : "#ecfdf5" - : isDarkMode - ? "#1e40af" - : "#eff6ff", - }; + node.color = this.nodeColor( + entry.hops === 1 ? "#10b981" : "#3b82f6", + entry.hops === 1 + ? isDarkMode ? "#064e3b" : "#ecfdf5" + : isDarkMode ? "#1e40af" : "#eff6ff" + ); } else if (announce.aspect === "nomadnetwork.node") { node.shape = "circularImage"; node._originalShape = "circularImage"; @@ -1277,17 +1373,12 @@ export default { entry.hops === 1 ? "/assets/images/network-visualiser/server_1hop.png" : "/assets/images/network-visualiser/server.png"; - node.color = { - border: entry.hops === 1 ? "#10b981" : "#8b5cf6", - background: - entry.hops === 1 - ? isDarkMode - ? "#064e3b" - : "#ecfdf5" - : isDarkMode - ? "#4c1d95" - : "#f5f3ff", - }; + node.color = this.nodeColor( + entry.hops === 1 ? "#10b981" : "#8b5cf6", + entry.hops === 1 + ? isDarkMode ? "#064e3b" : "#ecfdf5" + : isDarkMode ? "#4c1d95" : "#f5f3ff" + ); } node = { ...node, ...this.getNodeLODProps(node, this.currentLOD) };