Commit graph

746 commits

Author SHA1 Message Date
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
a25e4b6071 UXP v2.1.3: timeline.js — correct Premiere DOM API calls per official docs 2026-05-28 07:48:57 -04:00
046d99f57a UXP v2.1.3: import-flow — use window.path.join, replace fs.createWriteStream with fd-based chunked write 2026-05-28 07:47:44 -04:00
fcc737e05b UXP v2.1.2 release artifact 2026-05-28 07:20:14 -04:00
8f93302f45 UXP v2.1.2: bump manifest version 2026-05-28 07:20:09 -04:00
17ca9bfc75 UXP v2.1.2: import-flow — replace path.join (not in UXP) with manual join helper 2026-05-28 07:19:46 -04:00
f8fa0fa010 UXP v2.1.1 release artifact 2026-05-28 02:25:30 -04:00
907058de83 UXP v2.1.1: bump manifest version 2026-05-28 02:25:21 -04:00
bfe0316067 UXP v2.1.1: add conform-proj-select to Conform panel 2026-05-28 02:24:58 -04:00
5d94838830 UXP v2.1.1: main.js — fix recordImport from proxy/hires return values, add conform project select, fix conform panel 2026-05-28 02:24:07 -04:00
76fff5efc2 UXP v2.1.1: import-flow.js — expose _tempPath/_streamToFile, return {localPath,safeName} from proxy/hires 2026-05-28 02:22:58 -04:00
5432c2dfa1 UXP v2.1.0 release artifact 2026-05-28 02:21:36 -04:00
b3b2655272 UXP v2.1.0: bump version in manifest 2026-05-28 02:20:43 -04:00
16366267c4 UXP v2.1.0: main.js — full rewrite, wire all panels, tabs, export, conform, relink, mount live 2026-05-28 01:01:29 -04:00
066718c968 UXP v2.1.0: timeline.js — new module: sequence read, FCP XML, export, conform, batch relink via UXP premierepro API 2026-05-28 01:00:19 -04:00
60d0b09c63 UXP v2.1.0: ui.js — add formatDuration, sanitizeFilename, slide panel helpers, escapeXml 2026-05-28 00:59:21 -04:00
2608d7a465 UXP v2.1.0: library.js — project filter, status badges, details panel, growing tab, growing poll 2026-05-28 00:58:57 -04:00
cd18988d6d UXP v2.1.0: api.js — add projects, live-path, sequences, conform, batch-trim endpoints 2026-05-28 00:57:59 -04:00
be57eb0a50 UXP v2.1.0: CSS — full rewrite, all new panels, tabs, details, badges 2026-05-28 00:57:23 -04:00
25356ca439 UXP v2.1.0: full feature parity with V1 CEP — tabs, details, export, conform, relink, mount live 2026-05-28 00:56:15 -04:00
Claude
4bea3c94f8 fix(premiere-plugin-uxp): v2.0.2 — resolve temp folder defensively (no os.tmpdir)
UXP's `os` is a stripped subset of Node's — `os.tmpdir()` isn't exposed in
the build PPro 26.0.x ships, so both Import Proxy and Import Hi-Res failed
immediately with "os.tmpdir is not a function".

Replace with a defensive resolver tried in order:
  1. os.tmpdir if present (newer UXP builds)
  2. require('uxp').storage.localFileSystem.getTemporaryFolder() → .nativePath
     (the documented portable approach)
  3. process.env.TEMP / TMP / LOCALAPPDATA\\Temp (Windows always sets these)
  4. os.homedir() + AppData/Local/Temp

tempPath() is now async; both Import.proxy and Import.hires await it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 00:43:44 -04:00
Claude
f1a3d6a24a fix(premiere-plugin-uxp): v2.0.1 — replace unsupported CSS + surface load count
The v2.0.0 grid stayed empty in Premiere 26 because UXP's CSS engine
doesn't support `grid-template-columns: repeat(auto-fill, minmax(...))`
or `aspect-ratio`. Cards rendered with 0 height and the flex column
collapsed, so the actions row stuck to the top of the pane.

Switch to flex-wrap with fixed-width (140px) cards and explicit 80px
thumb heights — both work in UXP's stripped CSS.

Also fix the /auth/me response shape — it returns the user fields
directly, not wrapped in `{ user: ... }`. Header now shows
"display_name @ host" instead of falling back to bare host.

Add a toast on each library load reporting "Loaded N assets (total M)"
so we can tell empty-grid (zero assets) from CSS-broken-grid (cards
exist but invisible) at a glance.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 00:35:04 -04:00
Claude
91e4691230 feat(premiere-plugin-uxp): v2.0.0 — UXP port replacing CEP for import
CEP `csInterface.evalScript` callback is broken in Premiere Pro 26.0.x —
nothing called from the panel ever returns, so importFiles deadlocks. Adobe's
path forward is UXP. This is the minimum viable port that restores the
Import Proxy / Import Hi-Res workflow.

Scope (v2.0.0):
- Connect to a Dragonflight server (URL + Bearer token; persisted)
- Asset library (search, refresh, grid with thumbnails)
- Import Proxy via streamed download → Project.importFiles
- Import Hi-Res via presigned S3 URL → Project.importFiles

Layout:
  manifest.json     UXP v5, host=premierepro, minVersion=26.0.0
  index.html        Panel shell
  styles.css        Mirrors web UI dark tokens
  src/ui.js         DOM helpers, toast, progress, formatting
  src/api.js        HTTP client (Bearer; manual redirect-follow drops auth
                    when hopping to a different host per UXP security policy)
  src/library.js    Asset grid render + selection
  src/import-flow.js  Streaming download (fs.createWriteStream) +
                      premierepro.Project.importFiles into rootBin
  src/main.js       Bootstrap, event wiring
  build/pack.mjs    Packs into .ccx; installs via UnifiedPluginInstallerAgent

Coexists with services/premiere-plugin/ (CEP) — keeps the CEP panel for any
features that still work there while running v2.0.0 for import. Future v2.x
will add live preview, conform, timeline export, settings.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 00:19:28 -04:00
Claude
8b48f03f6b diag(premiere-plugin): v1.2.5 — no-op IIFE writes to Documents/ + reports lf.open result 2026-05-28 03:59:40 +00:00
Claude
9085835074 diag(premiere-plugin): v1.2.4 — fix pre-importFiles log syntax (safePath as string literal not bareword) 2026-05-28 03:50:42 +00:00
Claude
f5959620c8 diag(premiere-plugin): v1.2.3 — file-log around importFiles call
Writes timestamped pre/post lines to C:/Temp/df-import-log.txt around the importFiles call so we can see whether importFiles hangs (only pre line present) or returns and evalScript callback gets lost (both lines present). Diagnostic only.
2026-05-28 03:46:00 +00:00
Claude
e3afe38697 fix(premiere-plugin): suppress importFiles UI prompts + 60s timeout guard
app.project.importFiles() can deadlock if a hidden Premiere modal appears (off-screen, behind window, etc) — the evalScript callback never fires and the panel spinner hangs forever.

Two changes:

1) Pass suppressUI=true to all five importFiles call sites (main.js inline IIFE + 4 in premiere.jsx). Premiere proceeds even if it would have prompted (audio sample rate, project link, scale-to-frame, etc).

2) Wrap importFileToPremiereProject in a 60s timeout race so even if importFiles does block, the panel surfaces a real error instead of leaving the spinner stuck.

Bumps to v1.2.2.
2026-05-28 03:19:44 +00:00
Claude
e7eff0ee8c release(premiere-plugin): v1.2.1 with downloadFile Bearer + 15s timeout fix 2026-05-27 23:07:27 -04:00
Claude
e8ceb991a3 fix(premiere-plugin): inject Bearer in downloadFile + add 15s timeout
downloadFile() uses native https.get which bypasses the window.fetch interceptor that injects Authorization. Same-server URLs (proxy /video) hit requireAuth and 401. Inject the Bearer header manually when url starts with state.serverUrl.

Also add a 15s setTimeout so an unreachable presigned URL (or CEP-Node TLS hiccup on broadcastmgmt.cloud) fails fast with an error instead of hanging the spinner forever.
2026-05-28 03:00:02 +00:00
Zac Gaetano
ac7730195d fix(web-ui): forward X-Forwarded-Proto from outer proxy so mam-api emits Set-Cookie
This is the real cause of the login loop. mam-api sets its session cookie
with Secure=true (production config). express-session refuses to emit a
Secure Set-Cookie unless req.secure is true. With `app.set('trust proxy')`
on, req.secure derives from X-Forwarded-Proto.

web-ui's nginx was unconditionally sending `X-Forwarded-Proto: $scheme`.
Inside the web-ui container nginx listens on port 80, so $scheme is always
"http" — regardless of whether the outer NPM proxy terminated TLS. mam-api
saw http, decided the connection was insecure, and silently dropped the
Set-Cookie from the login response. Login succeeded server-side (session
row landed in PG, last_login_at updated) but the browser never received a
cookie, so the very next /auth/me check came back 401 and AuthGate bounced
to the login screen. Infinite loop.

The previous Connection: "upgrade" → $connection_upgrade fix wasn't wrong
(the hardcode is a real latent bug worth fixing) — it just wasn't the
proximate cause.

Fix: a second `map` directive forwards the outer X-Forwarded-Proto through
when present, falling back to $scheme only when no proxy header exists (so
direct localhost curls still work). Both /api/ and /capture/ now send the
correct value upstream, mam-api sees https, req.secure is true, Set-Cookie
flows through, login works.

Verified by curling the existing direct-to-mam-api path: with X-Forwarded-
Proto: https on the request, Set-Cookie comes back; without it, no
Set-Cookie. That's the exact difference between web-ui-proxied and
direct-to-mam-api in our previous diagnostic curls.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 22:11:27 -04:00
Zac Gaetano
c24c6156dc fix(web-ui): stop nginx from eating Set-Cookie on /api/ and /capture/
Login was infinite-looping in production. Server side was healthy (sessions
landing in PG, /me returning 200 to a manually-signed cookie) but the
browser never received `Set-Cookie`. Bisected the proxy chain layer by
layer with direct curls on the box:

  - mam-api direct (port 47432) → Set-Cookie present
  - web-ui nginx (port 47434)   → Set-Cookie STRIPPED
  - NPM (https://dragonflight.live) → Set-Cookie stripped (because web-ui ate it)

Root cause was this in /api/ and /capture/:

    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";

The literal "upgrade" was being sent on every request, not just real
WebSocket negotiations. Nginx then routes the upstream response through
its tunnel/upgrade code path, which doesn't preserve all response headers
the same way — Set-Cookie got silently dropped. mam-api doesn't speak
WebSockets today so it never sent a 101, and the bad pattern went
unnoticed until session-cookie auth shipped.

Fix is the standard conditional pattern: a `map` directive at the top of
default.conf computes $connection_upgrade as "upgrade" only when the
client actually requested Upgrade, otherwise "close". Both location blocks
now send `Connection $connection_upgrade` instead of the hardcoded literal.
WebSocket support on either location continues to work unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 22:00:35 -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
5571768706 feat(panel): add .connected-bar CSS for compact connected state 2026-05-27 19:31:16 -04:00
350c23f9d1 fix(panel): restore main.js — add disconnectFromServer + connected-bar toggle 2026-05-27 19:28:39 -04:00
Zac Gaetano
8028c4c4dd feat(auth): bound-hostname tokens for node-agent + return role from /me
- requireAuth bearer path now selects api_tokens.bound_hostname and users.role,
  populates req.tokenBoundHostname and req.user.role. /cluster/heartbeat can
  now authenticate via a bound api_token (issued via POST /auth/tokens with
  bound_hostname).
- routes/tokens.js POST accepts bound_hostname; GET returns it so users can
  see which tokens are bound.
- Remove /cluster/heartbeat from SERVICE_PATHS so requireAuth runs on it (the
  bearer auth handles the gate; the heartbeat handler still enforces the
  body.hostname === bound match).
- /auth/me now returns role (final-review I2). Closes the gap where every
  signed-in user appeared as 'viewer' in the UI regardless of actual role.
- loadUser SELECTs role for session auth.
- Backend tests still 37/15/0/22 — no test changes needed; existing token
  CRUD tests stay passing since bound_hostname is optional.
2026-05-27 19:27:59 -04:00
e6da1432e5 fix(panel): restore main.js with disconnect feature (was accidentally emptied) 2026-05-27 19:22:15 -04:00
e22cf625bf feat(panel): add disconnectFromServer(); toggle connection-form / connected-bar
- On connect success: hide form, show compact connected-bar with hostname
- On disconnect: clear assets, reset buttons, restore form
- Wire disconnect-btn click to disconnectFromServer()
2026-05-27 19:21:50 -04:00
552506ec7a feat(panel): collapse connection form when connected; add Disconnect button
On startup the full form shows. On successful connect the form hides and a
compact connected-bar appears with the server hostname and a Disconnect button.
2026-05-27 19:21:38 -04:00
a0a6bc9f20 feat(panel): migrate accent palette from hue-32 orange-red to hue-266 blue
Aligns CEP panel with Dragonflight web-ui tailwind.config.js color system.
All bg/accent/text/border tokens updated; signals unchanged.
2026-05-27 19:14:24 -04:00
Zac Gaetano
c8e98ffa0d fix(auth): sync DEV_USER_ID with migration 023 — use all-zeros UUID
Migration 023 was fixed in 9dc572b to use '00000000-0000-4000-8000-000000000000'
because 'v' isn't a valid hex digit, but the DEV_USER_ID constant in
middleware/auth.js still referenced the original '...000000000dev'. Every
route that passes DEV_USER_ID as a query parameter (users list, login lookup,
setup-required count) was throwing 22P02 invalid input syntax for type uuid.
The errors were swallowed by Promise.allSettled in the SPA's data load so the
app appeared to work in dev mode, but enabling AUTH_ENABLED=true would have
broken login entirely.
2026-05-27 19:08:07 -04:00
9dc572b913 fix(migration): replace invalid UUID in 023 dev user seed 2026-05-27 18:45:21 -04:00
Zac Gaetano
14ece1a160 Merge branch 'feat/auth-system' 2026-05-27 15:47:56 -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
8ede44ae87 docs(auth): flip AUTH_ENABLED default + document setup + recovery 2026-05-27 15:25:29 -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
Zac Gaetano
96effaaa3c fix(mam-api): TRUST_PROXY boot warning + CSRF integration tests + bounded rate-limit map
Fixes three issues in the authentication system:

C1: Add boot-time warning when AUTH_ENABLED=true but TRUST_PROXY!=true.
    Without TRUST_PROXY=true behind nginx, req.ip becomes the proxy IP for all
    clients, collapsing per-IP rate limiting into a shared pool. Operators must
    explicitly set TRUST_PROXY=true to make per-IP rate limiting effective.

C2: Mount requireUiHeader middleware in test helpers (auth.test.js,
    users.test.js, tokens.test.js). The CSRF header validation was not being
    exercised in the test suite. Tests now send X-Requested-With: dragonflight-ui
    headers that are actually validated by the middleware.

I1: Implement bounded rate-limit Map with MAX_ENTRIES=10000 and LRU eviction.
    Unbounded Maps are vulnerable to spray attacks: attackers can force memory
    exhaustion by requesting with distinct IPs. Now we evict the oldest entry
    (by insertion order) when the map reaches capacity.
2026-05-27 15:03:35 -04:00
Zac Gaetano
d209a192c3 feat(mam-api): login rate limit + X-Requested-With CSRF header check 2026-05-27 14:58:02 -04:00
Zac Gaetano
56b661ef65 feat(mam-api): API token CRUD — show raw once, bearer-authenticate via SHA-256 lookup 2026-05-27 14:52:07 -04:00
Zac Gaetano
b7f5a84d2d feat(mam-api): user CRUD + admin password reset + last-user delete guard 2026-05-27 14:47:03 -04:00
Zac Gaetano
0bbaf80d2a feat(mam-api): GET /auth/me + POST /auth/password 2026-05-27 14:42:53 -04:00
Zac Gaetano
d75a0241eb feat(mam-api): POST /auth/logout 2026-05-27 14:38:05 -04:00
Zac Gaetano
bcfc19e530 fix(mam-api): real dummy bcrypt hash + log last_login_at failures
Code-review feedback:
- Dummy hash for user-enumeration-defense timing was 63 chars (bcrypt strings
  are 60 chars). Worked by accident because bcrypt 5.x is lenient about
  trailing chars; a future tightening would silently regress the timing
  defense. Replaced with a real pre-computed bcrypt hash.
- last_login_at UPDATE now logs errors instead of silently swallowing them,
  matching the pattern in requireAuth for api_tokens.last_used_at.
- Removed dead import of comparePassword from auth.test.js.
2026-05-27 14:35:59 -04:00
Zac Gaetano
f8b6f7d5ef feat(mam-api): POST /auth/login + redirect-loop regression test 2026-05-27 14:28:18 -04:00
Zac Gaetano
c9f9698b58 feat(mam-api): POST /auth/setup — first-run admin creation 2026-05-27 14:24:56 -04:00
Zac Gaetano
49a9543942 feat(mam-api): auth router skeleton + setup-required endpoint 2026-05-27 14:21:32 -04:00
Zac Gaetano
cb7cc9a43e fix(mam-api): narrow cluster carve-out to /cluster/heartbeat only
Code-review feedback: startsWith('/cluster') was a prefix match that exposed
destructive operator endpoints (POST /containers/:id/restart, DELETE /:id,
GET /devices/blackmagic/*) unauthenticated. Only POST /heartbeat is genuine
node-agent traffic; everything else in cluster.js is operator/UI surface
that should go through requireAuth. Long-term: issue node-agent a bound
api_token and drop the carve-out entirely.
2026-05-27 14:18:27 -04:00
Zac Gaetano
9de4fe9ab9 feat(mam-api): mount requireAuth gate at /api/v1 with auth + cluster carve-outs 2026-05-27 14:13:21 -04:00
Zac Gaetano
88c3aa5149 fix(mam-api): SESSION_SECRET boot guard + cleaner CORS rejection
Code-review feedback:
- Hard-fail boot when AUTH_ENABLED=true and SESSION_SECRET is unset, so
  express-session can't silently use an in-memory random secret that
  invalidates sessions on restart and breaks multi-node clusters.
- CORS rejection now returns cb(null, false) instead of cb(new Error)
  so misconfigured origins surface as clean CORS errors in the browser
  instead of HTTP 500s. Log a warn line for operator visibility.
- pruneSessionInterval units comment.
2026-05-27 14:11:09 -04:00
Zac Gaetano
a094df03ea feat(mam-api): wire express-session + tighten CORS allowlist 2026-05-27 14:06:41 -04:00
Zac Gaetano
1a723fe4c2 fix(mam-api): requireAuth — stamp last_seen_at after user confirmation
Code-review feedback: writing last_seen_at = now before loadUser() lets
the stamp persist if the lookup throws (resave:false still writes when
modified), extending the idle window without confirming the user exists.
Also clarify DEV_USER_ID is a specific placeholder, not a generic sentinel.
2026-05-27 14:04:15 -04:00
Zac Gaetano
0248a68f57 feat(mam-api): requireAuth middleware — session + bearer + idle/absolute timeout 2026-05-27 13:59:50 -04:00
Zac Gaetano
3bca290e09 fix(mam-api): test glob — use find so npm test picks up files at any depth
/bin/sh (which npm uses) doesn't expand ** recursively. Task 1's smoke test
under test/ stopped being discovered once Task 3 added tests under test/auth/.
find + sort keeps depth-agnostic discovery portable across shells.
2026-05-27 13:54:12 -04:00
Zac Gaetano
3fc8116dd3 feat(mam-api): auth utilities — password hash/compare + token gen/hash/parse 2026-05-27 13:51:15 -04:00
Zac Gaetano
14931d6362 fix(mam-api): migration 023 — broaden ON CONFLICT + document password_updated_at backfill
Code-review feedback: ON CONFLICT (id) only catches id collisions; a pre-existing
'dev' username would trigger a unique_violation on the username index and roll
back the migration, hard-failing the mam-api boot. Switch to bare ON CONFLICT
DO NOTHING so any unique conflict is no-op-safe.
2026-05-27 13:48:08 -04:00
Zac Gaetano
1d3c0385dd feat(mam-api): migration 023 — auth timestamps + idempotent dev user seed 2026-05-27 13:44:07 -04:00
Zac Gaetano
5011d45391 chore(mam-api): wire node:test runner + test app + DB helper 2026-05-27 13:38:46 -04:00
1e51a4ca5d fix(premiere-plugin): align panel CSS with web-ui design system
Component-level alignment pass against services/web-ui/src/css/components/:

- btn-primary text: #070403 (near-black) → #f5f7fb (near-white, matches web-ui)
- btn-danger text: #fcf7f7 → #fbf6f6 (precise oklch(98% 0.005 25) conversion)
- btn-secondary border: border-strong → border; hover: bg-hover + border-strong
- button.secondary legacy: same border/hover fix
- asset-card bg: bg-surface → bg-panel (matches wd-card-asset)
- asset-card hover: remove accent glow + transform + shadow; border → var(--border) only
- asset-card hover brightness: moved to img child only (matches web-ui pattern)
- asset-card selected: remove box-shadow ring; bg → bg-raised
- asset-card border-radius: explicit 6px (was var(--r-md))
- asset-card transition: simplified to border-color with design-system easing
- asset-filename: font 11px → font: 500 12px/1.3 (web-ui uses 13px; 12px for panel density)
- asset-meta: 10px text-secondary → font: 400 11px/1.3 font-mono + text-tertiary + tabular-nums
- asset-status-badge: border-radius 100px → 3px (matches wd-badge)
- chip: pad/gap aligned with wd-badge; font: 600 10px/1; added chip--idle + chip--info variants
- form-label: 10px text-secondary → font: 600 11px/1 + text-tertiary + margin-bottom: 4px
- details-header-label: aligned to font shorthand + 0.08em spacing
- details-label: aligned to font: 600 10px/1
- export-panel-title: font shorthand
- Add @keyframes wd-shimmer + .skeleton utility
- Add @media (prefers-reduced-motion) block
- Update file header comment
2026-05-27 12:00:00 -04:00
ad9e1ef5f1 fix(premiere-plugin): replace oklch() with hex/rgba for CEP Chromium compat
CEP's embedded Chromium (used by Premiere Pro panels) does not support
oklch() color syntax. All color tokens were rendering as invalid/transparent,
causing the panel to appear unstyled. Converted all oklch() values to their
precise hex/rgba equivalents via OKLab→sRGB math. No design changes.
2026-05-27 10:44:39 -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
33239a780e design(premiere-plugin): align panel UI with web-ui design system
- Add motion tokens (ease-out-quart/expo, dur-fast/normal/slide)
- Add z-layer tokens, overlay, thumb-black, accent-hover/bright
- Restructure signal/status tokens; flip to signal-primary/status-alias pattern
- Add signal-info/info-bg for --status-blue backwards compat
- Buttons: md=32px, sm=28px, lg=36px, icon=28sq, font 500/13px
- Inputs/select: 32px tall, bg-deep background, focus-visible outline
- Slide panel: 460px wide, 52px header, 18px padding, ease-out-expo, min-height:0
- Asset card: intrinsic height, thumb-black thumbnail, border-faint, brightness hover
- Status badges: 18px, font-sans, 0.08em tracking
- Chips: 18px, font-sans, no border, signal-bg backgrounds
- Tabs: 36px, no text-transform, pill badge
- Action bar: bg-deep background
2026-05-27 10:09:45 -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
de311321f4 design(premiere-plugin): align panel UI with Dragonflight web-ui design system (v1.2.0)
Rewrites css/styles.css to mirror services/web-ui/src/css/components/*
exactly. Brings the Premiere panel into pixel-level parity with the
main Dragonflight web UI:

- Tokens: add --accent-hover, --accent-bright, --thumb-black, --overlay,
  --shadow, --signal-info, motion tokens (ease-out-quart, ease-out-expo,
  dur-fast/normal/slide), z-layer vars. Keep --status-* aliases pointing
  at --signal-* for main.js backwards-compat. Remove unused --accent-dim
  (hue 52 leftover).

- Buttons: match wd-btn — md=32px (was 28px), sm=28px, lg=36px, icon=28sq.
  focus-visible accent-subtle outline. active opacity 0.85. Replace
  hardcoded oklch(68%) hover with --accent-hover. btn-danger now solid
  signal-bad like wd-btn--danger (was transparent w/ red border).

- Inputs/select/search-input: 32px tall, bg-deep background (was
  bg-surface), accent-subtle focus outline matching wd-input.

- Slide panel: 460px wide (was 420), 52px header (was 40), 18px body
  padding, --overlay scrim, ease-out-expo transform. min-height:0 on body.

- Asset card: removed fixed 155px height (now intrinsic), thumb-black bg
  for thumbnails, brightness 1.04 hover filter mirroring wd-card-asset.

- Status badges: 18px tall like wd-badge, font-sans, 0.08em tracking.

- Chips: 18px tall, font-sans (was font-mono 20px), wd-badge proportions.

- Tabs: 36px, accent underline on active, badge styled as pill.

- Empty state, progress bar, preset cards, clip list, message banners,
  form groups, details panel, action bar, connection bar — all spacing
  + typography refined to web-ui standards.

Manifest bumped to 1.2.0. No JS changes required.: manifest.xml
2026-05-27 09:01:04 -04: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
48d54a32cf dashboard: add missing dash-* CSS classes; cluster: add stat-row/stat-card CSS
The Dashboard page was rendering as plaintext because all .dash-* CSS
classes (dash-section, dash-onair-*, dash-jobs-*, dash-cluster-*,
dash-statusbar, etc.) were missing. Added them with the full dark-theme
design-system styling matching the rest of the app.

The Cluster page's .stat-row and .stat-card classes were also missing,
causing node statistics (counts, CPU, GPUs, memory) to render unstyled.
Added grid-based stat row and card styles.
2026-05-27 04:09:15 +00:00
4172b0d70a rip out entire auth/login flow
- remove requireAuth from all route files
- delete auth.js, tokens.js, users.js routes
- delete auth middleware
- remove session middleware and all auth deps from index.js
- delete login.html and auth-guard.js from web-ui
2026-05-27 03:39:58 +00:00
opencode
9726dbb2df Revert "auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap"
This reverts commit 002e5acb82.
2026-05-27 03:28:05 +00:00
opencode
002e5acb82 auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap
Scope (locked in via planning Q&A):
  - Identity: local accounts only (PG users table) + existing bearer
    tokens for headless callers.
  - Transport: httpOnly cookie session for browser, Bearer for API.
  - RBAC: admin / editor / viewer roles, plus an orthogonal
    is_client flag for external (agency, talent, customer) accounts.
  - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env
    seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET
    to force-reset the named user (break-glass).
  - Rate limit: in-memory, 10 fails per 15min per (IP, username).
  - Password policy: \u22658 chars, mixed case, digit, symbol; small
    blocklist of common passwords; cannot equal username.
  - Self-service: change own display name + password. Everything
    else (role, is_client, other-user mgmt) is admin only.
  - Audit log: append-only table, indexed by actor + event_type +
    created_at, populated by every auth/admin event.

Files added:
  - services/mam-api/src/db/migrations/022-auth-rework.sql
        users.is_client + last_login_at + failed_attempts; audit_log
        table with FK to users (ON DELETE SET NULL).
  - services/mam-api/src/middleware/audit.js
        Fire-and-forget audit() helper. Caller never awaits, failure
        logs but never throws — auditing cannot break the request
        that triggered it.
  - services/mam-api/src/middleware/passwordPolicy.js
        Shared checkPassword(pw, { username }) used by setup, user
        create/update, and self-service password change.
  - services/mam-api/src/tasks/bootstrapAdmin.js
        Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER +
        ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR
        ADMIN_BOOTSTRAP_RESET=true).
  - services/mam-api/src/routes/audit.js
        Admin-only GET /audit (paginated, filter by event_type /
        actor / target / date) and GET /audit/event-types.
  - services/web-ui/public/modal-account-settings.jsx
        Profile + Password tabs. Triggered by sidebar user button.

Files rewritten:
  - services/mam-api/src/routes/auth.js
        - POST /login: regenerate(), no manual save(); audit success/
          fail/lockout; updates last_login_at + failed_attempts.
        - POST /logout: destroys session, audits logout.
        - GET /me: returns is_client + last_login_at. Synthetic admin
          when AUTH_ENABLED=false.
        - GET /setup-status: drives login.html UI state.
        - POST /setup: blocked once any user exists; password policy.
        - POST /password: self-service. Requires current pw, runs
          policy, audits, invalidates other sessions implicitly via
          users.js if changed by admin.
        - PATCH /me: self-service display_name update.
  - services/mam-api/src/routes/users.js
        - is_client field in create/update/list/get.
        - Guardrails: cannot delete or demote last admin, cannot
          delete self, admins cannot be flagged is_client.
        - Password change invalidates all sessions for that user
          (DELETE FROM sessions WHERE sess->>'userId' = id).
        - Audit on every mutation.
        - Password policy enforced.
  - services/mam-api/src/middleware/auth.js
        - requireAuth now exposes req.user.is_client.
        - New requireRole(["admin","editor"], { rejectClients: true })
          helper. Applied to cluster, sdk, capture routes (infra).
        - Synthetic user when AUTH_ENABLED=false has is_client=false.
  - services/mam-api/src/index.js
        - Loads bootstrap admin after migrations.
        - Wires /api/v1/audit.
        - Cleans up an earlier comment block.
  - services/web-ui/public/login.html
        - Password hint added next to setup-mode password field.
  - services/web-ui/public/shell.jsx
        - Sidebar user footer is a button that opens AccountSettings.
        - CLIENT badge next to role when is_client=true.
        - Nav filters: clients lose ingest tree + jobs + editor;
          viewers lose ingest + editor; only admins see the Admin
          section. Power button hidden when synthetic user.
  - services/web-ui/public/screens-admin.jsx
        - Users table: new Client column with inline toggle.
        - InviteUserModal: Client checkbox + password hint, gated
          off when role=admin.
        - Last login column replaces Created in primary view.
        - CSV export includes client + last_login.
  - services/web-ui/public/data.jsx
        - ZAMPP_DATA.ME carries is_client + display_name.
  - services/web-ui/public/index.html
        - Loads dist/modal-account-settings.js.
  - services/web-ui/public/styles-rest.css
        - .user-row grid widened to 6 columns.
  - docker-compose.yml
        - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars.

Deploy:
  cd /opt/wild-dragon
  git pull origin main
  # In .env:
  #   AUTH_ENABLED=true
  #   SESSION_SECRET=<openssl rand -hex 48>
  #   ADMIN_BOOTSTRAP_USER=admin
  #   ADMIN_BOOTSTRAP_PASSWORD=<strong>
  docker compose build mam-api web-ui
  docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-27 03:21:16 +00:00
a48e1d9dd7 dashboard: rebuild as control-room status board (on air / up next / attention / work) 2026-05-26 23:10:23 -04:00