Dispatch df:bins-changed custom event from onBinDrop and
AssetContextMenu.moveToBin so the bin rail counts update
immediately after moving an asset into a bin.
RenameProjectModal is exported to window from screens-projects.jsx,
so Library screen must reference it via window object and use
React.createElement instead of JSX syntax.
- Fix 500 error when creating bins: missing updated_at column on bins table
(migration 013 adds the column, schema.sql updated)
- Add drag-and-drop support for moving asset cards/list rows onto bin rail items
with visual droppable highlight
- Add right-click context menu on project rail items (Rename/Delete)
- Expose RenameProjectModal via window so Library screen can reuse it
- Bins context menu already existed — was hidden by the 500 error
Add 'trim' to job_type enum, create temp_segments table with
expiry/job/asset indexes, and add conform_source_sequence_id
to assets for lineage tracking.
Closes#33
End-to-end debugging against a live Premiere Pro 2025 + auth-enabled mam-api
surfaced four real bugs that made v1.0.0 install cleanly but never load,
plus the missing auth flow. All four are fixed and the panel is verified
connected (status dot green, Reconnect button shown, project list populated).
- manifest.xml: a comment in the <Resources> block contained "--" (inside
"--enable-nodejs"/"--mixed-context"), which is illegal per the XML spec.
CEP 12's strict parser logged
ERROR XPATH Double hyphen within comment
and skipped the panel entirely. Comment rewritten without double hyphens.
- manifest.xml: lacked the Version="X.Y" attribute on <ExtensionManifest>
and used a non-standard AbstractionLayers/empty <ExtensionList/>
structure. CEP rejected it with
Unsupported Manifest version ''
Manifest rewritten to the standard CSXS 7.0 schema (ExtensionList +
DispatchInfoList + RequiredRuntimeList), matching the working AMPP
panel template.
- main.js: re-declared `const csInterface = new CSInterface()` at top
level even though CSInterface.js already declared the same binding.
CEP 12 shares script-realm lexical scope across <script> tags, so the
second const threw
Identifier 'csInterface' has already been declared
The throw fired before setupEventListeners(), so the Connect button's
click handler was never attached. This is the root cause of the
original "clicking Connect does nothing" symptom; everything else was
secondary. Removed the duplicate declaration; main.js now uses the
binding from CSInterface.js.
- No auth support against AUTH_ENABLED=true servers. mam-api supports
Bearer tokens (POST /api/v1/tokens), so added:
• API token input field (password-masked) next to Server URL
• localStorage persistence on every keystroke
• window.fetch monkey-patch that injects
Authorization: Bearer <token>
on every request whose URL starts with the configured server.
Signed S3 download URLs are NOT touched.
Drive-by fixes that came out of the same debugging pass:
- Server URL input listener was 'change' (fires on blur); switched to
'input' so typing-then-clicking-Connect immediately commits.
- restoreSettings() now strips trailing slashes from the stored URL so
older saved values like 'http://host/' stop producing //api/v1 404s.
- CSS selector `input[type="text"].server-url` didn't match the new
password input → the token field was unstyled and effectively invisible.
Generalized to `input.server-url`; restructured the connection bar into
`.connection-controls--stacked` (flex column) of two `.server-input-row`
rows so two input fields fit cleanly.
- Build scripts now parse ExtensionBundleVersion from both element form
(<ExtensionBundleVersion>X</...>) and attribute form
(ExtensionBundleVersion="X"), since the manifest rewrite switched
schemas.
Version bumped 1.0.0 → 1.0.1. New artifacts committed at
services/premiere-plugin/build/releases/v1.0.1/ (.exe 2 MB, .zxp 35 KB).
v1.0.0 left in place so editors who downloaded it can verify they're on
the broken version.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
DELETE /jobs/:id was throwing "404 not found" when the operator tried to
cancel a running job. BullMQ refuses job.remove() while a job is in the
active state; the route caught that error and fell through to the
404 branch, which was misleading because the job actually exists — the
queue was just refusing to drop it from under the worker.
Fix:
- Detect 'active' state explicitly and call moveToFailed(err, '0', false)
first. Token '0' bypasses the per-worker lock check (the operator-side
cancel doesn't hold the worker lock). That transitions active -> failed
and frees the queue's concurrency slot.
- If moveToFailed itself fails (lock owned by a live worker), fall back
to job.discard() so at least the result is thrown away.
- If remove() then fails (stalled, broken state), drop the job's Redis
key directly via queue.client. Last-resort obliteration.
- Stop swallowing getJob() errors — if Redis is sad, surface it via
next(err) instead of returning a misleading 404.
- Return { cancelled: true } when the job was active, so the client
can show "Cancelled" rather than "Removed" in any future toast.
While here: thumbnail jobs now run with concurrency 4 by default
(proxy 2, conform 1, import 1 unchanged). Every queue defaulted to
concurrency 1 before, so a single stalled job blocked the entire queue.
All three are overridable via PROXY_CONCURRENCY / THUMBNAIL_CONCURRENCY
/ CONFORM_CONCURRENCY env vars for nodes with more headroom.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Jobs page only exposed a delete button for queued + done jobs, so a
stalled-active job (worker died holding a BullMQ concurrency slot) had
no way out from the UI. Operators were watching the queue back up
behind a single stuck thumbnail job with no kill switch.
- Running jobs now show a "Cancel" button (red text). Confirm copy
spells out that the worker may run a few seconds longer in the
background but the queue slot frees up immediately.
- Failed jobs now show the X icon for delete in addition to the
existing Retry button.
- Both routes hit the same DELETE /jobs/:id endpoint; BullMQ's
job.remove() works on any state including stalled-active.
- handleDelete takes an optional mode ('cancel' | 'delete') only to
customise the confirm prompt and error toast wording.
Right-aligned the action cell so the Retry/Cancel/Delete buttons sit
flush right like the rest of the table's actions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Before this commit /public had two parallel UIs: the React SPA (index.html
+ screens-*.jsx) and a stack of pre-SPA standalone pages (home.html,
recorders.html, jobs.html, ...). The SPA replaces every standalone page,
nothing in the .jsx tree links to them, and the only outside references
were login.html redirecting to home.html and the nginx fallback pointing
at home.html.
Delete 16 standalone pages (~9.2k lines of dead markup, ~430KB on disk):
_primitives-smoke.html api-tokens.html capture.html cluster.html
containers.html edit.html editor.html home.html
jobs.html player.html projects.html recorders.html
settings.html tokens.html upload.html users.html
Keep:
index.html — the React SPA shell
login.html — the sign-in / setup screen
Wire the redirects to the SPA:
- login.html post-signin: home.html -> /
- nginx try_files fallback: /home.html -> /index.html
After this, sign-in lands the operator on the real React app instead of
the stale 2025-era home page. The Editor screen continues to embed the
separate editor service via the /editor/ nginx proxy (unaffected).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
apiFetch now redirects to /login.html when the server returns 401, so
flipping AUTH_ENABLED=true on mam-api gives the user the login screen
instead of a half-loaded app that silently failed to fetch /auth/me.
While AUTH_ENABLED=false the server's /auth/me still returns a synthetic
200 user, so this branch is dormant — safe to deploy ahead of the env
flip on the server. After the flip the operator visits /login.html
(directly or via auto-redirect), runs the "Create admin account" flow
once, and lands back on the SPA with a real session.
Guards against a redirect loop if login.html itself somehow lands here.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Right-click any event block to open a context menu (Edit, Cancel,
Copy schedule ID, Delete) — actions per status mirror the List view so
the two surfaces stay in lockstep. Menu is viewport-clamped and
dismisses on outside click / scroll, same pattern as the asset menu in
the Library.
Drag-to-resize works for pending schedules only (the schedules PUT
rejects edits to running rows, and terminal statuses are read-only):
- Drag the left edge to move the start time
- Drag the right edge to move the end time
- Drag the body to shift the whole block in time
All gestures snap to 15-minute increments to match the new-schedule
click snap. Minimum duration is clamped to 5 minutes; the block clamps
to the visible day on both edges. While dragging the title shows the
preview range ("Start time → end time") and the block lifts with a
project-tinted shadow.
A short pointer click (< 4px travel) still opens the edit modal — the
click and drag share the same pointerdown so the operator never has
to know which gesture they made first.
Implementation: replaces the <button> block with a <div> hosting three
zones (left handle / body / right handle). Pointer events with
setPointerCapture so drags survive losing the cursor over the block,
and pointerup demotes back to click if travel was below threshold.
Optimistic local update on resize, PUT /schedules/:id with just the
two changed time fields, refetch to reconcile.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops dragonflight-premiere-panel-1.0.0-windows-setup.exe (2 MB) and
dragonflight-premiere-panel-1.0.0.zxp (35 KB) at
services/premiere-plugin/build/releases/v1.0.0/ so the binaries have
stable URLs on the forge without needing a separate release artifact
flow.
Heads-up: this commits 2 MB of binary into git history. Future bumps
should use Forgejo Releases (release assets are external to git history
and easy to delete) rather than another commit under releases/.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Time was clipping the full "done May 22 · 2:23 PM · 6h ago" string on
terminal-state rows; Progress's bar felt cramped next to the percent.
Node carries only "primary" / "—" so it can shrink, and Priority's
"normal" / "high" badge doesn't need 80px either. Net widening absorbed
by the flexible Asset column.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Jobs screen only displayed an asset name when the enqueueing code
stuffed assetName into the BullMQ job data. YouTube imports did that;
upload-triggered proxy/thumbnail jobs didn't — so everything except
YouTube showed em dashes in the Asset column.
Fix it centrally: after we collect jobs from BullMQ, look up names
in one bulk SELECT against the assets table for any job that has an
assetId but no asset_name. Applies to /jobs, /jobs/:id, and the SSE
events stream. Lookup failures fall through silently rather than
500-ing the whole list.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
End-to-end verification on a fresh Windows machine surfaced three issues:
1. pwsh isn't installed by default — Windows ships powershell.exe (5.1).
Switched all script invocations + docs from `pwsh` to `powershell`.
2. .NET's strict XML parser rejects manifest.xml because the <Resources>
comment legally contains `--` (inside `--enable-nodejs`/`--mixed-context`
CEF flag names). Switched build-installer.ps1 to regex extraction,
matching what build-zxp.mjs already does.
3. winget installs Inno Setup 6 to %LOCALAPPDATA%\Programs by default, not
Program Files (x86). Added the user-scope path to the ISCC.exe fallback
list.
Verified: `powershell -File build-all.ps1` produces both artifacts —
dragonflight-premiere-panel-1.0.0.zxp (35 KB, signature valid)
dragonflight-premiere-panel-1.0.0-windows-setup.exe (2 MB).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The EPG JSX components in screens-ingest.jsx ship with the YouTube branch
but the matching stylesheet got lost during the parallel-branch shuffle.
This adds the missing .epg-* block to styles-rest.css and replaces the
dead .cal-* (month-calendar) rules left over from the previous design.
What the styles cover:
- .epg-page / .epg-toolbar — top-level flex layout + date nav row
- .epg-status — sticky "on air" strip with pulse halo on the live dot
- .epg / .epg-corner / .epg-gutter / .epg-canvas-head / .epg-canvas —
the 2x2 sticky grid (top ruler + left gutter both sticky)
- .epg-ruler / .epg-ruler-tick — hour ticks
- .epg-row + .epg-block + .epg-block.live/.failed/.past — event blocks
with project-color 4px inner bar (no side-stripes; impeccable ban)
- .epg-now / .epg-now-pip — vertical hot-red now-line with broadcast glow
- .epg-week + .epg-week-day — stacked 7-day sections for week view
- .epg-empty — recorder-less / loading empty state
Also adds PRODUCT.md and DESIGN.md so future design passes have the
context files the impeccable skill requires. Both drafted from the
existing codebase (tokens, screen patterns) rather than synthesised
from a prompt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Premiere-running check passed a Boolean to Exec's var ResultCode (Integer)
parameter — Pascal type error. The block also did nothing useful: it only
checked but never warned or prompted. Drop it. {InstallDelete] of the legacy
folder still works through the Tasks checkbox + Check function.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Tokens screen referenced showCalc / setShowCalc in the Cost calculator
button and modal but never declared the state hook, so the component
threw ReferenceError on mount and rendered blank.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The initial pass referenced zxp-sign-cmd@0.2.2 which never shipped (latest
is 2.0.0) and used the v1.x callback API. v2 is promise-based — rewrote
build-zxp.mjs accordingly.
Also commits the freshly-generated self-signed cert + passphrase from the
first local build run. From now on every build reuses these so Adobe's
ZXP signature-continuity rule is satisfied across versions.
Verified end-to-end: `npm install && node build-zxp.mjs` produces
dist/dragonflight-premiere-panel-1.0.0.zxp (34.7 KB), signature verifies,
cert valid until 2051.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the manual robocopy / install-windows.ps1 flow with two real
distributable artifacts:
- dragonflight-premiere-panel-<version>.zxp (Mac + Win)
- dragonflight-premiere-panel-<version>-windows-setup.exe (Win)
The Windows installer copies the bundle to %APPDATA%\Adobe\CEP\extensions,
sets PlayerDebugMode=1 for CSXS 8..13, registers an uninstaller, and
offers to remove any legacy com.wilddragon.mam.panel folder so editors
don't end up with duplicate panels.
The .zxp is signed with a self-signed cert generated on first build and
committed to build/cert/ so signature continuity is preserved across
builds (Adobe rejects ZXP upgrades with a different cert fingerprint).
Also migrates the CEP bundle ID from com.wilddragon.mam.panel to
net.wilddragon.dragonflight.panel to match the wild-dragon -> dragonflight
repo rename. Manifest, .debug, CSInterface.js, install docs, and the
growing-files quickstart all updated.
build/ is normally swept by the root .gitignore; added an explicit
negation so the packaging pipeline stays tracked.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds Ingest → YouTube. UI takes a URL + project, API enqueues a BullMQ
"import" job, worker shells out to yt-dlp, lands the MP4 in S3 at the
same originals/{assetId}/... path uploads use, then hands off to the
existing proxy queue. Imported assets share one lifecycle with uploads
from that point on.
Worker container picks up yt-dlp + python3 (apk on alpine, apt on the
GPU variant). The new 'import' queue is registered in jobs.js so it
appears in the Jobs SSE stream and retry/delete work for free.
Spec: docs/superpowers/specs/2026-05-23-youtube-importer-design.md
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a paste-URL ingest path under Ingest → YouTube. Worker hosts
yt-dlp, downloads to S3, then hands off to the existing proxy +
thumbnail pipeline so imported assets share one lifecycle with uploads.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Nginx was serving css with `expires 1y; Cache-Control: public, immutable`,
which combined with version-less <link href="styles-rest.css"> meant every
browser permanently pinned whatever stylesheet it cached first. Users were
seeing pre-polish-round-2 CSS even after the new image was deployed —
the calendar grid rendered as a vertical stack of weekday names because
the .cal-* rules didn't exist in the cached file.
Move css into the same bucket as js: must-revalidate via ETag. Fonts,
icons, and raster assets stay in the immutable 1y bucket since they don't
change between deploys.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Time column now anchors on the wall clock when a job is in a terminal
state — "done 2:23 PM · 5m ago" / "failed May 22 · 14:24 · 1h ago" — so the
operator can correlate with logs and other timestamps without hovering.
Queued/running jobs keep the relative-only format since their timestamp is
constantly moving. Widen the column to 180px to accommodate the longer label.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- recorders: dispatch df:recorders-changed on create/start/stop/delete so the
list updates immediately instead of waiting for the 10s poll tick
- library: poll every 4s while any asset is live/processing (15s otherwise) and
listen for df:assets-changed so a stopped recorder's LIVE badge drops and
the thumbnail appears without a manual refresh
- auth: synthetic /auth/me (AUTH_ENABLED=false) now uses LOCAL_OPERATOR / USER /
USERNAME instead of hardcoding "Admin", and flags synthetic:true
- shell: Sidebar takes `me` as a prop, drops the misleading "Admin" fallback,
and surfaces an "auth off" hint when the response is synthetic
- jobs: replace the always-empty ETA column with a Time column that shows
queued/started/done/failed N ago (full timestamp on hover); widen column
- schedule: new month-calendar view (default) with events plotted on day cells
by status; clicking a day pre-fills the new-schedule modal with a 30-min
window on that day; List view kept behind a toggle
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- screens-admin.jsx S3SettingsCard: when /settings/s3 fails, log to
console and surface the message in the existing SettingsMsg banner
instead of silently returning empty fields. Also logs the response
payload on success so the next "endpoint blank" report is easier to
diagnose. (closes part of #15)
- screens-ingest.jsx recorder row: wrap the signal value in a dot+text
pair; add CSS so the dot pulses green when status=receiving and
matches the value color otherwise. The pulse is the kind of cue the
Live signal column was missing per #2.
Adds .launcher, .launcher-tile, .launcher-hero, .launcher-grid styles
plus .brand-logo replacement for the gradient-D mark in the sidebar.
The shipped img/dragon-logo.png has a light-gray background; we use
mix-blend-mode: screen so the black dragon silhouette sits on the
dark theme without showing the gray box, and a soft accent glow under
the hero version. Smaller sidebar version uses the same trick.
The sidebar header used a gradient "D" tile as a placeholder. Now it
uses the actual dragon-coiled-D logo so the brand reads consistently
between the launcher hero and the chrome.
Also adds a 'Dashboard' nav item directly under 'Home' so the
operations view is one click away.
The launcher (Home) and the operations view (Dashboard) are now
distinct routes. Home is the landing page; Dashboard is reached from
the sidebar or from the launcher's "Open dashboard" tile.
home → launcher (big-button entry into each section)
dashboard → operations view (was Home; metrics, recent activity, queue)
Crumb labels updated so Home stays one level (just the wordmark) and
Dashboard gets its own breadcrumb.
The original first-version home page (big-button launcher with the
Dragonflight wordmark) is back at /. The Frame.io-style metrics +
recent-activity layout we've been treating as "home" is now the
Dashboard, reachable from the sidebar and from the launcher's
"Open dashboard" button.
- Renames existing Home → Dashboard (all the cards, sparklines, live
feed, job-queue, cluster mini-list are unchanged).
- New Home component: hero with the dragon-coiled-D logo (existing
img/dragon-logo.png), wordmark "DRAGONFLIGHT", a tag line, and 5
big tiles (Library, Recorders, Editor, Jobs, Settings) plus a
smaller Dashboard tile. Live cluster + recorder status pip at the
bottom mirrors what's in the topbar.
- The launcher pulls /metrics/home so the tile counts ("34 assets",
"0 live", "0 running") reflect reality.
master but no browser-playable proxy
Previously the player's "Retry processing" button only appeared for
assets in status='error'. Old recorder captures (e.g. the archived
'sRT Test_…' clips from May) live as status='archived' or 'ready' with
original_s3_key set but proxy_s3_key null. The /stream endpoint
correctly returned {url: null, reason: 'no_proxy'} for those, but the
player just showed "Preview not yet available" with no path forward —
which reads as "ingest worked, won't play, no idea why."
Two changes:
1) Capture {reason, has_source} from /stream so the UI can tell why
playback isn't available.
2) Render a "Generate proxy" button (using the existing
POST /assets/:id/retry endpoint, which the backend now accepts for
any asset with original_s3_key but no proxy_s3_key) whenever the
stream lookup returned no_proxy and the source exists. Original
error-status retry path is preserved.
Closes the visible half of #1 — the user can now self-recover proxy-
less clips from the library without DB surgery.
Two changes for issue #7 (HLS cleanup + orphan reaper) and the user's
"SRT clips ingest but won't play" complaint:
1) New POST /assets/cleanup-live-orphans — lists every directory under
/live/<uuid>/ and deletes the ones whose UUIDs don't match an asset
row. These accumulate when a recorder crashes mid-capture: the live
HLS dir is created but no asset is ever finalized in the DB, so the
files just sit on disk forever.
2) POST /assets/:id/retry now also works for assets that are 'ready'
or 'archived' but have no proxy_s3_key. The original behavior (only
re-queue when status='error') made it impossible to re-generate a
proxy for older recorder captures that landed without one — the
user could see a thumbnail in the library but the player would just
show "Preview not yet available" with no retry path.
path instead of failing the video transcode
Previously IMAGE_CODECS contained the raster ffprobe codec names ('png',
'mjpeg', 'jpeg', 'webp', 'gif', 'tiff', 'bmp', 'jpegls') but not 'svg'.
An SVG-as-asset (e.g. an architecture diagram dragged into a project) was
correctly tagged media_type='image' in the DB but ffprobe reported its
codec as 'svg', which fell through to the video branch, found
durationMs===null, and died with 'Empty or truncated source: codec=svg,
resolution=0x0'. That clogs the failed-jobs list with red rows that have
nothing to do with broken captures.
Two fixes here:
1) Add 'svg' to IMAGE_CODECS so the existing transcodeImage()/poster
path handles it.
2) Also bail to the poster path when the asset row itself says
media_type='image', even if ffprobe didn't return a codec name we
recognize (defensive — catches future formats like AVIF without
requiring an explicit catalog update).
Closes part of #13.
- Topbar search now sits on bg-2 with a stronger border, subtle inset
highlight, and a hover state. Search icon and kbd hint get more
contrast. Focus state lifts the field with a soft accent ring.
- Search results dropdown gets a slightly inset header look so the
list reads as connected to the field.
- Right-click context menu (ctx-menu) gets a stronger background,
a tighter section header, separator color tuning, and a soft
outline so it feels like a popover instead of floating text.
Library's Bins section now always renders (not just when bins exist)
with a + button that prompts for a name and POSTs /api/v1/bins with
the open project's id. Bins re-fetch on project change so the rail
shows project-scoped bins when a project is open, or global view
otherwise.
Bins list now hydrates from local state instead of stale ZAMPP_DATA
so newly-created bins appear without a full reload. Without an open
project the + button is dimmed with a helpful tooltip — "Open a
project to create a bin".
- Schedule: pending rows get an 'Edit' button next to Cancel/Delete;
opens a modal that PUTs /schedules/:id with new name/times/recurrence
(recorder reassignment is intentionally locked — delete + recreate
to swap recorder)
- README rewritten: project renamed to dragonflight, full feature
catalog (ingest, growing-files, scheduler, library + comments,
jobs, settings, cluster), accurate ports, refreshed architecture
diagram, ops scripts inventory
Walks GET endpoints for auth, projects, assets, recorders, jobs, bins,
users, groups, cluster, settings, metrics, schedules, sdk, and the
freshly added comments routes. Deep-links one asset + one recorder by
ID so per-asset endpoints (stream, thumbnail, comments) get coverage.
Prints HTTP codes inline and exits non-zero on any failure. Treats
2xx/3xx as pass; 400/401 also pass since they indicate the route
exists and auth/validation is working as designed.
Usage:
deploy/api-smoke.sh # localhost:47432
API=http://10.0.0.25:47432 deploy/api-smoke.sh
NewRecorderModal: hardened ZAMPP_DATA hydration with defensive
defaults so first-load timing doesn't blow up the modal.