Commit graph

369 commits

Author SHA1 Message Date
f0f615688e release: add v1.1.0 ZXP artifact (Growing tab + visual system alignment) 2026-05-26 16:09:52 -04:00
55ff2e717f feat(cluster): full hardware breakdown per node
_normalizeNode:
- Maps cpu_usage, mem_used_mb/mem_total_mb from actual API fields
- Reads capabilities.gpus: name, memory_mb, device, bound status
  (bound = nvidia-smi confirmed driver, detected by name+memory_mb)
- Reads capabilities.blackmagic + blackmagic_model: model, port count,
  device paths

Node detail panel:
- GPUs: name, VRAM, device path, BOUND/UNBOUND badge (green if driver active)
- Capture cards: model name, port count badge, per-port device name
  with online/offline color coding

Stat row: adds Capture ports total count card

Topology SVG: shows GPU count and BMD port count under each node label

Fix: removeNode uses node.dbId (UUID) not node.id (hostname)
2026-05-26 18:06:30 +00:00
a03c85f08a feat: server-side filmstrip worker + fix scheduler crash + fix clip freeze
Root causes found:
1. Scheduler crashing every 15s: assets table has no error_message column.
   Fix: remove error_message from UPDATE in scheduler.js (#66 regression).

2. Clip freezing: client-side filmstrip seek loop runs on main thread,
   seeks same proxy the player is streaming → both stall → freeze.
   Fix: replace browser seek loop entirely with server-side FFmpeg worker.

3. No dedicated filmstrip worker: filmstrip was never pre-built server-side.

Changes:
- services/mam-api/src/db/migrations/018-add-filmstrip-s3-key.sql
  Add filmstrip_s3_key TEXT column to assets table

- services/worker/src/workers/filmstrip.js (new)
  BullMQ worker: downloads proxy, runs FFmpeg fps filter to extract
  28 evenly-spaced JPEG frames, base64-encodes them, uploads JSON
  array to S3 at filmstrips/<assetId>.json, stores key in DB

- services/worker/src/workers/thumbnail.js
  Queue filmstrip job automatically after thumbnail completes

- services/worker/src/index.js
  Register filmstrip worker (concurrency=2), export filmstripQueue
  singleton, close it on SIGTERM

- services/mam-api/src/routes/assets.js
  - filmstripQueue added
  - POST /reprocess?type=filmstrip now supported
  - GET /:id/filmstrip returns signed S3 URL for JSON frames

- services/mam-api/src/routes/jobs.js
  filmstrip queue visible in Jobs UI

- services/web-ui/public/screens-asset.jsx
  Replace browser seek loop with fetch of /assets/:id/filmstrip
  → fetch S3 JSON → render frames. Zero browser-side video seeking.
  Right-click and Files tab re-generate via API endpoint.
2026-05-26 16:39:44 +00:00
564cf6b18f fix: thumbnail img uses signed URL from API; switch transcoding to CPU libx264
- FilesTab: fetch /assets/:id/thumbnail (returns signed S3 URL JSON),
  display the resolved URL in <img> instead of pointing directly at the
  endpoint which returns JSON not image bytes
- Transcoding: settings updated on ZAMPP1 to gpu_transcode_enabled=false,
  codec=libx264 — NVENC not available in worker container (no GPU passthrough)
  The proxy worker already has a CPU fallback but this prevents the
  unnecessary failed GPU attempt on every job
2026-05-26 16:27:27 +00:00
89645f160e fix(filmstrip): seeked event never fires at t=0; add per-frame seek timeout
Two bugs:

1. Frame 0 sets currentTime=0 but probe starts at t=0 after onloadedmetadata,
   so 'seeked' never fires (no position change). Promise hangs until the 15s
   global timeout kills the whole build. Fix: when currentTime is already at
   target (within 0.05s), call done() immediately without waiting for seeked.

2. Seeks into unbuffered regions of large MP4s can stall indefinitely.
   Fix: 3s per-frame timeout captures the current decoded frame and moves on,
   so a slow/stalled seek doesn't block the remaining 27 frames.
2026-05-26 16:21:00 +00:00
e9eeb84c5f fix(files-tab): remove inline video preview from proxy row 2026-05-26 16:10:04 +00:00
4f98f2b773 feat(asset): filmstrip right-click menu + Files tab
Filmstrip:
- Right-click on the filmstrip opens a context menu with
  'Re-generate filmstrip' and 'Re-generate proxy'
- filmstripKey state forces the build effect to re-run on demand
  without waiting for a streamUrl/totalMs change
- Context menu dismisses on click, contextmenu, and scroll

Files tab (replaces empty Versions tab):
- Proxy: status badge, S3 key path, inline video preview, re-generate button
- Hi-res master: status badge and S3 key path
- Thumbnail: status badge, S3 key path, inline thumbnail image, re-generate button
- Filmstrip: status badge, frame count, scrollable strip of first 14 frames,
  re-generate button (disabled while building)
2026-05-26 16:07:33 +00:00
b3c61134fc fix(filmstrip): remove crossOrigin=anonymous from probe video element
The /video endpoint requires session auth (requireAuth middleware).
crossOrigin='anonymous' strips cookies from the request → 401 → video
never loads → 15s timeout → filmstrip stays empty for all clips.

Same-origin video does not need crossOrigin for canvas drawImage — the
taint restriction only applies to cross-origin resources.
2026-05-26 16:03:26 +00:00
07f8ffa6d5 feat: editor coming-soon bumper + embedded Premiere panel downloads
- Editor: overlay Coming Soon screen over NLE timeline (code preserved,
  bumper sits at z-index 100 with backdrop blur). Links to download
  ZXP and Windows installer directly from the bumper.

- Settings → Capture SDKs: new Premiere Panel section lists v1.0.0
  and v1.0.1 with ZXP + Windows Installer download buttons.
  Both releases embedded as static files in web-ui under /downloads/.

- nginx: /downloads/ location serves files as Content-Disposition
  attachment with 24h cache.

Files added:
  services/web-ui/public/downloads/dragonflight-premiere-panel-1.0.0.zxp
  services/web-ui/public/downloads/dragonflight-premiere-panel-1.0.0-windows-setup.exe
  services/web-ui/public/downloads/dragonflight-premiere-panel-1.0.1.zxp
  services/web-ui/public/downloads/dragonflight-premiere-panel-1.0.1-windows-setup.exe
2026-05-26 14:34:28 +00:00
8e0e94de3d fix: close all 24 open issues (#40–#94)
Bug fixes:
- #91: dockerApi() 10s socket timeout (Docker daemon hang)
- #77: await syncToAmpp() with .catch() — no longer fire-and-forget
- #75: migration 016 — add 'proxy','import' to job_type enum; add 'completed' to job_status
- #73: BullMQ orphan job cleanup on hard asset delete
- #70: batch-trim jobs table gets expires_at; trim-status auto-expires stale rows
- #66: scheduler tick marks stale live assets (>2h) as error
- #63: migration 017 — partial unique index prevents concurrent live asset overwrite
- #61: recorders.js uses getS3Bucket() not stale process.env.S3_BUCKET
- #60: already fixed (copy nulls proxy/thumbnail keys, requeues proxy)
- #40: already fixed (All projects clears openProject)
- #64: already fixed (sourceType/needsProxy handled)
- #90: GET /jobs now includes DB jobs table (trim jobs visible in UI)
- #74: nginx Content-Type header preserved; multer 500MB file size limit
- #68: GET /upload returns in-progress ingesting assets
- #58: /stream and /video endpoints fall back to original file for all video types
- #55: recorder poll .catch() logs auth errors cleanly; redirect stops interval
- #52: thumb-status and thumb-duration moved inside position:relative wrapper
- #50: ProjectCard gets onContextMenu handler with rename/delete menu
- #49: project context menu dismisses on contextmenu + scroll events

Features:
- #93: POST /assets/:id/reprocess?type=proxy|thumbnail — force re-queue any asset
  Asset ⋯ menu now shows 'Re-generate proxy' and 'Re-generate thumbnail' buttons

UI:
- Logo: brightness(0) invert(1) filter applied consistently in sidebar, launcher,
  and login — white logo pops on dark UI; inline style removed from login.html
2026-05-26 14:10:44 +00:00
fba671ad40 fix(#53): show error banner with retry when loadData() rejects 2026-05-25 17:42:39 -04:00
33c82cab1a fix(#38,#54): fix apiFetch Content-Type header order; fix normalizeAsset seed hash 2026-05-25 17:42:06 -04:00
548c2ab8a4 fix(#72,#59): remove nginx /health stub — API endpoint proxies through correctly 2026-05-25 17:38:40 -04:00
4c8c3b72bb fix filmstrip: append probe to DOM, fix race condition, add 15s timeout 2026-05-25 11:26:02 -04:00
7ea3a235da fix: filmstrip — fetch video chunk as Blob URL, append probe to DOM, add timeout 2026-05-25 11:21:38 -04:00
0481fb3ecf fix: filmstrip probe video — append to DOM, fix src/handler race, add timeout 2026-05-25 11:14:55 -04:00
37c406bf4d fix filmstrip: use hls.js for HLS stream frame capture, not only direct streams 2026-05-25 09:30:40 -04:00
b345f5f6a4 fix editor: use assetsRef to avoid stale closure in handleExternalDrop 2026-05-25 09:29:05 -04:00
OpenCode
87f14b7c71 Fix asset filmstrip and editor UX 2026-05-25 05:14:36 +00:00
c501d88c63 Auto refresh library after ingest 2026-05-25 01:13:19 -04:00
78539ec8b0 Fix editor timeline interactions 2026-05-25 01:10:45 -04:00
de895dd7f8 Fix library refresh behavior 2026-05-25 01:08:38 -04:00
3dad82d992 fix(editor): drag interactions, undo history, overflow clipping
Four critical fixes:
- Remove overflow:hidden on tlRef so Timeline.init's scroll survives re-renders
- Don't call _renderClips() inside mousedown (was destroying event target mid-drag)
- Use refs for undo history to eliminate stale closure in onClipsChanged callback
- Change .tl-clip-area overflow:hidden to overflow:visible so pointer events reach clip edges
2026-05-24 21:21:23 -04:00
4673efac6a fix(editor): setScale, hand pan, sort comparator, playhead sync, rename/delete, track selector 2026-05-24 21:03:12 -04:00
721f847b28 fix: remove openreel editor; fire df:assets-changed on upload/ingest complete 2026-05-24 20:36:04 -04:00
60e306d1db fix(hls): retry on playback failure with exponential backoff 2026-05-24 16:52:04 -04:00
ce31a45124 feat(editor): Phase 1 core NLE editor React SPA rewrite 2026-05-24 16:20:38 -04:00
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
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
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
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
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
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
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
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
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
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
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
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