Commit graph

648 commits

Author SHA1 Message Date
f21157f3c7 fix: refresh bin counts after asset move
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.
2026-05-24 14:50:22 -04:00
a5ab57d144 fix: add missing > to close bin rail div opening tag
Missing > after title attribute caused Babel parse error,
preventing Library component from being registered globally.
2026-05-24 14:46:36 -04:00
0ebc7ef777 fix: use window.RenameProjectModal via React.createElement
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.
2026-05-24 14:30:22 -04:00
d94ed00312 fix: apiFetch headers spread, droppable highlight, project rename, color stability, orphaned api.js removal
- Fix apiFetch headers spread bug (custom headers overwrote Content-Type)
- Track per-bin hover state for droppable highlight
- Refresh project rail after rename from Library screen
- Use ID-hash for project colors instead of array index
- Remove orphaned js/api.js (563 lines, never loaded)
- 'All projects' rail item clears openProject filter
- Add project boundary guard to drag-and-drop bin moves
- Stabilize refreshAssets useCallback with empty deps
- 'Last 24h' filter now actually filters by created_at
2026-05-24 14:20:00 -04:00
af905cf936 fix: bin creation 500 error + add drag-and-drop + project rename
- 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
2026-05-24 13:27:24 -04:00
c312991bac feat: implement advanced features (conform, auto-relink, GUI redesign, docs, tests)
- #30 FCP XML Export & Conform: slide panel UI, preset system, FCP XML generation,
  conform job submission with progress polling via BullMQ
- #31 Hi-Res Auto-Relink: clip list with checkboxes, batch-trim server endpoint,
  trimWorker with frame-accurate FFmpeg trimming, auto-relink in Premiere via
  ExtendScript, temp segment signed URL endpoint
- #32 GUI Redesign: complete rewrite with Wild Dragon OKLCH design tokens
  (accent oklch(45% 0.20 266)), slide panels, preset cards, chip components
- #34 Cleanup Task: existing task validated and properly registered
- #35 Testing: comprehensive 33-scenario E2E test plan
- #36 Documentation: advanced features guide with workflows, troubleshooting,
  presets table, and architecture overview
- #24 PR merge: verified mergeable

All server endpoints, worker queues, and ExtendScript functions wired together
2026-05-24 13:19:24 -04:00
77130ac769 feat(server): temp segments cleanup task
Hourly cron that deletes expired temp_segments from S3 and DB.
Implements issue #34. Registered alongside scheduler in index.js.
2026-05-24 12:43:08 -04:00
a016175fc8 feat(db): migration 012 — advanced features schema
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
2026-05-24 12:42:22 -04:00
543248b8c2 Merge remote-tracking branch 'origin/feat/premiere-installer' 2026-05-24 12:03:46 -04:00
eadafffb18 fix(premiere-plugin): v1.0.1 — actually load + connect under CEP 12
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>
2026-05-23 19:24:10 -04:00
a6d789279c Merge pull request 'fix(jobs): real cancel for active jobs + multi-threaded thumbnail worker' (#29) from fix/jobs-cancel-and-concurrency into main 2026-05-23 17:23:52 -04:00
91325a4267 fix(jobs): real cancel for active jobs + multi-threaded thumbnail worker
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>
2026-05-23 17:23:07 -04:00
2b85fb49df Merge pull request 'fix(jobs): cancel running + delete failed jobs so the queue can be unstuck' (#28) from fix/jobs-cancel-stuck into main 2026-05-23 16:54:50 -04:00
eb6c723713 fix(jobs): cancel running + delete failed jobs to unstick the queue
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>
2026-05-23 16:54:05 -04:00
6322b61a04 Merge pull request 'chore(web-ui): delete legacy standalone HTML pages; SPA is the only entry' (#27) from chore/cleanup-legacy-html into main 2026-05-23 16:49:39 -04:00
ff2865b5d8 chore(web-ui): delete legacy standalone HTML pages; SPA is the only entry
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>
2026-05-23 16:48:38 -04:00
53049d1c4d Merge pull request 'feat(auth): bounce to /login.html on 401 so AUTH_ENABLED=true gives a real login' (#26) from feat/auth-login-redirect into main 2026-05-23 16:41:25 -04:00
bec4bfaf31 feat(auth): bounce to /login.html on any 401 from the api wrapper
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>
2026-05-23 16:40:45 -04:00
0537378d82 Merge pull request 'feat(schedule): right-click menu + drag-to-resize on EPG event blocks' (#25) from feat/epg-resize-and-ctxmenu into main 2026-05-23 16:34:48 -04:00
3ffffd5b32 feat(schedule): right-click menu + drag-to-resize on EPG event blocks
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>
2026-05-23 16:33:57 -04:00
d1fcfcc8fd chore(premiere-plugin): commit v1.0.0 installer artifacts
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>
2026-05-23 16:30:39 -04:00
97f08b32de ui(jobs): widen Time + Progress columns, narrow Node + Priority
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>
2026-05-23 16:27:13 -04:00
9a6ae3b786 fix(jobs): backfill asset_name from DB so non-YouTube jobs show their asset
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>
2026-05-23 16:23:29 -04:00
8aece9cbc4 fix(premiere-plugin): make build pipeline portable to Windows PowerShell 5.1
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>
2026-05-23 16:22:46 -04:00
5699cff4d0 Merge pull request 'feat(schedule): EPG stylesheet + impeccable context (PRODUCT / DESIGN.md)' (#23) from polish/schedule-epg-styling into main 2026-05-23 16:20:48 -04:00
5882c68217 feat(schedule): EPG stylesheet + impeccable context (PRODUCT/DESIGN.md)
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>
2026-05-23 16:19:25 -04:00
0ff2625876 fix(premiere-plugin): remove broken Inno Setup [Code] heuristic
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>
2026-05-23 16:18:19 -04:00
c0d1251c1f fix(tokens): add missing showCalc state — page was crashing on render
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>
2026-05-23 16:18:19 -04:00
9266a1d471 fix(premiere-plugin): correct zxp-sign-cmd version + promise API; commit generated signing cert
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>
2026-05-23 16:17:31 -04:00
f874009329 feat(premiere-plugin): ZXP + Windows installer build pipeline
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>
2026-05-23 16:13:20 -04:00
9ad88e4df4 feat(ingest): YouTube importer — paste link, asset travels normal pipeline
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>
2026-05-23 16:05:41 -04:00
7a2710dc9a docs: design spec for YouTube importer
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>
2026-05-23 16:04:28 -04:00
674dccca4e Merge pull request 'fix(web-ui): CSS must-revalidate so deploys aren't masked by browser cache' (#22) from fix/css-cache-revalidate into main 2026-05-23 15:41:17 -04:00
f525506718 fix(web-ui): css must-revalidate so deployed styles are picked up immediately
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>
2026-05-23 15:40:26 -04:00
908cf8a62d Merge pull request 'jobs: show completion timestamp for done/failed jobs' (#21) from polish/jobs-completed-stamp into main 2026-05-23 15:29:37 -04:00
0551512fef feat(jobs): show absolute completion timestamp for done/failed jobs
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>
2026-05-23 15:26:24 -04:00
e8299fb9f6 polish: live refresh, schedule calendar, jobs times, real sidebar user (#20) 2026-05-23 15:18:55 -04:00
6a1d271576 feat(ui): polish round 2 — live refresh, schedule calendar, jobs times, real sidebar user
- 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>
2026-05-23 14:52:04 -04:00
7e64675aa5 fix: settings S3 surfaces fetch errors; recorder signal dot pulses
- 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.
2026-05-23 13:19:48 -04:00
2515258dd4 style(home): launcher styles + sidebar brand-logo treatment
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.
2026-05-23 10:56:10 -04:00
ccbebe172d feat(shell): add Dashboard nav entry; swap fake "D" mark for real logo
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.
2026-05-23 10:54:36 -04:00
74fc8323f0 feat(app): route dashboard separately from home; add to crumbs map
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.
2026-05-23 10:53:31 -04:00
740ab31f8c feat(app): wire the new Dashboard route alongside the Home launcher
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.
2026-05-23 10:48:42 -04:00
72fc9cb755 feat(home): restore launcher home page; move current home to Dashboard
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.
2026-05-23 10:48:06 -04:00
7a6296585c fix(asset): show 'Generate proxy' CTA when an asset has a hi-res
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.
2026-05-23 10:30:42 -04:00
1afb150237 feat(assets): cleanup-live-orphans + retry handles non-error states
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.
2026-05-23 10:28:42 -04:00
508e978fe5 fix(worker): route SVG (and other image assets) through the image-poster
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.
2026-05-23 10:26:59 -04:00
d07fb13401 ui: search + right-click menu polish so they read as real controls
- 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.
2026-05-23 09:53:17 -04:00
a8a2061eec fix(asset): comment composer shows real user from ZAMPP_DATA.ME, removes dead add-reviewer button 2026-05-23 09:15:20 -04:00
14d689aaf3 fix(shell): sidebar user name/avatar/role from ZAMPP_DATA.ME instead of hardcoded ZG 2026-05-23 09:13:37 -04:00