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
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>
- 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>
Projects:
- Per-row 3-dot menu in list view: Open / Rename / Delete (PATCH + DELETE)
- ProjectCard's bottom bar now shows real ready/in-flight/error counts
for the project's assets instead of fake 70/20 segments
- After mutations, project list refreshes from /projects + recomputes
asset counts client-side
Bins:
- GET /api/v1/bins now returns every bin across every project when
no project_id is supplied; result rows include project_name + asset_count
- Asset right-click 'Move to bin' filters to bins in the same project as
the asset and surfaces project_name as a tooltip
Jobs:
- 'Retry all failed' button in the header appears when there are
failed jobs and POSTs /retry for each one in parallel
- Failed-row error message now clips with title= tooltip so 3KB
ffmpeg stderr doesn't blow out the row layout
window.PROJECT_COLORS exposed for cross-screen access.
Proxy failures ("moov atom not found"):
- root cause: failed/aborted SRT/RTMP recordings still uploaded 0-byte
(or ftyp-only ~1KB) objects to S3, which ffmpeg can't probe
- worker proxy.js now bails on inputs < 4 KiB with a clear message
before handing the file to ffmpeg
- capture-manager.stop() returns framesReceived + empty flag
- capture shutdown handler skips POST /assets entirely on empty
sessions, instead calls new POST /assets/:id/mark-empty to flip
the pre-created live asset to 'error' with a note
Library asset right-click menu:
- new AssetContextMenu component on screens-library.jsx; right-click
any asset in grid or list view to open
- actions: Open, Rename, Move to bin (lists up to 10 bins), Remove
from bin, Copy asset ID, Delete permanently (hard=true)
- viewport-aware positioning (won't clip past window edges)
- dismisses on outside click / contextmenu / scroll
- Library now refreshes via /assets after mutations; normalizeAsset
exposed on window so the re-fetch shape matches boot
- ctx-menu styles in styles-rest.css