Commit graph

401 commits

Author SHA1 Message Date
f2d5f5aa16 feat(cluster): auto-assign node profiles from detected hardware
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>
2026-05-31 18:02:29 -04:00
d908c0c056 fix(playout+capture+monitors): preview recovery, SMB UNC, playout monitors
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>
2026-05-31 17:56:45 -04:00
43011bd794 Merge feat/playout-mcr into main
Playout/MCR, as-run log, redesigned dashboard, capture CIFS/growing-files,
SDI settings, cluster Add Node wizard, homepage refresh.

# Conflicts:
#	services/mam-api/src/routes/cluster.js
#	services/mam-api/src/routes/playout.js
#	services/mam-api/src/scheduler.js
#	services/playout/Dockerfile
#	services/playout/entrypoint.sh
#	services/web-ui/public/screens-home.jsx
2026-05-31 17:46:12 -04:00
19f0abeabe feat(cluster+home): Add Node wizard, homepage tagline/logo/settings tweaks
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>
2026-05-31 17:37:37 -04:00
f21bc490e8 feat(web-ui): redesigned Dashboard + playout as-run log
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>
2026-05-31 17:15:32 -04:00
d3ad2397fb fix(playout): serve preview m3u8 via /api to bypass the proxy's static cache
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>
2026-05-31 15:57:41 -04:00
5968d4f681 feat(settings/growing): storage warning, SMB auth + CIFS mount, per-recorder growing
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>
2026-05-31 14:50:36 -04:00
739d08d4b5 fix(web-ui): restore home screen icon fixes on feat/playout-mcr
playout tile: monitor -> signal, dashboard tile: home -> layout.
All feature-branch content (Dragon-ISO, tagline, etc.) preserved.
2026-05-31 13:56:19 -04:00
1db0e81efb fix(web-ui): restore nav icon fixes on feat/playout-mcr
youtube: import, schedule: clock, playout: signal.
Keeps getting wiped by other agents writing full file replacements.
2026-05-31 13:54:16 -04:00
40e987b4a2 fix(web-ui): restore icon audit fixes on feat/playout-mcr
jobs: bulleted list (not hamburger), import: added, grid: rx=1,
hdd: cylinder, proxy: sliders. Keeps getting wiped by other agents.
2026-05-31 13:52:37 -04:00
d778aa4cdb fix(playout): HLS preview path + live elapsed counter
- 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>
2026-05-31 13:16:57 -04:00
e51cf1aa9c feat(jobs): surface playout-stage queue in Jobs screen
- 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>
2026-05-31 12:06:08 -04:00
f7cf56ae0d fix(playout): silent-audio staging crash, home tiles, channel delete
- 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>
2026-05-31 12:03:20 -04:00
12115a053a feat(playout): fix 409 drag bug, add HLS preview, advanced playlist
- 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>
2026-05-31 11:45:25 -04:00
43656a5e88 shell: youtube nav icon: download → import
download is used by the Downloads section tile on the home screen.
YouTube ingest gets the new import icon (arrow entering box) instead.
2026-05-31 10:52:10 -04:00
68461af990 icons: add import icon (arrow entering box)
Distinct from download (vertical arrow+line). Used for YouTube ingest
to avoid sharing a glyph with the Downloads section tile.
2026-05-31 10:50:43 -04:00
8bc460025d screens-home: fix launcher tile icons
- Dashboard tile: home → layout (matches sidebar nav icon)
- Playout tile: monitor → signal (matches sidebar nav fix)
2026-05-31 00:19:34 -04:00
42064acefa shell: fix nav icon conflicts
- schedule: jobs → clock (was sharing hamburger icon with Jobs)
- playout: monitor → signal (was sharing TV icon with Monitors)
2026-05-30 13:30:42 -04:00
2e2b091653 icons: fix 4 icon issues found in audit
- jobs: replace hamburger (nav menu) with bulleted list (task queue)
- grid: add rx="1" to match library icon (consistency)
- hdd: replace circle+dot (vinyl) with cylinder (storage)
- proxy: replace upload-arrow with sliders (transcode/transform)
2026-05-30 13:29:18 -04:00
Zac
c502d4a16f feat(web-ui): update home tagline + add "Let's create" motto
Tagline "Self-hosted broadcast media-asset management" ->
"Media Asset Management & Production Platform"; add italic accent motto
"Let's create" below it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 16:04:28 +00:00
Zac
9d098e9778 feat(auth-ui): interactive permissions matrix, admin 2FA reset, Downloads button
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>
2026-05-30 15:59:27 +00:00
Zac
f837e57969 feat(web-ui): add Playout tile to home screen
Fetches /playout/channels separately and degrades silently when the
endpoint or schema is absent.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 14:59:59 +00:00
Zac
793011b78b feat(web-ui): MCR page — channels, playlist, transport, preview
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).
2026-05-30 14:02:25 +00:00
Zac
0c3a4b625f feat(mam-api,web-ui): Google OAuth (OIDC) sign-in
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>
2026-05-30 02:51:59 +00:00
Zac
fff0828d79 feat(mam-api,web-ui): TOTP two-factor authentication
Optional time-based 2FA on top of password login. TOTP core is hand-rolled
on node:crypto (RFC 6238) — no runtime dep — and verified against the RFC
test vectors.

- migration 027: users.totp_secret/totp_enabled + user_recovery_codes
- src/auth/totp.js: base32, secret gen, RFC 6238 verify, otpauth URI,
  recovery codes
- src/auth/mfa-tickets.js: short-lived single-use tickets bridging the two
  login steps (in-memory, single-instance like the rate-limiter)
- auth routes: /totp/setup, /totp/enable (returns recovery codes once),
  /totp/disable (password-confirmed); login returns {mfa_required, ticket}
  when enabled, /login/totp completes with a code or recovery code
- /auth/me and loadUser surface totp_enabled
- web-ui: login second-factor step; Settings -> Account TOTP enroll (QR +
  manual secret + recovery codes + disable)
- qrcode added as an optional dep; setup degrades to manual entry if absent
- tests: totp unit (RFC vectors) + integration (enable/login/recovery/disable)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 02:42:57 +00:00
Zac
ec026195eb feat(mam-api,web-ui): per-project RBAC (v2 auth layer)
Adds per-project access control on top of the flat v1 auth. admin keeps
global access; editor/viewer are scoped to projects granted to them (direct
or via group) at view (read-only) or edit (read-write) level.

- migration 026: project_access table + access_level enum
- src/auth/authz.js: central isAdmin/accessibleProjectIds/projectLevel/
  assertProjectAccess
- requireAdmin middleware; admin-gate /users, /auth/users, /groups
- enforce scoping on projects, assets, bins (list filter + per-resource
  view/edit + create checks); gate bulk asset maintenance + batch-trim
- grant API: GET/POST/DELETE /projects/:id/access
- web-ui: hide admin nav for non-admins, admin-route bounce, project
  "Manage access" modal, rewrite Policies tab
- tests: authz, project-access, assets-access (node:test, skip w/o DB)
- deferred routers carry TODO(authz) markers; .env.example documents the
  service-token-needs-admin/grants requirement

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 02:37:36 +00:00
9d6bbf8112 fix(mam-api): /stream returns MP4 url + separate hls_url (fixes Premiere import)
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>
2026-05-29 21:44:52 -04:00
39ef551489 feat(uxp): ship the icon-rail panel redesign as v2.2.2 (recover from redesign branch)
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>
2026-05-29 20:45:29 -04:00
8f26f1bd9a ui(web-ui): Projects above Library in nav + $ icon for Billing
- 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>
2026-05-29 20:17:42 -04:00
a7ef0397e1 fix(web-ui): show live download % on YouTube import bar
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>
2026-05-29 19:53:19 -04:00
9b47250388 feat(recorder): default All-Intra HEVC (NVENC) + custom bitrate, auto fps/res, source-bitrate warning
#2 Recorder codec/bitrate:
- Default recorder codec → hevc_nvenc (All-Intra HEVC NVENC); ProRes/H.264/DNxHR
  still selectable. recorders.js default flips prores_hq → hevc_nvenc.
- Custom target bitrate (Mbps) input, shown only for bitrate-controlled codecs
  (NVENC/x264/x265/DNxHD); ProRes shows quality-based (no bitrate).
- Framerate + resolution are auto-detected from source (manual fields removed).
- Container derived from codec (HEVC/ProRes/DNxHR → fragmented MOV, H.264 → MP4);
  drops the stub container picker (closes #150 direction).

#3 SRT/RTMP customization + bitrate warning:
- Same codec/bitrate/auto controls apply to network recorders (shared form).
- Warns in the modal when the configured target bitrate exceeds the probed
  source stream bitrate (via /recorders/probe) — re-encoding above source adds
  storage, not quality.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 17:04:00 -04:00
35fd9c0253 feat(web-ui): serve UXP (.ccx) plugin, remove legacy ZXP panel
Replaces the ZXP/CEP Premiere panel downloads with the UXP plugin:
- data.jsx PREMIERE_RELEASES → single UXP 2.2.2 entry (ccx pointer)
- home-page Premiere modal + Settings→Capture SDKs render the .ccx
  download and UXP install copy (UXP Developer Tool / Creative Cloud)
- add downloads/dragonflight-mam-2.2.2.ccx (built from premiere-plugin-uxp)
- remove the 1.0.0/1.0.1/1.1.0/1.2.0 .zxp + windows-setup.exe artifacts

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 14:20:46 -04: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
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
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
Zac Gaetano
7e3e6b2a28 fix(auth): force HTTPS on dragonflight.live so login cookies stick
User reported infinite login loop on dragonflight.live. Root cause: openresty
fronts both http:// and https:// without redirecting, and a user landing on
http:// gets the Set-Cookie response silently dropped — cookies are Secure-only
when TRUST_PROXY=true, and the CORS allowlist refuses the http:// origin.
Result: login appears to succeed, next request has no session cookie, AuthGate
bounces back to login.

Two defensive layers (the openresty box is not in our reach):
- web-ui index.html: tiny inline redirect; if location is http://dragonflight.live,
  rewrite to https:// before anything else runs. Bounded to that exact hostname
  so local / LAN access on http://172.18.91.x stays as-is.
- mam-api: emit Strict-Transport-Security on HTTPS responses when AUTH_ENABLED=true.
  After one successful HTTPS visit, browsers auto-upgrade future http:// requests
  on their own — closes the loophole even if someone bypasses the index.html JS.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 22:00:35 -04:00
Zac Gaetano
03d0d098f5 fix(auth): final-review integration fixes — Users page alias + PATCH, CSRF on uploads + heartbeat, drop .bak
Final-review findings:
- Mount usersRouter at /api/v1/users in addition to /api/v1/auth/users so the
  existing SPA Users page works; add PATCH /:id for inline edits (display_name,
  role, password).
- Add X-Requested-With: dragonflight-ui to raw XHR/fetch paths that bypass
  apiFetch (file uploads, SDK uploads, EDL export) — without it, requireUiHeader
  403s before reaching the route.
- Exempt SERVICE_PATHS (/cluster/heartbeat) from requireUiHeader so node-agent
  heartbeats keep working when NODE_TOKEN is unset.
- Remove stale auth.js.bak.
2026-05-27 15:42:42 -04:00
Zac Gaetano
2aec4636cb feat(web-ui): Settings → Account (change password) + API Tokens sections 2026-05-27 15:20:57 -04:00
Zac Gaetano
cfe21e315e feat(web-ui): LoginScreen + SetupScreen (layout B from brainstorm) 2026-05-27 15:17:33 -04:00
Zac Gaetano
7e240d86c8 feat(web-ui): AuthGate orchestration + replace 401 bounce with in-SPA re-render 2026-05-27 15:08:14 -04:00
ada8105948 chore(web-ui): bump Premiere panel latest to v1.2.0 in data.jsx (#125) 2026-05-27 10:14:12 -04:00
c84519b606 release(premiere-plugin): publish v1.2.0 ZXP 2026-05-27 10:12:22 -04:00
7a6113fc90 capture: live port signal presence indicators on Capture screen and nav badge
- Capture screen now polls /cluster/devices/blackmagic/signal every 3s
- Per-port chips show signal state (RECEIVING/CONNECTING/LOST/ERROR/IDLE) with pulsing dot
- BMD SVG card diagram rendered per node card
- Sidebar nav badge on Capture item shows live/total port count (pulsing green dot)
2026-05-27 13:53:32 +00:00
c48c7e6d7d feat(audio-tab): full audio track inspector with meters, mute/solo, faders
Issue #80 — replaces the stub AudioTab (two static waveforms) with a
broadcast-ops-grade audio panel:

- DB: add audio_metadata JSONB column to assets (migration 022)
- Worker: getMediaInfo now extracts per-stream audio metadata
  (codec, channels, channel_layout, sample_rate, bit_depth, bit_rate,
  language, title, disposition)
- Worker: proxy job persists audio_metadata into the assets row
- API: new GET /assets/:id/audio returns structured track list
- Frontend AudioTab: per-track rows with:
  - Track name/index with language badge
  - SVG waveform per track (color-coded)
  - L/R level meters via Web Audio API AnalyserNode
  - Per-track metadata row (codec, layout, sample rate, bit depth, bitrate)
  - Mute / Solo buttons with proper solo-logic
  - Per-track volume fader
  - Master section with summed L/R meters and master fader
- MetadataTab: show audio track summary when audio_metadata present
- CSS: full audio-tab layout, responsive collapse at 900px
2026-05-27 04:53:52 +00:00