Commit graph

804 commits

Author SHA1 Message Date
0ee0cb91ef build(capture): nvenc-enabled ffmpeg Dockerfile (validated build)
Brings the node-local NVENC ffmpeg build into the branch and fixes it:
- multi-stage build with nv-codec-headers + --enable-nvenc/--enable-cuvid
- removes --pkg-config-flags="--static", which broke x265 detection
  ("ERROR: x265 not found using pkg-config") and prevented the image from
  ever building. Shared linking is used; runtime stage already apt-installs
  the shared codec libs (libx265-199 etc).
- self-validating: build fails if nvenc encoders are absent.

Rebuilt image confirmed to expose hevc_nvenc/h264_nvenc/av1_nvenc on the L4.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 13:33:37 -04:00
9210b41589 fix(capture): working All-Intra HEVC NVENC config (validated on L4)
NVENC rejects -g 1 ("GopLength > numBFrames+1" at InitializeEncoder), so
true all-intra is achieved with -bf 0 -forced-idr 1 -g 600 plus
-force_key_frames expr:1 (forces an IDR on every frame). ffprobe confirms
all frames are pict_type=I. Container is fragmented MOV; MXF muxing of HEVC
fails on this ffmpeg build ("Operation not permitted").

Validated end-to-end via direct ffmpeg on zampp2/L4:
hevc, Main 10, 1920x1080, yuv420p10le, ptypes=[IIIIII...]

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 13:23:44 -04:00
f2542bc929 feat(nvenc): GPU sidecar passthrough + All-Intra HEVC capture codec
Phase 0.2 of the NVENC All-Intra HEVC ingest plan.

node-agent/handleSidecarStart:
- Accept useGpu: true in the sidecar start body
- When useGpu: adds Runtime=nvidia, DeviceRequests=[gpu], and injects
  NVIDIA_VISIBLE_DEVICES=all + NVIDIA_DRIVER_CAPABILITIES=video,compute,utility
  into the container env. CPU-codec recorders are unaffected (useGpu defaults false).

mam-api/recorders (start endpoint):
- Derive useGpu from recorder.recording_codec — true for hevc_nvenc/h264_nvenc
- Pass useGpu to remote sidecar start body
- Apply same Runtime/DeviceRequests to the local Docker spawn path

capture/capture-manager:
- Update hevc_nvenc codec entry with all-intra flags:
  -g 1 -bf 0 (every frame IDR, no B-frames — required for growing-file
  edit-while-record), -rc vbr, -profile:v main10, pixFmt p010le (10-bit 4:2:0)

Next: validation gate (§8) — test MXF OP1a then fragmented MOV on one
DeckLink channel, mount in Premiere while recording.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 12:35:23 -04:00
0f6c715a30 docs: All-Intra HEVC (NVENC) growing-file ingest design
Captures the current working system (capture sidecar, finalize flow, live monitor, capability-routed GPU worker pool, deploy gotchas) and the target design: GPU All-Intra HEVC master to offload the ProRes CPU wall while keeping edit-while-record, scaling to 8 signals + multi-vendor (Blackmagic/Deltacast/AJA). Includes a validation gate (prove Premiere growing-HEVC edit on one channel first).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 04:16:17 +00:00
fdec2e307d feat(worker): capability-routed GPU worker pool + per-node job attribution
WORKER_QUEUES env lets a worker subscribe to a subset of queues. Deploy one GPU-pinned container per card: heavy encodes (proxy/conform/trim) on Tesla P4 (zampp1) + L4 (zampp2) via NVENC; light jobs (thumbnail/filmstrip) on the 2x Quadro P400 (zampp1). BullMQ competing-consumers distribute across nodes. RUN_PROMOTION gates the growing-files scanner to one worker. Each worker stamps WORKER_LABEL onto job data so the Jobs UI Node column shows which node/GPU ran each job. Redis/DB/S3 for the zampp2 worker come from its .env (pointed at zampp1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 04:00:10 +00:00
92b460f503 fix(recorder): finalise live asset on stop + add live SDI monitor
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>
2026-05-29 03:20:20 +00:00
500599a955 fix: pass CAPTURE_TOKEN env var to mam-api container 2026-05-28 22:13:36 -04:00
634f1842bd fix: add Bearer auth to capture sidecar callback and pass CAPTURE_TOKEN
- capture/src/index.js: read MAM_API_TOKEN from env; include
  Authorization: Bearer header in shutdown callback fetches to mam-api
  (POST /assets and POST /assets/:id/mark-empty). Without this, mam-api
  AUTH_ENABLED=true rejects the callback with 401, leaving assets stuck in live
- recorders.js: pass MAM_API_TOKEN=${CAPTURE_TOKEN} in sidecar env so the
  capture container receives the token at boot
- api_tokens: inserted capture-sidecar token (unbound, prefix b3d3d3c4)
2026-05-29 01:57:39 +00:00
453103aee6 fix: use external MAM_API_URL for remote capture sidecars; add cluster metrics endpoint and dashboard resource graphs
- 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
2026-05-29 01:04:24 +00:00
6f64b55824 feat(ui): add 'Cancel all failed' button to Jobs screen
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.
2026-05-29 00:02:55 +00:00
303f12e0f9 ui: add Billing admin nav, drop Editor nav, replace Editor tile with Premiere panel download modal
- 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.
2026-05-29 00:01:19 +00:00
342b56af35 ui: full audit pass (fixes #146, #147, #148, #149, #151, #152, #153, #154, #155)
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)
2026-05-28 23:50:07 +00:00
f54c49d2dc fix(web-ui): fix duplicate DeckLink groups in new-recorder modal; refactor device pickers
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
2026-05-28 23:18:55 +00:00
888ca65045 feat(capture): Deltacast SDI framework — test-card capture, cluster detection, UI
## 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.
2026-05-28 23:12:40 +00:00
b6f5b9b407 fix(capture): disable concurrent SDI proxy ffmpeg — DeckLink Duo 2 rejects second reader
DeckLink Duo 2 does not support two simultaneous ffmpeg processes on the
same port. The second (proxy) process immediately gets 'Cannot Autodetect
input stream or No signal', producing an empty upload that could crash the
container before the hires upload completes.

Fix: remove the parallel proxy spawn for SDI entirely. proxyKey is now
always null for SDI recordings (same as SRT/RTMP). needsProxy=true is
already set when proxyKey is null, so the BullMQ worker generates the
proxy from the hires master after stop — same pattern that works for
network sources.

Also revert bad regex change: ffmpeg -sources decklink output on this
hardware uses hex-address format ('81:76669a80:00000000 [DeckLink Duo (1)]')
not bare indented names — original regex was correct.
2026-05-28 22:36:06 +00:00
354731a363 fix(capture): fix DeckLink device name enumeration for SDI port 2+; add per-take project selector on Recorders page
- 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.
2026-05-28 22:26:08 +00:00
Claude
1fcb927d26 feat(web-ui): library Download button + dismissable size warning (#145)
Adds an inline hi-res download trigger to the asset library.

UI:
- Small 22×22 download icon button in the top-right corner of each
  asset thumbnail. Hidden by default, fades in on card hover or focus
  so the resting-state grid stays clean.
- Only renders for assets that have an `original_s3_key` — proxies
  and unfinished captures never offer it.
- Mirrored as a "Download original…" entry in the right-click
  AssetContextMenu (between Rename and the bin actions).

Flow:
- First click (or any click while the warning is enabled) opens
  DownloadWarningModal: terse copy explaining the file is the full
  original ingest, can be multi-GB, and that speed depends on the
  user's network connection. Footer: Cancel · Download. Body: a
  "Don't show this again on this device" checkbox.
- Ticking the checkbox persists `df.lib.download.warnDismissed=1`
  in localStorage. Subsequent clicks skip the modal and start the
  download straight away.

Download itself reuses /api/v1/assets/:id/hires (presigned S3 URL)
— no proxy round-trip through mam-api, no in-browser progress UI
beyond what the browser already shows.

Spec: #145
Settings → Account "re-enable the warning" toggle is not in this
patch and will land separately.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 16:14:24 -04:00
Claude
6bc6478270 feat(worker): conform — queue proxy build for the conformed output
ProRes / DNxHR conformed outputs are unplayable in the browser
(HTML5 video: MEDIA_ERR_SRC_NOT_SUPPORTED). The library was
referencing the ProRes original as the only source.

After the asset row is inserted, queue an H.264 proxy build the same
way services/mam-api/src/routes/assets.js does on ingest:
  proxyQueue.add('generate', {
    assetId,
    inputKey:  outputKey,         // the conformed mov / mp4
    outputKey: `proxies/${id}.mp4`,
  });

The proxy worker writes the H.264 mp4, updates assets.proxy_s3_key,
and from then on /assets/:id/stream prefers the proxy over the
original. The library player can decode it natively.

Failure to enqueue is logged but doesn't fail the conform job — the
asset still exists and can have a proxy re-queued later.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 15:49:01 -04:00
Claude
446a563647 fix(worker): conform — write ProRes/DNxHR to MOV, not MP4
The final concat-demux + encode step erred with:
  [mp4] Could not find tag for codec prores in stream #0,
        codec not currently supported in container
  [out#0/mp4] Could not write header (incorrect codec parameters ?)

ProRes and DNxHR live in QuickTime (.mov), not MP4. The output path,
S3 key, and asset-row filename were all hardcoded to .mp4.

Pick the container from the codec:
  prores / prores_hq / prores_4444 / dnxhr_hq → mov
  h264 / h265 / anything else                 → mp4

outputExt is computed once at the top of the worker (before tmpfile
creation) and reused for the temp output, the S3 key
(jobs/<id>/conformed.<ext>), and the assets row's filename column.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 15:40:53 -04:00
Claude
71d8944a01 fix(worker): conform — use aformat for channel layout (ffmpeg 8 dropped aresample ocl)
ffmpeg 8.x removed the `ocl` shortcut option from aresample (it was a
deprecated alias for out_chlayout). The per-segment trim+normalise call
errored immediately:
  [fc#-1] Error applying option 'ocl' to filter 'aresample': Option not found

Split the chain: aresample handles the sample rate, aformat asserts +
auto-converts to stereo + fltp.

  aresample=48000,aformat=channel_layouts=stereo:sample_fmts=fltp

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 15:37:35 -04:00
Claude
686b90294b fix(worker): conform — 2-pass strategy (normalise on trim, demux on concat)
ffmpeg 8.x's concat filter kept dying with the opaque
  [fc#0] Error sending frames to consumers: Invalid argument
even after we locked fps + sample rate + pixel format + SAR in the
filter graph. Mixed sources (AV1+H.264, 23.98+60 fps, 44100+48000 Hz,
tv-range+unspecified-range pixel format) just don't survive the
concat filter cleanly in this build.

Switch to the more reliable 2-pass pattern:

1. At the trim step, re-encode each segment to a uniform intermediate
   spec: libx264 ultrafast, 1920x1080 (letterboxed), yuv420p,
   seqFps target rate, 48kHz stereo AAC. Per-segment ffmpeg.

2. At the concat step, use the concat *demuxer*. Because every input
   now matches exactly, the demuxer is well-behaved. Transcode the
   concatenated stream to the final target codec (ProRes 422 HQ etc).

Costs an extra intermediate encode (libx264 ultrafast ≈ realtime on
this hardware) but eliminates the filter-graph fragility on mixed-
source timelines, which is the workload that actually matters.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 15:34:52 -04:00
Claude
fcf4c8bbe7 fix(worker): conform — lock fps + sample rate in concat filter graph
After the demuxer → filter switch, concat still failed with
  [fc#0] Error sending frames to consumers: Invalid argument
on Job 8. The filter graph normalised pixels (scale+pad+yuv420p) but
left the time-domain axes mixed:

  segment-1: 23.98 fps video, 44100 Hz audio
  segment-2: 60    fps video, 48000 Hz audio
  segment-3: …

ffmpeg 8's concat filter requires identical frame rate + audio sample
rate + channel layout across inputs. Force them on each leg:

  video: fps=<seqFps>, setpts=PTS-STARTPTS
  audio: aresample=48000,
         aformat=channel_layouts=stereo:sample_fmts=fltp,
         asetpts=PTS-STARTPTS

setpts/asetpts re-zero each input's clock so concat's per-input PTS
window resets cleanly between segments.

Target fps comes from the sequence's frame_rate (rounded) — same axis
the sequence editor stores. Sample rate is pinned to 48000 (broadcast
standard) so the AAC encode is consistent.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 15:21:23 -04:00
Claude
94b6710e2d fix(worker): conform — concat-filter for mixed source formats
ffmpeg concat demuxer dies with "Error sending frames to consumers:
Invalid argument" when input segments don't share codec / pixel format
/ framerate / resolution. Mixed-source timelines hit this every time —
e.g. an AV1 clip + an H.264 clip going through the same concat.

Switch to the concat *filter*. It re-encodes through a filter graph
so disparate inputs are normalised inline. Each input is scaled to
1920x1080 with letterbox, format=yuv420p, audio resampled. concat=n=N
joins them into [outv]/[outa].

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 15:04:55 -04:00
Claude
6412b5c252 fix(worker): conform — preserve audio + map ProRes/DNxHR codecs
Three cooperating bugs left the rendered output silent and in the
wrong codec:

1. executor.js trimSegment used `-frames:v` with no audio mapping.
   ffmpeg dropped the audio track on each segment before they reached
   the concat step. Add `-c:a copy -shortest` so each segment carries
   its original audio.

2. conform.js audioFlag was `audio === 'include' ? aac : -an`. The
   panel's v2.2.1 defaults send `audio: 'broadcast'`, which didn't
   match 'include' → `-an` explicitly stripped audio at the encode
   step. Switch to the opposite default: only an explicit 'none' or
   'off' disables audio; everything else gets AAC 320k @ 48kHz.

3. conform.js video codec map only matched `codec === 'prores'`. The
   panel sends `'prores_hq'` (and the conform slide panel can send
   `'prores_4444'` / `'dnxhr_hq'`). All of those fell through to
   libx264 and silently rendered H.264 instead of the requested codec.
   Add a real codec map with the right prores_ks profiles (3=HQ,
   4=4444) and DNxHR. Skip -crf for ProRes since the profile encodes
   quality.

The asset-row metadata's `codec` column is normalised the same way so
the new asset record matches what was actually written.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 14:32:49 -04:00
Claude
56d7479a35 fix(mam-api): pass project_id into conform job so render can register the asset
The conform worker's final step INSERTs the rendered output into the
assets table:

  INSERT INTO assets (project_id, filename, display_name, …)
  VALUES ($1, …)
  -- project_id NOT NULL

It reads projectId from job.data, but the /sequences/:id/conform
endpoint never set it. Render finished cleanly, ffmpeg ran, output
uploaded to S3, then the final asset row INSERT failed:
  null value in column "project_id" of relation "assets"

Pass seq.project_id from the loaded sequence row. The rendered output
lands as an asset under the same project as its source sequence —
the natural target.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 14:24:04 -04:00
Claude
aeecb6e32a fix(worker): conform — resolve clips from sequence_clips instead of filename
Panel had been sending xmeml with clipitem/name = the local Premiere
file path's basename (e.g. "dragonflight-Interstellar - Docking Scene
1080p IMAX HD.mp4"). The worker's old filename lookup ran
  SELECT id, original_s3_key FROM assets WHERE filename = $1
which never matched, because the assets row's filename is the
original MAM ingest name without the "dragonflight-" prefix.

Fix: when job.data has sequenceId (always set by the conform endpoint
at routes/sequences.js:317), pull edits directly from sequence_clips,
which the panel already wrote with authoritative asset_id mappings on
push. We JOIN to assets for original_s3_key + filename and order by
(timeline_in_frames, track) so segment indices stay deterministic.

The XML is still parsed for sequence-level metadata (name, fps) when
provided, but its clipitems are no longer authoritative.

The legacy filename path (EDL input or fcpXml without sequenceId)
stays unchanged for backward compatibility.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 14:08:49 -04:00
Claude
0abef056e7 fix(uxp+mam-api): Export Timeline render — xmeml schema + BullMQ job poll
Two cooperating bugs left Export Timeline stuck at "Rendering Hi-Res"
forever:

A. worker emitted "Invalid FCP XML: no sequence element" because
   Timeline.generateFcpXml produced fcpxml (FCP X schema:
   <fcpxml><resources>/<library>/...) while the worker's parseFcpXml
   expects xmeml (FCP 7 schema: <xmeml><sequence>...). Two completely
   different formats.

   Rewrite generateFcpXml to emit xmeml v5 with the structure the
   parser walks:
     xmeml/sequence/{name,duration,rate{timebase,ntsc},
                     media/video/{format/samplecharacteristics,
                                  track[@currentExplodedTrackIndex]
                                  /clipitem/{name,duration,rate,in,out,
                                             start,end,file/{name,pathurl}}}}
   Clipitem in/out are SOURCE frames (the underlying media in/out);
   start/end are TIMELINE frames (the cut position). The worker uses
   the rate timebase to parse them.

B. /api/v1/jobs/:id rejected the panel's polls with
   "Invalid id — must be a UUID". The handlers below correctly parse
   BullMQ-prefixed ids ("conform:42"), but router.param('id',
   validateUuid('id')) ran first and 400'd everything that wasn't a
   UUID. The panel's pollConform swallows the resulting fetch error
   silently and polls forever.

   Drop the validator. Comment in the file explains why.

Bumps panel to v2.2.2.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 13:58:13 -04:00
Claude
540d333758 feat(uxp): v2.2.1 — Export Timeline is now one-click push + render → asset
Contract: clicking Export Timeline does the whole pipeline with no
prompts. Behavior matches what the user actually expected from the
button label:

  1. readActiveSequence — pulls the Premiere timeline + clip map
  2. resolveExportProject — picks the target MAM project. First run
     uses the first project on the server and caches its id in
     localStorage (df.uxp.exportProjectId). Subsequent runs reuse
     the cache. If the cached project was deleted server-side we
     transparently re-pick.
  3. Timeline.startConform with sensible defaults:
       codec=prores_hq, quality=high, resolution=source, audio=broadcast
     This both pushes the sequence + clip rows AND queues a real
     conform job (the prior Push-to-MAM button never queued a job,
     which is why "no jobs spin up" happened earlier).
  4. pollConform every 2s, mapping job progress 20→95% on the
     panel progress bar.
  5. On completion, toast + Library.refresh() so the rendered hi-res
     asset shows up in the grid without needing to click around.

The Conform slide panel stays wired for Advanced → Export & Conform
so power users can still override the codec/preset for one-off jobs.
The Push-only slide panel that this replaces is now orphaned chrome
and will be removed in a later cleanup.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 13:28:51 -04:00
Claude
e4e69973e5 ui(uxp): v2.2.0 — density pass: drop details panel, tighter buttons, collapsible Advanced
User feedback after v2.1.9: panel still chrome-heavy. The Asset Info
panel duplicates what the card already shows; 8 buttons across 3
full-width rows still claim too much vertical real estate.

Three surgical changes:

1. Drop the Asset Info details panel entirely. Card meta (name +
   duration + codec) already carries everything we showed in the
   key:value table. Library._showDetails / hideDetails become no-ops
   so the existing call sites in main.js + library.js don't need
   conditional branches.

2. Shrink .action-row .btn to 20px tall, 10.5px font, 6px horiz
   padding, 3px radius. Two rows of compact buttons fit where one
   bulky row used to.

3. Collapse Advanced section behind a toggle (▸ / ▾). Default
   collapsed so the main 6 buttons stay the primary action surface;
   click the row to expand and reveal Export & Conform / Fetch &
   Relink All.

Per DESIGN.md "density over whitespace."

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 13:03:56 -04:00
Claude
c3b087020d ui(uxp): v2.1.9 — visible version chip + diagnose multi-version install
UPIA stacks every install in its own
  C:\Program Files\...\UXP\Plugins\External\net.wilddragon.dragonflight.uxp_<version>\
folder without removing prior versions. After 10 deploys today there are
11 of them coexisting, and Premiere's loader can pick the wrong one,
which is why v2.1.8 didn't appear to land.

This change makes the running version visible at a glance:

- main.js reads manifest.json at runtime via require('uxp').storage
  .localFileSystem.getPluginFolder() so the displayed version is
  whatever Premiere actually loaded — never a hand-edited constant
  that could drift.
- index.html adds #panel-version inside the status strip (between
  host and ⋯) and #brand-version below the brand tag on connect.
- styles.css: small mono chip in --text-4, low key but readable.

If the chip ever shows the wrong version we know the loader picked
a stale dir; if it shows nothing the manifest read itself failed.

The install script needs to remove old _<version> dirs going forward;
the next commit will add that cleanup step to the deploy.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 12:00:50 -04:00
Claude
c2a6c1557b ui(uxp): v2.1.8 — density redesign on top of v2.1.7
Three changes, surgical so timeline.js / conform / relink / growing
all keep working:

A. Header → 24px status strip + ⋯ menu
   `connected-bar` rule kept as an alias to `.status-strip` so any code
   path that still emits the old class falls through cleanly. Markup
   replaced with .signal-dot + #connected-host + .btn-ghost ⋯ that
   toggles a .menu containing the Disconnect button. Menu auto-closes
   on outside click. Reclaims ~12px of permanent vertical chrome and
   removes the always-visible Disconnect.

B. Compact action footer
   `.action-row .btn` now: 22px tall, 11px font, 0.01em letterspacing.
   `.advanced-section .action-row .btn` goes a step smaller (20px /
   10.5px). Global `.btn` untouched so #connect-btn stays at full
   weight on the connect pane.

D. Token alignment with services/web-ui DESIGN.md
   --bg-0 #0B0D11 (was #0e0f12), --accent #5B7CFA (was #4f7cff),
   plus the full --text-1..4 / --success / --warning / --danger / --live
   palette. Legacy --ok / --warn aliased to --success / --warning so
   existing rules keep resolving.

C (per-card meta) was already in v2.1.7 — no change needed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 11:51:47 -04:00
Dragonflight Deploy
b04882a310 release(uxp): v2.1.7 — fix resource busy + export timeline robustness 2026-05-28 11:12:49 -04:00
60e5093c6b fix(uxp): null-safe Time object access in readActiveSequence
getStartTime/getEndTime/getInPoint/getOutPoint can return null for
non-clip track items (gaps, transitions) that slip past the
getProjectItem check. Accessing .seconds on null threw a TypeError
that the outer catch swallowed — silently dropping every clip and
leaving clips[] empty, so the export panel never opened.

Also skip clips where all four time values resolve to 0 (filler items).
2026-05-28 11:12:32 -04:00
382f432693 fix(uxp): resolve "Resource busy" on re-import of same asset
- _writeBuffer: catch EBUSY (Windows file-lock) and treat as success —
  the file is already there from the previous import and Premiere has it
  locked; no need to re-write it.
- proxy / hires: stat the destination first; if the file already exists
  skip the download entirely and go straight to importIntoProject.
- importIntoProject: importFiles returning false means the file is
  already in the Premiere project — not an error, treat as success.
2026-05-28 11:11:32 -04:00
Dragonflight Deploy
e533566ae2 release(uxp): v2.1.6 — fix thumbnail auth (bearer fetch + blob URL) 2026-05-28 09:53:34 -04:00
c3e4306d9f fix(uxp): fetch thumbnails via API.request() to carry Bearer token
img.src direct assignment never sends Authorization headers, so all
thumbnail requests returned 401 once the global auth gate was enabled.
Now fetches via API.request(), converts response to a blob URL, and
assigns that to img.src. Falls back to the placeholder div on error.
2026-05-28 09:41:26 -04:00
eeb0d9f65f UXP v2.1.5b: main.js relink handler await fix + artifact 2026-05-28 09:15:24 -04:00
7f0ca5922f UXP v2.1.5: main.js — fix relink handler: await getActiveProject, use requestExternal + arrayBuffer 2026-05-28 09:15:06 -04:00
469521d524 UXP v2.1.5 release artifact 2026-05-28 09:07:15 -04:00
07840441b9 UXP v2.1.5: bump manifest version 2026-05-28 09:07:00 -04:00
5774f61ac7 UXP v2.1.5: timeline.js — await all premierepro calls; runtime is async 2026-05-28 09:06:43 -04:00
1fb790a569 UXP v2.1.5: import-flow — await all premierepro calls (runtime is async despite docs saying sync) 2026-05-28 09:05:49 -04:00
11cb93aa51 UXP v2.1.4 release artifact 2026-05-28 08:32:55 -04:00
dbc67636b2 UXP v2.1.4: bump manifest version 2026-05-28 08:32:35 -04:00
460b590d46 UXP v2.1.4: timeline.js — replace API.requestFollow with API.requestExternal for batch relink S3 downloads 2026-05-28 08:32:18 -04:00
f3a640a7c5 UXP v2.1.4: api.js — remove redirect:manual (not supported in UXP fetch); UXP auto-follows redirects 2026-05-28 08:31:09 -04:00
baa289f6c3 UXP v2.1.4: import-flow — drop redirect:manual (not supported in UXP fetch, causes null body); use arrayBuffer() fallback if body.getReader unavailable 2026-05-28 08:30:30 -04:00
e5f218655e UXP v2.1.3 release artifact 2026-05-28 07:50:52 -04:00
8119b57b45 UXP v2.1.3: bump manifest version 2026-05-28 07:50:29 -04:00
9765fd91f7 UXP v2.1.3: main.js — fix inline relink handler (sync getActiveProject, no require inline) 2026-05-28 07:50:18 -04:00