Add Dante overlay with TX/RX highlighting and channel info
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,7 @@
|
|||||||
border: 1px solid #444;
|
border: 1px solid #444;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.location.top-level {
|
.location.top-level {
|
||||||
@@ -55,6 +56,7 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-row + .node-row {
|
.node-row + .node-row {
|
||||||
@@ -143,6 +145,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.children.horizontal {
|
.children.horizontal {
|
||||||
@@ -157,15 +160,134 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#mode-selector {
|
||||||
|
position: fixed;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
background: #333;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mode-selector button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: none;
|
||||||
|
background: #333;
|
||||||
|
color: #aaa;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mode-selector button:hover {
|
||||||
|
background: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mode-selector button.active {
|
||||||
|
background: #555;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dante-mode .node {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dante-mode .node.dante-tx {
|
||||||
|
opacity: 1;
|
||||||
|
background: #d62;
|
||||||
|
border: 2px solid #f84;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dante-mode .node.dante-rx {
|
||||||
|
opacity: 1;
|
||||||
|
background: #26d;
|
||||||
|
border: 2px solid #48f;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dante-mode .node.dante-tx.dante-rx {
|
||||||
|
background: linear-gradient(135deg, #d62 50%, #26d 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dante-mode .node .switch-port,
|
||||||
|
body.dante-mode .node .uplink,
|
||||||
|
body.dante-mode .node .root-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node .dante-info {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 9px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 100px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
z-index: 10;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node:has(.dante-info:hover) {
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node .dante-info:hover {
|
||||||
|
white-space: pre;
|
||||||
|
max-width: none;
|
||||||
|
width: max-content;
|
||||||
|
z-index: 100;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node .dante-info.tx-info {
|
||||||
|
background: #fca;
|
||||||
|
color: #630;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node .dante-info.rx-info {
|
||||||
|
background: #acf;
|
||||||
|
color: #036;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dante-mode .node.dante-tx .dante-info,
|
||||||
|
body.dante-mode .node.dante-rx .dante-info {
|
||||||
|
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 {
|
||||||
|
top: auto;
|
||||||
|
bottom: -8px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div id="mode-selector">
|
||||||
|
<button id="mode-network" class="active">Network</button>
|
||||||
|
<button id="mode-dante">Dante</button>
|
||||||
|
</div>
|
||||||
<div id="error"></div>
|
<div id="error"></div>
|
||||||
<div id="container"></div>
|
<div id="container"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function getLabel(node) {
|
function getLabel(node) {
|
||||||
if (node.names && node.names.length > 0) return node.names.join('\n');
|
if (node.names && node.names.length > 0) return node.names.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getShortLabel(node) {
|
||||||
|
if (node.names && node.names.length > 0) return node.names.join('\n');
|
||||||
if (node.interfaces && node.interfaces.length > 0) {
|
if (node.interfaces && node.interfaces.length > 0) {
|
||||||
const ips = [];
|
const ips = [];
|
||||||
node.interfaces.forEach(iface => {
|
node.interfaces.forEach(iface => {
|
||||||
@@ -270,10 +392,15 @@
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createNodeElement(node, switchConnection, nodeLocation, uplinkInfo) {
|
function createNodeElement(node, switchConnection, nodeLocation, uplinkInfo, danteInfo) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'node' + (isSwitch(node) ? ' switch' : '');
|
div.className = 'node' + (isSwitch(node) ? ' switch' : '');
|
||||||
|
|
||||||
|
if (danteInfo) {
|
||||||
|
if (danteInfo.isTx) div.classList.add('dante-tx');
|
||||||
|
if (danteInfo.isRx) div.classList.add('dante-rx');
|
||||||
|
}
|
||||||
|
|
||||||
if (!isSwitch(node) && switchConnection) {
|
if (!isSwitch(node) && switchConnection) {
|
||||||
const portEl = document.createElement('div');
|
const portEl = document.createElement('div');
|
||||||
portEl.className = 'switch-port';
|
portEl.className = 'switch-port';
|
||||||
@@ -302,6 +429,20 @@
|
|||||||
div.appendChild(uplinkEl);
|
div.appendChild(uplinkEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (danteInfo && danteInfo.isTx) {
|
||||||
|
const txEl = document.createElement('div');
|
||||||
|
txEl.className = 'dante-info tx-info';
|
||||||
|
txEl.textContent = '→ ' + danteInfo.txTo.join('\n\n→ ');
|
||||||
|
div.appendChild(txEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (danteInfo && danteInfo.isRx) {
|
||||||
|
const rxEl = document.createElement('div');
|
||||||
|
rxEl.className = 'dante-info rx-info';
|
||||||
|
rxEl.textContent = '← ' + danteInfo.rxFrom.join('\n\n← ');
|
||||||
|
div.appendChild(rxEl);
|
||||||
|
}
|
||||||
|
|
||||||
div.addEventListener('click', () => {
|
div.addEventListener('click', () => {
|
||||||
const json = JSON.stringify(node, null, 2);
|
const json = JSON.stringify(node, null, 2);
|
||||||
navigator.clipboard.writeText(json).then(() => {
|
navigator.clipboard.writeText(json).then(() => {
|
||||||
@@ -312,12 +453,12 @@
|
|||||||
return div;
|
return div;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderLocation(loc, assignedNodes, isTopLevel, switchConnections, switchUplinks) {
|
function renderLocation(loc, assignedNodes, isTopLevel, switchConnections, switchUplinks, danteNodes) {
|
||||||
const nodes = assignedNodes.get(loc) || [];
|
const nodes = assignedNodes.get(loc) || [];
|
||||||
const hasNodes = nodes.length > 0;
|
const hasNodes = nodes.length > 0;
|
||||||
|
|
||||||
const childElements = loc.children
|
const childElements = loc.children
|
||||||
.map(child => renderLocation(child, assignedNodes, false, switchConnections, switchUplinks))
|
.map(child => renderLocation(child, assignedNodes, false, switchConnections, switchUplinks, danteNodes))
|
||||||
.filter(el => el !== null);
|
.filter(el => el !== null);
|
||||||
|
|
||||||
if (!hasNodes && childElements.length === 0) {
|
if (!hasNodes && childElements.length === 0) {
|
||||||
@@ -344,7 +485,8 @@
|
|||||||
switchRow.className = 'node-row';
|
switchRow.className = 'node-row';
|
||||||
switches.forEach(node => {
|
switches.forEach(node => {
|
||||||
const uplink = switchUplinks.get(node.typeid);
|
const uplink = switchUplinks.get(node.typeid);
|
||||||
switchRow.appendChild(createNodeElement(node, null, loc, uplink));
|
const danteInfo = danteNodes.get(node.typeid);
|
||||||
|
switchRow.appendChild(createNodeElement(node, null, loc, uplink, danteInfo));
|
||||||
});
|
});
|
||||||
container.appendChild(switchRow);
|
container.appendChild(switchRow);
|
||||||
}
|
}
|
||||||
@@ -354,7 +496,8 @@
|
|||||||
nodeRow.className = 'node-row';
|
nodeRow.className = 'node-row';
|
||||||
nonSwitches.forEach(node => {
|
nonSwitches.forEach(node => {
|
||||||
const conn = switchConnections.get(node.typeid);
|
const conn = switchConnections.get(node.typeid);
|
||||||
nodeRow.appendChild(createNodeElement(node, conn, loc, null));
|
const danteInfo = danteNodes.get(node.typeid);
|
||||||
|
nodeRow.appendChild(createNodeElement(node, conn, loc, null, danteInfo));
|
||||||
});
|
});
|
||||||
container.appendChild(nodeRow);
|
container.appendChild(nodeRow);
|
||||||
}
|
}
|
||||||
@@ -447,6 +590,46 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const danteFlows = data.dante_flows || [];
|
||||||
|
const danteNodes = new Map();
|
||||||
|
|
||||||
|
danteFlows.forEach(flow => {
|
||||||
|
const sourceId = flow.source?.typeid;
|
||||||
|
if (!sourceId) return;
|
||||||
|
|
||||||
|
if (!danteNodes.has(sourceId)) {
|
||||||
|
danteNodes.set(sourceId, { isTx: false, isRx: false, txTo: [], rxFrom: [] });
|
||||||
|
}
|
||||||
|
const sourceInfo = danteNodes.get(sourceId);
|
||||||
|
sourceInfo.isTx = true;
|
||||||
|
|
||||||
|
(flow.subscribers || []).forEach(sub => {
|
||||||
|
const subId = sub.node?.typeid;
|
||||||
|
if (!subId) return;
|
||||||
|
|
||||||
|
const subName = getShortLabel(sub.node);
|
||||||
|
const channels = sub.channels || [];
|
||||||
|
const channelSummary = channels.length > 0 ? '\n ' + channels.join('\n ') : '';
|
||||||
|
const txEntry = subName + channelSummary;
|
||||||
|
if (!sourceInfo.txTo.some(e => e.startsWith(subName))) {
|
||||||
|
sourceInfo.txTo.push(txEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!danteNodes.has(subId)) {
|
||||||
|
danteNodes.set(subId, { isTx: false, isRx: false, txTo: [], rxFrom: [] });
|
||||||
|
}
|
||||||
|
const subInfo = danteNodes.get(subId);
|
||||||
|
subInfo.isRx = true;
|
||||||
|
|
||||||
|
const sourceName = getShortLabel(flow.source);
|
||||||
|
const rxChannelSummary = channels.length > 0 ? '\n ' + channels.join('\n ') : '';
|
||||||
|
const rxEntry = sourceName + rxChannelSummary;
|
||||||
|
if (!subInfo.rxFrom.some(e => e.startsWith(sourceName))) {
|
||||||
|
subInfo.rxFrom.push(rxEntry);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const switchUplinks = new Map();
|
const switchUplinks = new Map();
|
||||||
if (allSwitches.length > 0 && switchLinks.length > 0) {
|
if (allSwitches.length > 0 && switchLinks.length > 0) {
|
||||||
const adjacency = new Map();
|
const adjacency = new Map();
|
||||||
@@ -527,7 +710,7 @@
|
|||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
locationTree.forEach(loc => {
|
locationTree.forEach(loc => {
|
||||||
const el = renderLocation(loc, assignedNodes, true, switchConnections, switchUplinks);
|
const el = renderLocation(loc, assignedNodes, true, switchConnections, switchUplinks, danteNodes);
|
||||||
if (el) container.appendChild(el);
|
if (el) container.appendChild(el);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -548,7 +731,8 @@
|
|||||||
switchRow.className = 'node-row';
|
switchRow.className = 'node-row';
|
||||||
switches.forEach(node => {
|
switches.forEach(node => {
|
||||||
const uplink = switchUplinks.get(node.typeid);
|
const uplink = switchUplinks.get(node.typeid);
|
||||||
switchRow.appendChild(createNodeElement(node, null, null, uplink));
|
const danteInfo = danteNodes.get(node.typeid);
|
||||||
|
switchRow.appendChild(createNodeElement(node, null, null, uplink, danteInfo));
|
||||||
});
|
});
|
||||||
unassignedLoc.appendChild(switchRow);
|
unassignedLoc.appendChild(switchRow);
|
||||||
}
|
}
|
||||||
@@ -558,7 +742,8 @@
|
|||||||
nodeRow.className = 'node-row';
|
nodeRow.className = 'node-row';
|
||||||
nonSwitches.forEach(node => {
|
nonSwitches.forEach(node => {
|
||||||
const conn = switchConnections.get(node.typeid);
|
const conn = switchConnections.get(node.typeid);
|
||||||
nodeRow.appendChild(createNodeElement(node, conn, null, null));
|
const danteInfo = danteNodes.get(node.typeid);
|
||||||
|
nodeRow.appendChild(createNodeElement(node, conn, null, null, danteInfo));
|
||||||
});
|
});
|
||||||
unassignedLoc.appendChild(nodeRow);
|
unassignedLoc.appendChild(nodeRow);
|
||||||
}
|
}
|
||||||
@@ -570,6 +755,27 @@
|
|||||||
init().catch(e => {
|
init().catch(e => {
|
||||||
document.getElementById('error').textContent = e.message;
|
document.getElementById('error').textContent = e.message;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function setMode(mode) {
|
||||||
|
if (mode === 'dante') {
|
||||||
|
document.body.classList.add('dante-mode');
|
||||||
|
document.getElementById('mode-dante').classList.add('active');
|
||||||
|
document.getElementById('mode-network').classList.remove('active');
|
||||||
|
window.location.hash = 'dante';
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove('dante-mode');
|
||||||
|
document.getElementById('mode-network').classList.add('active');
|
||||||
|
document.getElementById('mode-dante').classList.remove('active');
|
||||||
|
window.location.hash = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('mode-network').addEventListener('click', () => setMode('network'));
|
||||||
|
document.getElementById('mode-dante').addEventListener('click', () => setMode('dante'));
|
||||||
|
|
||||||
|
if (window.location.hash === '#dante') {
|
||||||
|
setMode('dante');
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
13
types.go
13
types.go
@@ -60,12 +60,21 @@ func (s IPSet) Slice() []string {
|
|||||||
|
|
||||||
type NameSet map[string]bool
|
type NameSet map[string]bool
|
||||||
|
|
||||||
|
func sortNamesByLength(names []string) {
|
||||||
|
sort.Slice(names, func(i, j int) bool {
|
||||||
|
if len(names[i]) != len(names[j]) {
|
||||||
|
return len(names[i]) < len(names[j])
|
||||||
|
}
|
||||||
|
return names[i] < names[j]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s NameSet) MarshalJSON() ([]byte, error) {
|
func (s NameSet) MarshalJSON() ([]byte, error) {
|
||||||
names := make([]string, 0, len(s))
|
names := make([]string, 0, len(s))
|
||||||
for name := range s {
|
for name := range s {
|
||||||
names = append(names, name)
|
names = append(names, name)
|
||||||
}
|
}
|
||||||
sort.Strings(names)
|
sortNamesByLength(names)
|
||||||
return json.Marshal(names)
|
return json.Marshal(names)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +238,7 @@ func (n *Node) DisplayName() string {
|
|||||||
for name := range n.Names {
|
for name := range n.Names {
|
||||||
names = append(names, name)
|
names = append(names, name)
|
||||||
}
|
}
|
||||||
sort.Strings(names)
|
sortNamesByLength(names)
|
||||||
return strings.Join(names, "/")
|
return strings.Join(names, "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user