Commit graph

690 commits

Author SHA1 Message Date
280fc9dff2 fix: XSS in renderTags and stale api.js version in player.html
Tag values were inserted into innerHTML unsanitized — a tag containing
HTML would execute as markup. Switch to DOM-only construction for the
tag badges. Also bump api.js cache-buster to v=6.
2026-05-19 00:30:54 -04:00
f1e0453b0a fix: bump api.js cache-buster to v=6 in capture.html 2026-05-19 00:28:50 -04:00
9f7cb91cc2 fix: prevent JS injection via token name in confirmRevoke onclick
Token names containing single quotes (e.g. "O'Brien's key") broke the
onclick attribute string by closing the JS string literal early.
Apply JSON.stringify+esc pattern so name is safely embedded as a
JSON string literal instead of a raw single-quoted string.
2026-05-19 00:27:31 -04:00
f8e42b886d fix(sequences): apply correct 59.94 DF framesToTC to EDL export
sequences.js had the same `if (rem >= DROP)` bug as timecode.js — any
frame ≥ 4 in the first non-drop minute of each 10-minute group would
produce a timecode offset by one minute. EDL files exported from the
editor would have wrong in/out points for nearly every event.

Applies the FRAMES_FIRST_MIN (3600) boundary check fix, matching the
correction already made to services/web-ui/public/js/timecode.js.
2026-05-19 00:22:17 -04:00
d18fa2f761 feat(library): add Retry button for error-status assets in library grid
Error assets now show an amber circular-arrow action button on hover.
Clicking it calls POST /api/v1/assets/:id/retry, resets status to
'processing', and refreshes the grid — no manual DB intervention needed
when a proxy job fails.
2026-05-19 00:20:19 -04:00
130906ef42 feat(api.js): add retryAsset() helper for POST /assets/:id/retry 2026-05-19 00:17:39 -04:00
d3e12deb18 feat(assets): add POST /:id/retry to re-queue errored assets
Assets stuck in status='error' had no recovery path without manual DB
edits. Adds a retry endpoint that re-dispatches the proxy job, which
chains into thumbnail generation automatically and restores the asset
to 'processing' → 'ready' without operator intervention.
2026-05-19 00:17:00 -04:00
2bb731c7fc fix(users): prevent JS injection in delete onclick handlers for users/groups
confirmDeleteUser and confirmDeleteGroup were building onclick handlers
like onclick="confirmDeleteUser('id','NAME')" using esc() which doesn't
escape single quotes.  Usernames or group names containing ' would break
the JS string; a crafted value like `'; alert(1)//` is stored XSS.

Fix: use JSON.stringify(value) to produce a properly-escaped double-quoted
JS string literal, then esc() to HTML-encode the surrounding quotes for
safe embedding in the HTML attribute.  Same technique now used in both
renderUsers() and renderGroups().
2026-05-19 00:11:06 -04:00
1e8cde81be fix(projects): prevent JS injection via bin names in onclick handlers
binCard() was building onclick="renameBinPrompt('id', 'NAME')" by
calling esc() then .replace(/'/g, "\\'").  The problem: esc() converts
' to ', so the replace never fires on raw single quotes.  When the
HTML parser evaluates the attribute it decodes ' back to ', breaking
the JS string — and for injected payloads like `'; alert(1)//` this is
stored XSS.

Fix: use JSON.stringify(b.name) to produce a properly-escaped double-
quoted JS string literal, then esc() to HTML-encode the surrounding
double-quotes for safe embedding in the HTML attribute.
2026-05-19 00:09:49 -04:00
58e2e539f8 fix(upload): scope original S3 keys under assetId to prevent collisions
Both /init and /simple were keying originals as
`originals/${projectId}/${filename}`.  Two uploads of the same filename
into the same project would share a key — the second upload would silently
overwrite the first file in S3 while both assets remained in the DB with
the same original_s3_key.

Changed to `originals/${assetId}/${filename}` (matching the proxies/
convention) so every asset has its own unique S3 prefix.
2026-05-19 00:08:13 -04:00
4f8964e807 fix(tokens): add requireAuth middleware to token routes
Token CRUD endpoints had no authentication guard.  Without it,
unauthenticated requests could reach the handler — GET would return
empty results silently, and POST could attempt to insert a token with
user_id = NULL.  All other route files in this codebase apply
requireAuth explicitly; tokens.js was simply missing it.
2026-05-19 00:07:41 -04:00
0ea8d7ce33 fix(timeline): cap right-trim at source asset boundary
When duration_ms is known, dragging the right-trim handle past the end
of the source clip could push timeline_out_frames beyond what the source
material covers.  Cap the delta so neither timeline_out_frames nor
source_out_frames can extend past the available source frames.

Also changed assetFrames fallback from origSrcOut (prevents any extension
when duration is unknown) to null, so the guard is simply skipped when
we don't have duration metadata.
2026-05-19 00:02:34 -04:00
3c689ccddf fix(timecode): correct framesToTC for all frames beyond position 3
The previous algorithm used `if (rem >= DROP)` (i.e. rem >= 4) to decide
whether to advance to the next minute group.  This fired immediately at
frame 4, still inside minute 0 of the 10-minute non-drop group, producing
00:01:00;00 for what should be 00:00:00;04.  Every timecode display in
the editor was wrong for any position past the first four frames.

Each 10-minute block has one 3600-frame non-drop minute followed by nine
3596-frame drop minutes.  The fix checks `rem < FRAMES_FIRST_MIN` (3600)
to identify the non-drop minute, then subtracts it before dividing into
drop-minute slots.  Frame labels within drop minutes are shifted by DROP
(+4) so the first usable label is :00;04 as per SMPTE 12M.
2026-05-19 00:01:18 -04:00
b23700f30a fix(recorders): use already-imported uuidv4 instead of dynamic import
Dynamic `(await import('uuid')).v4()` inside the /start route handler
re-imports the module every call (though Node caches it). uuidv4 is
already imported at the top of the file.
2026-05-18 23:56:00 -04:00
0f37d01b2d fix(editor): keyboard tool shortcuts now actually switch the active tool
V/C/H key shortcuts called updateToolbarActive() which only updated button
visual state — Timeline.setTool() was never called so the cursor stayed on
the previous tool. Fix by calling Timeline.setTool() inside updateToolbarActive.

Also bump api.js reference to ?v=6 to match other pages.
2026-05-18 23:53:38 -04:00
fb3b998cfd fix(worker/thumbnail): mark asset ready even when thumbnail extraction fails
If the thumbnail job throws (network blip, ffmpeg error, short clip), the
asset was left stuck in status='processing' indefinitely. Since the proxy
already exists and the asset is playable, set status='ready' in the catch
block before re-throwing so BullMQ can still record the failure.
2026-05-18 23:51:04 -04:00
ff892a1ad5 fix(capture): use duration_ms field for recent captures duration display
The asset schema stores duration as duration_ms (milliseconds).
renderRecent() was checking c.duration (always undefined) so duration
always showed as '—'. Fix to use c.duration_ms / 1000.
2026-05-18 23:50:05 -04:00
08e5ba6298 fix(jobs): fetchJobs → loadJobs, add credentials to inline api helper
killJob() referenced fetchJobs() which is undefined — the correct name is
loadJobs(). Also the inline api() wrapper was missing credentials:'include'
so any API call on the jobs page would fail with a 401 in prod.
2026-05-18 23:48:56 -04:00
e472075087 fix(library): evict stale thumb URL on image load error, re-observe for retry
When a signed S3 URL expires the img fires onerror. Previously the stale URL
stayed in thumbCache so the broken image would persist. Now we delete the cache
entry, clear the loaded class, and re-add the element to the IntersectionObserver
so the next time it scrolls into view a fresh signed URL is fetched.
2026-05-18 23:46:12 -04:00
e6314be92d fix(assets): strip internal full_count column from list response
The window function COUNT(*) OVER() leaks `full_count` on every row.
Strip it before sending so callers only see actual asset fields.
2026-05-18 23:44:14 -04:00
660afb94bb feat(editor): show fps/codec/resolution/duration in media panel asset list
- Add two-line layout to media panel items: name on top, metadata below
- fmtMs() converts duration_ms to MM:SS or HH:MM:SS for display
- Meta line shows resolution · codec · fps · duration, skipping null fields
- Assets with no extracted metadata (no proxy yet) show name only
- Active item meta line inherits accent color at reduced opacity
2026-05-18 23:37:56 -04:00
508cf8d41b feat(recorders): add Edit Recorder panel with PATCH support
- Edit (pencil) button appears on idle recorder cards; hidden while recording
- openEditPanel() pre-populates all form fields from existing recorder state
- openPanel() resets editingId and restores "New recorder" defaults
- closePanel() clears editingId and removes any stale probe result
- handleSaveRecorder() dispatches PATCH /recorders/:id in edit mode, POST otherwise
- Fix field name bugs in create path: codec→recording_codec, resolution→recording_resolution,
  proxy_config object→proxy_enabled/proxy_codec/proxy_resolution flat fields
- Badge in card now reads rec.recording_codec (correct DB field) instead of rec.codec
- Bump api.js cache-buster to v=6
2026-05-18 23:35:16 -04:00
79d44826fe feat(api.js): add patchRecorder() helper for PATCH /recorders/:id 2026-05-18 23:32:33 -04:00
7260b188c5 fix: remove dead DB UPDATE calls in conform worker
The jobs table row no longer exists for conform jobs (POST /jobs/conform
now goes directly to BullMQ). The UPDATE queries were no-ops (WHERE id = NULL)
so they're safe to remove. BullMQ tracks completed/failed status itself.
2026-05-18 23:28:13 -04:00
e895a2f2df fix: show duration overlay on asset cards using duration_ms
asset.duration is not a DB field — it's duration_ms (milliseconds).
Divide by 1000 before passing to formatDuration() which expects seconds.
2026-05-18 23:27:03 -04:00
a9ca7be1d5 feat: add PATCH /recorders/:id endpoint to edit recorder settings
Allows updating name, source_type, source_config, recording_codec,
recording_resolution, proxy_enabled, proxy_codec, proxy_resolution,
and project_id. Blocked while the recorder is actively recording.
2026-05-18 23:24:27 -04:00
29b5910fff feat: migrate editor sequences schema into auto-run migrations directory
Moved from schema_patch_editor.sql. All statements are idempotent
(IF NOT EXISTS / DO $$ BEGIN blocks) so safe to re-apply.
2026-05-18 23:23:33 -04:00
ffad0051f9 feat: migrate groups/tokens schema into auto-run migrations directory
Moved from schema_patch_groups_tokens.sql. All statements are idempotent
(IF NOT EXISTS / CREATE INDEX IF NOT EXISTS) so safe to re-apply.
2026-05-18 23:23:23 -04:00
717fdcd611 feat: extract and store fps/codec/resolution/duration_ms from source file
Uses getMediaInfo (ffprobe) on the downloaded original before transcoding.
Populates the asset record so the library can display accurate metadata.
2026-05-18 23:22:56 -04:00
817eaff8b1 feat: add getMediaInfo to executor.js using ffprobe JSON output
Exposes video stream fps/codec/resolution and container duration/size
so the proxy worker can populate asset metadata after transcoding.
2026-05-18 23:22:26 -04:00
48b69879cb fix: conform route broken SQL — remove dead DB insert, use BullMQ directly
The POST /conform route was inserting into the jobs table with non-existent
columns (project_id, metadata) and an invalid enum value ('pending'). Since
GET /jobs reads entirely from BullMQ, the DB insert was both incorrect and
redundant. Now we just enqueue the BullMQ job and return its ID.
2026-05-18 23:22:14 -04:00
596f755a6c fix: remove stray Wild Dragon brand remnant from editor.html 2026-05-18 23:14:14 -04:00
656c820638 feat: wire editor.html as primary editor, fix its sidebar/branding
- All pages: Editor nav link now points to editor.html (in-house NLE)
- Removes the :47435 OpenReel resolver script from all pages
- editor.html: canonical Z-AMPP sidebar (all 10 nav items, correct icons)
- editor.html: Z-AMPP brand logo, removes Wild Dragon SVG mark
- editor.html: removes Google Fonts import
- editor.html: adds auth-guard.js
2026-05-18 23:11:53 -04:00
910bbf8d3f merge: bring NLE editor pages (editor.html, timeline.js, timecode.js) from main 2026-05-18 23:02:51 -04:00
e8e26dd4d8 fix: remove Google Fonts, fix editor link to :47435, fix page titles
- Remove @import Google Fonts from common.css (was blocking CSS on LAN)
- Update Editor nav link on all pages to dynamically resolve to :47435
  (OpenReel SPA) using inline script so it works on any hostname
- Fix page titles from Wild Dragon -> Z-AMPP across all pages
- Resolver: <a href="#" id="editor-nav-link"> + IIFE sets href at load
2026-05-18 22:56:51 -04:00
1f31d1037d merge: bring sequences/auth/admin backend + auth-guard frontend into fix/library-and-signal-indicator 2026-05-18 21:25:36 -04:00
8ab71239e3 feat(ui): add Open in Editor action to library cards 2026-05-18 20:14:29 -04:00
78a887a3e0 feat(ui): add NLE editor page (editor.html) 2026-05-18 20:10:25 -04:00
2fabc73299 fix(ui): prevent keydown listener accumulation on re-init 2026-05-18 20:05:34 -04:00
10152b5ad7 feat(ui): add DOM-based timeline engine (select, razor, playhead) 2026-05-18 20:02:41 -04:00
ad6e836345 feat(ui): add sequence API helpers to api.js 2026-05-18 19:58:35 -04:00
7d8ccc95e9 feat(ui): add 59.94 DF timecode utility module 2026-05-18 19:58:34 -04:00
7b8d8d4818 feat(api): register sequences route 2026-05-18 19:54:41 -04:00
1816c7fa1e fix(api): guard DELETE 404, verify sequence before clip wipe, add clip validation 2026-05-18 19:53:14 -04:00
05d49b7199 feat(api): add sequences route with CRUD, clip sync, and EDL export 2026-05-18 19:50:29 -04:00
eb248c690f fix(db): use DO blocks for idempotent ALTER TABLE (ADD CONSTRAINT IF NOT EXISTS is not valid PG syntax) 2026-05-18 19:48:11 -04:00
c662df94c4 fix(db): add CHECK constraints, UNIQUE, and asset_id index to sequences schema 2026-05-18 19:46:42 -04:00
b12d8c619a feat(db): add sequences and sequence_clips tables 2026-05-18 19:41:21 -04:00
0964645910 docs: add Phase 1 NLE Editor implementation plan 2026-05-18 19:34:53 -04:00
9032853629 docs: finalize spec with user decisions (auto-save, multi-sequence, 59.94fps, gap-ok) 2026-05-18 19:23:01 -04:00