Track sACN emitters and receivers with peer linking

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ian Gulliver
2026-01-28 21:27:35 -08:00
parent c6109c28f0
commit 7aac3c0559
5 changed files with 190 additions and 63 deletions

View File

@@ -535,9 +535,18 @@
opacity: 0.3;
}
body.sacn-mode .node.sacn-consumer {
body.sacn-mode .node.sacn-out {
opacity: 1;
background: #26d;
background: #287;
}
body.sacn-mode .node.sacn-in {
opacity: 1;
background: #268;
}
body.sacn-mode .node.sacn-out.sacn-in {
background: linear-gradient(135deg, #287 50%, #268 50%);
}
body.sacn-mode .node .switch-port,
@@ -553,13 +562,20 @@
padding: 1px 6px;
border-radius: 8px;
white-space: nowrap;
background: #468;
color: #fff;
max-width: 114px;
overflow: hidden;
text-overflow: ellipsis;
}
.node .sacn-info.out-info {
background: #254;
}
.node .sacn-info.in-info {
background: #245;
}
.node .sacn-hover {
position: absolute;
top: -8px;
@@ -607,10 +623,23 @@
will-change: transform;
}
body.sacn-mode .node.sacn-consumer .sacn-info {
body.sacn-mode .node.sacn-out .sacn-info,
body.sacn-mode .node.sacn-in .sacn-info {
display: block;
}
body.sacn-mode .node.sacn-out.sacn-in .sacn-in-hover {
top: auto;
bottom: -8px;
}
body.sacn-mode .node.sacn-out.sacn-in .sacn-in-hover .sacn-detail-wrapper {
bottom: auto;
top: 100%;
padding-bottom: 0;
padding-top: 8px;
}
.sacn-info .lbl,
.sacn-detail .lbl {
color: #888;
@@ -673,7 +702,7 @@
display: none;
}
body.sacn-mode .node:not(.sacn-consumer):hover .node-info-wrapper {
body.sacn-mode .node:not(.sacn-out):not(.sacn-in):hover .node-info-wrapper {
display: none;
}
@@ -1226,7 +1255,8 @@
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 (sacnInfo?.isOut) div.classList.add('sacn-out');
if (sacnInfo?.isIn) div.classList.add('sacn-in');
// Switch port connection
if (!isSwitch(node) && switchConnection) {
@@ -1463,24 +1493,49 @@
if (container) container.remove();
}
// sACN
if (sacnInfo?.isConsumer) {
let container = div.querySelector(':scope > .sacn-hover');
// sACN out
if (sacnInfo?.isOut) {
let container = div.querySelector(':scope > .sacn-out-hover');
if (!container) {
container = document.createElement('div');
container.className = 'sacn-hover';
container.innerHTML = '<div class="sacn-info"><span class="lbl">←</span> <span class="sacn-pill-text"></span></div><div class="sacn-detail-wrapper"><div class="sacn-detail"></div></div>';
container.className = 'sacn-hover sacn-out-hover';
container.innerHTML = '<div class="sacn-info out-info"><span class="lbl">←</span> <span class="sacn-pill-text"></span></div><div class="sacn-detail-wrapper"><div class="sacn-detail"></div></div>';
div.appendChild(container);
}
const textEl = container.querySelector('.sacn-pill-text');
const sacnMore = sacnInfo.universes.length > 1 ? ', ...' : '';
textEl.textContent = sacnInfo.universes[0] + sacnMore;
const firstOut = sacnInfo.outputs[0];
const outLabel = firstOut.firstTarget || firstOut.display;
const outMore = sacnInfo.outputs.length > 1 ? ', ...' : '';
textEl.textContent = outLabel + outMore;
const detail = container.querySelector('.sacn-detail');
detail.innerHTML = '';
buildClickableList(detail, sacnInfo.universes, '←', (l, v) => l + ' ' + v);
buildClickableList(detail, sacnInfo.outputs.map(o => o.display), '←', (l, v) => l + ' ' + v);
} else {
const container = div.querySelector(':scope > .sacn-hover');
const container = div.querySelector(':scope > .sacn-out-hover');
if (container) container.remove();
}
// sACN in
if (sacnInfo?.isIn) {
let container = div.querySelector(':scope > .sacn-in-hover');
if (!container) {
container = document.createElement('div');
container.className = 'sacn-hover sacn-in-hover';
container.innerHTML = '<div class="sacn-info in-info"><span class="lbl">→</span> <span class="sacn-pill-text"></span></div><div class="sacn-detail-wrapper"><div class="sacn-detail"></div></div>';
div.appendChild(container);
}
const textEl = container.querySelector('.sacn-pill-text');
const firstIn = sacnInfo.inputs[0];
const inLabel = firstIn.firstTarget || firstIn.display;
const inMore = sacnInfo.inputs.length > 1 ? ', ...' : '';
textEl.textContent = inLabel + inMore;
const detail = container.querySelector('.sacn-detail');
detail.innerHTML = '';
buildClickableList(detail, sacnInfo.inputs.map(i => i.display), '→', (l, v) => l + ' ' + v);
} else {
const container = div.querySelector(':scope > .sacn-in-hover');
if (container) container.remove();
}
@@ -1944,15 +1999,51 @@
const sacnData = data.sacn_nodes || [];
const sacnNodes = new Map();
const sacnUniverseInputs = new Map();
const sacnUniverseOutputs = new Map();
sacnData.forEach(sn => {
const name = getShortLabel(sn.node);
(sn.inputs || []).forEach(u => {
if (!sacnUniverseInputs.has(u)) sacnUniverseInputs.set(u, []);
sacnUniverseInputs.get(u).push(name);
});
(sn.outputs || []).forEach(u => {
if (!sacnUniverseOutputs.has(u)) sacnUniverseOutputs.set(u, []);
sacnUniverseOutputs.get(u).push(name);
});
});
const sacnCollapseNames = (names) => {
const counts = {};
names.forEach(n => counts[n] = (counts[n] || 0) + 1);
return Object.entries(counts).map(([name, count]) => count > 1 ? name + ' x' + count : name);
};
sacnData.forEach(sn => {
const nodeId = sn.node?.typeid;
if (!nodeId) return;
const universes = (sn.universes || []).slice().sort((a, b) => a - b).map(u => String(u));
const inputs = (sn.inputs || []).slice().sort((a, b) => a - b).map(u => {
const sources = sacnCollapseNames(sacnUniverseOutputs.get(u) || []);
if (sources.length > 0) {
return { display: sources[0] + ' [' + u + ']', firstTarget: sources[0] };
}
return { display: String(u), firstTarget: null };
});
const outputs = (sn.outputs || []).slice().sort((a, b) => a - b).map(u => {
const dests = sacnCollapseNames(sacnUniverseInputs.get(u) || []);
if (dests.length > 0) {
return { display: dests[0] + ' [' + u + ']', firstTarget: dests[0] };
}
return { display: String(u), firstTarget: null };
});
sacnNodes.set(nodeId, {
isConsumer: universes.length > 0,
universes: universes
isOut: outputs.length > 0,
isIn: inputs.length > 0,
outputs: outputs,
inputs: inputs
});
});