feat(ingest): show capture port on recorder cards + growing indicator on schedule blocks

- Recorder cards now display a Port chip showing the bridge port (deltacast)
  or SDI device index (blackmagic) so operators can see at a glance which
  physical input each recorder is bound to.

- Schedule blocks render with a green accent + flame glyph when the backing
  recorder has growing_enabled=true, so the EPG view distinguishes
  edit-while-record slots from regular close-then-publish recordings.

🤖 Generated with Claude Code
This commit is contained in:
Claude 2026-06-02 04:09:17 +00:00
parent 8a675992c2
commit 6895fbc5af

View file

@ -492,6 +492,18 @@ function HlsPreview({ assetId, recorderId, muted = true, controls = false, class
/* ===== Recorders ===== */
function _normRecorder(r) {
const cfg = r.source_config || {};
// Surface the capture port for SDI / Deltacast recorders so the recorder card
// can show which physical input the recorder is bound to. For Deltacast,
// cfg.port is the bridge port index (0-7). For Blackmagic SDI, cfg.device
// is something like /dev/blackmagic/dv0 we slice off the trailing index.
let capturePort = null;
if (r.source_type === 'deltacast') {
capturePort = cfg.port != null ? `Port ${cfg.port}` : null;
} else if (r.source_type === 'sdi') {
const dev = cfg.device || '';
const m = dev.match(/(\d+)$/);
if (m) capturePort = `SDI ${m[1]}`;
}
return {
...r,
source: r.source_type || '·',
@ -500,6 +512,7 @@ function _normRecorder(r) {
res: r.recording_resolution || '·',
framerate: r.recording_framerate || 'native',
node: r.node_id ? r.node_id.slice(0, 8) : 'primary',
capturePort,
elapsed: '·',
bitrate: '·',
health: 100,
@ -708,6 +721,11 @@ function RecorderRow({ recorder: initialRecorder, onRefresh }) {
<StatusDot status={recorder.status} /> {recorder.status.toUpperCase()}
</span>
<span className="badge outline">{recorder.source}</span>
{recorder.capturePort && (
<span className="badge outline" title="Capture port" style={{ background: 'rgba(74,158,255,0.12)', borderColor: 'rgba(74,158,255,0.4)', color: 'var(--accent)' }}>
<Icon name="signal" size={10} style={{ marginRight: 4, verticalAlign: -1 }} />{recorder.capturePort}
</span>
)}
</div>
<div className="recorder-sub mono">{recorder.url}</div>
<div className="recorder-sub">
@ -1471,29 +1489,38 @@ function _EventBlock({ event, recorder, dayStart, dayEnd, pph, now, projects, on
const left = (startMin / 60) * pph;
const width = Math.max(40, ((endMin - startMin) / 60) * pph);
// Schedules backed by a recorder configured to write a growing file get a
// green accent so operators can tell at a glance which slots will produce
// an edit-while-recording deliverable (vs. close-then-publish).
const isGrowing = !!(recorder && recorder.growing_enabled);
const classes = ['epg-block'];
if (isLive) classes.push('live');
if (isFailed) classes.push('failed');
else if (isPast) classes.push('past');
if (drag && drag.moved) classes.push('dragging');
if (canDrag) classes.push('resizable');
if (isGrowing) classes.push('growing');
const blockColor = isGrowing ? '#2ecc71' : (color || 'var(--text-3)');
return (
<div
ref={blockRef}
className={classes.join(' ')}
style={{ left, width, '--epg-block-color': color || 'var(--text-3)' }}
style={{ left, width, '--epg-block-color': blockColor }}
onContextMenu={(ev) => { ev.preventDefault(); ev.stopPropagation(); onContextMenu(event, ev); }}
onPointerMove={onPointerMove}
onPointerUp={endDrag}
onPointerCancel={endDrag}
title={event.name + ' · ' + _fmtTime(dispStart) + ' → ' + _fmtTime(dispEnd) + (event.error_message ? ' · ' + event.error_message : '')}>
title={event.name + (isGrowing ? ' · GROWING' : '') + ' · ' + _fmtTime(dispStart) + ' → ' + _fmtTime(dispEnd) + (event.error_message ? ' · ' + event.error_message : '')}>
<span className="epg-block-bar" />
{/* Body click edit, body drag move. We hang the click on pointerup
so the threshold check above can demote a drag back to a click. */}
<div className="epg-block-body" onPointerDown={(ev) => startDrag(ev, 'body')}>
<span className="epg-block-name">{event.name}</span>
<span className="epg-block-time mono">{_fmtTime(dispStart)}{drag && drag.moved ? ' → ' + _fmtTime(dispEnd) : ''}</span>
{isGrowing && <span className="epg-block-glyph growing" title="growing file (edit-while-record)" style={{ color: '#2ecc71' }}></span>}
{isLive && <span className="epg-block-glyph live" title="on air"></span>}
{isFailed && <span className="epg-block-glyph failed" title={event.error_message || 'failed'}>!</span>}
</div>