diff --git a/static/index.html b/static/index.html
index 7c257d6..b623319 100644
--- a/static/index.html
+++ b/static/index.html
@@ -122,10 +122,6 @@
}
.node .switch-port {
- position: absolute;
- top: -8px;
- left: 50%;
- transform: translateX(-50%);
font-size: 10px;
font-weight: normal;
background: #444;
@@ -133,16 +129,21 @@
padding: 1px 6px;
border-radius: 8px;
white-space: nowrap;
- overflow: visible;
- max-width: none;
}
.node .switch-port.external {
border: 1px dashed #c9f;
}
- .node .switch-port .link-stats-wrapper,
- .node .uplink .link-stats-wrapper {
+ .node .port-hover,
+ .node .uplink-hover {
+ position: absolute;
+ top: -8px;
+ left: 50%;
+ transform: translateX(-50%);
+ }
+
+ .node .link-stats-wrapper {
display: none;
position: absolute;
bottom: 100%;
@@ -151,9 +152,9 @@
padding-bottom: 8px;
}
- .node .switch-port .link-stats,
- .node .uplink .link-stats {
+ .node .link-stats {
font-size: 10px;
+ font-weight: normal;
white-space: pre;
text-align: left;
background: #333;
@@ -172,34 +173,29 @@
color: #888;
}
- .node .switch-port::after,
- .node .uplink::after {
+ .node .port-hover::after,
+ .node .uplink-hover::after {
content: '';
position: absolute;
top: 0;
- bottom: 0;
+ bottom: -8px;
left: 50%;
transform: translateX(-50%);
width: 120px;
}
- .node .switch-port:hover .link-stats-wrapper,
- .node .uplink:hover .link-stats-wrapper {
+ .node .port-hover:hover .link-stats-wrapper,
+ .node .uplink-hover:hover .link-stats-wrapper {
display: block;
will-change: transform;
}
- .node .switch-port:hover,
- .node .uplink:hover {
+ .node .port-hover:hover,
+ .node .uplink-hover:hover {
z-index: 100;
- will-change: transform;
}
.node .uplink {
- position: absolute;
- top: -8px;
- left: 50%;
- transform: translateX(-50%);
font-size: 10px;
font-weight: normal;
background: #444;
@@ -207,8 +203,6 @@
padding: 1px 6px;
border-radius: 8px;
white-space: nowrap;
- overflow: visible;
- max-width: none;
}
.node .root-label {
@@ -337,17 +331,12 @@
.node .dante-info {
display: none;
- position: absolute;
- top: -8px;
- left: 50%;
- transform: translateX(-50%);
font-size: 10px;
font-weight: normal;
padding: 1px 6px;
border-radius: 8px;
white-space: nowrap;
color: #fff;
- z-index: 10;
}
.node .dante-info.tx-info {
@@ -358,7 +347,14 @@
background: #358;
}
- .node .dante-info .dante-detail-wrapper {
+ .node .dante-hover {
+ position: absolute;
+ top: -8px;
+ left: 50%;
+ transform: translateX(-50%);
+ }
+
+ .node .dante-detail-wrapper {
display: none;
position: absolute;
bottom: 100%;
@@ -367,8 +363,9 @@
padding-bottom: 8px;
}
- .node .dante-info .dante-detail {
+ .node .dante-detail {
font-size: 10px;
+ font-weight: normal;
white-space: pre;
text-align: left;
background: #333;
@@ -378,21 +375,21 @@
line-height: 1.4;
}
- .node .dante-info::after {
+ .node .dante-hover::after {
content: '';
position: absolute;
top: 0;
- bottom: 0;
+ bottom: -8px;
left: 50%;
transform: translateX(-50%);
width: 120px;
}
- .node .dante-info:hover {
+ .node .dante-hover:hover {
z-index: 100;
}
- .node .dante-info:hover .dante-detail-wrapper {
+ .node .dante-hover:hover .dante-detail-wrapper {
display: block;
will-change: transform;
}
@@ -402,16 +399,12 @@
display: block;
}
- body.dante-mode .node.dante-tx.dante-rx .dante-info.tx-info {
- top: -8px;
- }
-
- body.dante-mode .node.dante-tx.dante-rx .dante-info.rx-info {
+ body.dante-mode .node.dante-tx.dante-rx .dante-rx-hover {
top: auto;
bottom: -8px;
}
- body.dante-mode .node.dante-tx.dante-rx .dante-info.rx-info .dante-detail-wrapper {
+ body.dante-mode .node.dante-tx.dante-rx .dante-rx-hover .dante-detail-wrapper {
bottom: auto;
top: 100%;
padding-bottom: 0;
@@ -445,17 +438,12 @@
.node .artnet-info {
display: none;
- position: absolute;
- top: -8px;
- left: 50%;
- transform: translateX(-50%);
font-size: 10px;
font-weight: normal;
padding: 1px 6px;
border-radius: 8px;
white-space: nowrap;
color: #fff;
- z-index: 10;
}
.node .artnet-info.out-info {
@@ -466,7 +454,14 @@
background: #245;
}
- .node .artnet-info .artnet-detail-wrapper {
+ .node .artnet-hover {
+ position: absolute;
+ top: -8px;
+ left: 50%;
+ transform: translateX(-50%);
+ }
+
+ .node .artnet-detail-wrapper {
display: none;
position: absolute;
bottom: 100%;
@@ -475,8 +470,9 @@
padding-bottom: 8px;
}
- .node .artnet-info .artnet-detail {
+ .node .artnet-detail {
font-size: 10px;
+ font-weight: normal;
white-space: pre;
text-align: left;
background: #333;
@@ -486,21 +482,21 @@
line-height: 1.4;
}
- .node .artnet-info::after {
+ .node .artnet-hover::after {
content: '';
position: absolute;
top: 0;
- bottom: 0;
+ bottom: -8px;
left: 50%;
transform: translateX(-50%);
width: 120px;
}
- .node .artnet-info:hover {
+ .node .artnet-hover:hover {
z-index: 100;
}
- .node .artnet-info:hover .artnet-detail-wrapper {
+ .node .artnet-hover:hover .artnet-detail-wrapper {
display: block;
will-change: transform;
}
@@ -510,16 +506,12 @@
display: block;
}
- body.artnet-mode .node.artnet-out.artnet-in .artnet-info.out-info {
- top: -8px;
- }
-
- body.artnet-mode .node.artnet-out.artnet-in .artnet-info.in-info {
+ body.artnet-mode .node.artnet-out.artnet-in .artnet-in-hover {
top: auto;
bottom: -8px;
}
- body.artnet-mode .node.artnet-out.artnet-in .artnet-info.in-info .artnet-detail-wrapper {
+ body.artnet-mode .node.artnet-out.artnet-in .artnet-in-hover .artnet-detail-wrapper {
bottom: auto;
top: 100%;
padding-bottom: 0;
@@ -544,10 +536,6 @@
.node .sacn-info {
display: none;
- position: absolute;
- top: -8px;
- left: 50%;
- transform: translateX(-50%);
font-size: 10px;
font-weight: normal;
padding: 1px 6px;
@@ -555,10 +543,16 @@
white-space: nowrap;
background: #468;
color: #fff;
- z-index: 10;
}
- .node .sacn-info .sacn-detail-wrapper {
+ .node .sacn-hover {
+ position: absolute;
+ top: -8px;
+ left: 50%;
+ transform: translateX(-50%);
+ }
+
+ .node .sacn-detail-wrapper {
display: none;
position: absolute;
bottom: 100%;
@@ -567,8 +561,9 @@
padding-bottom: 8px;
}
- .node .sacn-info .sacn-detail {
+ .node .sacn-detail {
font-size: 10px;
+ font-weight: normal;
white-space: pre;
text-align: left;
background: #333;
@@ -578,21 +573,21 @@
line-height: 1.4;
}
- .node .sacn-info::after {
+ .node .sacn-hover::after {
content: '';
position: absolute;
top: 0;
- bottom: 0;
+ bottom: -8px;
left: 50%;
transform: translateX(-50%);
width: 120px;
}
- .node .sacn-info:hover {
+ .node .sacn-hover:hover {
z-index: 100;
}
- .node .sacn-info:hover .sacn-detail-wrapper {
+ .node .sacn-hover:hover .sacn-detail-wrapper {
display: block;
will-change: transform;
}
@@ -638,6 +633,7 @@
border-radius: 6px;
padding: 6px 8px;
font-size: 10px;
+ font-weight: normal;
white-space: pre;
text-align: left;
line-height: 1.4;
@@ -666,11 +662,11 @@
display: none;
}
- .node:has(.switch-port:hover) .node-info-wrapper,
- .node:has(.uplink:hover) .node-info-wrapper,
- .node:has(.dante-info:hover) .node-info-wrapper,
- .node:has(.artnet-info:hover) .node-info-wrapper,
- .node:has(.sacn-info:hover) .node-info-wrapper {
+ .node:has(.port-hover:hover) .node-info-wrapper,
+ .node:has(.uplink-hover:hover) .node-info-wrapper,
+ .node:has(.dante-hover:hover) .node-info-wrapper,
+ .node:has(.artnet-hover:hover) .node-info-wrapper,
+ .node:has(.sacn-hover:hover) .node-info-wrapper {
display: none;
}
@@ -1111,6 +1107,10 @@
}
let anonCounter = 0;
+ const nodeElements = new Map();
+ const locationElements = new Map();
+ let usedNodeIds = new Set();
+ let usedLocationIds = new Set();
function buildLocationTree(locations, parent) {
if (!locations) return [];
@@ -1183,54 +1183,70 @@
}
function createNodeElement(node, switchConnection, nodeLocation, uplinkInfo, danteInfo, artnetInfo, sacnInfo, hasError, isUnreachable) {
- const div = document.createElement('div');
+ let div = nodeElements.get(node.typeid);
+ if (!div) {
+ div = document.createElement('div');
+ div.dataset.typeid = node.typeid;
+ div.addEventListener('click', () => {
+ const nodeData = div._nodeData;
+ if (!nodeData) return;
+ let copyText = nodeData.names?.length > 0 ? nodeData.names.join('\n') : getLabel(nodeData);
+ navigator.clipboard.writeText(copyText).then(() => {
+ div.classList.add('copied');
+ setTimeout(() => div.classList.remove('copied'), 300);
+ });
+ });
+ nodeElements.set(node.typeid, div);
+ }
+ div._nodeData = node;
+
div.className = 'node' + (isSwitch(node) ? ' switch' : '');
- div.dataset.typeid = node.typeid;
if (hasError) div.classList.add('has-error');
if (isUnreachable) div.classList.add('unreachable');
+ if (danteInfo?.isTx) div.classList.add('dante-tx');
+ if (danteInfo?.isRx) div.classList.add('dante-rx');
+ if (artnetInfo?.isOut) div.classList.add('artnet-out');
+ if (artnetInfo?.isIn) div.classList.add('artnet-in');
+ if (sacnInfo?.isConsumer) div.classList.add('sacn-consumer');
- if (danteInfo) {
- if (danteInfo.isTx) div.classList.add('dante-tx');
- if (danteInfo.isRx) div.classList.add('dante-rx');
- }
-
- if (artnetInfo) {
- if (artnetInfo.isOut) div.classList.add('artnet-out');
- if (artnetInfo.isIn) div.classList.add('artnet-in');
- }
-
- if (sacnInfo && sacnInfo.isConsumer) {
- div.classList.add('sacn-consumer');
- }
-
+ // Switch port connection
if (!isSwitch(node) && switchConnection) {
- const portEl = document.createElement('div');
+ let container = div.querySelector(':scope > .port-hover');
+ if (!container) {
+ container = document.createElement('div');
+ container.className = 'port-hover';
+ container.innerHTML = '
';
+ div.appendChild(container);
+ }
+ const portEl = container.querySelector('.switch-port');
portEl.className = 'switch-port';
- if (switchConnection.external) {
- portEl.classList.add('external');
- }
- if (switchConnection.showSwitchName) {
- portEl.textContent = switchConnection.switchName + ':' + switchConnection.port;
- } else {
- portEl.textContent = switchConnection.port;
- }
+ if (switchConnection.external) portEl.classList.add('external');
const speedClass = getSpeedClass(switchConnection.speed);
if (speedClass) portEl.classList.add(speedClass);
+ portEl.textContent = switchConnection.showSwitchName
+ ? switchConnection.switchName + ':' + switchConnection.port
+ : switchConnection.port;
+
+ const statsEl = container.querySelector('.link-stats');
+ statsEl.innerHTML = '';
const errIn = switchConnection.errors?.in || 0;
const errOut = switchConnection.errors?.out || 0;
- const statsWrapper = document.createElement('div');
- statsWrapper.className = 'link-stats-wrapper';
- const statsInfo = document.createElement('div');
- statsInfo.className = 'link-stats';
const r = switchConnection.rates;
- buildLinkStats(statsInfo, switchConnection.speed, errIn, errOut,
+ buildLinkStats(statsEl, switchConnection.speed, errIn, errOut,
r ? {rxBytes: r.outBytes, rxPkts: r.outPkts, txBytes: r.inBytes, txPkts: r.inPkts} : null);
- statsWrapper.appendChild(statsInfo);
- portEl.appendChild(statsWrapper);
- div.appendChild(portEl);
+ } else {
+ const container = div.querySelector(':scope > .port-hover');
+ if (container) container.remove();
}
- const labelEl = document.createElement('span');
+ // Label
+ let labelEl = div.querySelector(':scope > .node-label');
+ if (!labelEl) {
+ labelEl = document.createElement('span');
+ labelEl.className = 'node-label';
+ div.appendChild(labelEl);
+ }
+ labelEl.innerHTML = '';
if (node.names && node.names.length > 0) {
node.names.forEach((name, idx) => {
if (idx > 0) labelEl.appendChild(document.createTextNode('\n'));
@@ -1249,13 +1265,22 @@
} else {
labelEl.textContent = getLabel(node);
}
- div.appendChild(labelEl);
- const nodeInfoWrapper = document.createElement('div');
- nodeInfoWrapper.className = 'node-info-wrapper';
- const nodeInfo = document.createElement('div');
- nodeInfo.className = 'node-info';
- if (node.interfaces) {
+ // Node info popup
+ const hasNodeInfo = node.interfaces && (
+ node.interfaces.some(i => i.ips?.length > 0) ||
+ node.interfaces.some(i => i.mac)
+ );
+ if (hasNodeInfo) {
+ let wrapper = div.querySelector(':scope > .node-info-wrapper');
+ if (!wrapper) {
+ wrapper = document.createElement('div');
+ wrapper.className = 'node-info-wrapper';
+ wrapper.innerHTML = '';
+ div.appendChild(wrapper);
+ }
+ const nodeInfo = wrapper.querySelector('.node-info');
+ nodeInfo.innerHTML = '';
const ips = [];
const macs = [];
node.interfaces.forEach(iface => {
@@ -1274,135 +1299,170 @@
addClickableValue(nodeInfo, 'MAC', mac, plainLines);
});
if (plainLines.length > 0) {
- nodeInfo.addEventListener('click', (e) => {
+ nodeInfo.onclick = (e) => {
e.stopPropagation();
navigator.clipboard.writeText(plainLines.join('\n'));
- });
+ };
}
- }
- if (nodeInfo.textContent) {
- nodeInfoWrapper.appendChild(nodeInfo);
- div.appendChild(nodeInfoWrapper);
+ } else {
+ const wrapper = div.querySelector(':scope > .node-info-wrapper');
+ if (wrapper) wrapper.remove();
}
+ // Switch uplink / root label
if (isSwitch(node) && uplinkInfo === 'ROOT') {
- const rootEl = document.createElement('div');
- rootEl.className = 'root-label';
- rootEl.textContent = 'ROOT';
- div.appendChild(rootEl);
+ const container = div.querySelector(':scope > .uplink-hover');
+ if (container) container.remove();
+
+ let rootEl = div.querySelector(':scope > .root-label');
+ if (!rootEl) {
+ rootEl = document.createElement('div');
+ rootEl.className = 'root-label';
+ rootEl.textContent = 'ROOT';
+ div.appendChild(rootEl);
+ }
} else if (isSwitch(node) && uplinkInfo) {
- const uplinkEl = document.createElement('div');
+ const rootEl = div.querySelector(':scope > .root-label');
+ if (rootEl) rootEl.remove();
+
+ let container = div.querySelector(':scope > .uplink-hover');
+ if (!container) {
+ container = document.createElement('div');
+ container.className = 'uplink-hover';
+ container.innerHTML = '';
+ div.appendChild(container);
+ }
+ const uplinkEl = container.querySelector('.uplink');
uplinkEl.className = 'uplink';
- uplinkEl.textContent = uplinkInfo.localPort + ' → ' + uplinkInfo.parentName + ':' + uplinkInfo.remotePort;
const speedClass = getSpeedClass(uplinkInfo.speed);
if (speedClass) uplinkEl.classList.add(speedClass);
+ uplinkEl.textContent = uplinkInfo.localPort + ' → ' + uplinkInfo.parentName + ':' + uplinkInfo.remotePort;
+
+ const statsEl = container.querySelector('.link-stats');
+ statsEl.innerHTML = '';
const errIn = uplinkInfo.errors?.in || 0;
const errOut = uplinkInfo.errors?.out || 0;
- const statsWrapper = document.createElement('div');
- statsWrapper.className = 'link-stats-wrapper';
- const statsInfo = document.createElement('div');
- statsInfo.className = 'link-stats';
const r = uplinkInfo.rates;
- buildLinkStats(statsInfo, uplinkInfo.speed, errIn, errOut,
+ buildLinkStats(statsEl, uplinkInfo.speed, errIn, errOut,
r ? {rxBytes: r.inBytes, rxPkts: r.inPkts, txBytes: r.outBytes, txPkts: r.outPkts} : null);
- statsWrapper.appendChild(statsInfo);
- uplinkEl.appendChild(statsWrapper);
- div.appendChild(uplinkEl);
+ } else {
+ const rootEl = div.querySelector(':scope > .root-label');
+ if (rootEl) rootEl.remove();
+ const container = div.querySelector(':scope > .uplink-hover');
+ if (container) container.remove();
}
- if (danteInfo && danteInfo.isTx) {
- const txEl = document.createElement('div');
- txEl.className = 'dante-info tx-info';
+ // Dante TX
+ if (danteInfo?.isTx) {
+ let container = div.querySelector(':scope > .dante-tx-hover');
+ if (!container) {
+ container = document.createElement('div');
+ container.className = 'dante-hover dante-tx-hover';
+ container.innerHTML = '→
';
+ div.appendChild(container);
+ }
+ const textEl = container.querySelector('.dante-pill-text');
const firstDest = danteInfo.txTo[0].split('\n')[0];
const txMore = danteInfo.txTo.length > 1 ? ', ...' : '';
- txEl.innerHTML = '→ ' + firstDest + txMore;
- const detailWrapper = document.createElement('div');
- detailWrapper.className = 'dante-detail-wrapper';
- const detail = document.createElement('div');
- detail.className = 'dante-detail';
+ textEl.textContent = firstDest + txMore;
+
+ const detail = container.querySelector('.dante-detail');
+ detail.innerHTML = '';
buildDanteDetail(detail, danteInfo.txTo, '→');
- detailWrapper.appendChild(detail);
- txEl.appendChild(detailWrapper);
- div.appendChild(txEl);
+ } else {
+ const container = div.querySelector(':scope > .dante-tx-hover');
+ if (container) container.remove();
}
- if (danteInfo && danteInfo.isRx) {
- const rxEl = document.createElement('div');
- rxEl.className = 'dante-info rx-info';
+ // Dante RX
+ if (danteInfo?.isRx) {
+ let container = div.querySelector(':scope > .dante-rx-hover');
+ if (!container) {
+ container = document.createElement('div');
+ container.className = 'dante-hover dante-rx-hover';
+ container.innerHTML = '←
';
+ div.appendChild(container);
+ }
+ const textEl = container.querySelector('.dante-pill-text');
const firstSource = danteInfo.rxFrom[0].split('\n')[0];
const rxMore = danteInfo.rxFrom.length > 1 ? ', ...' : '';
- rxEl.innerHTML = '← ' + firstSource + rxMore;
- const detailWrapper = document.createElement('div');
- detailWrapper.className = 'dante-detail-wrapper';
- const detail = document.createElement('div');
- detail.className = 'dante-detail';
+ textEl.textContent = firstSource + rxMore;
+
+ const detail = container.querySelector('.dante-detail');
+ detail.innerHTML = '';
buildDanteDetail(detail, danteInfo.rxFrom, '←');
- detailWrapper.appendChild(detail);
- rxEl.appendChild(detailWrapper);
- div.appendChild(rxEl);
+ } else {
+ const container = div.querySelector(':scope > .dante-rx-hover');
+ if (container) container.remove();
}
- if (artnetInfo && artnetInfo.isOut) {
- const outEl = document.createElement('div');
- outEl.className = 'artnet-info out-info';
+ // Art-Net out
+ if (artnetInfo?.isOut) {
+ let container = div.querySelector(':scope > .artnet-out-hover');
+ if (!container) {
+ container = document.createElement('div');
+ container.className = 'artnet-hover artnet-out-hover';
+ container.innerHTML = '←
';
+ div.appendChild(container);
+ }
+ const textEl = container.querySelector('.artnet-pill-text');
const firstOut = artnetInfo.outputs[0];
const outLabel = firstOut.firstTarget || firstOut.display;
const outMore = artnetInfo.outputs.length > 1 ? ', ...' : '';
- outEl.innerHTML = '← ' + outLabel + outMore;
- const detailWrapper = document.createElement('div');
- detailWrapper.className = 'artnet-detail-wrapper';
- const detail = document.createElement('div');
- detail.className = 'artnet-detail';
+ textEl.textContent = outLabel + outMore;
+
+ const detail = container.querySelector('.artnet-detail');
+ detail.innerHTML = '';
buildClickableList(detail, artnetInfo.outputs.map(o => o.display), '←', (l, v) => l + ' ' + v);
- detailWrapper.appendChild(detail);
- outEl.appendChild(detailWrapper);
- div.appendChild(outEl);
+ } else {
+ const container = div.querySelector(':scope > .artnet-out-hover');
+ if (container) container.remove();
}
- if (artnetInfo && artnetInfo.isIn) {
- const inEl = document.createElement('div');
- inEl.className = 'artnet-info in-info';
+ // Art-Net in
+ if (artnetInfo?.isIn) {
+ let container = div.querySelector(':scope > .artnet-in-hover');
+ if (!container) {
+ container = document.createElement('div');
+ container.className = 'artnet-hover artnet-in-hover';
+ container.innerHTML = '→
';
+ div.appendChild(container);
+ }
+ const textEl = container.querySelector('.artnet-pill-text');
const firstIn = artnetInfo.inputs[0];
const inLabel = firstIn.firstTarget || firstIn.display;
const inMore = artnetInfo.inputs.length > 1 ? ', ...' : '';
- inEl.innerHTML = '→ ' + inLabel + inMore;
- const detailWrapper = document.createElement('div');
- detailWrapper.className = 'artnet-detail-wrapper';
- const detail = document.createElement('div');
- detail.className = 'artnet-detail';
+ textEl.textContent = inLabel + inMore;
+
+ const detail = container.querySelector('.artnet-detail');
+ detail.innerHTML = '';
buildClickableList(detail, artnetInfo.inputs.map(i => i.display), '→', (l, v) => l + ' ' + v);
- detailWrapper.appendChild(detail);
- inEl.appendChild(detailWrapper);
- div.appendChild(inEl);
+ } else {
+ const container = div.querySelector(':scope > .artnet-in-hover');
+ if (container) container.remove();
}
- if (sacnInfo && sacnInfo.isConsumer) {
- const sacnEl = document.createElement('div');
- sacnEl.className = 'sacn-info';
- const sacnMore = sacnInfo.universes.length > 1 ? ', ...' : '';
- sacnEl.innerHTML = '← ' + sacnInfo.universes[0] + sacnMore;
- const detailWrapper = document.createElement('div');
- detailWrapper.className = 'sacn-detail-wrapper';
- const detail = document.createElement('div');
- detail.className = 'sacn-detail';
- buildClickableList(detail, sacnInfo.universes, '←', (l, v) => l + ' ' + v);
- detailWrapper.appendChild(detail);
- sacnEl.appendChild(detailWrapper);
- div.appendChild(sacnEl);
- }
-
- div.addEventListener('click', () => {
- let copyText;
- if (node.names && node.names.length > 0) {
- copyText = node.names.join('\n');
- } else {
- copyText = getLabel(node);
+ // sACN
+ if (sacnInfo?.isConsumer) {
+ let container = div.querySelector(':scope > .sacn-hover');
+ if (!container) {
+ container = document.createElement('div');
+ container.className = 'sacn-hover';
+ container.innerHTML = '←
';
+ div.appendChild(container);
}
- navigator.clipboard.writeText(copyText).then(() => {
- div.classList.add('copied');
- setTimeout(() => div.classList.remove('copied'), 300);
- });
- });
+ const textEl = container.querySelector('.sacn-pill-text');
+ const sacnMore = sacnInfo.universes.length > 1 ? ', ...' : '';
+ textEl.textContent = sacnInfo.universes[0] + sacnMore;
+
+ const detail = container.querySelector('.sacn-detail');
+ detail.innerHTML = '';
+ buildClickableList(detail, sacnInfo.universes, '←', (l, v) => l + ' ' + v);
+ } else {
+ const container = div.querySelector(':scope > .sacn-hover');
+ if (container) container.remove();
+ }
+
return div;
}
@@ -1418,57 +1478,110 @@
return null;
}
- const container = document.createElement('div');
+ usedLocationIds.add(loc.id);
+ let container = locationElements.get(loc.id);
+ if (!container) {
+ container = document.createElement('div');
+ container.dataset.locid = loc.id;
+ locationElements.set(loc.id, container);
+ }
let classes = 'location';
if (loc.anonymous) classes += ' anonymous';
if (isTopLevel) classes += ' top-level';
container.className = classes;
- const nameEl = document.createElement('div');
- nameEl.className = 'location-name';
+ let nameEl = container.querySelector(':scope > .location-name');
+ if (!nameEl) {
+ nameEl = document.createElement('div');
+ nameEl.className = 'location-name';
+ container.insertBefore(nameEl, container.firstChild);
+ }
nameEl.textContent = loc.name;
- container.appendChild(nameEl);
+
+ const switchRowId = loc.id + '_sw';
+ const nodeRowId = loc.id + '_nd';
if (hasNodes) {
const switches = nodes.filter(n => isSwitch(n));
const nonSwitches = nodes.filter(n => !isSwitch(n));
if (switches.length > 0) {
- const switchRow = document.createElement('div');
- switchRow.className = 'node-row';
+ let switchRow = container.querySelector(':scope > .node-row[data-rowid="' + switchRowId + '"]');
+ if (!switchRow) {
+ switchRow = document.createElement('div');
+ switchRow.className = 'node-row';
+ switchRow.dataset.rowid = switchRowId;
+ const insertPt = container.querySelector(':scope > .node-row, :scope > .children');
+ container.insertBefore(switchRow, insertPt);
+ }
+ const currentIds = new Set(switches.map(n => n.typeid));
+ Array.from(switchRow.children).forEach(ch => {
+ if (!currentIds.has(ch.dataset.typeid)) ch.remove();
+ });
switches.forEach(node => {
+ usedNodeIds.add(node.typeid);
const uplink = switchUplinks.get(node.typeid);
const danteInfo = danteNodes.get(node.typeid);
const artnetInfo = artnetNodes.get(node.typeid);
const sacnInfo = sacnNodes.get(node.typeid);
const hasError = errorNodeIds.has(node.typeid);
const isUnreachable = unreachableNodeIds.has(node.typeid);
- switchRow.appendChild(createNodeElement(node, null, loc, uplink, danteInfo, artnetInfo, sacnInfo, hasError, isUnreachable));
+ const el = createNodeElement(node, null, loc, uplink, danteInfo, artnetInfo, sacnInfo, hasError, isUnreachable);
+ if (el.parentNode !== switchRow) switchRow.appendChild(el);
});
- container.appendChild(switchRow);
+ } else {
+ const switchRow = container.querySelector(':scope > .node-row[data-rowid="' + switchRowId + '"]');
+ if (switchRow) switchRow.remove();
}
if (nonSwitches.length > 0) {
- const nodeRow = document.createElement('div');
- nodeRow.className = 'node-row';
+ let nodeRow = container.querySelector(':scope > .node-row[data-rowid="' + nodeRowId + '"]');
+ if (!nodeRow) {
+ nodeRow = document.createElement('div');
+ nodeRow.className = 'node-row';
+ nodeRow.dataset.rowid = nodeRowId;
+ const insertPt = container.querySelector(':scope > .children');
+ container.insertBefore(nodeRow, insertPt);
+ }
+ const currentIds = new Set(nonSwitches.map(n => n.typeid));
+ Array.from(nodeRow.children).forEach(ch => {
+ if (!currentIds.has(ch.dataset.typeid)) ch.remove();
+ });
nonSwitches.forEach(node => {
+ usedNodeIds.add(node.typeid);
const conn = switchConnections.get(node.typeid);
const danteInfo = danteNodes.get(node.typeid);
const artnetInfo = artnetNodes.get(node.typeid);
const sacnInfo = sacnNodes.get(node.typeid);
const hasError = errorNodeIds.has(node.typeid);
const isUnreachable = unreachableNodeIds.has(node.typeid);
- nodeRow.appendChild(createNodeElement(node, conn, loc, null, danteInfo, artnetInfo, sacnInfo, hasError, isUnreachable));
+ const el = createNodeElement(node, conn, loc, null, danteInfo, artnetInfo, sacnInfo, hasError, isUnreachable);
+ if (el.parentNode !== nodeRow) nodeRow.appendChild(el);
});
- container.appendChild(nodeRow);
+ } else {
+ const nodeRow = container.querySelector(':scope > .node-row[data-rowid="' + nodeRowId + '"]');
+ if (nodeRow) nodeRow.remove();
}
+ } else {
+ const switchRow = container.querySelector(':scope > .node-row[data-rowid="' + switchRowId + '"]');
+ if (switchRow) switchRow.remove();
+ const nodeRow = container.querySelector(':scope > .node-row[data-rowid="' + nodeRowId + '"]');
+ if (nodeRow) nodeRow.remove();
}
if (childElements.length > 0) {
- const childrenContainer = document.createElement('div');
+ let childrenContainer = container.querySelector(':scope > .children');
+ if (!childrenContainer) {
+ childrenContainer = document.createElement('div');
+ container.appendChild(childrenContainer);
+ }
childrenContainer.className = 'children ' + loc.direction;
- childElements.forEach(el => childrenContainer.appendChild(el));
- container.appendChild(childrenContainer);
+ childElements.forEach(el => {
+ if (el.parentNode !== childrenContainer) childrenContainer.appendChild(el);
+ });
+ } else {
+ const childrenContainer = container.querySelector(':scope > .children');
+ if (childrenContainer) childrenContainer.remove();
}
return container;
@@ -1912,58 +2025,93 @@
}
const container = document.getElementById('container');
- container.innerHTML = '';
+ usedNodeIds = new Set();
+ usedLocationIds = new Set();
locationTree.forEach(loc => {
const el = renderLocation(loc, assignedNodes, true, switchConnections, switchUplinks, danteNodes, artnetNodes, sacnNodes, errorNodeIds, unreachableNodeIds);
- if (el) container.appendChild(el);
+ if (el && el.parentNode !== container) container.appendChild(el);
});
+ let unassignedLoc = locationElements.get('__unassigned__');
if (unassignedNodes.length > 0) {
- const unassignedLoc = document.createElement('div');
- unassignedLoc.className = 'location top-level';
-
- const nameEl = document.createElement('div');
- nameEl.className = 'location-name';
- nameEl.textContent = 'Unassigned';
- unassignedLoc.appendChild(nameEl);
+ if (!unassignedLoc) {
+ unassignedLoc = document.createElement('div');
+ unassignedLoc.className = 'location top-level';
+ const nameEl = document.createElement('div');
+ nameEl.className = 'location-name';
+ nameEl.textContent = 'Unassigned';
+ unassignedLoc.appendChild(nameEl);
+ locationElements.set('__unassigned__', unassignedLoc);
+ }
const switches = unassignedNodes.filter(n => isSwitch(n));
const nonSwitches = unassignedNodes.filter(n => !isSwitch(n));
+ let switchRow = unassignedLoc.querySelector(':scope > .node-row.switch-row');
if (switches.length > 0) {
- const switchRow = document.createElement('div');
- switchRow.className = 'node-row';
+ if (!switchRow) {
+ switchRow = document.createElement('div');
+ switchRow.className = 'node-row switch-row';
+ unassignedLoc.appendChild(switchRow);
+ }
+ const currentIds = new Set(switches.map(n => n.typeid));
+ Array.from(switchRow.children).forEach(ch => {
+ if (!currentIds.has(ch.dataset.typeid)) ch.remove();
+ });
switches.forEach(node => {
+ usedNodeIds.add(node.typeid);
const uplink = switchUplinks.get(node.typeid);
const danteInfo = danteNodes.get(node.typeid);
const artnetInfo = artnetNodes.get(node.typeid);
const sacnInfo = sacnNodes.get(node.typeid);
const hasError = errorNodeIds.has(node.typeid);
const isUnreachable = unreachableNodeIds.has(node.typeid);
- switchRow.appendChild(createNodeElement(node, null, null, uplink, danteInfo, artnetInfo, sacnInfo, hasError, isUnreachable));
+ const el = createNodeElement(node, null, null, uplink, danteInfo, artnetInfo, sacnInfo, hasError, isUnreachable);
+ if (el.parentNode !== switchRow) switchRow.appendChild(el);
});
- unassignedLoc.appendChild(switchRow);
+ } else if (switchRow) {
+ switchRow.remove();
}
+ let nodeRow = unassignedLoc.querySelector(':scope > .node-row:not(.switch-row)');
if (nonSwitches.length > 0) {
- const nodeRow = document.createElement('div');
- nodeRow.className = 'node-row';
+ if (!nodeRow) {
+ nodeRow = document.createElement('div');
+ nodeRow.className = 'node-row';
+ unassignedLoc.appendChild(nodeRow);
+ }
+ const currentIds = new Set(nonSwitches.map(n => n.typeid));
+ Array.from(nodeRow.children).forEach(ch => {
+ if (!currentIds.has(ch.dataset.typeid)) ch.remove();
+ });
nonSwitches.forEach(node => {
+ usedNodeIds.add(node.typeid);
const conn = switchConnections.get(node.typeid);
const danteInfo = danteNodes.get(node.typeid);
const artnetInfo = artnetNodes.get(node.typeid);
const sacnInfo = sacnNodes.get(node.typeid);
const hasError = errorNodeIds.has(node.typeid);
const isUnreachable = unreachableNodeIds.has(node.typeid);
- nodeRow.appendChild(createNodeElement(node, conn, null, null, danteInfo, artnetInfo, sacnInfo, hasError, isUnreachable));
+ const el = createNodeElement(node, conn, null, null, danteInfo, artnetInfo, sacnInfo, hasError, isUnreachable);
+ if (el.parentNode !== nodeRow) nodeRow.appendChild(el);
});
- unassignedLoc.appendChild(nodeRow);
+ } else if (nodeRow) {
+ nodeRow.remove();
}
- container.appendChild(unassignedLoc);
+ if (unassignedLoc.parentNode !== container) container.appendChild(unassignedLoc);
+ usedLocationIds.add('__unassigned__');
+ } else if (unassignedLoc) {
+ unassignedLoc.remove();
}
+ locationElements.forEach((el, id) => {
+ if (!usedLocationIds.has(id) && el.parentNode) {
+ el.remove();
+ }
+ });
+
updateErrorPanel();
updateBroadcastStats(data.broadcast_stats);
}