diff --git a/services/web-ui/public/screens-ingest.jsx b/services/web-ui/public/screens-ingest.jsx
index 08354fb..ce29443 100644
--- a/services/web-ui/public/screens-ingest.jsx
+++ b/services/web-ui/public/screens-ingest.jsx
@@ -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 }) {
{recorder.status.toUpperCase()}
{recorder.source}
+ {recorder.capturePort && (
+
+ {recorder.capturePort}
+
+ )}
{recorder.url}
@@ -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 (
{ 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 : '')}>
{/* 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. */}
startDrag(ev, 'body')}>
{event.name}
{_fmtTime(dispStart)}{drag && drag.moved ? ' → ' + _fmtTime(dispEnd) : ''}
+ {isGrowing && ▶}
{isLive && ●}
{isFailed && !}