Growing root cause (4th attempt): Premiere doesn't import H.264-in-.ts
("unsupported compression type"); its growing-file support is MXF OP1a.
Prior MXF/DNxHR failed because DNxHR is VBR and never flushes the incremental
index — XDCAM HD422 (mpeg2video, CBR) DOES write index segments into body
partitions mid-record (proven live via SIGKILL: 5 index segments, readable,
no footer). Growing master is now MXF OP1a / XDCAM HD422 4:2:2 CBR + PCM s16le,
operator bitrate as CBR (default 50M). live-path returns .mxf to match.
GUI: bitrate input is now always editable in growing mode (was hidden for
ProRes-selected codecs); codec menu shown disabled-with-explanation under
growing (it had only looked "missing" due to a stale served bundle).
Requires Premiere prefs: Media > "Automatically refresh growing files" ON,
and disable the two XMP-write-on-import options.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
screens-playout.jsx declared a top-level function fmtDuration(secs) that, in
the shared global script scope, overwrote data.jsx's fmtDuration(ms). After
the playout redesign loaded, normalizeAsset(duration_ms) hit the seconds-based
version, rendering every asset duration x1000 (15000ms shown as 4:10:00).
Rename the playout-local helpers to playoutFmtDur/playoutFmtTC.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Growing master was H.264 High 4:2:2 Intra (high422/yuv422p) — ffprobe/VLC
open it but Adobe Premiere's H.264 importer only accepts 8-bit 4:2:0, so it
refused ("won't import"). Switch growing video to -profile:v high
-pix_fmt yuv420p (still -g 1 all-intra). Also the growing branch ignored the
operator's bitrate; now applies -b:v/-maxrate/-bufsize. Modal notes that
growing mode fixes codec/container (bitrate still applies).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add v2.2.3 to downloads (streaming write fix for large imports)
- Fix duration bug: worker now overwrites with ffprobe result instead of preserving capture wall-clock estimate
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Drop in the redesigned timeline-centric Playout (PGM monitor, transport,
SCTE-35 card, as-run drawer) from the on-node redesign, fully wired to the
real playout API (channels/transport/HLS preview w/ error-recovery/as-run);
no mock data. In-page ConfirmModal for destructive actions.
SCTE-35: new playout_scte_breaks table (migration 033), endpoints to
schedule/trigger/list/cancel breaks (POST/GET/DELETE /channels/:id/scte[/trigger]),
scheduler due-break sweep, engine triggerScte + auto-return + as-run 'scte'
rows + on-air SCTE-BREAK state and timeline AD markers. In-stream SCTE-35
cue injection is a documented stub (CasparCG FFMPEG consumer exposes no
scte35 muxer) — scheduling/triggering/countdown/as-run are functional.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#162 local-spawn stop now uses /stop?t=180 + waits for asset to leave 'live'
before removing the container (no more SIGKILL-corrupted masters / stuck-live).
#163 validateRecorderConfig guard (PCM!=MP4, HEVC!=MXF, NVENC needs GPU) on
create+PATCH; codec presets in new-recorder modal.
#159 container list reads Docker /stats memory (N/A when null) + UI render.
#160 primary node self-populates version + uptime on the Cluster screen.
#145 asset-detail Download original gated by dismissable size warning.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace 17 native window.confirm() destructive prompts with an in-page
ConfirmModal/useConfirm (added to visuals.jsx) across jobs/asset/editor/
ingest/projects/admin/playout/library. Add "Created by Wild Dragon LLC"
footer to the home launcher.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per-node "Capture Drivers / SDKs" panel installs Blackmagic / AJA / Deltacast
/ NDI drivers without SSH. node-agent gains NODE_TOKEN-gated /driver/install
+ /driver/status (spawns a one-shot privileged ubuntu container that bind-
mounts host kernel paths + the repo and runs deploy/install-driver.sh);
mam-api adds admin-gated /cluster/:id/install-driver + /driver-status.
Driver files live in-repo under sdk/<vendor>/ (private repo); binaries are
admin-supplied per each sdk/<vendor>/README.md. Vendor allowlist throughout.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
onboard-node.sh auto-detects GPU (nvidia-smi/lspci) and SDI capture cards
(blackmagic/deltacast) and computes PROFILES (worker [+gpu] [+capture])
automatically; explicit NODE_ROLE/PROFILES still override. Add Node wizard
drops the role picker — node self-configures from hardware.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Playout preview: add hls.js ERROR handler (recover media/network errors,
resume on stall) + live-edge tuning — first transient error no longer halts
<video> to black. Purge stale HLS segments before re-mux (re)start so a
prior/duplicate sidecar session can't corrupt the live playlist.
Growing files: normalize growing_smb_mount (smb://, \host\share, host/share)
to CIFS UNC //host/share in capture-manager — mount no longer fails and
falls back to S3.
Monitors: surface playout channels as monitor tiles (live HLS on-air,
idle placeholder otherwise) in a labeled Playout group.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cluster: AddNodeModal on Admin->Cluster mints a node token via /auth/tokens
and emits a ready-to-paste curl|bash onboarding command. New admin-only
GET /cluster/onboard-info returns apiUrl/scriptUrl/branch. Role->PROFILES
mapping (worker/capture/gpu); gate worker-l4 behind compose profile [gpu].
Home: restore "Let's Create" kicker + one-line "Media Asset Management &
Production Platform" tagline; animated accent pulse behind the dragon logo
(reduced-motion safe); move Settings tile to a centered bottom row.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dashboard (screens-home.jsx): rebuild to new design, fully live-wired.
Dropped fabricated figures per "real data" rule (object-store %, uptime,
storage breakdown); repurposed ingest cell to real Assets-24h count.
Fixed undefined refs and double-rendered Resources section.
Playout: as-run writer in scheduler.js writeAsRun() off the health-tick
/status poll; AsRunPanel UI + missing CSS in styles-playout.css.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause of the persistent black preview, fully isolated: ZAMPP1's nginx
serves the live .m3u8 fresh on every request (no-store works there), but
the PUBLIC reverse proxy (159.112.211.103 -> ZAMPP1) caches the static
.m3u8 by path with a multi-second TTL, ignoring both the origin's no-store
and query params. hls.js reloads the playlist ~every second, always landing
inside that TTL, so it sees the live playlist as never advancing
("live playlist MISSED" forever), never establishes the timeline, and never
loads a fragment -> readyState 0 (black). Proven: rapid reads via ZAMPP1
localhost advance (404->405); the same rapid reads via the public URL are
stuck; query-busting doesn't help (proxy caches by path).
Fix: serve the playlist through GET /api/v1/playout/channels/:id/hls/index.m3u8
instead of the static /media/live path. /api/ is not proxy-cached (the live
status poll already updates fine through it), so hls.js always gets the fresh
live edge. Segment (.ts) lines are rewritten to absolute /media/live/<id>/
URLs so they still load from the static path (immutable; caching them is
correct). ProgramMonitor points hls.js at the /api playlist and sends the
session cookie (xhrSetup withCredentials) since /api is auth-gated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Definitive root cause of the black preview, proven in-browser: the live
.m3u8 was served Cache-Control: no-cache, so the browser cached the
playlist and served a STALE copy to hls.js's reloads (cache:'default'
stuck at one MEDIA-SEQUENCE while cache:'reload' advanced). hls.js saw the
live playlist as never advancing -> "live playlist MISSED" forever ->
never established the timeline -> never loaded a fragment -> readyState 0
(black), even though the stream itself is clean and advancing server-side.
Fix: serve live HLS (/live and /media/live) with
"no-store, no-cache, must-revalidate" + Pragma no-cache so the browser
never caches the playlist and every reload fetches the fresh live edge.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements docs/superpowers/specs/2026-05-31-storage-settings-growing-smb-design.md.
1. Storage warning banner at the top of Settings → Storage (set-once /
path-change-corrupts-data warning).
2. Growing-files SMB credentials + system CIFS mount (Approach A):
- settings.js: new global keys growing_smb_mount / growing_smb_username /
growing_smb_vers; growing_smb_password is write-only (GET returns only
growing_smb_password_exists; growing_smb_password_clear:true removes it).
- GrowingSettingsCard: SMB mount/username/password (masked, "saved" state) +
CIFS version fields.
- capture Dockerfile: add cifs-utils + util-linux.
- capture-manager: on growing start, mount //host/share at /growing using a
root-only credentials file (creds never on the command line); unmount on
stop; mount failure falls back to S3 streaming so a recording is never lost.
- recorders.js: pass GROWING_SMB_* env; don't host-bind /growing when a CIFS
mount is configured (an empty mountpoint is required).
3. Per-recorder growing mode (global toggle removed):
- Removed the global "capture writes to local SMB share first" checkbox; the
growing card is now SMB-infrastructure-only.
- recorders.js reads the per-recorder recorders.growing_enabled column
(already present from migration 014) instead of the global setting;
RECORDER_FIELDS += growing_enabled.
- New-recorder modal: "Growing-files mode" toggle.
- storage.js overview: "enabled" now means the SMB landing zone is configured
(mount source set), surfaced as smb_mount; health strip labels updated.
No DB migration required (recorders.growing_enabled exists; new settings are
key/value rows).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- nginx.conf: add /media/live/ location serving from the media volume
mount. CasparCG sidecar writes HLS preview to /media/live/<id>/ but
nginx only had /live/ (capture volume). Without this, preview
requests returned the SPA shell instead of the .m3u8 playlist.
- ProgramMonitor: add live elapsed counter (MM:SS, ticks every 500ms)
driven by engine.currentItemStartedAt. Shows alongside clip index.
Adds a ⚠ pip when lastError is set (e.g. NDI SDK missing) without
blocking operation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- jobs.js: add playout-stage BullMQ queue to QUEUES; asset_id from
job data is already resolved to a name by attachAssetNames
- screens-jobs.jsx: map type 'playout-stage' -> kind 'Stage' with
monitor icon
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- playout-stage: skip loudnorm pass 2 when measured_I=-inf (silent or
no-audio clip); fall back to plain AAC transcode so staging completes
instead of erroring out
- screens-home: add Playout tile; replace Premiere panel tile with
Downloads tile opening a combined modal (Premiere panel releases +
Dragon-ISO link to forge.wilddragon.net/WildDragonLLC/dragon-iso)
- screens-playout: add Delete channel button (visible only when stopped);
removes channel from list and selects next on confirm
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix event bubbling: e.stopPropagation() in onItemDrop prevents
duplicate POST when dropping on an existing playlist item
- Wrap all drop handlers in try/catch with inline error display
- ProgramMonitor: replace text placeholder with hls.js video player
loading /media/live/<channel_id>/index.m3u8; falls back to native
HLS on Safari; destroys Hls instance on channel stop/unmount
- Playlist: per-item duration (MM:SS), staging progress bar with
animated stripe while staging, now-playing highlight + ▶ indicator
driven by engine.currentIndex from 4s status poll
- Playlist footer: clip count + total duration sum
- Transport: Play button disabled + shows '⏳ N staging' until all
items are media_status=ready, eliminating the staging-not-ready 409
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend (routes/users.js):
- GET / now returns totp_enabled so the UI can show 2FA status
- GET /:id/access — admin-only effective per-project access (MAX over direct +
group grants), labels via=direct|group:<name>; admins report all/edit
- POST /:id/totp/disable — admin clears a locked-out user's 2FA without their
password (self-service disable still requires it); dev user blocked
- role validated against {admin,editor,viewer} on create + PATCH (was unchecked)
Frontend:
- Users>Policies tab: static prose replaced with interactive per-user matrix —
inline role select, 2FA badge, Reset-2FA action, lazy per-user access expander
- Home "Premiere panel" tile -> "Downloads"; modal renamed, adds Teams ISO row
(disabled "coming soon" until the .exe is supplied); UXP .ccx link unchanged
- data.jsx: window.TEAMS_ISO placeholder ({available:false})
Not runtime-tested in browser yet. Teams ISO .exe still pending from user.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Fetches /playout/channels separately and degrades silently when the
endpoint or schema is absent.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
screens-playout.jsx + styles-playout.css: program monitor (HLS preview from
the sidecar), media bin, drag-drop playlist editor, transport controls. Plain
HTML5 drag-drop, no extra library. Talks to /api/v1/playout via
ZAMPP_API.fetch.
Wired into the shell: "Playout" under Operations, breadcrumb mapping, route
case in app.jsx, stylesheet + dist/screens-playout.js script in index.html.
Format dropdown defaults to 1080p5994 (matches the new channel default).
Optional "Sign in with Google" with auto-provisioning, fully config-gated:
without GOOGLE_CLIENT_ID/SECRET and OAUTH_REDIRECT_URL the routes 404 and the
button is hidden, so deployments without SSO are unaffected.
- migration 028: users.google_sub (unique) + email; password_hash nullable
for OAuth-only accounts
- src/auth/google-oauth.js: lazy google-auth-library, ID-token verify,
GOOGLE_ALLOWED_DOMAIN enforcement, requires email_verified === true
- auth routes: /auth/google (state-CSRF redirect), /auth/google/callback,
/auth/google/enabled; reuses establishSession
- web-ui: "Sign in with Google" on the login screen (shown only when enabled),
friendly callback error handling
- .env.example documents all new vars
Security hardening (from review of this + the TOTP work):
- resolveGoogleUser links ONLY by google_sub, never by email — a Google login
can never seize a pre-existing local account (account-takeover fix)
- a Google-linked account with TOTP still requires the second factor (ticket
in session, /?mfa=1 step) instead of bypassing it
- /login/totp now applies the per-IP login backoff
- recovery-code consumption is atomic (WHERE used_at IS NULL + rowCount)
- concurrent first-login race on google_sub is caught and re-resolved
- tests: google-oauth config helpers + google-link takeover/dedup regression
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The HLS-VOD work made GET /assets/:id/stream return the HLS playlist URL as
`url` whenever hls_s3_key was set. The Premiere plugin's "Import Proxy"
downloads `url` to a file and imports it — so it was saving an .m3u8 playlist
as .mp4, and Premiere rejected it ("unsupported compression type"). This hit
every YouTube asset (all get HLS generated), regardless of codec.
/stream now returns the directly-downloadable MP4 proxy as `url` (type mp4)
and the HLS playlist as a separate `hls_url`. The web player prefers `hls_url`
(so in-browser HLS playback is unchanged), while the already-installed plugin
gets a real MP4 again — no plugin reinstall needed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The redesigned UXP panel (left icon rail, compact list-view toggle, hover
tooltips, single Export menu) was committed only to redesign/panel-icon-rail
and never merged, so main + the website kept serving the old blocky-button
build under the same version number (2.2.2). That branch had diverged off an
old main and is missing recent worker/HLS/NVENC/import work, so it can't be
merged wholesale — cherry-pick just the plugin instead.
- services/premiere-plugin-uxp: replace source with the redesigned panel
(adds src/tooltip.js; reworks index.html + styles.css + src/*). Verified
byte-identical to the build installed on BMG-PC-Edit.
- web-ui/public/downloads/dragonflight-mam-2.2.2.ccx: swap the served
artifact to the redesigned 34708-byte build (download link unchanged).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Reorder the Workspace nav group so Projects sits above Library.
- Add a lucide-style `dollar` icon and use it for the Billing nav item
(was borrowing the `token` key icon).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The import queue row's progress bar read only `r.progress`, which pollRow
never updated numerically — it tracked asset.status (ingesting/processing/
ready) but not a percentage, so the bar sat at 0 until the asset flipped to
ready, then snapped to 100 ("blank until it finishes").
pollRow now also fetches GET /jobs/<jobId> and feeds the BullMQ job's numeric
progress (worker emits 2..100 across the yt-dlp download + S3 upload) into the
bar, so it fills during the download. Falls back to status when the job is
evicted post-completion. Also reaffirm 'downloading' label while ingesting and
poll a bit faster (2s) since short clips finish quickly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stuck-live fix: capture sidecar now finalises the pre-created live asset by id (new POST /assets/:id/finalize) instead of POSTing a new asset (409 collision); node-agent gives the sidecar a 180s stop grace so the S3 upload + callback complete; node-agent logs sidecar start/stop for diagnostics.
Live SDI monitor: HLS preview is now a 2nd output of the hires ffmpeg (single DeckLink read, split to ProRes/S3 + H.264/HLS); node-agent serves /live over HTTP; mam-api proxies GET /recorders/:id/live/* to the recorder node; web-ui HlsPreview loads from the proxied URL.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- recorders.js: when isRemote=true, replace MAM_API_URL in sidecar env with
http://<NODE_IP>:<PORT_MAM_API> so capture containers on worker host network
can reach mam-api (fixes assets stuck in live status after recorder stop)
- cluster.js: add GET /api/v1/cluster/metrics endpoint returning per-node
cpu/ram/gpu utilization; update heartbeat handler to persist metrics JSONB
- web-ui: add Resources panel to dashboard with live CPU/RAM/GPU bars per node,
polling /api/v1/cluster/metrics every 5s
Pair with the existing 'Retry all failed'. Drops every failed job from
the queue at once. Single confirm prompt. Optimistic local update so the
list clears instantly instead of waiting for the 5s poll tick.
.jobs-cancel-all CSS tinted danger-red without being a loud .btn danger,
matching the per-row Cancel pattern.
- shell.jsx: add Billing item under Admin (routes to the parody pricing
page); drop Editor from the Operations section
- app.jsx: route 'billing' to TokensParody; remove 'editor' and
'tokens-parody' routes
- screens-admin.jsx: rename parody h1 from 'Tokens' to 'Billing'; drop
the cross-link from the real Tokens page (no longer needed)
- screens-home.jsx: replace the Editor launcher tile with a 'Premiere
panel' tile that opens a new PremiereDownloadModal listing every
registered release (ZXP + Windows installer) with version + LATEST
badge + release date + notes
- styles-screens.css: .premiere-release-* row styles for the modal
Editor screen + nav button retired; users get the Premiere panel as the
recommended editor instead, with all download options in one place from
Home.
Sweep of 9 web-ui audit findings from tracker #156. Issue #150 (modal
codec stubs) deferred per user request.
## #146 sweep em-dashes (186 to 0)
- Replace placeholder '—' with '·' across all jsx
- Convert ' — ' to ': ' or '. ' in copy where context permits
- Comment-only em-dashes converted to ASCII dash
- Sweep css files too (16 comments)
## #147 remove glassmorphism + accent gradients
- Strip 8 backdrop-filter declarations from styles-screens.css and
styles-asset.css. Only legit modal scrim in styles-modal.css remains.
- Replace .job-progress-fill gradient with solid var(--accent)
- Replace .monitor-tile.audio gradient with flat var(--bg-1)
## #148 extract Jobs inline styles to CSS
- Cut 19 inline style={{...}} blocks in screens-jobs.jsx to 1 (dynamic
width on progress bar). Live DOM was 487 inline-styled elements due
to per-row repetition; now ~0.
- Added job-row-kind, job-row-asset, job-row-node, job-row-time,
job-row-actions, job-row-status-* utility classes in styles-screens.css
## #149 sidebar IA reorganized
- Replace flat NAV_TREE + ADMIN_TREE with NAV_SECTIONS:
Workspace / Ingest / Operations / Admin
- Move Capture out of Ingest into Operations (it's a live-signal monitor,
not an ingest action)
- Drop the 0/N capture badge from nav (belongs in topbar)
- Add BETA badge to Editor
## #151 redesign Editor 'Coming Soon' bumper
- Replace fullscreen glassmorphism + gradient + glow overlay with a flat
beta banner across the top of the editor area
- New .editor-beta-banner CSS class (flat, accent-soft tint, no blur)
## #152 hide Tokens parody, restore real API token mgmt
- New top-level Tokens admin page wraps existing ApiTokensSection
- Old parody renamed to TokensParody, accessible at /tokens-parody route
- Add window-level df:nav event for cross-component routing
## #153 make Home actually useful
- New activity strip below the launcher grid: 'Recording now' tiles for
live recorders, 'Last 24 hours' tiles for newly created assets, plus
an attention strip when there are failed jobs or errored recorders
- Each item is clickable and routes to the relevant screen
## #154 aria-labels on icon-only buttons
- Projects + Library grid/list view toggles now have aria-label + title
## #155 page-header pattern
- Dashboard now renders a proper .page-header h1 with subtitle + alert
badge + cluster status pip
- Library toolbar-title promoted to h1 for screen-reader hierarchy
- Document Home/Library/Editor full-bleed exceptions in DESIGN.md
- Editor's chrome is the beta banner (covered by #151)
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
## capture service
- capture-manager.js: add 'deltacast' source_type to _buildInputArgs.
Uses 'deltacast://<index>' with ffmpeg deltacast demuxer when
/dev/deltacast<N> exists; falls back to lavfi testsrc2 + sine test card
(matching deltacast-sdi-recorder standalone app) when hardware absent.
- routes/capture.js: add GET /devices/deltacast endpoint (enumerates
/dev/deltacast* + DELTACAST_PORT_COUNT env fallback). Extend /probe to
handle source_type=deltacast.
## node-agent
- detectHardware(): add 'deltacast' array to capabilities payload.
Enumerates /dev/deltacast* nodes; falls back to DELTACAST_PORT_COUNT env.
Adds DELTACAST_MODEL env support. Logs dc= count in heartbeat line.
- sidecar /start: bind /dev/deltacast* device nodes into capture containers
when sourceType='deltacast'.
## mam-api
- cluster.js: add GET /cluster/devices/deltacast and
GET /cluster/devices/deltacast/signal endpoints — same shape as
blackmagic equivalents for UI parity.
- recorders.js /start: pass DELTACAST_PORT_COUNT env to capture container;
bind /dev/deltacast* device nodes on local spawn.
- migration 024: ALTER TYPE source_type ADD VALUE 'deltacast' (idempotent).
- schema.sql: add 'deltacast' to source_type ENUM for fresh installs.
## web-ui
- modal-new-recorder.jsx: add 'Deltacast' source type card; fetch
/cluster/devices/deltacast on selection; port picker with TEST CARD
badge when hardware absent; falls through to manual index entry if
no devices detected.
- capture-manager.js, routes/capture.js: fix ffmpeg -sources decklink
parse regex from v4l2 hex-address format (never matched DeckLink output)
to correct indented-line format. Port 2+ (index 1+) was falling through
to a wrong model-name fallback, causing ffmpeg to open the wrong input
and produce black frames. Now logs the detected device list and the
selected name at start.
- recorders.js (/start): accept per-take projectId override in request
body. If provided, clips go to that project instead of the recorder's
default project_id. Used for both the live-asset INSERT and the
PROJECT_ID env var passed to the capture container.
- screens-ingest.jsx (RecorderRow): add project dropdown shown when
recorder is stopped. Defaults to the recorder's configured project;
operator can change it before hitting Record without editing the
recorder config.