2026-05-22 10:04:25 -04:00
|
|
|
// screens-projects.jsx
|
2026-05-22 08:17:06 -04:00
|
|
|
|
2026-05-22 12:17:54 -04:00
|
|
|
function NewProjectModal({ onClose, onCreated }) {
|
|
|
|
|
const [name, setName] = React.useState('');
|
|
|
|
|
const [saving, setSaving] = React.useState(false);
|
|
|
|
|
const [err, setErr] = React.useState(null);
|
|
|
|
|
|
|
|
|
|
const create = () => {
|
|
|
|
|
if (!name.trim()) { setErr('Name is required'); return; }
|
|
|
|
|
setSaving(true); setErr(null);
|
|
|
|
|
window.ZAMPP_API.fetch('/projects', { method: 'POST', body: JSON.stringify({ name: name.trim() }) })
|
|
|
|
|
.then(p => { onCreated(p); onClose(); })
|
|
|
|
|
.catch(e => { setSaving(false); setErr(e.message || 'Failed to create project'); });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="modal-backdrop" onClick={onClose}>
|
|
|
|
|
<div className="modal" style={{ width: 420 }} onClick={e => e.stopPropagation()}>
|
|
|
|
|
<div className="modal-head">
|
|
|
|
|
<div style={{ fontSize: 15, fontWeight: 600 }}>New project</div>
|
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
|
|
|
<button className="icon-btn" aria-label="Close" onClick={onClose}><Icon name="x" /></button>
|
2026-05-22 12:17:54 -04:00
|
|
|
</div>
|
|
|
|
|
<div className="modal-body">
|
|
|
|
|
<div className="field">
|
|
|
|
|
<label className="field-label">Project name</label>
|
|
|
|
|
<input className="field-input" value={name} onChange={e => setName(e.target.value)}
|
|
|
|
|
placeholder="e.g. Sunday Night Game" autoFocus
|
|
|
|
|
onKeyDown={e => e.key === 'Enter' && !saving && create()} />
|
|
|
|
|
</div>
|
|
|
|
|
{err && <div style={{ fontSize: 12, color: 'var(--danger)', marginTop: 4 }}>{err}</div>}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="modal-foot">
|
|
|
|
|
<button className="btn ghost" onClick={onClose}>Cancel</button>
|
|
|
|
|
<span style={{ flex: 1 }} />
|
|
|
|
|
<button className="btn primary" onClick={create} disabled={saving}>
|
|
|
|
|
{saving ? 'Creating…' : 'Create project'}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-22 08:17:06 -04:00
|
|
|
function Projects({ onOpenProject, navigate }) {
|
2026-05-23 00:17:36 -04:00
|
|
|
const [projects, setProjects] = React.useState(window.ZAMPP_DATA?.PROJECTS || []);
|
|
|
|
|
const ASSETS = window.ZAMPP_DATA?.ASSETS || [];
|
2026-05-22 10:04:25 -04:00
|
|
|
const [search, setSearch] = React.useState('');
|
|
|
|
|
const [view, setView] = React.useState('grid');
|
2026-05-22 12:17:54 -04:00
|
|
|
const [showNew, setShowNew] = React.useState(false);
|
2026-05-23 00:08:59 -04:00
|
|
|
const [menuFor, setMenuFor] = React.useState(null);
|
2026-05-23 09:02:23 -04:00
|
|
|
const [renamingProject, setRenamingProject] = React.useState(null);
|
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
|
|
|
const [accessProject, setAccessProject] = React.useState(null);
|
|
|
|
|
const isAdmin = window.ZAMPP_DATA?.ME?.role === 'admin';
|
|
|
|
|
const manageAccess = (p) => { setMenuFor(null); setAccessProject(p); };
|
2026-05-22 12:17:54 -04:00
|
|
|
|
2026-05-23 00:08:59 -04:00
|
|
|
const refresh = React.useCallback(() => {
|
|
|
|
|
window.ZAMPP_API.fetch('/projects')
|
|
|
|
|
.then(list => {
|
|
|
|
|
const updated = (list || []).map((p, i) => ({
|
|
|
|
|
...p,
|
|
|
|
|
color: (window.ZAMPP_DATA.PROJECTS.find(x => x.id === p.id) || {}).color
|
2026-05-24 14:20:00 -04:00
|
|
|
|| (window.PROJECT_COLORS ? window.PROJECT_COLORS[p.id.charCodeAt(p.id.length - 1) % window.PROJECT_COLORS.length] : null)
|
2026-05-23 00:08:59 -04:00
|
|
|
|| 'var(--accent)',
|
|
|
|
|
assets: (ASSETS || []).filter(a => a.project_id === p.id).length,
|
|
|
|
|
updated: window.ZAMPP_API.fmtRelative(p.updated_at),
|
|
|
|
|
}));
|
|
|
|
|
window.ZAMPP_DATA.PROJECTS = updated;
|
|
|
|
|
setProjects(updated);
|
|
|
|
|
})
|
|
|
|
|
.catch(() => {});
|
|
|
|
|
}, [ASSETS]);
|
|
|
|
|
|
|
|
|
|
const onCreated = (p) => { refresh(); };
|
|
|
|
|
|
2026-05-23 09:02:23 -04:00
|
|
|
const renameProject = (p) => { setMenuFor(null); setRenamingProject(p); };
|
2026-05-23 00:08:59 -04:00
|
|
|
|
|
|
|
|
const deleteProject = (p) => {
|
|
|
|
|
setMenuFor(null);
|
|
|
|
|
if (!confirm('Delete project "' + p.name + '"?\nThis fails if there are still assets attached.')) return;
|
|
|
|
|
window.ZAMPP_API.fetch('/projects/' + p.id, { method: 'DELETE' })
|
|
|
|
|
.then(refresh)
|
|
|
|
|
.catch(e => alert('Delete failed: ' + e.message));
|
2026-05-22 12:17:54 -04:00
|
|
|
};
|
|
|
|
|
|
2026-05-23 00:08:59 -04:00
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (!menuFor) return;
|
|
|
|
|
const close = () => setMenuFor(null);
|
|
|
|
|
window.addEventListener('click', close);
|
|
|
|
|
return () => window.removeEventListener('click', close);
|
|
|
|
|
}, [menuFor]);
|
|
|
|
|
|
2026-05-22 12:17:54 -04:00
|
|
|
let filtered = projects;
|
|
|
|
|
if (search) filtered = filtered.filter(p => p.name.toLowerCase().includes(search.toLowerCase()));
|
2026-05-22 08:17:06 -04:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="page">
|
|
|
|
|
<div className="page-header">
|
|
|
|
|
<h1>Projects</h1>
|
2026-05-22 12:17:54 -04:00
|
|
|
<span className="subtitle">{filtered.length} projects</span>
|
2026-05-22 08:17:06 -04:00
|
|
|
<div className="spacer" />
|
|
|
|
|
<div className="search" style={{ width: 240 }}>
|
|
|
|
|
<Icon name="search" className="search-icon" />
|
|
|
|
|
<input value={search} onChange={e => setSearch(e.target.value)} placeholder="Search projects…" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="tab-group">
|
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
|
|
|
<button className={view === 'grid' ? 'active' : ''} onClick={() => setView('grid')} aria-label="Grid view" title="Grid view"><Icon name="grid" size={12} /></button>
|
|
|
|
|
<button className={view === 'list' ? 'active' : ''} onClick={() => setView('list')} aria-label="List view" title="List view"><Icon name="list" size={12} /></button>
|
2026-05-22 08:17:06 -04:00
|
|
|
</div>
|
2026-05-22 12:17:54 -04:00
|
|
|
<button className="btn primary" onClick={() => setShowNew(true)}><Icon name="plus" />New project</button>
|
2026-05-22 08:17:06 -04:00
|
|
|
</div>
|
|
|
|
|
<div className="page-body">
|
2026-05-22 12:17:54 -04:00
|
|
|
{filtered.length === 0 ? (
|
|
|
|
|
<div style={{ padding: 40, textAlign: 'center', color: 'var(--text-3)' }}>
|
|
|
|
|
{search ? 'No matching projects.' : 'No projects yet.'}
|
|
|
|
|
{!search && (
|
|
|
|
|
<div style={{ marginTop: 12 }}>
|
|
|
|
|
<button className="btn primary" onClick={() => setShowNew(true)}><Icon name="plus" />New project</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2026-05-22 10:04:25 -04:00
|
|
|
) : view === 'grid' ? (
|
2026-05-22 08:17:06 -04:00
|
|
|
<div className="projects-grid">
|
2026-05-26 10:10:44 -04:00
|
|
|
{filtered.map(p => (
|
|
|
|
|
<ProjectCard
|
|
|
|
|
key={p.id}
|
|
|
|
|
project={p}
|
|
|
|
|
assets={ASSETS}
|
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
|
|
|
canManageAccess={isAdmin}
|
2026-05-26 10:10:44 -04:00
|
|
|
onOpen={() => onOpenProject(p)}
|
|
|
|
|
onRename={() => renameProject(p)}
|
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
|
|
|
onManageAccess={() => manageAccess(p)}
|
2026-05-26 10:10:44 -04:00
|
|
|
onDelete={() => deleteProject(p)}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
2026-05-22 08:17:06 -04:00
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="panel">
|
2026-05-22 10:04:25 -04:00
|
|
|
<div className="list-row head" style={{ padding: '12px 16px', gridTemplateColumns: '1fr 100px 120px 120px 80px' }}>
|
|
|
|
|
<div>Project</div><div>Assets</div><div>Storage</div><div>Updated</div><div></div>
|
2026-05-22 08:17:06 -04:00
|
|
|
</div>
|
2026-05-22 12:17:54 -04:00
|
|
|
{filtered.map(p => (
|
2026-05-22 10:04:25 -04:00
|
|
|
<div key={p.id} className="list-row" style={{ padding: '12px 16px', gridTemplateColumns: '1fr 100px 120px 120px 80px', borderBottom: '1px solid var(--border)', cursor: 'pointer' }} onClick={() => onOpenProject(p)}>
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
2026-05-22 12:17:54 -04:00
|
|
|
<div style={{ width: 8, height: 32, borderRadius: 2, background: p.color || 'var(--accent)' }} />
|
2026-05-22 10:04:25 -04:00
|
|
|
<div>{p.name}</div>
|
2026-05-22 08:17:06 -04:00
|
|
|
</div>
|
2026-05-22 12:17:54 -04:00
|
|
|
<div className="col-sub">{p.assets || 0}</div>
|
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
|
|
|
<div className="col-sub">·</div>
|
|
|
|
|
<div className="col-sub">{p.updated || '·'}</div>
|
2026-05-23 00:08:59 -04:00
|
|
|
<div style={{ position: 'relative' }} onClick={e => e.stopPropagation()}>
|
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
|
|
|
<button className="icon-btn" aria-label="Project actions" onClick={e => { e.stopPropagation(); setMenuFor(menuFor === p.id ? null : p.id); }}><Icon name="more" /></button>
|
2026-05-23 00:08:59 -04:00
|
|
|
{menuFor === p.id && (
|
|
|
|
|
<div className="row-menu" onClick={e => e.stopPropagation()}>
|
|
|
|
|
<button onClick={() => { setMenuFor(null); onOpenProject(p); }}><Icon name="library" size={11} />Open</button>
|
|
|
|
|
<button onClick={() => renameProject(p)}><Icon name="edit" size={11} />Rename…</button>
|
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
|
|
|
{isAdmin && <button onClick={() => manageAccess(p)}><Icon name="users" size={11} />Manage access…</button>}
|
2026-05-23 00:08:59 -04:00
|
|
|
<button className="danger" onClick={() => deleteProject(p)}><Icon name="trash" size={11} />Delete</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2026-05-22 08:17:06 -04:00
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2026-05-22 12:17:54 -04:00
|
|
|
{showNew && <NewProjectModal onClose={() => setShowNew(false)} onCreated={onCreated} />}
|
2026-05-23 09:02:23 -04:00
|
|
|
{renamingProject && (
|
|
|
|
|
<RenameProjectModal
|
|
|
|
|
project={renamingProject}
|
|
|
|
|
onClose={() => setRenamingProject(null)}
|
|
|
|
|
onSaved={() => { setRenamingProject(null); refresh(); }}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
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
|
|
|
{accessProject && (
|
|
|
|
|
<ProjectAccessModal
|
|
|
|
|
project={accessProject}
|
|
|
|
|
onClose={() => setAccessProject(null)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Admin-only: grant/revoke per-project access to users and groups.
|
|
|
|
|
// Backed by GET/POST/DELETE /api/v1/projects/:id/access.
|
|
|
|
|
function ProjectAccessModal({ project, onClose }) {
|
|
|
|
|
const [grants, setGrants] = React.useState([]);
|
|
|
|
|
const [users, setUsers] = React.useState([]);
|
|
|
|
|
const [groups, setGroups] = React.useState([]);
|
|
|
|
|
const [loading, setLoading] = React.useState(true);
|
|
|
|
|
const [err, setErr] = React.useState(null);
|
|
|
|
|
|
|
|
|
|
// Add-grant form state.
|
|
|
|
|
const [subjType, setSubjType] = React.useState('user');
|
|
|
|
|
const [subjId, setSubjId] = React.useState('');
|
|
|
|
|
const [level, setLevel] = React.useState('view');
|
|
|
|
|
|
|
|
|
|
const loadGrants = React.useCallback(() => {
|
|
|
|
|
return window.ZAMPP_API.fetch('/projects/' + project.id + '/access')
|
|
|
|
|
.then(list => setGrants(list || []))
|
|
|
|
|
.catch(e => setErr(e.message));
|
|
|
|
|
}, [project.id]);
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
Promise.all([
|
|
|
|
|
loadGrants(),
|
|
|
|
|
window.ZAMPP_API.fetch('/users').then(setUsers).catch(() => setUsers([])),
|
|
|
|
|
window.ZAMPP_API.fetch('/groups').then(setGroups).catch(() => setGroups([])),
|
|
|
|
|
]).finally(() => setLoading(false));
|
|
|
|
|
}, [loadGrants]);
|
|
|
|
|
|
|
|
|
|
const addGrant = () => {
|
|
|
|
|
if (!subjId) return;
|
|
|
|
|
setErr(null);
|
|
|
|
|
window.ZAMPP_API.fetch('/projects/' + project.id + '/access', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: JSON.stringify({ subject_type: subjType, subject_id: subjId, level }),
|
|
|
|
|
})
|
|
|
|
|
.then(() => { setSubjId(''); return loadGrants(); })
|
|
|
|
|
.catch(e => setErr(e.message || 'Failed to add grant'));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const revoke = (g) => {
|
|
|
|
|
window.ZAMPP_API.fetch('/projects/' + project.id + '/access/' + g.subject_type + '/' + g.subject_id, { method: 'DELETE' })
|
|
|
|
|
.then(loadGrants)
|
|
|
|
|
.catch(e => setErr(e.message || 'Failed to revoke'));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Candidates for the picker — exclude subjects that already have a grant.
|
|
|
|
|
const grantedIds = new Set(grants.filter(g => g.subject_type === subjType).map(g => g.subject_id));
|
|
|
|
|
const candidates = (subjType === 'user' ? users : groups).filter(c => !grantedIds.has(c.id));
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="modal-backdrop" onClick={onClose}>
|
|
|
|
|
<div className="modal" style={{ width: 520 }} onClick={e => e.stopPropagation()}>
|
|
|
|
|
<div className="modal-head">
|
|
|
|
|
<div style={{ fontSize: 15, fontWeight: 600 }}>Manage access · {project.name}</div>
|
|
|
|
|
<button className="icon-btn" aria-label="Close" onClick={onClose}><Icon name="x" /></button>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="modal-body">
|
|
|
|
|
<div style={{ fontSize: 12, color: 'var(--text-3)', marginBottom: 12 }}>
|
|
|
|
|
Admins always have full access. Grant specific users or groups view (read-only) or
|
|
|
|
|
edit (read-write) access to this project.
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Add-grant row */}
|
|
|
|
|
<div style={{ display: 'grid', gridTemplateColumns: '90px 1fr 90px auto', gap: 8, alignItems: 'end', marginBottom: 14 }}>
|
|
|
|
|
<div className="field" style={{ marginBottom: 0 }}>
|
|
|
|
|
<label className="field-label">Type</label>
|
|
|
|
|
<select className="field-input" value={subjType} style={{ appearance: 'auto' }}
|
|
|
|
|
onChange={e => { setSubjType(e.target.value); setSubjId(''); }}>
|
|
|
|
|
<option value="user">User</option>
|
|
|
|
|
<option value="group">Group</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="field" style={{ marginBottom: 0 }}>
|
|
|
|
|
<label className="field-label">{subjType === 'user' ? 'User' : 'Group'}</label>
|
|
|
|
|
<select className="field-input" value={subjId} style={{ appearance: 'auto' }}
|
|
|
|
|
onChange={e => setSubjId(e.target.value)}>
|
|
|
|
|
<option value="">Pick a {subjType}…</option>
|
|
|
|
|
{candidates.map(c => (
|
|
|
|
|
<option key={c.id} value={c.id}>
|
|
|
|
|
{subjType === 'user' ? ('@' + c.username + (c.display_name ? ' · ' + c.display_name : '')) : c.name}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="field" style={{ marginBottom: 0 }}>
|
|
|
|
|
<label className="field-label">Level</label>
|
|
|
|
|
<select className="field-input" value={level} style={{ appearance: 'auto' }}
|
|
|
|
|
onChange={e => setLevel(e.target.value)}>
|
|
|
|
|
<option value="view">View</option>
|
|
|
|
|
<option value="edit">Edit</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<button className="btn primary sm" onClick={addGrant} disabled={!subjId}>Add</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{err && <div style={{ fontSize: 12, color: 'var(--danger)', marginBottom: 8 }}>{err}</div>}
|
|
|
|
|
|
|
|
|
|
{/* Existing grants */}
|
|
|
|
|
<div className="panel">
|
|
|
|
|
{loading && <div style={{ padding: 16, color: 'var(--text-3)', fontSize: 12.5 }}>Loading…</div>}
|
|
|
|
|
{!loading && grants.length === 0 && (
|
|
|
|
|
<div style={{ padding: '20px 0', textAlign: 'center', color: 'var(--text-3)', fontSize: 12.5 }}>
|
|
|
|
|
No grants yet — only admins can see this project.
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{!loading && grants.map(g => (
|
|
|
|
|
<div key={g.subject_type + ':' + g.subject_id}
|
|
|
|
|
style={{ display: 'grid', gridTemplateColumns: '20px 1fr 70px 80px', gap: 10, alignItems: 'center', padding: '10px 14px', borderBottom: '1px solid var(--border)' }}>
|
|
|
|
|
<Icon name={g.subject_type === 'group' ? 'users' : 'user'} size={13} />
|
|
|
|
|
<div>
|
|
|
|
|
<div style={{ fontSize: 13, fontWeight: 500 }}>{g.subject_name || '(deleted)'}</div>
|
|
|
|
|
{g.username && <div className="mono" style={{ fontSize: 11, color: 'var(--text-3)' }}>@{g.username}</div>}
|
|
|
|
|
</div>
|
|
|
|
|
<span className={`badge ${g.level === 'edit' ? 'accent' : 'neutral'}`}>{g.level}</span>
|
|
|
|
|
<button className="btn ghost sm danger" onClick={() => revoke(g)}>Revoke</button>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="modal-foot">
|
|
|
|
|
<button className="btn primary sm" onClick={onClose}>Done</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-05-23 09:02:23 -04:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function RenameProjectModal({ project, onClose, onSaved }) {
|
|
|
|
|
const [name, setName] = React.useState(project.name || '');
|
|
|
|
|
const [saving, setSaving] = React.useState(false);
|
|
|
|
|
const [err, setErr] = React.useState(null);
|
|
|
|
|
const submit = () => {
|
|
|
|
|
const trimmed = name.trim();
|
|
|
|
|
if (!trimmed || trimmed === project.name) { onClose(); return; }
|
|
|
|
|
setSaving(true); setErr(null);
|
|
|
|
|
window.ZAMPP_API.fetch('/projects/' + project.id, { method: 'PATCH', body: JSON.stringify({ name: trimmed }) })
|
|
|
|
|
.then(onSaved)
|
|
|
|
|
.catch(e => { setSaving(false); setErr(e.message); });
|
|
|
|
|
};
|
|
|
|
|
return (
|
|
|
|
|
<div className="modal-backdrop" onClick={onClose}>
|
|
|
|
|
<div className="modal" style={{ width: 420 }} onClick={e => e.stopPropagation()}>
|
|
|
|
|
<div className="modal-head">
|
|
|
|
|
<div style={{ fontSize: 15, fontWeight: 600 }}>Rename project</div>
|
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
|
|
|
<button className="icon-btn" aria-label="Close" onClick={onClose}><Icon name="x" /></button>
|
2026-05-23 09:02:23 -04:00
|
|
|
</div>
|
|
|
|
|
<div className="modal-body">
|
|
|
|
|
<div className="field">
|
|
|
|
|
<label className="field-label">Project name</label>
|
|
|
|
|
<input className="field-input" autoFocus value={name}
|
|
|
|
|
onChange={e => setName(e.target.value)}
|
|
|
|
|
onKeyDown={e => { if (e.key === 'Enter') submit(); if (e.key === 'Escape') onClose(); }} />
|
|
|
|
|
</div>
|
|
|
|
|
{err && <div style={{ fontSize: 12, color: 'var(--danger)', marginTop: 4 }}>{err}</div>}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="modal-foot">
|
|
|
|
|
<button className="btn ghost sm" onClick={onClose}>Cancel</button>
|
|
|
|
|
<button className="btn primary sm" onClick={submit} disabled={saving || !name.trim()}>{saving ? 'Saving…' : 'Rename'}</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-05-22 08:17:06 -04:00
|
|
|
</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
|
|
|
function ProjectCard({ project, assets, onOpen, onRename, onManageAccess, onDelete, canManageAccess }) {
|
2026-05-23 00:08:59 -04:00
|
|
|
const ofProject = assets.filter(a => a.project_id === project.id);
|
|
|
|
|
const thumbAssets = ofProject.slice(0, 4);
|
|
|
|
|
|
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
|
|
|
// Real status distribution - ready vs processing/live vs error.
|
2026-05-23 00:08:59 -04:00
|
|
|
const total = ofProject.length || 1;
|
|
|
|
|
const ready = ofProject.filter(a => a.status === 'ready').length;
|
|
|
|
|
const inFlight = ofProject.filter(a => a.status === 'processing' || a.status === 'live').length;
|
|
|
|
|
const errored = ofProject.filter(a => a.status === 'error').length;
|
|
|
|
|
const readyPct = (ready / total) * 100;
|
|
|
|
|
const inFlightPct = (inFlight / total) * 100;
|
|
|
|
|
const errPct = (errored / total) * 100;
|
|
|
|
|
|
2026-05-26 10:10:44 -04:00
|
|
|
// #50: context menu state for grid card
|
|
|
|
|
const [ctx, setCtx] = React.useState(null);
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (!ctx) return;
|
|
|
|
|
const close = () => setCtx(null);
|
|
|
|
|
window.addEventListener('click', close);
|
|
|
|
|
window.addEventListener('contextmenu', close);
|
|
|
|
|
window.addEventListener('scroll', close, true);
|
|
|
|
|
return () => {
|
|
|
|
|
window.removeEventListener('click', close);
|
|
|
|
|
window.removeEventListener('contextmenu', close);
|
|
|
|
|
window.removeEventListener('scroll', close, true);
|
|
|
|
|
};
|
|
|
|
|
}, [ctx]);
|
|
|
|
|
|
|
|
|
|
const handleContextMenu = (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
setCtx({ x: e.clientX, y: e.clientY });
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-22 08:17:06 -04:00
|
|
|
return (
|
2026-05-26 10:10:44 -04:00
|
|
|
<div className="project-card" onClick={onOpen} onContextMenu={handleContextMenu}>
|
2026-05-22 08:17:06 -04:00
|
|
|
<div className="project-thumb-grid">
|
2026-05-22 10:04:25 -04:00
|
|
|
{Array.from({ length: 4 }).map((_, i) => (
|
2026-05-22 08:17:06 -04:00
|
|
|
<div key={i} className="project-thumb-cell">
|
2026-05-22 10:04:25 -04:00
|
|
|
{thumbAssets[i]
|
|
|
|
|
? <AssetThumb asset={thumbAssets[i]} />
|
|
|
|
|
: <FauxFrame />}
|
2026-05-22 08:17:06 -04:00
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="project-card-body">
|
2026-05-22 10:04:25 -04:00
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
2026-05-22 12:17:54 -04:00
|
|
|
<span style={{ width: 10, height: 10, borderRadius: 2, background: project.color || 'var(--accent)' }} />
|
2026-05-22 08:17:06 -04:00
|
|
|
<span style={{ fontWeight: 600, fontSize: 14 }}>{project.name}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="project-meta">
|
2026-05-23 00:08:59 -04:00
|
|
|
<span>{ofProject.length} asset{ofProject.length === 1 ? '' : 's'}</span>
|
2026-05-22 08:17:06 -04:00
|
|
|
<span>·</span>
|
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
|
|
|
<span>updated {project.updated || '·'}</span>
|
2026-05-22 08:17:06 -04:00
|
|
|
</div>
|
2026-05-23 00:08:59 -04:00
|
|
|
{ofProject.length > 0 ? (
|
|
|
|
|
<div className="project-bar" title={`ready ${ready} · in-flight ${inFlight} · errored ${errored}`}>
|
|
|
|
|
{ready > 0 && <div className="project-segment" style={{ width: readyPct + '%', background: 'var(--success)' }} />}
|
|
|
|
|
{inFlight > 0 && <div className="project-segment" style={{ width: inFlightPct + '%', background: 'var(--accent)' }} />}
|
|
|
|
|
{errored > 0 && <div className="project-segment" style={{ width: errPct + '%', background: 'var(--danger)' }} />}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="project-bar"><div className="project-segment" style={{ width: '100%', background: 'var(--bg-3)' }} /></div>
|
|
|
|
|
)}
|
2026-05-22 08:17:06 -04:00
|
|
|
</div>
|
2026-05-26 10:10:44 -04:00
|
|
|
{ctx && (
|
|
|
|
|
<div className="row-menu" style={{ position: 'fixed', top: ctx.y, left: ctx.x, zIndex: 9999 }} onClick={e => e.stopPropagation()}>
|
|
|
|
|
<button onClick={() => { setCtx(null); onOpen(); }}><Icon name="library" size={11} />Open</button>
|
|
|
|
|
<button onClick={() => { setCtx(null); onRename && onRename(); }}><Icon name="edit" size={11} />Rename…</button>
|
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
|
|
|
{canManageAccess && <button onClick={() => { setCtx(null); onManageAccess && onManageAccess(); }}><Icon name="users" size={11} />Manage access…</button>}
|
2026-05-26 10:10:44 -04:00
|
|
|
<button className="danger" onClick={() => { setCtx(null); onDelete && onDelete(); }}><Icon name="trash" size={11} />Delete</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-05-22 08:17:06 -04:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.Projects = Projects;
|
2026-05-24 13:27:24 -04:00
|
|
|
window.RenameProjectModal = RenameProjectModal;
|
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
|
|
|
window.ProjectAccessModal = ProjectAccessModal;
|