From f54c49d2dcd05a6d34d4623d614f8931e6d0ffb7 Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Thu, 28 May 2026 23:18:55 +0000 Subject: [PATCH] fix(web-ui): fix duplicate DeckLink groups in new-recorder modal; refactor device pickers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /cluster/devices/blackmagic endpoint returns one entry per port (flat array). The old SDI picker iterated over each entry and synthesised port_count buttons per entry — 4 entries × 4 synthesised ports = 16 buttons rendered as 4 identical duplicate groups. Fix: extract DevicePortPicker component that groups the flat per-port response by node_id (Map keyed on node_id, one group per physical node, ports sorted by index). One button rendered per actual API entry. Also extract ManualDevicePicker for the fallback empty-state dropdowns. Both components shared between SDI and Deltacast pickers. Visual improvements: - Port label shows device node (io0, io1…) from device path instead of redundant index number - Node header only shows model+hostname, not repeated per port - TEST CARD badge styled inline for Deltacast test-card ports --- services/web-ui/public/modal-new-recorder.jsx | 239 ++++++++++-------- 1 file changed, 134 insertions(+), 105 deletions(-) diff --git a/services/web-ui/public/modal-new-recorder.jsx b/services/web-ui/public/modal-new-recorder.jsx index ea87bba..f10292a 100644 --- a/services/web-ui/public/modal-new-recorder.jsx +++ b/services/web-ui/public/modal-new-recorder.jsx @@ -1,4 +1,103 @@ -// modal-new-recorder.jsx — New Recorder dialog (SRT / RTMP / SDI) +// modal-new-recorder.jsx — New Recorder dialog (SRT / RTMP / SDI / Deltacast) + +/** + * DevicePortPicker — groups a flat per-port API response by node_id and + * renders one button per actual port. Replaces the old code that iterated + * over entries and synthesised port counts, which caused duplicate groups. + * + * props: + * ports — flat array from /cluster/devices/blackmagic or /deltacast + * each entry: { node_id, hostname, model, index, device, present? } + * selectedIdx — currently selected device_index + * selectedNode — currently selected node_id + * onSelect(idx, nodeId) + * portLabel — e.g. "SDI" or "Port" + * showTestBadge — show TEST CARD badge when present===false + */ +function DevicePortPicker({ ports, selectedIdx, selectedNode, onSelect, portLabel = 'Port', showTestBadge = false }) { + // Group by node_id (stable — one group per physical node) + const groups = React.useMemo(() => { + const map = new Map(); + for (const p of ports) { + const key = p.node_id || p.hostname || 'unknown'; + if (!map.has(key)) map.set(key, { nodeId: p.node_id || p.hostname || '', hostname: p.hostname || key, model: p.model || '', ports: [] }); + map.get(key).ports.push(p); + } + // Sort ports within each group by index + for (const g of map.values()) g.ports.sort((a, b) => a.index - b.index); + return Array.from(map.values()); + }, [ports]); + + return ( +
+ {groups.map(group => ( +
1 ? 12 : 4 }}> + {/* Node header — only show when multiple groups, or always for clarity */} +
+ {group.model ? group.model.toUpperCase() + ' · ' : ''}{group.hostname} +
+ {group.ports.map(port => { + const active = selectedIdx === port.index && selectedNode === group.nodeId; + return ( + + ); + })} +
+ ))} +
+ ); +} + +/** + * ManualDevicePicker — fallback when no devices detected. Lets the operator + * pick node + index from dropdowns. + */ +function ManualDevicePicker({ nodes, nodeId, deviceIdx, portLabel, portCount, onNodeChange, onIdxChange, emptyNote }) { + return ( +
+
+ {emptyNote || `No ${portLabel} devices auto-detected. Configure manually:`} +
+
+
+ + +
+
+ + +
+
+
+ ); +} function ProbeResult({ result }) { if (!result.ok) { @@ -212,54 +311,24 @@ function NewRecorderModal({ open, onClose }) {
Detecting DeckLink devices…
)} {sdiDevices !== null && sdiDevices.length > 0 && ( -
- {sdiDevices.map((dev, di) => ( -
-
- {(dev.model || dev.device || 'DeckLink').toUpperCase()} · {dev.hostname} -
- {Array.from({ length: dev.port_count || 4 }, (_, i) => i).map(idx => ( - - ))} -
- ))} -
+ { setSdiDeviceIdx(idx); setSdiNodeId(nodeId); }} + portLabel="SDI" + /> )} {sdiDevices !== null && sdiDevices.length === 0 && ( -
-
- No DeckLink devices auto-detected. Configure manually: -
-
-
- - -
-
- - -
-
-
+ )} )} @@ -271,66 +340,26 @@ function NewRecorderModal({ open, onClose }) {
Detecting Deltacast devices…
)} {dcDevices !== null && dcDevices.length > 0 && ( -
- {(() => { - // Group by node - const byNode = {}; - dcDevices.forEach(dev => { - const key = dev.node_id || dev.hostname || 'unknown'; - if (!byNode[key]) byNode[key] = { ...dev, ports: [] }; - byNode[key].ports.push(dev); - }); - return Object.values(byNode).map((node, ni) => ( -
-
- {(node.model || 'Deltacast').toUpperCase()} · {node.hostname} -
- {node.ports.map(port => ( - - ))} -
- )); - })()} -
+ { setDcDeviceIdx(idx); setDcNodeId(nodeId); }} + portLabel="Port" + showTestBadge + /> )} {dcDevices !== null && dcDevices.length === 0 && ( -
-
- No Deltacast devices detected. Configure manually (test-card mode): -
-
-
- - -
-
- - -
-
-
+ )} )}