Double-clicking a clip name in the library shows an in-place text input.
Enter/blur commits the new display_name via PATCH; Escape cancels.
Clicking the card body or action buttons still work normally.
Adds an "All / Ready / Processing / Error / Live" pill filter row and
a "Newest / Oldest / Name / Duration / Size" sort selector to the asset
toolbar. Both operate client-side on the loaded asset list so there is
no additional API overhead. State resets to "All / Newest" whenever a
different project or bin is selected.
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.
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.
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.
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.
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.
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().
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
- 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
- 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
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.
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.
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.
- 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
- 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