diff --git a/static/index.html b/static/index.html
index 1ad5840..f37c569 100644
--- a/static/index.html
+++ b/static/index.html
@@ -55,6 +55,7 @@
display: flex;
flex-direction: column;
gap: 20px;
+ overflow: visible;
}
.location {
@@ -117,6 +118,7 @@
overflow-wrap: break-word;
white-space: pre-line;
margin-top: 8px;
+ z-index: 1;
}
.node .switch-port {
@@ -369,6 +371,118 @@
box-shadow: 0 0 0 3px #f66, 0 0 0 6px #f90;
}
+ .node:hover {
+ z-index: 100;
+ }
+
+ .node .node-info {
+ display: none;
+ position: absolute;
+ top: 100%;
+ left: 50%;
+ transform: translateX(-50%);
+ margin-top: 4px;
+ background: #333;
+ border: 1px solid #555;
+ border-radius: 6px;
+ padding: 8px;
+ font-size: 10px;
+ white-space: nowrap;
+ z-index: 1000;
+ text-align: left;
+ }
+
+ .node .node-info::before {
+ content: '';
+ position: absolute;
+ bottom: 100%;
+ left: 0;
+ right: 0;
+ height: 8px;
+ }
+
+ .node:hover .node-info {
+ display: block;
+ will-change: transform;
+ }
+
+ .node:has(.switch-port:hover) .node-info,
+ .node:has(.uplink:hover) .node-info {
+ display: none;
+ }
+
+ .node .node-info .info-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 2px 0;
+ }
+
+ .node .node-info .info-label {
+ color: #888;
+ min-width: 28px;
+ }
+
+ .node .node-info .info-value {
+ color: #eee;
+ font-family: monospace;
+ }
+
+ .node .node-info .copy-btn {
+ padding: 2px 4px;
+ border: none;
+ background: transparent;
+ color: #888;
+ cursor: pointer;
+ font-size: 12px;
+ line-height: 1;
+ width: 20px;
+ text-align: center;
+ }
+
+ .node .node-info .copy-btn:hover {
+ color: #ccc;
+ }
+
+ .node .node-info .copy-btn.copied {
+ color: #4f4;
+ }
+
+ .node .node-info .info-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 2px 0;
+ }
+
+ .node .node-info .info-label {
+ color: #888;
+ min-width: 28px;
+ }
+
+ .node .node-info .info-value {
+ color: #eee;
+ font-family: monospace;
+ }
+
+ .node .node-info .copy-btn {
+ padding: 2px 4px;
+ border: none;
+ background: transparent;
+ color: #888;
+ cursor: pointer;
+ font-size: 12px;
+ line-height: 1;
+ }
+
+ .node .node-info .copy-btn:hover {
+ color: #ccc;
+ }
+
+ .node .node-info .copy-btn.copied {
+ color: #4f4;
+ }
+
#error-panel {
position: fixed;
top: 50px;
@@ -664,6 +778,7 @@
return '??';
}
+
function getNodeIdentifiers(node) {
const ids = [];
if (node.names) {
@@ -813,6 +928,58 @@
labelEl.textContent = getLabel(node);
div.appendChild(labelEl);
+ const nodeInfo = document.createElement('div');
+ nodeInfo.className = 'node-info';
+ if (node.interfaces) {
+ const ips = [];
+ const macs = [];
+ node.interfaces.forEach(iface => {
+ if (iface.ips) iface.ips.forEach(ip => { if (!ips.includes(ip)) ips.push(ip); });
+ if (iface.mac && !macs.includes(iface.mac)) macs.push(iface.mac);
+ });
+ ips.sort();
+ macs.sort();
+ ips.forEach(ip => {
+ const row = document.createElement('div');
+ row.className = 'info-row';
+ row.innerHTML = 'IP' + ip + '';
+ const btn = document.createElement('button');
+ btn.className = 'copy-btn';
+ btn.textContent = '⧉';
+ btn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ navigator.clipboard.writeText(ip).then(() => {
+ btn.classList.add('copied');
+ btn.textContent = '✓';
+ setTimeout(() => { btn.classList.remove('copied'); btn.textContent = '⧉'; }, 500);
+ });
+ });
+ row.appendChild(btn);
+ nodeInfo.appendChild(row);
+ });
+ macs.forEach(mac => {
+ const row = document.createElement('div');
+ row.className = 'info-row';
+ row.innerHTML = 'MAC' + mac + '';
+ const btn = document.createElement('button');
+ btn.className = 'copy-btn';
+ btn.textContent = '⧉';
+ btn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ navigator.clipboard.writeText(mac).then(() => {
+ btn.classList.add('copied');
+ btn.textContent = '✓';
+ setTimeout(() => { btn.classList.remove('copied'); btn.textContent = '⧉'; }, 500);
+ });
+ });
+ row.appendChild(btn);
+ nodeInfo.appendChild(row);
+ });
+ }
+ if (nodeInfo.children.length > 0) {
+ div.appendChild(nodeInfo);
+ }
+
if (isSwitch(node) && uplinkInfo === 'ROOT') {
const rootEl = document.createElement('div');
rootEl.className = 'root-label';