From eedc4cd1d7849900ec8bc3b5b81a251af7d3bfbd Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Wed, 4 Feb 2026 09:33:52 -0800 Subject: [PATCH] Add flow validity icons to artmap mappings in flow view --- static/js/flow.js | 41 +++++++++++++++++++++++++++++++++++++---- static/style.css | 30 +++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/static/js/flow.js b/static/js/flow.js index 1ba7c12..3be4a0d 100644 --- a/static/js/flow.js +++ b/static/js/flow.js @@ -169,7 +169,7 @@ export function showFlowView(flowSpec) { if (path) paths.push({ path, sourceId, destId: clickedNodeId }); }); } else { - error = 'Node is not a source or destination for universe ' + universe; + error = 'Node is not a source or destination for universe ' + universeDisplay; } } } else { @@ -183,7 +183,7 @@ export function showFlowView(flowSpec) { }); }); } - if (!error && paths.length === 0) error = 'No active flows for universe ' + universe; + if (!error && paths.length === 0) error = 'No active flows for universe ' + universeDisplay; } } else { error = 'Unknown protocol: ' + protocol; @@ -350,6 +350,21 @@ export function renderFlowPath(pathInfo, nodesByTypeId, flowUniverse, flowProtoc relevantMappings.forEach(m => { const mappingEl = document.createElement('div'); mappingEl.className = 'artmap-mapping'; + const fromMatches = m.from.protocol === flowProtocol && m.from.universe === flowUniverse; + const target = fromMatches ? m.to : m.from; + const targetValid = hasValidFlow(nodesByTypeId, target.protocol, target.universe); + const leftIcon = document.createElement('span'); + leftIcon.className = 'flow-validity-icon left'; + const rightIcon = document.createElement('span'); + rightIcon.className = 'flow-validity-icon right'; + const activeIcon = fromMatches ? rightIcon : leftIcon; + if (targetValid) { + activeIcon.textContent = '●'; + activeIcon.classList.add('valid'); + } else { + activeIcon.textContent = '⊘'; + activeIcon.classList.add('invalid'); + } const fromSpan = document.createElement('span'); fromSpan.className = 'from'; fromSpan.textContent = formatArtmapAddr(m.from); @@ -358,13 +373,13 @@ export function renderFlowPath(pathInfo, nodesByTypeId, flowUniverse, flowProtoc const toSpan = document.createElement('span'); toSpan.className = 'to'; toSpan.textContent = formatArtmapAddr(m.to); + mappingEl.appendChild(leftIcon); mappingEl.appendChild(fromSpan); mappingEl.appendChild(arrowSpan); mappingEl.appendChild(toSpan); + mappingEl.appendChild(rightIcon); mappingEl.addEventListener('click', (e) => { e.stopPropagation(); - const fromMatches = m.from.protocol === flowProtocol && m.from.universe === flowUniverse; - const target = fromMatches ? m.to : m.from; openFlowHash(target.protocol, target.universe, nodeName); }); mappingsEl.appendChild(mappingEl); @@ -392,6 +407,24 @@ function getRelevantMappings(mappings, protocol, universe) { }); } +function hasValidFlow(nodesByTypeId, protocol, universe) { + let hasSources = false; + let hasDests = false; + for (const node of nodesByTypeId.values()) { + if (protocol === 'sacn') { + if ((node.sacn_outputs || []).includes(universe)) hasSources = true; + const groups = node.multicast_groups || []; + const unicastInputs = node.sacn_unicast_inputs || []; + if (groups.some(g => g === 'sacn:' + universe) || unicastInputs.includes(universe)) hasDests = true; + } else if (protocol === 'artnet') { + if ((node.artnet_inputs || []).includes(universe)) hasSources = true; + if ((node.artnet_outputs || []).includes(universe)) hasDests = true; + } + if (hasSources && hasDests) return true; + } + return false; +} + export function closeFlowView() { const overlay = document.getElementById('flow-overlay'); if (overlay) overlay.style.display = 'none'; diff --git a/static/style.css b/static/style.css index aee1f6f..ec47d8e 100644 --- a/static/style.css +++ b/static/style.css @@ -982,7 +982,7 @@ body.sacn-mode .node.sacn-out.sacn-in .sacn-in-hover .sacn-detail-wrapper { .artmap-mapping { display: grid; - grid-template-columns: 1fr auto 1fr; + grid-template-columns: 1em 1fr auto 1fr 1em; gap: 6px; font-size: 11px; color: #aaf; @@ -991,6 +991,26 @@ body.sacn-mode .node.sacn-out.sacn-in .sacn-in-hover .sacn-detail-wrapper { border-radius: 3px; } +.artmap-mapping .flow-validity-icon { + font-size: 10px; +} + +.artmap-mapping .flow-validity-icon.left { + text-align: left; +} + +.artmap-mapping .flow-validity-icon.right { + text-align: right; +} + +.artmap-mapping .flow-validity-icon.valid { + color: #4c4; +} + +.artmap-mapping .flow-validity-icon.invalid { + color: #e44; +} + .artmap-mapping .from { text-align: right; } @@ -1004,6 +1024,14 @@ body.sacn-mode .node.sacn-out.sacn-in .sacn-in-hover .sacn-detail-wrapper { color: #ccf; } +.artmap-mapping:hover .flow-validity-icon.valid { + color: #6f6; +} + +.artmap-mapping:hover .flow-validity-icon.invalid { + color: #f66; +} + .node.has-error { box-shadow: 0 0 0 3px #f66; }