Commit graph

31 commits

Author SHA1 Message Date
Zac Gaetano
cb7cc9a43e fix(mam-api): narrow cluster carve-out to /cluster/heartbeat only
Code-review feedback: startsWith('/cluster') was a prefix match that exposed
destructive operator endpoints (POST /containers/:id/restart, DELETE /:id,
GET /devices/blackmagic/*) unauthenticated. Only POST /heartbeat is genuine
node-agent traffic; everything else in cluster.js is operator/UI surface
that should go through requireAuth. Long-term: issue node-agent a bound
api_token and drop the carve-out entirely.
2026-05-27 14:18:27 -04:00
Zac Gaetano
9de4fe9ab9 feat(mam-api): mount requireAuth gate at /api/v1 with auth + cluster carve-outs 2026-05-27 14:13:21 -04:00
Zac Gaetano
88c3aa5149 fix(mam-api): SESSION_SECRET boot guard + cleaner CORS rejection
Code-review feedback:
- Hard-fail boot when AUTH_ENABLED=true and SESSION_SECRET is unset, so
  express-session can't silently use an in-memory random secret that
  invalidates sessions on restart and breaks multi-node clusters.
- CORS rejection now returns cb(null, false) instead of cb(new Error)
  so misconfigured origins surface as clean CORS errors in the browser
  instead of HTTP 500s. Log a warn line for operator visibility.
- pruneSessionInterval units comment.
2026-05-27 14:11:09 -04:00
Zac Gaetano
a094df03ea feat(mam-api): wire express-session + tighten CORS allowlist 2026-05-27 14:06:41 -04:00
4172b0d70a rip out entire auth/login flow
- remove requireAuth from all route files
- delete auth.js, tokens.js, users.js routes
- delete auth middleware
- remove session middleware and all auth deps from index.js
- delete login.html and auth-guard.js from web-ui
2026-05-27 03:39:58 +00:00
opencode
9726dbb2df Revert "auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap"
This reverts commit 002e5acb82.
2026-05-27 03:28:05 +00:00
opencode
002e5acb82 auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap
Scope (locked in via planning Q&A):
  - Identity: local accounts only (PG users table) + existing bearer
    tokens for headless callers.
  - Transport: httpOnly cookie session for browser, Bearer for API.
  - RBAC: admin / editor / viewer roles, plus an orthogonal
    is_client flag for external (agency, talent, customer) accounts.
  - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env
    seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET
    to force-reset the named user (break-glass).
  - Rate limit: in-memory, 10 fails per 15min per (IP, username).
  - Password policy: \u22658 chars, mixed case, digit, symbol; small
    blocklist of common passwords; cannot equal username.
  - Self-service: change own display name + password. Everything
    else (role, is_client, other-user mgmt) is admin only.
  - Audit log: append-only table, indexed by actor + event_type +
    created_at, populated by every auth/admin event.

Files added:
  - services/mam-api/src/db/migrations/022-auth-rework.sql
        users.is_client + last_login_at + failed_attempts; audit_log
        table with FK to users (ON DELETE SET NULL).
  - services/mam-api/src/middleware/audit.js
        Fire-and-forget audit() helper. Caller never awaits, failure
        logs but never throws — auditing cannot break the request
        that triggered it.
  - services/mam-api/src/middleware/passwordPolicy.js
        Shared checkPassword(pw, { username }) used by setup, user
        create/update, and self-service password change.
  - services/mam-api/src/tasks/bootstrapAdmin.js
        Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER +
        ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR
        ADMIN_BOOTSTRAP_RESET=true).
  - services/mam-api/src/routes/audit.js
        Admin-only GET /audit (paginated, filter by event_type /
        actor / target / date) and GET /audit/event-types.
  - services/web-ui/public/modal-account-settings.jsx
        Profile + Password tabs. Triggered by sidebar user button.

Files rewritten:
  - services/mam-api/src/routes/auth.js
        - POST /login: regenerate(), no manual save(); audit success/
          fail/lockout; updates last_login_at + failed_attempts.
        - POST /logout: destroys session, audits logout.
        - GET /me: returns is_client + last_login_at. Synthetic admin
          when AUTH_ENABLED=false.
        - GET /setup-status: drives login.html UI state.
        - POST /setup: blocked once any user exists; password policy.
        - POST /password: self-service. Requires current pw, runs
          policy, audits, invalidates other sessions implicitly via
          users.js if changed by admin.
        - PATCH /me: self-service display_name update.
  - services/mam-api/src/routes/users.js
        - is_client field in create/update/list/get.
        - Guardrails: cannot delete or demote last admin, cannot
          delete self, admins cannot be flagged is_client.
        - Password change invalidates all sessions for that user
          (DELETE FROM sessions WHERE sess->>'userId' = id).
        - Audit on every mutation.
        - Password policy enforced.
  - services/mam-api/src/middleware/auth.js
        - requireAuth now exposes req.user.is_client.
        - New requireRole(["admin","editor"], { rejectClients: true })
          helper. Applied to cluster, sdk, capture routes (infra).
        - Synthetic user when AUTH_ENABLED=false has is_client=false.
  - services/mam-api/src/index.js
        - Loads bootstrap admin after migrations.
        - Wires /api/v1/audit.
        - Cleans up an earlier comment block.
  - services/web-ui/public/login.html
        - Password hint added next to setup-mode password field.
  - services/web-ui/public/shell.jsx
        - Sidebar user footer is a button that opens AccountSettings.
        - CLIENT badge next to role when is_client=true.
        - Nav filters: clients lose ingest tree + jobs + editor;
          viewers lose ingest + editor; only admins see the Admin
          section. Power button hidden when synthetic user.
  - services/web-ui/public/screens-admin.jsx
        - Users table: new Client column with inline toggle.
        - InviteUserModal: Client checkbox + password hint, gated
          off when role=admin.
        - Last login column replaces Created in primary view.
        - CSV export includes client + last_login.
  - services/web-ui/public/data.jsx
        - ZAMPP_DATA.ME carries is_client + display_name.
  - services/web-ui/public/index.html
        - Loads dist/modal-account-settings.js.
  - services/web-ui/public/styles-rest.css
        - .user-row grid widened to 6 columns.
  - docker-compose.yml
        - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars.

Deploy:
  cd /opt/wild-dragon
  git pull origin main
  # In .env:
  #   AUTH_ENABLED=true
  #   SESSION_SECRET=<openssl rand -hex 48>
  #   ADMIN_BOOTSTRAP_USER=admin
  #   ADMIN_BOOTSTRAP_PASSWORD=<strong>
  docker compose build mam-api web-ui
  docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-27 03:21:16 +00:00
opencode
d1f9557dd1 auth: park login flow — circle back
Auth work is parked until after ship. While AUTH_ENABLED=false:
  - login.html now auto-redirects to / on load (no one should ever see
    the login screen while auth is off; it was confusing).
  - sidebar power button is hidden entirely when /auth/me returns a
    synthetic user, so there's no broken-feeling no-op control.
  - Removed connect-pg-simple createTableIfMissing flag in case
    v9.0.1's handling of that option was responsible for the recent
    boot 502 (the schema is created by migration 021 anyway).

The /auth/login + session.regenerate() + cookie fix from c34a721
stays in place — when we re-enable auth it'll work end-to-end. The
sessions table from migration 021 stays. Operator action to restore
auth later: set AUTH_ENABLED=true + SESSION_SECRET=<random> in the
mam-api environment and restart.
2026-05-27 03:04:37 +00:00
opencode
65684aa577 fix(auth): ensure sessions table exists + log session.save errors
The redirect loop after successful login was almost certainly the
`sessions` table never being created. `schema.sql` defines it but
only runs on first-init via the postgres entrypoint; instances
bootstrapped via mam-api's own migration loop never got the table.
express-session's `req.session.save()` then failed silently and the
cookie pointed at a sid that wasn't in the store — every subsequent
request looked like a brand-new visitor.

  - New migration 021-ensure-sessions-table.sql (idempotent).
  - connect-pg-simple now configured with `createTableIfMissing: true`
    as belt-and-braces.
  - `POST /auth/login` now explicitly waits for session.save() and
    surfaces both regenerate() and save() errors instead of treating
    them as 'success'. Logs sid + req.secure + req.protocol so we can
    confirm trust-proxy is doing the right thing behind NPM.
2026-05-27 02:54:25 +00:00
opencode
cfcbec0c85 fix(auth): make AUTH_ENABLED=true workable end-to-end
Three concrete issues kept the login flow broken on dragonflight.live:

1. mam-api trusted no proxy headers, so behind nginx/Cloudflare the
   session cookie's `secure` flag and the rate-limiter's IP keying
   both saw the wrong values. Now sets `app.set('trust proxy', 1)`.

2. Session config was tied to NODE_ENV and lacked sameSite/name. Now:
   - SESSION_COOKIE_SECURE env (default: true when AUTH_ENABLED) so a
     site behind HTTPS gets Secure cookies regardless of NODE_ENV.
   - `sameSite: 'lax'` for predictable post-login redirects.
   - Renamed to `df.sid` so it's obvious in DevTools.
   - `rolling: true` extends the 7-day TTL on active use.
   - SESSION_SECRET is now required when AUTH_ENABLED=true; the
     server refuses to start with a dev default in prod.

3. login.html silently showed the sign-in panel even when no users
   exist or auth is off:
   - New GET /auth/setup-status reports {needs_setup, user_count,
     auth_enabled}.
   - login.html calls it on load and auto-flips into setup mode when
     needs_setup is true, or shows an explicit "auth is off" flash
     when auth_enabled is false (the previous symptom: logout button
     did nothing because /auth/me returned a synthetic admin no matter
     what).
   - Added a `.flash.info` style for the new neutral notice.

4. Sidebar logout used to call /auth/logout then `window.location
   .reload()`. With auth off that reload landed back on the synthetic-
   admin app and looked like nothing happened. It now redirects to
   /login.html in all states so the operator sees feedback (and the
   server-side messaging about auth being off) instead of a no-op.

Deploy notes for zampp1:
  - Set AUTH_ENABLED=true and a random SESSION_SECRET in the
    mam-api environment (e.g. /opt/wild-dragon/.env).
  - Restart mam-api.
  - First load of /login.html will auto-route to the setup form so
    you can create the first admin.
2026-05-27 02:47:09 +00:00
opencode
04ce096e67 chore: 1.2 ship-prep sweep — close 38 issues
Frontend / UX / a11y
- Sidebar collapse/expand toggle with localStorage persistence (#142)
- Settings sections wrap inputs in <form> with Enter-to-submit + native
  validation; password autocomplete=new-password (#141, #138)
- Asset thumbnails get descriptive alt text (#140)
- Production deploy now precompiles JSX via esbuild and loads the
  production React UMD instead of dev builds + in-browser Babel (#139,
  #122)
- Search wrapper gets role=search; global search input gets aria-label,
  role=combobox, aria-controls/aria-expanded/aria-activedescendant
  wiring (#137, #135)
- Dashboard and Library no longer share the same nav icon (#136)
- Sidebar collapses off-canvas with a topbar menu button below 768 px;
  mobile default is collapsed (#134)
- --text-3 bumped to #8B92A0 for WCAG AA contrast on --bg-0 (#133)
- Schedule and Library routes were rendering empty inside the .main
  flex container — switched to flex:1 + min-height:0 (#131, #132,
  editor + asset detail get the same fix)
- Jobs nav badge now polls /jobs?status=active every 10 s and reflects
  the live count (#130, #113)
- aria-label sweep on every icon-only button (#126)
- Premiere panel release list moved to window.PREMIERE_RELEASES in
  data.jsx; Editor + Settings read from the same source (#125)
- Typo setPgMclips → setPgmClips (#124)
- Stray console.error / console.warn calls gated behind
  window.DF_LOG.{warn,error} (#123)
- Hardcoded /api/v1 paths route through window.ZAMPP_API_PREFIX (#115)
- Schedule rows no longer crash on null recorder_id (#117)
- EditorKeyboard guards against document.activeElement === null (#116)
- Unmount-safe timers for PasswordResetModal, Containers, Editor (#111)
- Player seek clamps below totalMs, server-side range clamping +
  uncached 416 on EOF, client-side EOF-stall watchdog (#143)
- Duration badge overlap fix on narrow asset cards (#52)

Backend / security / reliability
- GET /recorders fixed N+1: single LATERAL JOIN for live_asset_id;
  Docker inspects bounded to actually-recording rows (#121)
- Upload disk-storage (multer.diskStorage) streams parts to S3 instead
  of buffering 500 MB in RAM (#120)
- /assets list clamps limit to MAX_LIMIT=500 to prevent OOM (#119)
- SDK upload archive listing + post-extract sanitize block zip-slip /
  tar-slip and symlink escapes (#118)
- Migrations track applied state in schema_migrations, run in a
  transaction, and exit non-zero on failure (#107)
- node-agent BMD_COUNT override uses BMD_DEVICE_PREFIX; filesystem
  detection wins (#109, #127)
- GPU_COUNT override now merges with nvidia-smi enrichment (#108)
- /cluster/heartbeat requires a node-bound token or admin user;
  tokens carry bound_hostname (#106)
- /recorders/:id/start error responses no longer echo the Docker
  create payload — env vars stay out of client responses (#105)
- /recorders/probe restricts schemes (srt/rtmp/rtsp/udp/rtp), blocks
  private + loopback hosts for non-admins, denies common service
  ports (#104)
- Scheduler tick guarded by a Postgres advisory lock; pending/running
  rows claimed via UPDATE...RETURNING + FOR UPDATE SKIP LOCKED to
  survive multi-node deploys (#103)
- UUID validateUuid('id') param middleware on every /:id route (#102)
- Error handler scrubs Postgres error messages and 5xx detail (#101)
- Graceful SIGTERM/SIGINT shutdown — stops scheduler, drains the HTTP
  server, ends the pool, 25 s force-exit watchdog (#100)
- AMPP sync moved from fire-and-forget to a persisted retry queue
  (ampp_sync_status / attempts / next_attempt_at + scheduler retry
  loop with exponential backoff) (#77)

Migrations
- 019: api_tokens.bound_hostname (#106)
- 020: assets.ampp_sync_status + retry bookkeeping (#77)

Other
- Defer #92 Growing-files per-upload toggle, #80 Audio tab, #57
  Dashboard redesign, #56 Editor SPA polish phase 3, #114 S3
  migration tool to v1.3
2026-05-27 02:06:14 +00:00
64d739b40d feat(admin): unified Storage settings page with mount/bucket health diagnostics
- Collapses S3 + Growing-files nav into single 'Storage' section
- Adds GET /api/v1/storage/overview with fs/df probes + HeadBucket check
- MountHealthStrip shows green/red pills, free space, S3 latency
- Reuses existing S3SettingsCard + GrowingSettingsCard below health strip
2026-05-26 22:45:50 +00: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
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
claude
90a9e4361a feat(comments): persistent frame-anchored comments on asset detail
- migration 010: asset_comments table (id, asset_id, user_id, body,
  frame_ms, resolved, timestamps) with index on asset_id+created_at
- new routes mounted at /api/v1/assets/:assetId/comments — GET/POST/
  PATCH/DELETE with author join (display_name + initials), nullable
  user_id so comments still attach when AUTH_ENABLED is off
- Asset detail loads comments from the API on mount instead of the
  empty ZAMPP_DATA.COMMENTS seed; addComment POSTs and merges the
  returned row; resolved-toggle and delete are wired
- CommentsList: new trash-icon delete action per comment, helpful
  empty-state copy ('Add one below to mark a frame'), tooltips on
  the timestamp and resolved buttons

Now editor comments survive page reload, are visible to other users
via the same API, and pin reliably to frame_ms (integer) instead of
a parsed HH:MM:SS:FF string.
2026-05-23 04:21:11 +00:00
claude
ef4c301149 feat(home,users): real metrics, working Users row actions + Groups CRUD
- Home: new /api/v1/metrics/home endpoint buckets last 24h of assets,
  jobs done/failed into hourly counts; sparklines now render real
  time-series instead of decorative sine waves
- Home stat cards are now clickable (route to relevant page) and the
  delta lines show real activity ("+N added in last 24h", "N completed")
- Home live-feed tiles use HlsPreview for recorders with a live_asset_id
- Users: row 3-dot menu is now a real popover with Rename / Reset
  password / Delete actions wired to PATCH /users/:id and DELETE
- Users: role is now an inline <select> that PATCHes immediately
- Users: Created column replaces fake 'last active' (no last_login
  tracking yet); group count is real
- Groups tab is now functional — list groups, create, expand to
  show + manage members (add/remove), delete; backed by existing
  /api/v1/groups CRUD
- Policies tab is now an honest 'coming soon' stub
- New icons: key, lock, edit; new .row-menu popover styles
2026-05-23 03:30:10 +00:00
claude
53196d38ce feat(scheduler): recorder scheduling — UI, CRUD, tick loop, recurrence
- New Ingest → Schedule page: upcoming/past/all tabs, status badges
  (pending / recording / completed / cancelled / failed), 10s
  auto-refresh, cancel/delete actions
- New Schedule modal: name, recorder dropdown, datetime-local start/end,
  recurrence (one-shot / daily / weekly), sensible defaults (+5min / +35min)
- Backend: migration 009 (recorder_schedules), routes/schedules.js
  (list/create/edit/cancel/delete), scheduler.js tick loop polling every
  15s; transitions trigger /recorders/:id/start and /stop via in-process
  HTTP so we reuse the full container orchestration path
- Recurring schedules: tick loop auto-queues the next occurrence on
  completion (daily = +24h, weekly = +7d)
- Sidebar + app.jsx route wired in, schedule-row table style added
2026-05-23 03:19:24 +00:00
claude
6398879b56 feat: SDK deployment UI, proxy encoding global settings, S3 env fallback
- Settings: drop AMPP tab, rename GPU/Transcoding → Proxy encoding
  with explicit 'applied to every ingested file' wording, expose
  CPU codec/preset options when GPU is off
- New Capture SDKs tab (Settings): upload Blackmagic / AJA / Deltacast
  SDK archives (.zip / .tar.gz) staged to /sdk/<vendor>/ inside mam-api;
  BMD is fully wired into the FFmpeg build pipeline, AJA + Deltacast
  staging-only pending FFmpeg patches
- mam-api: new /api/v1/sdk routes (multer upload, extract, list, delete);
  Dockerfile gets unzip+tar; docker-compose mounts /mnt/NVME/MAM/sdk:/sdk
- proxy worker now reads proxy-encoding settings from DB on every job,
  builds args for libx264 / NVENC / VAAPI, falls back to libx264 on
  hardware-encode failure
- settings GET /s3 falls back to S3_* env vars when DB is empty so the
  UI reflects what's actually wired (fixes 'not configured' false alarm)
2026-05-23 02:58:32 +00:00
4a3a672cbe cluster: stable hostname for mam-api, jq-based smoke test
mam-api self-heartbeat now reads NODE_HOSTNAME so primary rows survive container restarts instead of resurrecting with the random container ID. test-cluster.sh rewritten to use jq (the python f-strings had a parse bug that silently passed the IP check) and limited the docker-bridge alarm to 172.17.x since the user LAN occupies 172.18.0.0/16.
2026-05-21 11:50:52 +00:00
74299629e6 feat: detect GPUs via nvidia-smi and populate cluster_nodes capabilities 2026-05-20 17:25:11 -04:00
a4b9b5be82 fix: prefer NODE_IP env var in getLocalIp() for Docker deployments 2026-05-20 16:16:09 -04:00
7032cee6b3 feat: call loadS3ConfigFromDb() on startup after migrations 2026-05-20 15:53:26 -04:00
090452969c feat(api): register system + cluster routes; add self-heartbeat on startup 2026-05-19 23:50:19 -04:00
1f31d1037d merge: bring sequences/auth/admin backend + auth-guard frontend into fix/library-and-signal-indicator 2026-05-18 21:25:36 -04:00
7b8d8d4818 feat(api): register sequences route 2026-05-18 19:54:41 -04:00
28a4b24911 feat(mam-api): wire users, groups, and tokens routes into main app 2026-05-18 12:51:14 -04:00
7d76f9c549 feat(growing-files): Phase 1 - live HLS preview during recording
While a recorder is running, the capture container tees an HLS
stream into /live/<assetId>/ alongside the ProRes master upload.
The asset row is pre-created at recorder start with status='live'
so the clip appears in the library immediately. /api/v1/assets/:id/stream
returns the HLS playlist URL until recording stops, then proxy.

* docker-compose: shared wild-dragon-live mount on api/capture/web-ui
* migration 001-add-live-status: idempotent ALTER TYPE for asset_status
* mam-api: runMigrations() on boot; recorders.js pre-creates live asset
  + passes ASSET_ID; assets.js POST upserts on existing live row instead
  of inserting a duplicate, and stream route returns HLS for live assets
* capture: parallel HLS ffmpeg into /live/<assetId>/; ASSET_ID env
* web-ui: nginx serves /live/, preview.js loads hls.js, LIVE badge added
2026-05-18 07:29:50 -04:00
f745122ef0 fix(auth+bugs): optional auth bypass, login routes, conform column name, panel metadata fields, login page: index.js 2026-05-15 23:40:12 -04:00
4630a18dde feat: AMPP folder sync integration — pre-create folder hierarchy on upload, expose lookup endpoint for Script Task: index.js 2026-04-18 13:42:09 -04:00
53d4124514 Phase 2: services/mam-api/src/index.js 2026-04-07 22:05:39 -04:00
9a9259628a add services/mam-api/src/index.js 2026-04-07 21:58:25 -04:00