diff --git a/web/views/index.erb b/web/views/index.erb
index fc4e47f..bf6a127 100644
--- a/web/views/index.erb
+++ b/web/views/index.erb
@@ -694,6 +694,7 @@ var(--fg); }
};
let allNodes = [];
let allNeighbors = [];
+ let nodesById = new Map();
let shortInfoAnchor = null;
let lastChatDate;
const NODE_LIMIT = 1000;
@@ -1472,6 +1473,43 @@ var(--fg); }
return `${padded}`;
}
+ function rebuildNodeIndex(nodes) {
+ nodesById = new Map();
+ if (!Array.isArray(nodes)) return;
+ for (const node of nodes) {
+ if (!node || typeof node !== 'object') continue;
+ const nodeId = typeof node.node_id === 'string'
+ ? node.node_id
+ : (typeof node.nodeId === 'string' ? node.nodeId : null);
+ if (!nodeId) continue;
+ nodesById.set(nodeId, node);
+ }
+ }
+
+ function getNeighborNodesFor(nodeId) {
+ if (typeof nodeId !== 'string' || nodeId.length === 0) return [];
+ const seen = new Set();
+ const neighbors = [];
+ if (!Array.isArray(allNeighbors) || allNeighbors.length === 0) return neighbors;
+ for (const entry of allNeighbors) {
+ if (!entry || typeof entry !== 'object') continue;
+ const sourceId = typeof entry.node_id === 'string' ? entry.node_id : null;
+ if (sourceId !== nodeId) continue;
+ const neighborId = typeof entry.neighbor_id === 'string' ? entry.neighbor_id : null;
+ if (!neighborId || seen.has(neighborId)) continue;
+ seen.add(neighborId);
+ const neighborNode = nodesById.get(neighborId);
+ if (neighborNode) {
+ neighbors.push(neighborNode);
+ continue;
+ }
+ const placeholder = { node_id: neighborId };
+ applyNodeNameFallback(placeholder);
+ neighbors.push(placeholder);
+ }
+ return neighbors;
+ }
+
function formatShortInfoUptime(value) {
if (value == null || value === '') return '';
const num = Number(value);
@@ -1542,6 +1580,21 @@ var(--fg); }
if (roleValue !== '—') {
lines.push(`Role: ${escapeHtml(roleValue)}`);
}
+ let neighborLineHtml = '';
+ const neighborNodes = getNeighborNodesFor(info.nodeId);
+ if (neighborNodes.length) {
+ const neighborParts = neighborNodes
+ .map(node => renderShortHtml(
+ node && (node.short_name ?? node.shortName),
+ node && node.role,
+ node && (node.long_name ?? node.longName),
+ node
+ ))
+ .filter(html => html && html.length);
+ if (neighborParts.length) {
+ neighborLineHtml = `Neighbors: ${neighborParts.join(' ')}`;
+ }
+ }
const modelValue = fmtHw(info.hwModel);
if (modelValue) {
lines.push(`Model: ${escapeHtml(modelValue)}`);
@@ -1554,6 +1607,9 @@ var(--fg); }
appendTelemetryLine(lines, 'Temperature', info.temperature, fmtTemperature);
appendTelemetryLine(lines, 'Humidity', info.humidity, fmtHumidity);
appendTelemetryLine(lines, 'Pressure', info.pressure, fmtPressure);
+ if (neighborLineHtml) {
+ lines.push(neighborLineHtml);
+ }
shortInfoContent.innerHTML = lines.join('
');
shortInfoAnchor = target;
shortInfoOverlay.hidden = false;
@@ -2062,6 +2118,20 @@ var(--fg); }
if (n.uptime_seconds) {
lines.push(`Uptime: ${timeHum(n.uptime_seconds)}`);
}
+ const mapNeighborNodes = getNeighborNodesFor(n.node_id ?? n.nodeId ?? '');
+ if (mapNeighborNodes.length) {
+ const neighborParts = mapNeighborNodes
+ .map(node => renderShortHtml(
+ node && (node.short_name ?? node.shortName),
+ node && node.role,
+ node && (node.long_name ?? node.longName),
+ node
+ ))
+ .filter(html => html && html.length);
+ if (neighborParts.length) {
+ lines.push(`Neighbors: ${neighborParts.join(' ')}`);
+ }
+ }
marker.bindPopup(lines.join('
'));
marker.addTo(markersLayer);
pts.push([lat, lon]);
@@ -2154,6 +2224,7 @@ var(--fg); }
}
renderChatLog(nodes, messages);
allNodes = nodes;
+ rebuildNodeIndex(allNodes);
allNeighbors = Array.isArray(neighborTuples) ? neighborTuples : [];
applyFilter();
statusEl.textContent = 'updated ' + new Date().toLocaleTimeString();