ui: full audit pass (fixes #146, #147, #148, #149, #151, #152, #153, #154, #155)
Sweep of 9 web-ui audit findings from tracker #156. Issue #150 (modal
codec stubs) deferred per user request.
## #146 sweep em-dashes (186 to 0)
- Replace placeholder '—' with '·' across all jsx
- Convert ' — ' to ': ' or '. ' in copy where context permits
- Comment-only em-dashes converted to ASCII dash
- Sweep css files too (16 comments)
## #147 remove glassmorphism + accent gradients
- Strip 8 backdrop-filter declarations from styles-screens.css and
styles-asset.css. Only legit modal scrim in styles-modal.css remains.
- Replace .job-progress-fill gradient with solid var(--accent)
- Replace .monitor-tile.audio gradient with flat var(--bg-1)
## #148 extract Jobs inline styles to CSS
- Cut 19 inline style={{...}} blocks in screens-jobs.jsx to 1 (dynamic
width on progress bar). Live DOM was 487 inline-styled elements due
to per-row repetition; now ~0.
- Added job-row-kind, job-row-asset, job-row-node, job-row-time,
job-row-actions, job-row-status-* utility classes in styles-screens.css
## #149 sidebar IA reorganized
- Replace flat NAV_TREE + ADMIN_TREE with NAV_SECTIONS:
Workspace / Ingest / Operations / Admin
- Move Capture out of Ingest into Operations (it's a live-signal monitor,
not an ingest action)
- Drop the 0/N capture badge from nav (belongs in topbar)
- Add BETA badge to Editor
## #151 redesign Editor 'Coming Soon' bumper
- Replace fullscreen glassmorphism + gradient + glow overlay with a flat
beta banner across the top of the editor area
- New .editor-beta-banner CSS class (flat, accent-soft tint, no blur)
## #152 hide Tokens parody, restore real API token mgmt
- New top-level Tokens admin page wraps existing ApiTokensSection
- Old parody renamed to TokensParody, accessible at /tokens-parody route
- Add window-level df:nav event for cross-component routing
## #153 make Home actually useful
- New activity strip below the launcher grid: 'Recording now' tiles for
live recorders, 'Last 24 hours' tiles for newly created assets, plus
an attention strip when there are failed jobs or errored recorders
- Each item is clickable and routes to the relevant screen
## #154 aria-labels on icon-only buttons
- Projects + Library grid/list view toggles now have aria-label + title
## #155 page-header pattern
- Dashboard now renders a proper .page-header h1 with subtitle + alert
badge + cluster status pip
- Library toolbar-title promoted to h1 for screen-reader hierarchy
- Document Home/Library/Editor full-bleed exceptions in DESIGN.md
- Editor's chrome is the beta banner (covered by #151)
2026-05-28 19:50:07 -04:00
|
|
|
// app.jsx - main shell
|
2026-05-22 08:13:03 -04:00
|
|
|
|
2026-05-22 10:02:55 -04:00
|
|
|
const ACCENT = '#5B7CFA';
|
2026-05-22 08:13:03 -04:00
|
|
|
|
|
|
|
|
function App() {
|
2026-05-22 10:02:55 -04:00
|
|
|
const [route, setRoute] = React.useState('home');
|
2026-05-22 08:13:03 -04:00
|
|
|
const [openAsset, setOpenAsset] = React.useState(null);
|
|
|
|
|
const [openProject, setOpenProject] = React.useState(null);
|
|
|
|
|
const [showNewRecorder, setShowNewRecorder] = React.useState(false);
|
2026-05-22 10:02:55 -04:00
|
|
|
const [dataReady, setDataReady] = React.useState(false);
|
2026-05-25 17:42:39 -04:00
|
|
|
const [loadError, setLoadError] = React.useState(null);
|
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-26 22:06:14 -04:00
|
|
|
const [sidebarCollapsed, setSidebarCollapsed] = React.useState(() => {
|
|
|
|
|
try {
|
|
|
|
|
const stored = localStorage.getItem('df.sidebar.collapsed');
|
|
|
|
|
if (stored != null) return stored === '1';
|
|
|
|
|
// Default: collapsed on mobile, expanded on desktop.
|
|
|
|
|
return typeof window !== 'undefined' && window.matchMedia && window.matchMedia('(max-width: 768px)').matches;
|
|
|
|
|
} catch { return false; }
|
|
|
|
|
});
|
|
|
|
|
const toggleSidebar = React.useCallback(() => {
|
|
|
|
|
setSidebarCollapsed(prev => {
|
|
|
|
|
const next = !prev;
|
|
|
|
|
try { localStorage.setItem('df.sidebar.collapsed', next ? '1' : '0'); } catch {}
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
}, []);
|
2026-05-22 08:13:03 -04:00
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
2026-05-22 10:02:55 -04:00
|
|
|
document.documentElement.style.setProperty('--accent', ACCENT);
|
|
|
|
|
document.documentElement.style.setProperty('--accent-soft', hexToRgba(ACCENT, 0.14));
|
|
|
|
|
document.documentElement.style.setProperty('--accent-soft-2', hexToRgba(ACCENT, 0.22));
|
|
|
|
|
document.documentElement.style.setProperty('--accent-text', lighten(ACCENT, 0.25));
|
|
|
|
|
document.documentElement.style.setProperty('--accent-hover', lighten(ACCENT, 0.08));
|
|
|
|
|
}, []);
|
2026-05-22 08:13:03 -04:00
|
|
|
|
2026-05-22 10:02:55 -04:00
|
|
|
React.useEffect(() => {
|
|
|
|
|
window.ZAMPP_API.loadData()
|
|
|
|
|
.then(() => setDataReady(true))
|
2026-05-25 17:42:39 -04:00
|
|
|
.catch(err => { console.error('[Dragonflight] load failed:', err); setLoadError(err.message || 'Failed to load'); setDataReady(true); });
|
2026-05-22 10:02:55 -04:00
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const navigate = (id) => { setOpenAsset(null); setRoute(id); };
|
ui: full audit pass (fixes #146, #147, #148, #149, #151, #152, #153, #154, #155)
Sweep of 9 web-ui audit findings from tracker #156. Issue #150 (modal
codec stubs) deferred per user request.
## #146 sweep em-dashes (186 to 0)
- Replace placeholder '—' with '·' across all jsx
- Convert ' — ' to ': ' or '. ' in copy where context permits
- Comment-only em-dashes converted to ASCII dash
- Sweep css files too (16 comments)
## #147 remove glassmorphism + accent gradients
- Strip 8 backdrop-filter declarations from styles-screens.css and
styles-asset.css. Only legit modal scrim in styles-modal.css remains.
- Replace .job-progress-fill gradient with solid var(--accent)
- Replace .monitor-tile.audio gradient with flat var(--bg-1)
## #148 extract Jobs inline styles to CSS
- Cut 19 inline style={{...}} blocks in screens-jobs.jsx to 1 (dynamic
width on progress bar). Live DOM was 487 inline-styled elements due
to per-row repetition; now ~0.
- Added job-row-kind, job-row-asset, job-row-node, job-row-time,
job-row-actions, job-row-status-* utility classes in styles-screens.css
## #149 sidebar IA reorganized
- Replace flat NAV_TREE + ADMIN_TREE with NAV_SECTIONS:
Workspace / Ingest / Operations / Admin
- Move Capture out of Ingest into Operations (it's a live-signal monitor,
not an ingest action)
- Drop the 0/N capture badge from nav (belongs in topbar)
- Add BETA badge to Editor
## #151 redesign Editor 'Coming Soon' bumper
- Replace fullscreen glassmorphism + gradient + glow overlay with a flat
beta banner across the top of the editor area
- New .editor-beta-banner CSS class (flat, accent-soft tint, no blur)
## #152 hide Tokens parody, restore real API token mgmt
- New top-level Tokens admin page wraps existing ApiTokensSection
- Old parody renamed to TokensParody, accessible at /tokens-parody route
- Add window-level df:nav event for cross-component routing
## #153 make Home actually useful
- New activity strip below the launcher grid: 'Recording now' tiles for
live recorders, 'Last 24 hours' tiles for newly created assets, plus
an attention strip when there are failed jobs or errored recorders
- Each item is clickable and routes to the relevant screen
## #154 aria-labels on icon-only buttons
- Projects + Library grid/list view toggles now have aria-label + title
## #155 page-header pattern
- Dashboard now renders a proper .page-header h1 with subtitle + alert
badge + cluster status pip
- Library toolbar-title promoted to h1 for screen-reader hierarchy
- Document Home/Library/Editor full-bleed exceptions in DESIGN.md
- Editor's chrome is the beta banner (covered by #151)
2026-05-28 19:50:07 -04:00
|
|
|
|
|
|
|
|
// Window-level nav event so deeply nested components (like the Tokens
|
|
|
|
|
// "see the parody" link) can route without prop drilling.
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
const handler = (e) => { if (e && e.detail) navigate(e.detail); };
|
|
|
|
|
window.addEventListener('df:nav', handler);
|
|
|
|
|
return () => window.removeEventListener('df:nav', handler);
|
|
|
|
|
}, []);
|
2026-05-22 23:53:19 -04:00
|
|
|
const openProjectFromAnywhere = (p) => { setOpenAsset(null); setOpenProject(p); setRoute('library'); };
|
2026-05-22 08:13:03 -04:00
|
|
|
|
|
|
|
|
const crumbs = React.useMemo(() => {
|
|
|
|
|
if (openAsset) return [
|
2026-05-22 10:02:55 -04:00
|
|
|
{ label: 'Library', to: 'library' },
|
|
|
|
|
{ label: openAsset.project || 'Library', to: 'library' },
|
2026-05-22 08:13:03 -04:00
|
|
|
{ label: openAsset.name },
|
|
|
|
|
];
|
|
|
|
|
if (openProject) return [
|
2026-05-22 10:02:55 -04:00
|
|
|
{ label: 'Projects', to: 'projects' },
|
2026-05-22 08:13:03 -04:00
|
|
|
{ label: openProject.name },
|
|
|
|
|
];
|
|
|
|
|
const labels = {
|
2026-05-23 10:53:31 -04:00
|
|
|
home: ['Home'], dashboard: ['Dashboard'],
|
2026-05-23 10:48:42 -04:00
|
|
|
library: ['Library'], projects: ['Projects'],
|
2026-05-22 10:02:55 -04:00
|
|
|
upload: ['Ingest', 'Upload'], recorders: ['Ingest', 'Recorders'],
|
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-22 23:19:24 -04:00
|
|
|
schedule: ['Ingest', 'Schedule'],
|
2026-05-23 16:05:41 -04:00
|
|
|
youtube: ['Ingest', 'YouTube'],
|
2026-05-22 10:02:55 -04:00
|
|
|
capture: ['Ingest', 'Capture'], monitors: ['Ingest', 'Monitors'],
|
feat(web-ui): MCR page — channels, playlist, transport, preview
screens-playout.jsx + styles-playout.css: program monitor (HLS preview from
the sidecar), media bin, drag-drop playlist editor, transport controls. Plain
HTML5 drag-drop, no extra library. Talks to /api/v1/playout via
ZAMPP_API.fetch.
Wired into the shell: "Playout" under Operations, breadcrumb mapping, route
case in app.jsx, stylesheet + dist/screens-playout.js script in index.html.
Format dropdown defaults to 1080p5994 (matches the new channel default).
2026-05-30 09:18:08 -04:00
|
|
|
jobs: ['Jobs'], editor: ['Editor'], playout: ['Operations', 'Playout'],
|
2026-05-22 10:02:55 -04:00
|
|
|
users: ['Admin', 'Users & Groups'], tokens: ['Admin', 'Tokens'],
|
|
|
|
|
containers: ['Admin', 'Containers'], cluster: ['Admin', 'Cluster'],
|
|
|
|
|
settings: ['Admin', 'Settings'],
|
2026-05-22 08:13:03 -04:00
|
|
|
};
|
2026-05-22 10:02:55 -04:00
|
|
|
return (labels[route] || ['Home']).map(label => ({ label }));
|
2026-05-22 08:13:03 -04:00
|
|
|
}, [route, openAsset, openProject]);
|
|
|
|
|
|
2026-05-22 10:02:55 -04:00
|
|
|
if (!dataReady) {
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<style>{'@keyframes _df_spin{to{transform:rotate(360deg)}}'}</style>
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100vh', flexDirection: 'column', gap: 14, background: 'var(--bg-0)' }}>
|
|
|
|
|
<div style={{ width: 28, height: 28, borderRadius: '50%', border: '2px solid var(--border)', borderTopColor: ACCENT, animation: '_df_spin 0.8s linear infinite' }} />
|
|
|
|
|
<div style={{ fontSize: 12, color: 'var(--text-3)', fontFamily: 'var(--font-mono)' }}>Loading Dragonflight…</div>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 17:42:39 -04:00
|
|
|
if (loadError) {
|
|
|
|
|
return (
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100vh', flexDirection: 'column', gap: 12, background: 'var(--bg-0)' }}>
|
|
|
|
|
<div style={{ fontSize: 14, fontWeight: 600, color: 'var(--danger)' }}>Failed to load</div>
|
|
|
|
|
<div style={{ fontSize: 12, color: 'var(--text-3)', maxWidth: 360, textAlign: 'center' }}>{loadError}</div>
|
|
|
|
|
<button className="btn primary sm" onClick={() => window.location.reload()}>Retry</button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
feat(mam-api,web-ui): per-project RBAC (v2 auth layer)
Adds per-project access control on top of the flat v1 auth. admin keeps
global access; editor/viewer are scoped to projects granted to them (direct
or via group) at view (read-only) or edit (read-write) level.
- migration 026: project_access table + access_level enum
- src/auth/authz.js: central isAdmin/accessibleProjectIds/projectLevel/
assertProjectAccess
- requireAdmin middleware; admin-gate /users, /auth/users, /groups
- enforce scoping on projects, assets, bins (list filter + per-resource
view/edit + create checks); gate bulk asset maintenance + batch-trim
- grant API: GET/POST/DELETE /projects/:id/access
- web-ui: hide admin nav for non-admins, admin-route bounce, project
"Manage access" modal, rewrite Policies tab
- tests: authz, project-access, assets-access (node:test, skip w/o DB)
- deferred routers carry TODO(authz) markers; .env.example documents the
service-token-needs-admin/grants requirement
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 22:37:36 -04:00
|
|
|
// Admin-only destinations. Non-admins who reach one (deep link, keyboard
|
|
|
|
|
// router, stale tab) get bounced home instead of a broken/forbidden page.
|
|
|
|
|
// The API enforces the same rules — this is just UX.
|
|
|
|
|
const ADMIN_ROUTES = new Set(['users', 'containers', 'cluster', 'settings']);
|
|
|
|
|
const isAdmin = window.ZAMPP_DATA?.ME?.role === 'admin';
|
|
|
|
|
const effectiveRoute = (ADMIN_ROUTES.has(route) && !isAdmin) ? 'home' : route;
|
|
|
|
|
|
2026-05-22 08:13:03 -04:00
|
|
|
let content;
|
|
|
|
|
if (openAsset) {
|
|
|
|
|
content = <AssetDetail asset={openAsset} onClose={() => setOpenAsset(null)} />;
|
|
|
|
|
} else {
|
feat(mam-api,web-ui): per-project RBAC (v2 auth layer)
Adds per-project access control on top of the flat v1 auth. admin keeps
global access; editor/viewer are scoped to projects granted to them (direct
or via group) at view (read-only) or edit (read-write) level.
- migration 026: project_access table + access_level enum
- src/auth/authz.js: central isAdmin/accessibleProjectIds/projectLevel/
assertProjectAccess
- requireAdmin middleware; admin-gate /users, /auth/users, /groups
- enforce scoping on projects, assets, bins (list filter + per-resource
view/edit + create checks); gate bulk asset maintenance + batch-trim
- grant API: GET/POST/DELETE /projects/:id/access
- web-ui: hide admin nav for non-admins, admin-route bounce, project
"Manage access" modal, rewrite Policies tab
- tests: authz, project-access, assets-access (node:test, skip w/o DB)
- deferred routers carry TODO(authz) markers; .env.example documents the
service-token-needs-admin/grants requirement
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 22:37:36 -04:00
|
|
|
switch (effectiveRoute) {
|
2026-05-22 10:02:55 -04:00
|
|
|
case 'home': content = <Home navigate={navigate} />; break;
|
2026-05-23 10:48:42 -04:00
|
|
|
case 'dashboard': content = <Dashboard navigate={navigate} />; break;
|
2026-05-24 14:20:00 -04:00
|
|
|
case 'library': content = <Library navigate={navigate} onOpenAsset={setOpenAsset} openProject={openProject} onClearProject={() => setOpenProject(null)} />; break;
|
2026-05-22 23:53:19 -04:00
|
|
|
case 'projects': content = <Projects navigate={navigate} onOpenProject={openProjectFromAnywhere} />; break;
|
2026-05-22 10:02:55 -04:00
|
|
|
case 'upload': content = <Upload navigate={navigate} />; break;
|
|
|
|
|
case 'recorders': content = <Recorders navigate={navigate} onNew={() => setShowNewRecorder(true)} />; break;
|
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-22 23:19:24 -04:00
|
|
|
case 'schedule': content = <Schedule navigate={navigate} />; break;
|
2026-05-23 16:05:41 -04:00
|
|
|
case 'youtube': content = <YouTubeImport navigate={navigate} />; break;
|
2026-05-22 10:02:55 -04:00
|
|
|
case 'capture': content = <Capture navigate={navigate} />; break;
|
|
|
|
|
case 'monitors': content = <Monitors navigate={navigate} />; break;
|
|
|
|
|
case 'jobs': content = <Jobs navigate={navigate} />; break;
|
feat(web-ui): MCR page — channels, playlist, transport, preview
screens-playout.jsx + styles-playout.css: program monitor (HLS preview from
the sidecar), media bin, drag-drop playlist editor, transport controls. Plain
HTML5 drag-drop, no extra library. Talks to /api/v1/playout via
ZAMPP_API.fetch.
Wired into the shell: "Playout" under Operations, breadcrumb mapping, route
case in app.jsx, stylesheet + dist/screens-playout.js script in index.html.
Format dropdown defaults to 1080p5994 (matches the new channel default).
2026-05-30 09:18:08 -04:00
|
|
|
case 'playout': content = <Playout navigate={navigate} />; break;
|
2026-05-22 10:02:55 -04:00
|
|
|
case 'users': content = <Users />; break;
|
|
|
|
|
case 'tokens': content = <Tokens />; break;
|
2026-05-28 20:01:19 -04:00
|
|
|
case 'billing': content = <TokensParody />; break;
|
2026-05-22 10:02:55 -04:00
|
|
|
case 'containers':content = <Containers />; break;
|
|
|
|
|
case 'cluster': content = <Cluster />; break;
|
|
|
|
|
case 'settings': content = <Settings />; break;
|
|
|
|
|
default: content = <Home navigate={navigate} />;
|
2026-05-22 08:13:03 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
ui: full audit pass (fixes #146, #147, #148, #149, #151, #152, #153, #154, #155)
Sweep of 9 web-ui audit findings from tracker #156. Issue #150 (modal
codec stubs) deferred per user request.
## #146 sweep em-dashes (186 to 0)
- Replace placeholder '—' with '·' across all jsx
- Convert ' — ' to ': ' or '. ' in copy where context permits
- Comment-only em-dashes converted to ASCII dash
- Sweep css files too (16 comments)
## #147 remove glassmorphism + accent gradients
- Strip 8 backdrop-filter declarations from styles-screens.css and
styles-asset.css. Only legit modal scrim in styles-modal.css remains.
- Replace .job-progress-fill gradient with solid var(--accent)
- Replace .monitor-tile.audio gradient with flat var(--bg-1)
## #148 extract Jobs inline styles to CSS
- Cut 19 inline style={{...}} blocks in screens-jobs.jsx to 1 (dynamic
width on progress bar). Live DOM was 487 inline-styled elements due
to per-row repetition; now ~0.
- Added job-row-kind, job-row-asset, job-row-node, job-row-time,
job-row-actions, job-row-status-* utility classes in styles-screens.css
## #149 sidebar IA reorganized
- Replace flat NAV_TREE + ADMIN_TREE with NAV_SECTIONS:
Workspace / Ingest / Operations / Admin
- Move Capture out of Ingest into Operations (it's a live-signal monitor,
not an ingest action)
- Drop the 0/N capture badge from nav (belongs in topbar)
- Add BETA badge to Editor
## #151 redesign Editor 'Coming Soon' bumper
- Replace fullscreen glassmorphism + gradient + glow overlay with a flat
beta banner across the top of the editor area
- New .editor-beta-banner CSS class (flat, accent-soft tint, no blur)
## #152 hide Tokens parody, restore real API token mgmt
- New top-level Tokens admin page wraps existing ApiTokensSection
- Old parody renamed to TokensParody, accessible at /tokens-parody route
- Add window-level df:nav event for cross-component routing
## #153 make Home actually useful
- New activity strip below the launcher grid: 'Recording now' tiles for
live recorders, 'Last 24 hours' tiles for newly created assets, plus
an attention strip when there are failed jobs or errored recorders
- Each item is clickable and routes to the relevant screen
## #154 aria-labels on icon-only buttons
- Projects + Library grid/list view toggles now have aria-label + title
## #155 page-header pattern
- Dashboard now renders a proper .page-header h1 with subtitle + alert
badge + cluster status pip
- Library toolbar-title promoted to h1 for screen-reader hierarchy
- Document Home/Library/Editor full-bleed exceptions in DESIGN.md
- Editor's chrome is the beta banner (covered by #151)
2026-05-28 19:50:07 -04:00
|
|
|
// Home (launcher) suppresses the topbar - it's a full-bleed landing page.
|
2026-05-23 10:53:31 -04:00
|
|
|
const hideTopbar = !openAsset && route === 'home';
|
|
|
|
|
|
2026-05-22 08:13:03 -04:00
|
|
|
return (
|
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-26 22:06:14 -04:00
|
|
|
<div className="app" data-density="comfortable" data-grid-size="md" data-sidebar={sidebarCollapsed ? 'collapsed' : 'expanded'}>
|
|
|
|
|
<Sidebar active={openAsset ? 'library' : route} onNavigate={navigate} me={window.ZAMPP_DATA?.ME} collapsed={sidebarCollapsed} onToggle={toggleSidebar} />
|
2026-05-22 08:13:03 -04:00
|
|
|
<div className="main">
|
2026-05-23 10:53:31 -04:00
|
|
|
{!openAsset && !hideTopbar && (
|
2026-05-22 23:53:19 -04:00
|
|
|
<Topbar
|
|
|
|
|
crumbs={crumbs}
|
|
|
|
|
onNavigate={navigate}
|
|
|
|
|
onOpenAsset={setOpenAsset}
|
|
|
|
|
onOpenProject={openProjectFromAnywhere}
|
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-26 22:06:14 -04:00
|
|
|
onToggleSidebar={toggleSidebar}
|
2026-05-22 23:53:19 -04:00
|
|
|
/>
|
|
|
|
|
)}
|
2026-05-22 08:13:03 -04:00
|
|
|
{content}
|
|
|
|
|
</div>
|
|
|
|
|
{showNewRecorder && <NewRecorderModal open={showNewRecorder} onClose={() => setShowNewRecorder(false)} />}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hexToRgba(hex, a) {
|
2026-05-22 10:02:55 -04:00
|
|
|
const h = hex.replace('#', '');
|
2026-05-22 08:13:03 -04:00
|
|
|
const r = parseInt(h.slice(0, 2), 16);
|
|
|
|
|
const g = parseInt(h.slice(2, 4), 16);
|
|
|
|
|
const b = parseInt(h.slice(4, 6), 16);
|
2026-05-22 10:02:55 -04:00
|
|
|
return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';
|
2026-05-22 08:13:03 -04:00
|
|
|
}
|
|
|
|
|
function lighten(hex, amt) {
|
2026-05-22 10:02:55 -04:00
|
|
|
const h = hex.replace('#', '');
|
2026-05-22 08:13:03 -04:00
|
|
|
const r = Math.min(255, parseInt(h.slice(0, 2), 16) + Math.round(amt * 255));
|
|
|
|
|
const g = Math.min(255, parseInt(h.slice(2, 4), 16) + Math.round(amt * 255));
|
|
|
|
|
const b = Math.min(255, parseInt(h.slice(4, 6), 16) + Math.round(amt * 255));
|
2026-05-22 10:02:55 -04:00
|
|
|
return 'rgb(' + r + ',' + g + ',' + b + ')';
|
2026-05-22 08:13:03 -04:00
|
|
|
}
|
|
|
|
|
|
2026-05-22 10:02:55 -04:00
|
|
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
2026-05-27 15:08:14 -04:00
|
|
|
const AuthGate = window.AuthGateComponent;
|
|
|
|
|
root.render(<AuthGate><App /></AuthGate>);
|