dragonflight/services/web-ui/public/screens-admin.jsx

2378 lines
116 KiB
React
Raw Normal View History

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
// screens-admin.jsx - Users, Tokens, Containers, Cluster (graph), Settings
function _normalizeNode(n, x, y) {
const cap = n.capabilities || {};
// GPUs: capabilities.gpus entries with name+memory_mb = driver-bound (nvidia-smi confirmed).
// Entries with only type+device = detected by /dev file but driver status unknown.
const gpus = (cap.gpus || []).map(g => ({
name: g.name || (g.type ? g.type.toUpperCase() : 'GPU'),
memMb: g.memory_mb || null,
index: g.index ?? 0,
device: g.device || null,
bound: !!(g.name && g.memory_mb), // name+memory = nvidia-smi confirmed driver bound
}));
// Blackmagic DeckLink: capabilities.blackmagic + capabilities.blackmagic_model
const bmdPorts = (cap.blackmagic || []).map(b => ({
index: b.index ?? 0,
device: b.device || null,
model: cap.blackmagic_model || null,
online: b.online !== false,
}));
const memUsedMb = n.mem_used_mb || n.memory_used_mb || (n.mem && n.mem < 1000 ? n.mem * 1024 : n.mem || 0);
const memTotalMb = n.mem_total_mb || n.memory_total_mb || (n.memTotal && n.memTotal < 1000 ? n.memTotal * 1024 : n.memTotal || 0);
return {
id: n.hostname || n.id || n.name || 'node',
dbId: n.id,
role: n.role || 'worker',
status: n.status || (n.online ? 'online' : 'offline'),
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
ip: n.ip_address || n.ip || '·',
version: n.version || '·',
uptime: n.uptime || '·',
cpu: parseFloat(n.cpu_usage || n.cpu || n.cpu_percent || 0),
mem: Math.round(memUsedMb / 1024 * 10) / 10,
memTotal: Math.round(memTotalMb / 1024 * 10) / 10,
// Raw capabilities for the hardware panel
gpus,
bmdPorts,
// Legacy flat arrays kept for the stat-row summary cards
gpuCount: gpus.length,
bmdCount: bmdPorts.length,
x, y,
};
}
function InviteUserModal({ onCreated, onClose }) {
const [form, setForm] = React.useState({ username: '', display_name: '', password: '', role: 'viewer' });
const [saving, setSaving] = React.useState(false);
const [err, setErr] = React.useState(null);
const submit = () => {
if (!form.username || !form.password) { setErr('Username and password are required'); return; }
setSaving(true); setErr(null);
window.ZAMPP_API.fetch('/users', { method: 'POST', body: JSON.stringify(form) })
.then(user => { onCreated(user); onClose(); })
.catch(e => { setSaving(false); setErr(e.message || 'Failed to create user'); });
};
const onKey = e => { if (e.key === 'Enter') submit(); };
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 }}>Invite user</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>
</div>
<div className="modal-body">
<div className="field">
<label className="field-label">Username</label>
<input className="field-input" value={form.username} autoFocus
onChange={e => setForm(p => ({...p, username: e.target.value}))}
onKeyDown={onKey} placeholder="jsmith" />
</div>
<div className="field">
<label className="field-label">Display name</label>
<input className="field-input" value={form.display_name}
onChange={e => setForm(p => ({...p, display_name: e.target.value}))}
onKeyDown={onKey} placeholder="John Smith" />
</div>
<div className="field">
<label className="field-label">Password</label>
<input className="field-input" type="password" value={form.password}
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
autoComplete="new-password"
onChange={e => setForm(p => ({...p, password: e.target.value}))}
onKeyDown={onKey} placeholder="Temporary password" />
</div>
<div className="field">
<label className="field-label">Role</label>
<select className="field-input" value={form.role}
onChange={e => setForm(p => ({...p, role: e.target.value}))}
style={{ appearance: 'auto' }}>
<option value="viewer">Viewer</option>
<option value="editor">Editor</option>
<option value="admin">Admin</option>
</select>
</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}>{saving ? 'Creating…' : 'Create user'}</button>
</div>
</div>
</div>
);
}
function Users() {
const [users, setUsers] = React.useState(window.ZAMPP_DATA.USERS || []);
const [groups, setGroups] = React.useState([]);
const [tab, setTab] = React.useState("users");
const [showInvite, setShowInvite] = React.useState(false);
const [editingUser, setEditingUser] = React.useState(null);
const [resetUser, setResetUser] = React.useState(null);
const [menuFor, setMenuFor] = React.useState(null); // row id whose menu is open
const refreshUsers = React.useCallback(() => {
window.ZAMPP_API.fetch('/users')
.then(list => {
const normalized = (list || []).map(u => ({
...u,
name: u.display_name || u.username,
initials: (u.display_name || u.username || '??').slice(0, 2).toUpperCase(),
group_count: u.group_count ?? 0,
}));
setUsers(normalized);
window.ZAMPP_DATA.USERS = normalized;
})
.catch(() => {});
}, []);
const refreshGroups = React.useCallback(() => {
window.ZAMPP_API.fetch('/groups')
.then(list => setGroups(list || []))
.catch(() => setGroups([]));
}, []);
React.useEffect(() => { refreshUsers(); refreshGroups(); }, [refreshUsers, refreshGroups]);
// Click-outside closes any open row menu so the user can dismiss it without picking.
React.useEffect(() => {
if (!menuFor) return;
const close = () => setMenuFor(null);
window.addEventListener('click', close);
return () => window.removeEventListener('click', close);
}, [menuFor]);
const exportCsv = () => {
const rows = [['Username', 'Name', 'Role', 'Groups', 'Created']].concat(
users.map(u => [u.username || '', u.name || '', u.role || '', u.group_count || 0, u.created_at || ''])
);
const csv = rows.map(r => r.map(c => '"' + String(c).replace(/"/g, '""') + '"').join(',')).join('\n');
const a = document.createElement('a');
a.href = 'data:text/csv;charset=utf-8,' + encodeURIComponent(csv);
a.download = 'users.csv';
a.click();
};
const onCreated = () => { refreshUsers(); setShowInvite(false); };
const deleteUser = (u) => {
setMenuFor(null);
if (!confirm(`Delete user "${u.name}" (@${u.username})?\nThis cannot be undone.`)) return;
window.ZAMPP_API.fetch('/users/' + u.id, { method: 'DELETE' })
.then(refreshUsers)
.catch(e => alert('Delete failed: ' + e.message));
};
const resetPassword = (u) => { setMenuFor(null); setResetUser(u); };
const changeRole = (u, newRole) => {
if (u.role === newRole) return;
window.ZAMPP_API.fetch('/users/' + u.id, { method: 'PATCH', body: JSON.stringify({ role: newRole }) })
.then(refreshUsers)
.catch(e => alert('Role change failed: ' + e.message));
};
return (
<div className="page">
<div className="page-header">
<h1>Users &amp; Groups</h1>
<div className="spacer" />
{tab === 'users' && (<>
<button className="btn ghost sm" onClick={exportCsv}><Icon name="download" />Export</button>
<button className="btn primary" onClick={() => setShowInvite(true)}><Icon name="plus" />Invite user</button>
</>)}
</div>
<div className="page-body">
<div className="tab-group" style={{ width: "fit-content", marginBottom: 12 }}>
<button className={tab === "users" ? "active" : ""} onClick={() => setTab("users")}>Users · {users.length}</button>
<button className={tab === "groups" ? "active" : ""} onClick={() => setTab("groups")}>Groups · {groups.length}</button>
<button className={tab === "policies" ? "active" : ""} onClick={() => setTab("policies")}>Policies</button>
</div>
{tab === 'users' && (
<div className="panel">
<div className="user-row head">
<div>User</div>
<div>Role</div>
<div>Groups</div>
<div>Created</div>
<div></div>
</div>
{users.length === 0 && (
<div style={{ padding: "32px 0", textAlign: "center", color: "var(--text-3)" }}>No users found</div>
)}
{users.map(u => (
<div key={u.id} className="user-row" style={{ position: 'relative' }}>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<div className="avatar" style={{ width: 32, height: 32, fontSize: 11, background: avatarColor(u.initials || u.id) }}>{u.initials || '??'}</div>
<div>
<div style={{ fontWeight: 500, fontSize: 13 }}>{u.name}</div>
<div className="mono" style={{ fontSize: 11, color: "var(--text-3)" }}>@{u.username}</div>
</div>
</div>
<div>
<select value={u.role || 'viewer'}
onChange={e => changeRole(u, e.target.value)}
className="field-input"
style={{ width: 90, padding: '3px 6px', fontSize: 11.5, appearance: 'auto' }}>
<option value="admin">admin</option>
<option value="editor">editor</option>
<option value="viewer">viewer</option>
</select>
</div>
<div className="mono" style={{ fontSize: 11.5, color: "var(--text-3)" }}>
{u.group_count || 0} {u.group_count === 1 ? 'group' : 'groups'}
</div>
<div className="mono" style={{ fontSize: 11.5, color: "var(--text-3)" }}>
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
{u.created_at ? new Date(u.created_at).toLocaleDateString() : u.lastSeen || '·'}
</div>
<div style={{ position: 'relative' }}>
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="User actions" onClick={e => { e.stopPropagation(); setMenuFor(menuFor === u.id ? null : u.id); }}>
<Icon name="more" />
</button>
{menuFor === u.id && (
<div className="row-menu" onClick={e => e.stopPropagation()}>
<button onClick={() => { setMenuFor(null); setEditingUser(u); }}>
<Icon name="edit" size={12} />Rename
</button>
<button onClick={() => resetPassword(u)}>
<Icon name="key" size={12} />Reset password
</button>
<button className="danger" onClick={() => deleteUser(u)}>
<Icon name="trash" size={12} />Delete
</button>
</div>
)}
</div>
</div>
))}
</div>
)}
{tab === 'groups' && <GroupsPanel groups={groups} users={users} onChange={refreshGroups} />}
{tab === 'policies' && (
<div className="panel" style={{ padding: '32px 24px', color: 'var(--text-2)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<Icon name="lock" size={16} />
<div style={{ fontWeight: 600, fontSize: 14 }}>Access model</div>
</div>
<div style={{ fontSize: 12.5, color: 'var(--text-3)', lineHeight: 1.7, maxWidth: 640 }}>
<div style={{ marginBottom: 8 }}>
<strong style={{ color: 'var(--text-2)' }}>admin</strong> full access to every
project plus user, group, cluster, and system administration.
</div>
<div style={{ marginBottom: 8 }}>
<strong style={{ color: 'var(--text-2)' }}>editor / viewer</strong> see only the
projects they've been granted. A <em>view</em> grant is read-only; an
<em> edit</em> grant allows changes. Grants can target an individual user or a group.
</div>
<div>
Manage a project's grants from the <strong style={{ color: 'var(--text-2)' }}>Projects</strong> page
a project's <em>Manage access</em> menu. Group membership is managed on the
Groups tab above.
</div>
</div>
</div>
)}
</div>
{showInvite && <InviteUserModal onCreated={onCreated} onClose={() => setShowInvite(false)} />}
{editingUser && (
<EditUserModal user={editingUser}
onClose={() => setEditingUser(null)}
onSaved={() => { setEditingUser(null); refreshUsers(); }}
/>
)}
{resetUser && (
<PasswordResetModal user={resetUser}
onClose={() => setResetUser(null)}
onSaved={() => setResetUser(null)}
/>
)}
</div>
);
}
function EditUserModal({ user, onClose, onSaved }) {
const [name, setName] = React.useState(user.display_name || user.name || '');
const [saving, setSaving] = React.useState(false);
const [err, setErr] = React.useState(null);
const submit = () => {
setSaving(true); setErr(null);
window.ZAMPP_API.fetch('/users/' + user.id, { method: 'PATCH', body: JSON.stringify({ display_name: name.trim() }) })
.then(onSaved)
.catch(e => { setSaving(false); setErr(e.message); });
};
return (
<div className="modal-backdrop" onClick={onClose}>
<div className="modal" style={{ width: 400 }} onClick={e => e.stopPropagation()}>
<div className="modal-head">
<div style={{ fontSize: 15, fontWeight: 600 }}>Rename user</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>
</div>
<div className="modal-body">
<div className="field">
<label className="field-label">Display name</label>
<input className="field-input" autoFocus value={name}
onChange={e => setName(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') submit(); }} />
<div className="mono" style={{ fontSize: 11, color: 'var(--text-3)', marginTop: 4 }}>username @{user.username} cannot be changed</div>
</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…' : 'Save'}</button>
</div>
</div>
</div>
);
}
function PasswordResetModal({ user, onClose, onSaved }) {
const [pw, setPw] = React.useState('');
const [pw2, setPw2] = React.useState('');
const [show, setShow] = React.useState(false);
const [saving, setSaving] = React.useState(false);
const [err, setErr] = React.useState(null);
const [done, setDone] = React.useState(false);
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
// #111 - guard async resolution / delayed onSaved against unmount.
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 mountedRef = React.useRef(true);
const savedTimerRef = React.useRef(null);
React.useEffect(() => () => {
mountedRef.current = false;
if (savedTimerRef.current) clearTimeout(savedTimerRef.current);
}, []);
const valid = pw.length >= 8 && pw === pw2;
const submit = () => {
if (!valid) return;
setSaving(true); setErr(null);
window.ZAMPP_API.fetch('/users/' + user.id, { method: 'PATCH', body: JSON.stringify({ password: pw }) })
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
.then(() => {
if (!mountedRef.current) return;
setSaving(false); setDone(true);
savedTimerRef.current = setTimeout(() => { if (mountedRef.current) onSaved(); }, 1200);
})
.catch(e => { if (mountedRef.current) { setSaving(false); setErr(e.message); } });
};
return (
<div className="modal-backdrop" onClick={onClose}>
<div className="modal" style={{ width: 400 }} onClick={e => e.stopPropagation()}>
<div className="modal-head">
<div style={{ fontSize: 15, fontWeight: 600 }}>Reset password · @{user.username}</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>
</div>
<div className="modal-body">
{done ? (
<div style={{ textAlign: 'center', padding: '12px 0', color: 'var(--success)', fontSize: 13 }}>
<Icon name="check" size={16} /> Password updated.
</div>
) : (<>
<div className="field">
<label className="field-label">New password</label>
<div style={{ position: 'relative' }}>
<input className="field-input" autoFocus type={show ? 'text' : 'password'}
value={pw} onChange={e => setPw(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter' && valid) submit(); }}
style={{ paddingRight: 36 }} />
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={show ? 'Hide password' : 'Show password'} style={{ position: 'absolute', right: 4, top: '50%', transform: 'translateY(-50%)' }}
onClick={() => setShow(s => !s)} type="button" tabIndex={-1}>
<Icon name={show ? 'eye-off' : 'eye'} size={13} />
</button>
</div>
<div style={{ fontSize: 11, color: pw.length > 0 && pw.length < 8 ? 'var(--danger)' : 'var(--text-3)', marginTop: 4 }}>
Minimum 8 characters
</div>
</div>
<div className="field">
<label className="field-label">Confirm password</label>
<input className="field-input" type={show ? 'text' : 'password'}
value={pw2} onChange={e => setPw2(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter' && valid) submit(); }} />
{pw2.length > 0 && pw !== pw2 && (
<div style={{ fontSize: 11, color: 'var(--danger)', marginTop: 4 }}>Passwords do not match</div>
)}
</div>
{err && <div style={{ fontSize: 12, color: 'var(--danger)', marginTop: 4 }}>{err}</div>}
</>)}
</div>
{!done && (
<div className="modal-foot">
<button className="btn ghost sm" onClick={onClose}>Cancel</button>
<button className="btn primary sm" onClick={submit} disabled={saving || !valid}>
{saving ? 'Saving…' : 'Reset password'}
</button>
</div>
)}
</div>
</div>
);
}
function GroupsPanel({ groups, users, onChange }) {
const [creating, setCreating] = React.useState(false);
const [newName, setNewName] = React.useState('');
const [newDesc, setNewDesc] = React.useState('');
const [expandedId, setExpandedId] = React.useState(null);
const [members, setMembers] = React.useState({}); // groupId -> [user]
const createGroup = () => {
if (!newName.trim()) return;
window.ZAMPP_API.fetch('/groups', { method: 'POST', body: JSON.stringify({ name: newName.trim(), description: newDesc.trim() || null }) })
.then(() => { setCreating(false); setNewName(''); setNewDesc(''); onChange(); })
.catch(e => alert('Create failed: ' + e.message));
};
const deleteGroup = (g) => {
if (!confirm(`Delete group "${g.name}"?`)) return;
window.ZAMPP_API.fetch('/groups/' + g.id, { method: 'DELETE' })
.then(onChange)
.catch(e => alert('Delete failed: ' + e.message));
};
const toggle = (g) => {
if (expandedId === g.id) { setExpandedId(null); return; }
setExpandedId(g.id);
window.ZAMPP_API.fetch('/groups/' + g.id + '/members')
.then(list => setMembers(m => ({ ...m, [g.id]: list || [] })))
.catch(() => setMembers(m => ({ ...m, [g.id]: [] })));
};
const addMember = (g, userId) => {
if (!userId) return;
window.ZAMPP_API.fetch('/groups/' + g.id + '/members', { method: 'POST', body: JSON.stringify({ user_id: userId }) })
.then(() => {
window.ZAMPP_API.fetch('/groups/' + g.id + '/members')
.then(list => setMembers(m => ({ ...m, [g.id]: list || [] })));
onChange();
})
.catch(e => alert('Add failed: ' + e.message));
};
const removeMember = (g, uid) => {
window.ZAMPP_API.fetch('/groups/' + g.id + '/members/' + uid, { method: 'DELETE' })
.then(() => {
setMembers(m => ({ ...m, [g.id]: (m[g.id] || []).filter(u => u.id !== uid) }));
onChange();
})
.catch(e => alert('Remove failed: ' + e.message));
};
return (
<div>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 12 }}>
<div style={{ flex: 1, fontSize: 12, color: 'var(--text-3)' }}>
Groups let you bundle users for project access. Memberships are checked when role-based access is enforced.
</div>
<button className="btn primary sm" onClick={() => setCreating(true)}><Icon name="plus" size={11} />New group</button>
</div>
{creating && (
<div className="panel" style={{ padding: 12, marginBottom: 12, display: 'grid', gridTemplateColumns: '1fr 2fr auto auto', gap: 8, alignItems: 'end' }}>
<div className="field" style={{ marginBottom: 0 }}>
<label className="field-label">Group name</label>
<input className="field-input" autoFocus value={newName} onChange={e => setNewName(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') createGroup(); }} placeholder="broadcasters" />
</div>
<div className="field" style={{ marginBottom: 0 }}>
<label className="field-label">Description (optional)</label>
<input className="field-input" value={newDesc} onChange={e => setNewDesc(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') createGroup(); }} placeholder="On-air operators" />
</div>
<button className="btn primary sm" onClick={createGroup} disabled={!newName.trim()}>Create</button>
<button className="btn ghost sm" onClick={() => { setCreating(false); setNewName(''); setNewDesc(''); }}>Cancel</button>
</div>
)}
<div className="panel">
{groups.length === 0 && !creating && (
<div style={{ padding: '32px 0', textAlign: 'center', color: 'var(--text-3)', fontSize: 12.5 }}>
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
No groups yet: click <em>New group</em> above to create one.
</div>
)}
{groups.map(g => {
const isOpen = expandedId === g.id;
const groupMembers = members[g.id] || [];
const nonMembers = users.filter(u => !groupMembers.some(m => m.id === u.id));
return (
<div key={g.id} style={{ borderBottom: '1px solid var(--border)' }}>
<div style={{ padding: '12px 16px', display: 'grid', gridTemplateColumns: '1.6fr 2fr 90px 80px', alignItems: 'center', gap: 12 }}>
<div>
<div style={{ fontWeight: 500, fontSize: 13 }}>{g.name}</div>
<div className="mono" style={{ fontSize: 11, color: 'var(--text-3)' }}>{g.id.slice(0, 8)}</div>
</div>
<div style={{ fontSize: 12, color: 'var(--text-3)' }}>{g.description || <span style={{ fontStyle: 'italic' }}>no description</span>}</div>
<div className="mono" style={{ fontSize: 11.5, color: 'var(--text-3)' }}>{g.member_count || 0} member{g.member_count === 1 ? '' : 's'}</div>
<div style={{ display: 'flex', gap: 4, justifyContent: 'flex-end' }}>
<button className="btn ghost sm" onClick={() => toggle(g)}>{isOpen ? 'Hide' : 'Members'}</button>
<button className="btn ghost sm danger" onClick={() => deleteGroup(g)}>Delete</button>
</div>
</div>
{isOpen && (
<div style={{ padding: '0 16px 16px 16px', background: 'var(--bg-2)' }}>
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 10 }}>
{groupMembers.length === 0 && <span style={{ fontSize: 12, color: 'var(--text-3)' }}>No members yet.</span>}
{groupMembers.map(m => (
<span key={m.id} className="badge outline" style={{ display: 'inline-flex', alignItems: 'center', gap: 6, padding: '4px 8px' }}>
@{m.username}
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="Remove member" style={{ width: 16, height: 16, padding: 0 }} onClick={() => removeMember(g, m.id)} title="Remove">
<Icon name="x" size={9} />
</button>
</span>
))}
</div>
{nonMembers.length > 0 && (
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<span style={{ fontSize: 11.5, color: 'var(--text-3)' }}>Add member:</span>
<select className="field-input" defaultValue=""
onChange={e => { addMember(g, e.target.value); e.target.value = ''; }}
style={{ width: 200, padding: '4px 8px', fontSize: 12, appearance: 'auto' }}>
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
<option value="" disabled>Pick a user</option>
{nonMembers.map(u => <option key={u.id} value={u.id}>@{u.username}: {u.name}</option>)}
</select>
</div>
)}
</div>
)}
</div>
);
})}
</div>
</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
// Real Tokens admin page: wraps ApiTokensSection (defined further down) in a
// .page shell so it can be a top-level admin nav destination. The old parody
// pricing page lives below as TokensParody and is now routed at /billing in
// the Admin section.
function Tokens() {
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
return (
<div className="page">
<div className="page-header">
<h1>Tokens</h1>
<span className="subtitle">API tokens for the Premiere panel, node-agents, and external integrations</span>
</div>
<div className="page-body">
<ApiTokensSection />
</div>
</div>
);
}
function TokensParody() {
const [burned, setBurned] = React.useState(14340);
const [rate, setRate] = React.useState(2.4);
const [showCalc, setShowCalc] = React.useState(false);
React.useEffect(() => {
const i = setInterval(() => {
setBurned(b => b + Math.floor(Math.random() * 8) + 1);
setRate(r => Math.max(0.8, Math.min(8, r + (Math.random() - 0.5) * 0.4)));
}, 800);
return () => clearInterval(i);
}, []);
const burnSpark = React.useMemo(() => Array.from({ length: 40 }, (_, i) => 100 + i * 8 + Math.sin(i * 0.7) * 12), []);
const competitorSpark = React.useMemo(() => Array.from({ length: 40 }, (_, i) => 200 + i * 22 + Math.cos(i * 0.5) * 30), []);
const yourCostSpark = React.useMemo(() => Array.from({ length: 40 }, () => 1), []);
const [events, setEvents] = React.useState([
{ t: "21:14:02", action: "preview thumbnail generated", cost: 4 },
{ t: "21:14:01", action: "user clicked play", cost: 12 },
{ t: "21:13:58", action: "API health check", cost: 8 },
{ t: "21:13:54", action: "asset metadata read", cost: 2 },
{ t: "21:13:51", action: "session token refreshed", cost: 18 },
{ t: "21:13:47", action: "scrubbed timeline 1 frame", cost: 6 },
{ t: "21:13:42", action: "took a deep breath near the API", cost: 24 },
]);
React.useEffect(() => {
const actions = [
"preview thumbnail generated", "user clicked play", "API health check",
"scrubbed timeline 1 frame", "asset metadata read", "session token refreshed",
"checked job queue", "rendered a tooltip", "loaded sidebar icon",
"blinked", "made eye contact with the cluster", "opened a modal (twice)",
"asset list pagination request", "thought about a comment", "moved cursor near 'Save'",
];
const i = setInterval(() => {
const now = new Date();
const t = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`;
const a = actions[Math.floor(Math.random() * actions.length)];
const c = Math.floor(Math.random() * 28) + 1;
setEvents(ev => [{ t, action: a, cost: c }, ...ev].slice(0, 12));
}, 1600);
return () => clearInterval(i);
}, []);
const tiers = [
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
{ name: "Starter", desc: "For \"evaluation only\": definitely not production", price: "$2,400", per: "/ month", tokens: "100k tokens", popular: false, color: "#6B7280" },
{ name: "Broadcast", desc: "Most teams. Most pain.", price: "$28,000", per: "/ month", tokens: "1.5M tokens", popular: true, color: "#5B7CFA" },
{ name: "Enterprise", desc: "If you have to ask, you can't afford it (but you'll ask anyway)", price: "$call us", per: "and bring lawyers", tokens: "∞ tokens", popular: false, color: "#B57CFA" },
];
return (
<div className="page">
<div className="page-header">
<h1>Billing</h1>
<span className="subtitle">Token-metered pricing parody · You actually pay <strong style={{ color: "var(--success)" }}>$0.00</strong></span>
<div className="spacer" />
<span className="badge warning"><Icon name="alert" size={10} /> SATIRE</span>
<button className="btn ghost sm" onClick={() => setShowCalc(!showCalc)}><Icon name="sliders" />Cost calculator</button>
</div>
<div className="page-body">
<div style={{ textAlign: 'center', padding: '8px 0 36px' }}>
<h2 style={{ fontSize: 30, fontWeight: 700, letterSpacing: '-0.02em', lineHeight: 1.3, margin: 0 }}>
<span style={{ textDecoration: 'line-through', color: 'var(--text-3)', fontWeight: 500 }}>Per-seat</span>
{' · '}
<span style={{ textDecoration: 'line-through', color: 'var(--text-3)', fontWeight: 500 }}>Per-stream</span>
{' · '}
<span style={{ textDecoration: 'line-through', color: 'var(--text-3)', fontWeight: 500 }}>Per-month</span>
<br />
<span style={{ fontSize: 52, fontWeight: 800, color: 'var(--accent-text)', letterSpacing: '-0.03em' }}>Per Token.</span>
</h2>
</div>
<div className="token-hero">
<div className="token-burn-card">
<div className="token-card-label">TOKENS BURNED THIS SESSION</div>
<div className="token-counter">
<span className="token-flame">🔥</span>
<span className="token-big mono">{burned.toLocaleString()}</span>
</div>
<div className="token-rate">
<span className="mono" style={{ color: "var(--danger)" }}> {rate.toFixed(1)}k/sec</span>
<span style={{ color: "var(--text-3)", marginLeft: 10 }}>burning since you logged in</span>
</div>
<div style={{ marginTop: 12 }}>
<Sparkline data={burnSpark} color="#FF5B5B" />
</div>
</div>
<div className="token-actual-card">
<div className="token-card-label">WHAT YOU ACTUALLY PAY</div>
<div className="token-actual-amount">
<span style={{ fontSize: 48, fontWeight: 700, letterSpacing: "-0.04em" }}>$0</span>
<span style={{ fontSize: 18, color: "var(--text-3)" }}>.00</span>
</div>
<div style={{ fontSize: 12, color: "var(--text-3)", lineHeight: 1.5 }}>
Dragonflight is self-hosted. The tokens above are imaginary.<br />
Imagine them as a stress test for your sanity.
</div>
<div style={{ marginTop: 12 }}>
<Sparkline data={yourCostSpark} color="#2DD4A8" fill={false} />
</div>
</div>
</div>
<div className="token-comparison">
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="token-card-label" style={{ padding: "16px 16px 0" }}>HOURLY BURN: DRAGONFLIGHT vs. THE OTHER GUYS</div>
<div className="token-compare-chart">
<ChartLine
series={[
{ label: "AMPP-style competitor", data: competitorSpark, color: "#FF5B5B" },
{ label: "Dragonflight (yours)", data: yourCostSpark.map((_, i) => i < 20 ? 1 : 1), color: "#2DD4A8" },
]}
/>
<div className="token-compare-legend">
<div><span className="dot" style={{ background: "#FF5B5B" }} />Competitor: $1,247/hr and rising</div>
<div><span className="dot" style={{ background: "#2DD4A8" }} />Dragonflight: $0.00/hr forever</div>
</div>
</div>
</div>
<div className="token-grid">
<div>
<div className="token-card-label" style={{ marginBottom: 8 }}>LIVE BILLING EVENTS</div>
<div className="panel">
{events.map((e, i) => (
<div key={i} className={`token-event ${i === 0 ? "fresh" : ""}`}>
<span className="mono" style={{ color: "var(--text-3)", fontSize: 11 }}>{e.t}</span>
<span style={{ flex: 1, fontSize: 12.5 }}>{e.action}</span>
<span className="mono" style={{ color: "var(--danger)", fontWeight: 600 }}>+{e.cost} tk</span>
</div>
))}
</div>
</div>
<div>
<div className="token-card-label" style={{ marginBottom: 8 }}>PRICING TIERS WE DIDN'T COPY</div>
<div className="token-tiers">
{tiers.map(t => (
<div key={t.name} className={`token-tier ${t.popular ? "popular" : ""}`}>
{t.popular && <span className="token-tier-badge">MOST PAIN</span>}
<div className="token-tier-name" style={{ color: t.color }}>{t.name}</div>
<div className="token-tier-desc">{t.desc}</div>
<div className="token-tier-price">
<span style={{ fontSize: 26, fontWeight: 700, letterSpacing: "-0.02em" }}>{t.price}</span>
<span style={{ fontSize: 12, color: "var(--text-3)", marginLeft: 4 }}>{t.per}</span>
</div>
<div className="token-tier-tokens mono">{t.tokens}</div>
<button className="btn subtle sm" disabled style={{ width: "100%", marginTop: 8 }}>Not for sale</button>
</div>
))}
</div>
</div>
</div>
{showCalc && <CostCalculator onClose={() => setShowCalc(false)} />}
<div className="token-footnote">
<Icon name="alert" size={14} />
<div>
<strong>Disclaimer:</strong> No actual tokens were billed in the making of this page. Wild Dragon's broadcast platform
is self-hosted infrastructure. Any resemblance to per-API-call broadcast token economies, living or dead, is satirical
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
and protected as commentary. If you came here looking for actual API tokens, that page no longer exists: service
credentials are managed through the cluster's own JWT issuer.
</div>
</div>
</div>
</div>
);
}
function ChartLine({ series }) {
const w = 600, h = 140;
return (
<svg viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" style={{ width: "100%", height: 140 }}>
<defs>
<pattern id="cgrid" width="60" height="28" patternUnits="userSpaceOnUse">
<path d="M 60 0 L 0 0 0 28" fill="none" stroke="rgba(255,255,255,0.04)" strokeWidth="1" />
</pattern>
</defs>
<rect width={w} height={h} fill="url(#cgrid)" />
{series.map((s, si) => {
const max = Math.max(...series.flatMap(x => x.data), 1);
const pts = s.data.map((d, i) => {
const x = (i / (s.data.length - 1)) * w;
const y = h - (d / max) * (h - 10) - 4;
return `${x},${y}`;
}).join(" ");
const area = `0,${h} ${pts} ${w},${h}`;
return (
<g key={si}>
<polygon points={area} fill={s.color} opacity="0.1" />
<polyline points={pts} fill="none" stroke={s.color} strokeWidth="2" />
<circle cx={w} cy={h - (s.data[s.data.length - 1] / max) * (h - 10) - 4} r="4" fill={s.color} />
<circle cx={w} cy={h - (s.data[s.data.length - 1] / max) * (h - 10) - 4} r="8" fill={s.color} opacity="0.3">
<animate attributeName="r" values="4;14;4" dur="2s" repeatCount="indefinite" />
<animate attributeName="opacity" values="0.6;0;0.6" dur="2s" repeatCount="indefinite" />
</circle>
</g>
);
})}
</svg>
);
}
function CostCalculator({ onClose }) {
const [users, setUsers] = React.useState(12);
const [assets, setAssets] = React.useState(500);
const [clicks, setClicks] = React.useState(2000);
const cost = users * 240 + assets * 8 + clicks * 0.12;
return (
<div className="modal-backdrop" onClick={onClose}>
<div className="modal" style={{ width: 520 }} onClick={e => e.stopPropagation()}>
<div className="modal-head">
<div>
<div style={{ fontSize: 15, fontWeight: 600 }}>Token Cost Calculator</div>
<div style={{ fontSize: 12, color: "var(--text-3)", marginTop: 2 }}>What it would cost on AMPP-style pricing</div>
</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>
</div>
<div className="modal-body">
<CalcSlider label="Users" value={users} onChange={setUsers} min={1} max={100} unit=" people" />
<CalcSlider label="Assets in library" value={assets} onChange={setAssets} min={50} max={10000} step={50} unit="" />
<CalcSlider label="UI clicks per day" value={clicks} onChange={setClicks} min={100} max={20000} step={100} unit="" />
<div style={{ background: "var(--bg-2)", border: "1px solid var(--border)", borderRadius: 8, padding: 16, marginTop: 8 }}>
<div style={{ fontSize: 11, color: "var(--text-3)", textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 600 }}>You would be paying</div>
<div style={{ fontSize: 36, fontWeight: 700, color: "var(--danger)", letterSpacing: "-0.02em", marginTop: 4 }}>
${cost.toLocaleString("en-US", { maximumFractionDigits: 0 })}<span style={{ fontSize: 14, color: "var(--text-3)", fontWeight: 400, marginLeft: 4 }}>/ month</span>
</div>
<div style={{ marginTop: 8, padding: 10, background: "var(--success-soft)", borderRadius: 6, fontSize: 12.5, color: "var(--success)" }}>
<strong>Your actual Dragonflight cost:</strong> $0.00. You're welcome.
</div>
</div>
</div>
</div>
</div>
);
}
function CalcSlider({ label, value, onChange, min, max, step = 1, unit }) {
return (
<div>
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 6, fontSize: 12 }}>
<span style={{ color: "var(--text-2)" }}>{label}</span>
<span className="mono" style={{ color: "var(--text-1)", fontWeight: 600 }}>{value.toLocaleString()}{unit}</span>
</div>
<input
type="range"
min={min} max={max} step={step}
value={value}
onChange={e => onChange(Number(e.target.value))}
style={{ width: "100%", accentColor: "var(--accent)" }}
/>
</div>
);
}
function Containers() {
const [containers, setContainers] = 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 [restartFlashState, setRestartFlashState] = React.useState(null);
const [logsModalState, setLogsModalState] = React.useState(null);
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
// #111 - guard restart-flash timers against unmount.
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 mountedRef = React.useRef(true);
const flashTimerRef = React.useRef(null);
React.useEffect(() => () => {
mountedRef.current = false;
if (flashTimerRef.current) clearTimeout(flashTimerRef.current);
}, []);
const setRestartFlashSafe = (v) => { if (mountedRef.current) setRestartFlashState(v); };
const scheduleFlashClear = (ms) => {
if (flashTimerRef.current) clearTimeout(flashTimerRef.current);
flashTimerRef.current = setTimeout(() => setRestartFlashSafe(null), ms);
};
function load() {
setContainers(null);
window.ZAMPP_API.fetch('/cluster/containers')
.then(data => setContainers(Array.isArray(data) ? data : (data.containers || [])))
.catch(() => setContainers([]));
}
React.useEffect(() => { load(); }, []);
const running = (containers || []).filter(c => c.state === 'running').length;
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 restartFlash = restartFlashState;
const logsModal = logsModalState;
const setLogsModal = setLogsModalState;
const showLogs = (c) => setLogsModal(c);
const restartContainer = (c) => {
if (!window.confirm('Restart container "' + c.name + '"?\nIn-flight requests will be dropped.')) 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
setRestartFlashSafe({ name: c.name, status: 'pending' });
window.ZAMPP_API.fetch('/cluster/containers/' + encodeURIComponent(c.id || c.name) + '/restart', { method: 'POST' })
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
.then(() => {
if (!mountedRef.current) return;
setRestartFlashSafe({ name: c.name, status: 'ok' });
load();
scheduleFlashClear(3000);
})
.catch(e => {
setRestartFlashSafe({ name: c.name, status: 'fail', error: e.message });
scheduleFlashClear(5000);
});
};
return (
<div className="page">
<div className="page-header">
<h1>Containers</h1>
<span className="subtitle">Docker Compose services across the cluster</span>
<div className="spacer" />
{containers !== null && containers.length > 0 && (
<div className="status-pip">
<span className="dot" />
<span>{running} / {containers.length} running</span>
</div>
)}
<button className="btn ghost sm" onClick={load}><Icon name="refresh" />Refresh</button>
</div>
{restartFlash && (
<div style={{
position: 'fixed', bottom: 20, right: 20, zIndex: 200,
background: restartFlash.status === 'ok' ? 'var(--success-soft)'
: restartFlash.status === 'fail' ? 'var(--danger-soft)'
: 'var(--bg-2)',
color: restartFlash.status === 'ok' ? 'var(--success)'
: restartFlash.status === 'fail' ? 'var(--danger)' : 'var(--text-2)',
border: '1px solid', borderColor: restartFlash.status === 'ok' ? 'var(--success)'
: restartFlash.status === 'fail' ? 'var(--danger)' : 'var(--border)',
borderRadius: 6, padding: '10px 14px', fontSize: 12.5, maxWidth: 320,
}}>
{restartFlash.status === 'pending' && `Restarting ${restartFlash.name}`}
{restartFlash.status === 'ok' && `${restartFlash.name} restarted.`}
{restartFlash.status === 'fail' && `${restartFlash.name}: ${restartFlash.error}`}
</div>
)}
{logsModal && (
<div className="modal-backdrop" onClick={() => setLogsModal(null)}>
<div className="modal" style={{ width: 540 }} onClick={e => e.stopPropagation()}>
<div className="modal-head">
<div style={{ fontSize: 15, fontWeight: 600 }}>Logs · {logsModal.name}</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={() => setLogsModal(null)}><Icon name="x" /></button>
</div>
<div className="modal-body">
<div style={{ fontSize: 12, color: 'var(--text-3)', marginBottom: 8 }}>
Live log streaming over the websocket isn't wired yet. SSH to the host that runs this container and tail the logs directly:
</div>
<code className="mono" style={{ display: 'block', background: 'var(--bg-2)', padding: 10, borderRadius: 5, fontSize: 11.5, overflowX: 'auto' }}>
docker compose logs -f {logsModal.name}
</code>
<div style={{ fontSize: 11.5, color: 'var(--text-3)', marginTop: 10 }}>
Or grab the last 200 lines:&nbsp;
<span className="mono">docker logs --tail 200 {logsModal.name}</span>
</div>
</div>
<div className="modal-foot">
<button className="btn ghost sm" onClick={() => {
if (navigator.clipboard) navigator.clipboard.writeText('docker compose logs -f ' + logsModal.name).catch(() => {});
}}>Copy command</button>
<button className="btn primary sm" onClick={() => setLogsModal(null)}>Close</button>
</div>
</div>
</div>
)}
<div className="page-body">
{containers === null && (
<div style={{ textAlign: 'center', padding: '64px 0', color: 'var(--text-3)' }}>Loading</div>
)}
{containers !== null && containers.length === 0 && (
<div style={{ textAlign: 'center', padding: '64px 0', color: 'var(--text-3)' }}>
<div style={{ fontSize: 32, marginBottom: 12 }}>🐳</div>
<div style={{ fontWeight: 500, fontSize: 14 }}>No containers returned</div>
<div style={{ fontSize: 12, marginTop: 6 }}>Confirm <code>/var/run/docker.sock</code> is mounted in the mam-api container</div>
</div>
)}
{containers !== null && containers.length > 0 && (
<div className="panel">
<div className="container-row head">
<div>Container</div>
<div>Image</div>
<div>State</div>
<div>CPU</div>
<div>Memory</div>
<div>Ports</div>
<div></div>
</div>
{containers.map(c => (
<div key={c.id || c.name} className="container-row">
<div>
<div style={{ fontWeight: 500, fontSize: 13 }}>{c.name}</div>
<div className="mono" style={{ fontSize: 11, color: "var(--text-3)" }}>up {c.uptime}</div>
</div>
<div className="mono" style={{ fontSize: 11.5, color: "var(--text-2)" }}>{c.image}</div>
<div>
<span className="badge success"><StatusDot status="online" /> RUNNING</span>
{c.healthy && <span style={{ fontSize: 10.5, color: "var(--success)", marginLeft: 6 }}>healthy</span>}
</div>
<div className="mono" style={{ fontSize: 11.5 }}>
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<div style={{ width: 40, height: 4, background: "var(--bg-3)", borderRadius: 99, overflow: "hidden" }}>
<div style={{ width: `${Math.min((c.cpu || 0) * 4, 100)}%`, height: "100%", background: "var(--accent)" }} />
</div>
<span>{(c.cpu || 0).toFixed(1)}%</span>
</div>
</div>
<div className="mono" style={{ fontSize: 11.5 }}>{c.mem} MB</div>
<div className="mono" style={{ fontSize: 10.5, color: "var(--text-3)" }}>{c.ports}</div>
<div style={{ display: "flex", gap: 4 }}>
<button className="btn ghost sm" onClick={() => showLogs(c)}>Logs</button>
<button className="btn ghost sm" onClick={() => restartContainer(c)}>Restart</button>
</div>
</div>
))}
</div>
)}
</div>
</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
// BmdCardPanel - capture-card section inside the Cluster node detail panel.
// Shows port chips with live video-presence dots AND the BMD SVG card diagram.
// ────────────────────────────────────────────────────────────────────────────
function BmdCardPanel({ sel, portSignals }) {
const svgRef = React.useRef(null);
// Build the port-index → signal-entry map for the selected node.
const nodeSignalMap = React.useMemo(() => {
const map = new Map();
sel.bmdPorts.forEach((p) => {
const key = `${sel.dbId}:${p.index}`;
const entry = portSignals[key];
if (entry) map.set(p.index, entry.signal);
});
return map;
}, [sel.dbId, sel.bmdPorts, portSignals]);
// (Re-)render the SVG card diagram whenever the node or signals change.
React.useEffect(() => {
if (!svgRef.current || !window.BMDCards) return;
if (sel.bmdPorts.length === 0) return;
svgRef.current.innerHTML = '';
const svg = window.BMDCards.render({
model: sel.bmdPorts[0].model || '',
deviceCount: sel.bmdCount,
compact: true,
portSignals: nodeSignalMap,
});
svgRef.current.appendChild(svg);
}, [sel.dbId, sel.bmdCount, nodeSignalMap]);
return (
<div>
<div style={{ fontSize: 11, color: "var(--text-3)", marginBottom: 6, display: "flex", alignItems: "center", gap: 6 }}>
<Icon name="video" size={11} />
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
Capture cards {sel.bmdCount > 0 ? `(${sel.bmdCount} port${sel.bmdCount !== 1 ? 's' : ''})` : '(none reported)'}
</div>
{sel.bmdPorts.length === 0 && (
<div style={{ fontSize: 11.5, color: "var(--text-4)", padding: "4px 0" }}>No DeckLink cards detected on this node</div>
)}
{sel.bmdPorts.length > 0 && (
<div style={{ padding: "8px 10px", background: "var(--bg-2)", borderRadius: 5, border: "1px solid rgba(91,124,250,0.2)" }}>
{/* Card header */}
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 8 }}>
<Icon name="video" size={13} style={{ color: "var(--accent)" }} />
<span style={{ fontSize: 12, fontWeight: 600, color: "var(--text-1)" }}>
{sel.bmdPorts[0].model || "Blackmagic DeckLink"}
</span>
<span style={{ marginLeft: "auto", fontSize: 10, fontWeight: 600, padding: "2px 6px", borderRadius: 3, background: "rgba(91,124,250,0.15)", color: "var(--accent)" }}>
{sel.bmdCount} PORT{sel.bmdCount !== 1 ? 'S' : ''}
</span>
</div>
{/* Port chips with signal state */}
<div style={{ display: "flex", flexWrap: "wrap", gap: 4, marginBottom: 10 }}>
{sel.bmdPorts.map((p) => {
const sigEntry = portSignals[`${sel.dbId}:${p.index}`];
const sig = sigEntry ? sigEntry.signal : (p.online !== false ? null : 'offline');
const { label, color } = _signalChip(sig);
const isReceiving = sig === 'receiving';
return (
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 key={p.index} title={sigEntry ? `${sigEntry.recorder_name || 'recorder'}: ${label}` : label}
style={{
display: "flex", alignItems: "center", gap: 5,
fontSize: 10.5, fontFamily: "var(--font-mono)",
padding: "3px 8px", borderRadius: 3,
background: isReceiving ? "rgba(45,212,168,0.1)" : "rgba(255,255,255,0.04)",
border: `1px solid ${isReceiving ? "rgba(45,212,168,0.3)" : "var(--border)"}`,
}}>
{/* Signal presence dot */}
<span style={{
width: 6, height: 6, borderRadius: "50%", flexShrink: 0,
background: sig ? color : "var(--text-4)",
animation: isReceiving ? "signalPulse 1.4s ease-in-out infinite" : "none",
}} />
<span style={{ color: "var(--text-2)" }}>
{p.device ? p.device.split('/').pop() : `port ${p.index}`}
</span>
{sig && (
<span style={{ color, fontSize: 9, fontWeight: 700, marginLeft: 2, letterSpacing: "0.04em" }}>
{label}
</span>
)}
{sigEntry && sigEntry.currentFps != null && (
<span style={{ color: "var(--text-4)", fontSize: 9 }}>
{Number(sigEntry.currentFps).toFixed(1)} fps
</span>
)}
</div>
);
})}
</div>
{/* BMD SVG card diagram */}
<div ref={svgRef} className="bmd-card-diagram" />
</div>
)}
</div>
);
}
// Signal state → { label, color } for the port chip indicator.
function _signalChip(sig) {
switch (sig) {
case 'receiving': return { label: 'RECEIVING', color: 'var(--success)' };
case 'connecting': return { label: 'CONNECTING', color: 'var(--accent)' };
case 'lost': return { label: 'LOST', color: 'var(--danger)' };
case 'error': return { label: 'ERROR', color: 'var(--danger)' };
case 'idle': return { label: 'IDLE', color: 'var(--text-3)' };
case 'no-recorder': return { label: 'NO RECORDER', color: 'var(--text-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
default: return { label: sig || '·', color: 'var(--text-4)' };
}
}
function Cluster() {
const [nodesData, setNodesData] = React.useState(window.ZAMPP_DATA.NODES);
const [hovered, setHovered] = React.useState(null);
// Map of "node_id:portIndex" → signal entry from /cluster/devices/blackmagic/signal
const [portSignals, setPortSignals] = React.useState({});
const refresh = React.useCallback(() => {
window.ZAMPP_API.fetch('/cluster')
.then(data => {
window.ZAMPP_DATA.NODES = data;
setNodesData(data);
})
.catch(() => {});
}, []);
// Poll live video-presence state for all DeckLink ports every 5 s.
React.useEffect(() => {
const poll = () => {
window.ZAMPP_API.fetch('/cluster/devices/blackmagic/signal')
.then(entries => {
const map = {};
(entries || []).forEach(e => { map[`${e.node_id}:${e.index}`] = e; });
setPortSignals(map);
})
.catch(() => {});
};
poll();
const id = setInterval(poll, 5000);
return () => clearInterval(id);
}, []);
const nodesArr = Array.isArray(nodesData) ? nodesData : (nodesData?.nodes || []);
const NODES = React.useMemo(() => {
if (!nodesArr.length) return [];
const primaryRaw = nodesArr.find(n => n.role === 'primary') || nodesArr[0];
const others = nodesArr.filter(n => n !== primaryRaw);
const primary = _normalizeNode(primaryRaw, 0.5, 0.46);
const positioned = others.map((n, i) => {
const angle = others.length <= 1
? Math.PI / 2
: (i / others.length) * 2 * Math.PI - Math.PI / 2;
return _normalizeNode(n, 0.5 + 0.32 * Math.cos(angle), 0.46 + 0.35 * Math.sin(angle));
});
return [primary, ...positioned];
}, [nodesData]);
const [selected, setSelected] = React.useState(null);
const sel = selected || NODES[0] || null;
const W = 720, H = 460;
if (!NODES.length) {
return (
<div className="page">
<div className="page-header">
<h1>Cluster</h1>
<div className="spacer" />
<button className="btn ghost sm" onClick={refresh}><Icon name="refresh" />Refresh</button>
</div>
<div className="page-body">
<div style={{ textAlign: 'center', padding: '64px 0', color: 'var(--text-3)' }}>No cluster nodes available</div>
</div>
</div>
);
}
const primary = NODES.find(n => n.role === 'primary') || NODES[0];
const edges = NODES.filter(n => n.id !== primary.id).map(n => ({
from: primary,
to: n,
alive: n.status === 'online',
}));
const [adviceModal, setAdviceModal] = React.useState(null); // {title, lines:[], commands?}
const addNode = () => setAdviceModal({
title: 'Add a worker node',
lines: [
'Worker nodes auto-register with the cluster on first heartbeat.',
'Run these on the new host (replace 10.0.0.25 with this MAM\'s IP):',
],
commands: [
'git clone https://forge.wilddragon.net/zgaetano/dragonflight.git /opt/dragonflight',
'cd /opt/dragonflight && cp .env.example .env && nano .env # set NODE_ROLE=worker, point at MAM IP',
'docker compose -f docker-compose.worker.yml up -d',
],
});
const drainNode = (node) => setAdviceModal({
title: `Drain ${node.id}`,
lines: [
'Automated drain isn\'t implemented yet. The safe sequence is:',
'1. Stop scheduling new jobs to this node (kill its node-agent).',
'2. Let in-progress jobs finish.',
'3. Remove the node from cluster membership.',
],
commands: [`ssh ${node.ip || node.id} 'docker stop node-agent'`],
});
const removeNode = (node) => {
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
if (!window.confirm('Remove node ' + node.id + ' from the cluster?\nThis does not stop the machine: it only removes it from cluster membership.')) return;
window.ZAMPP_API.fetch('/cluster/nodes/' + encodeURIComponent(node.dbId || node.id), { method: 'DELETE' })
.then(() => refresh())
.catch(e => setAdviceModal({ title: 'Remove failed', lines: [e.message] }));
};
const nodeLogsHint = (node) => setAdviceModal({
title: `Logs for ${node.id}`,
lines: ['Live log streaming over the websocket isn\'t wired yet. SSH to the host and tail there:'],
commands: [`ssh ${node.ip || node.id} 'docker compose -f /opt/dragonflight/docker-compose.yml logs -f'`],
});
return (
<div className="page">
<div className="page-header">
<h1>Cluster</h1>
<span className="subtitle">{NODES.filter(n => n.status === "online").length} of {NODES.length} nodes online</span>
<div className="spacer" />
<div className="status-pip"><span className="dot" /><span>Live</span></div>
<button className="btn ghost sm" onClick={refresh}><Icon name="refresh" />Refresh</button>
<button className="btn primary" onClick={addNode}><Icon name="plus" />Add node</button>
</div>
<div className="page-body">
<div className="stat-row" style={{ padding: 0, marginBottom: 16 }}>
<div className="stat-card">
<div className="label"><Icon name="cluster" size={12} />Nodes</div>
<div className="value">{NODES.length}</div>
</div>
<div className="stat-card">
<div className="label"><Icon name="cpu" size={12} />Avg CPU</div>
<div className="value">{Math.round(NODES.reduce((a, n) => a + (n.cpu || 0), 0) / NODES.length)} <span style={{ fontSize: 14, color: "var(--text-3)" }}>%</span></div>
</div>
<div className="stat-card">
<div className="label"><Icon name="gpu" size={12} />GPUs</div>
<div className="value">{NODES.reduce((a, n) => a + n.gpuCount, 0)}</div>
</div>
<div className="stat-card">
<div className="label"><Icon name="video" size={12} />Capture ports</div>
<div className="value">{NODES.reduce((a, n) => a + n.bmdCount, 0)}</div>
</div>
<div className="stat-card">
<div className="label"><Icon name="hdd" size={12} />Avg Memory</div>
<div className="value">{Math.round(NODES.reduce((a, n) => a + (n.mem || 0), 0) / NODES.length)} <span style={{ fontSize: 14, color: "var(--text-3)" }}>GB</span></div>
</div>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 340px", gap: 16, alignItems: "start" }}>
<div className="cluster-canvas">
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "12px 16px", borderBottom: "1px solid var(--border)" }}>
<span style={{ fontWeight: 600, fontSize: 13 }}>Topology</span>
<span style={{ fontSize: 11, color: 'var(--text-3)' }}>{NODES.length} node{NODES.length === 1 ? '' : 's'}</span>
</div>
<svg viewBox={`0 0 ${W} ${H}`} style={{ display: "block", width: "100%", height: "auto" }}>
<defs>
<radialGradient id="nodeGlow">
<stop offset="0%" stopColor="rgba(91,124,250,0.3)" />
<stop offset="100%" stopColor="rgba(91,124,250,0)" />
</radialGradient>
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="rgba(255,255,255,0.025)" strokeWidth="1" />
</pattern>
</defs>
<rect width={W} height={H} fill="url(#grid)" />
{edges.map((e, i) => {
const x1 = e.from.x * W, y1 = e.from.y * H;
const x2 = e.to.x * W, y2 = e.to.y * H;
return (
<g key={i}>
<line x1={x1} y1={y1} x2={x2} y2={y2}
stroke={e.alive ? "var(--accent)" : "var(--text-4)"}
strokeWidth="1"
strokeDasharray={e.alive ? "0" : "4 3"}
opacity={e.alive ? 0.5 : 0.25}
/>
{e.alive && (
<circle r="3" fill="var(--accent)">
<animateMotion dur={`${2 + i * 0.4}s`} repeatCount="indefinite"
path={`M ${x1} ${y1} L ${x2} ${y2}`} />
</circle>
)}
</g>
);
})}
{NODES.map(n => {
const cx = n.x * W, cy = n.y * H;
const isSelected = sel && sel.id === n.id;
const color = n.status === "online" ? "var(--success)" : "var(--text-4)";
return (
<g key={n.id} transform={`translate(${cx}, ${cy})`}
style={{ cursor: "pointer" }}
onMouseEnter={() => setHovered(n.id)}
onMouseLeave={() => setHovered(null)}
onClick={() => setSelected(n)}>
{n.status === "online" && (
<circle r="44" fill="url(#nodeGlow)">
<animate attributeName="r" values="34;48;34" dur="3s" repeatCount="indefinite" />
</circle>
)}
<circle r={isSelected ? 26 : 22} fill="var(--bg-2)" stroke={isSelected ? "var(--accent)" : "var(--border-stronger)"} strokeWidth={isSelected ? 2 : 1} />
<circle r="6" cx="-13" cy="-13" fill={color} />
{n.role === "primary" && <path d="M -4 -2 L 0 2 L 4 -2 L 0 -6 Z" fill="var(--accent)" stroke="none" />}
{n.role !== "primary" && <text textAnchor="middle" y="3" fill="var(--text-2)" fontSize="10" fontFamily="var(--font-mono)">{n.role[0].toUpperCase()}</text>}
<text textAnchor="middle" y="40" fill={isSelected ? "var(--text-1)" : "var(--text-2)"} fontSize="11" fontWeight={isSelected ? 600 : 500}>{n.id}</text>
<text textAnchor="middle" y="54" fill="var(--text-3)" fontSize="10" fontFamily="var(--font-mono)">{n.ip}</text>
{(n.gpuCount > 0 || n.bmdCount > 0) && (
<text textAnchor="middle" y="67" fill="var(--text-4)" fontSize="9.5" fontFamily="var(--font-mono)">
{[n.gpuCount > 0 && `${n.gpuCount}×GPU`, n.bmdCount > 0 && `${n.bmdCount}×BMD`].filter(Boolean).join(' ')}
</text>
)}
</g>
);
})}
</svg>
</div>
{sel && (
<div className="panel">
<div style={{ padding: "12px 16px", borderBottom: "1px solid var(--border)", display: "flex", alignItems: "center", gap: 8 }}>
<StatusDot status={sel.status} />
<span style={{ fontWeight: 600, fontSize: 13 }}>{sel.id}</span>
<span className={`badge ${sel.role === "primary" ? "accent" : "neutral"}`}>{sel.role}</span>
</div>
<div style={{ padding: 14, display: "flex", flexDirection: "column", gap: 10 }}>
<DetailRow k="Status" v={<span style={{ color: sel.status === "online" ? "var(--success)" : "var(--text-3)" }}>{sel.status}</span>} />
<DetailRow k="IP" v={sel.ip} mono />
<DetailRow k="Version" v={sel.version} mono />
<DetailRow k="Uptime" v={sel.uptime} mono />
<DetailRow k="CPU" v={
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<div style={{ width: 80, height: 4, background: "var(--bg-3)", borderRadius: 99, overflow: "hidden" }}>
<div style={{ width: `${sel.cpu}%`, height: "100%", background: "var(--accent)" }} />
</div>
<span className="mono">{sel.cpu}%</span>
</div>
} />
{sel.memTotal > 0 && (
<DetailRow k="Memory" v={
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<div style={{ width: 80, height: 4, background: "var(--bg-3)", borderRadius: 99, overflow: "hidden" }}>
<div style={{ width: `${(sel.mem / sel.memTotal) * 100}%`, height: "100%", background: "var(--purple)" }} />
</div>
<span className="mono">{sel.mem} / {sel.memTotal} GB</span>
</div>
} />
)}
{/* ── GPU hardware ── */}
<div>
<div style={{ fontSize: 11, color: "var(--text-3)", marginBottom: 6, display: "flex", alignItems: "center", gap: 6 }}>
<Icon name="gpu" size={11} />
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
GPUs {sel.gpuCount > 0 ? `(${sel.gpuCount})` : '(none reported)'}
</div>
{sel.gpus.length === 0 && (
<div style={{ fontSize: 11.5, color: "var(--text-4)", padding: "4px 0" }}>No GPUs detected on this node</div>
)}
{sel.gpus.map((g, i) => (
<div key={i} style={{
padding: "7px 10px", background: "var(--bg-2)", borderRadius: 5, marginBottom: 4,
border: g.bound ? "1px solid rgba(91,250,138,0.25)" : "1px solid var(--border)",
display: "flex", alignItems: "center", gap: 8,
}}>
<Icon name="gpu" size={12} style={{ color: g.bound ? "var(--success)" : "var(--text-3)", flexShrink: 0 }} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 12, fontWeight: 600, color: "var(--text-1)" }}>{g.name}</div>
{g.memMb && (
<div style={{ fontSize: 11, color: "var(--text-3)", fontFamily: "var(--font-mono)", marginTop: 1 }}>
{g.memMb >= 1024 ? (g.memMb / 1024).toFixed(1) + ' GB' : g.memMb + ' MB'} VRAM
</div>
)}
{g.device && <div style={{ fontSize: 10.5, color: "var(--text-4)", fontFamily: "var(--font-mono)" }}>{g.device}</div>}
</div>
<span style={{
fontSize: 10, fontWeight: 600, padding: "2px 6px", borderRadius: 3,
background: g.bound ? "rgba(91,250,138,0.15)" : "rgba(255,255,255,0.05)",
color: g.bound ? "var(--success)" : "var(--text-3)",
}}>
{g.bound ? "BOUND" : "UNBOUND"}
</span>
</div>
))}
</div>
{/* ── Capture cards ── */}
<BmdCardPanel sel={sel} portSignals={portSignals} />
<div style={{ display: "flex", gap: 6, marginTop: 6 }}>
<button className="btn ghost sm" onClick={() => nodeLogsHint(sel)}>Logs</button>
<button className="btn ghost sm" onClick={() => drainNode(sel)}>Drain</button>
{sel.role !== "primary" && <button className="btn danger sm" onClick={() => removeNode(sel)}>Remove</button>}
</div>
</div>
</div>
)}
</div>
</div>
{adviceModal && (
<div className="modal-backdrop" onClick={() => setAdviceModal(null)}>
<div className="modal" style={{ width: 560 }} onClick={e => e.stopPropagation()}>
<div className="modal-head">
<div style={{ fontSize: 15, fontWeight: 600 }}>{adviceModal.title}</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={() => setAdviceModal(null)}><Icon name="x" /></button>
</div>
<div className="modal-body">
{(adviceModal.lines || []).map((l, i) => (
<div key={i} style={{ fontSize: 12.5, color: 'var(--text-2)', marginBottom: 6, lineHeight: 1.55 }}>{l}</div>
))}
{(adviceModal.commands || []).map((c, i) => (
<code key={i} className="mono" style={{ display: 'block', background: 'var(--bg-2)', padding: 10, borderRadius: 5, fontSize: 11.5, marginTop: 8, overflowX: 'auto' }}>{c}</code>
))}
</div>
<div className="modal-foot">
{adviceModal.commands && adviceModal.commands.length > 0 && (
<button className="btn ghost sm" onClick={() => {
if (navigator.clipboard) navigator.clipboard.writeText(adviceModal.commands.join('\n')).catch(() => {});
}}>Copy commands</button>
)}
<button className="btn primary sm" onClick={() => setAdviceModal(null)}>Got it</button>
</div>
</div>
</div>
)}
</div>
);
}
function DetailRow({ k, v, mono }) {
return (
<div style={{ display: "grid", gridTemplateColumns: "90px 1fr", alignItems: "center", fontSize: 12 }}>
<span style={{ color: "var(--text-3)" }}>{k}</span>
<span className={mono ? "mono" : ""} style={{ fontSize: mono ? 11.5 : 12 }}>{v}</span>
</div>
);
}
function AccountSection() {
const [current, setCurrent] = React.useState('');
const [next, setNext] = React.useState('');
const [confirm, setConfirm] = React.useState('');
const [msg, setMsg] = React.useState(null); // { kind: 'ok'|'err', text }
const [busy, setBusy] = React.useState(false);
const submit = async () => {
setMsg(null);
if (next !== confirm) { setMsg({ kind: 'err', text: 'Passwords do not match' }); return; }
if (next.length < 12) { setMsg({ kind: 'err', text: 'New password must be at least 12 characters' }); return; }
setBusy(true);
try {
const r = await fetch('/api/v1/auth/password', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'dragonflight-ui' },
body: JSON.stringify({ current_password: current, new_password: next }),
});
if (r.status === 204) {
setMsg({ kind: 'ok', text: 'Password updated' });
setCurrent(''); setNext(''); setConfirm('');
} else {
const body = await r.json().catch(() => ({}));
setMsg({ kind: 'err', text: body.error || 'Failed (' + r.status + ')' });
}
} finally { setBusy(false); }
};
return (
<section className="panel" style={{ padding: 16, marginBottom: 16 }}>
<h3 style={{ fontSize: 10.5, fontWeight: 600, color: 'var(--text-3)', letterSpacing: '0.08em', textTransform: 'uppercase', marginBottom: 12 }}>Account</h3>
<div style={{ display: 'grid', gridTemplateColumns: '160px 1fr', gap: 10, alignItems: 'center', maxWidth: 480 }}>
<label>Current password</label>
<input type="password" autoComplete="current-password" className="field-input" value={current} onChange={e => setCurrent(e.target.value)} />
<label>New password</label>
<input type="password" autoComplete="new-password" className="field-input" value={next} onChange={e => setNext(e.target.value)} />
<label>Confirm new password</label>
<input type="password" autoComplete="new-password" className="field-input" value={confirm} onChange={e => setConfirm(e.target.value)} />
</div>
{msg && (
<div style={{ marginTop: 10, fontSize: 11.5, color: msg.kind === 'ok' ? 'var(--success)' : 'var(--danger)' }}>{msg.text}</div>
)}
<button className="btn primary sm" style={{ marginTop: 12 }} disabled={busy || !current || !next || !confirm} onClick={submit}>
Change password
</button>
</section>
);
}
// Two-factor (TOTP) enrollment + management. Reflects window.ZAMPP_DATA.ME.totp_enabled.
function TotpSection() {
const me = window.ZAMPP_DATA?.ME || {};
const [enabled, setEnabled] = React.useState(!!me.totp_enabled);
const [phase, setPhase] = React.useState('idle'); // idle | enrolling | recovery
const [enroll, setEnroll] = React.useState(null); // { secret, otpauth_uri, qr }
const [code, setCode] = React.useState('');
const [recovery, setRecovery] = React.useState(null); // string[]
const [disablePw, setDisablePw] = React.useState('');
const [showDisable, setShowDisable] = React.useState(false);
const [msg, setMsg] = React.useState(null);
const [busy, setBusy] = React.useState(false);
const api = (path, body) => fetch('/api/v1/auth' + path, {
method: 'POST', credentials: 'include',
headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'dragonflight-ui' },
body: JSON.stringify(body || {}),
});
const startSetup = async () => {
setMsg(null); setBusy(true);
try {
const r = await api('/totp/setup');
if (r.status === 200) { setEnroll(await r.json()); setPhase('enrolling'); }
else setMsg({ kind: 'err', text: (await r.json().catch(() => ({}))).error || 'Setup failed' });
} finally { setBusy(false); }
};
const confirmEnable = async () => {
setMsg(null); setBusy(true);
try {
const r = await api('/totp/enable', { code: code.trim() });
const body = await r.json().catch(() => ({}));
if (r.status === 200) {
setRecovery(body.recovery_codes || []); setPhase('recovery');
setEnabled(true); setCode('');
window.ZAMPP_DATA.ME = { ...(window.ZAMPP_DATA.ME || {}), totp_enabled: true };
} else setMsg({ kind: 'err', text: body.error || 'Could not enable' });
} finally { setBusy(false); }
};
const disable = async () => {
setMsg(null); setBusy(true);
try {
const r = await api('/totp/disable', { password: disablePw });
if (r.status === 204) {
setEnabled(false); setShowDisable(false); setDisablePw(''); setPhase('idle');
window.ZAMPP_DATA.ME = { ...(window.ZAMPP_DATA.ME || {}), totp_enabled: false };
setMsg({ kind: 'ok', text: 'Two-factor disabled' });
} else setMsg({ kind: 'err', text: (await r.json().catch(() => ({}))).error || 'Could not disable' });
} finally { setBusy(false); }
};
return (
<section className="panel" style={{ padding: 16, marginBottom: 16 }}>
<h3 style={{ fontSize: 10.5, fontWeight: 600, color: 'var(--text-3)', letterSpacing: '0.08em', textTransform: 'uppercase', marginBottom: 12 }}>Two-factor authentication</h3>
{/* Status line */}
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: phase === 'idle' ? 0 : 14 }}>
<span className={`badge ${enabled ? 'success' : 'neutral'}`}>{enabled ? 'Enabled' : 'Disabled'}</span>
<span style={{ fontSize: 12, color: 'var(--text-3)' }}>
{enabled ? 'An authenticator code is required at sign-in.' : 'Add a time-based code from an authenticator app.'}
</span>
<span style={{ flex: 1 }} />
{!enabled && phase === 'idle' && <button className="btn primary sm" disabled={busy} onClick={startSetup}>Set up</button>}
{enabled && phase !== 'recovery' && !showDisable && <button className="btn ghost sm danger" onClick={() => setShowDisable(true)}>Disable</button>}
</div>
{/* Enrolling: show QR / secret + code field */}
{phase === 'enrolling' && enroll && (
<div style={{ display: 'grid', gridTemplateColumns: '160px 1fr', gap: 16, alignItems: 'start' }}>
<div>
{enroll.qr
? <img src={enroll.qr} alt="TOTP QR code" style={{ width: 160, height: 160, borderRadius: 6, background: '#fff', padding: 6 }} />
: <div style={{ fontSize: 11.5, color: 'var(--text-3)' }}>Scan the secret below in your app.</div>}
</div>
<div>
<div style={{ fontSize: 12, color: 'var(--text-2)', marginBottom: 8 }}>
Scan the QR with Google Authenticator, Authy, or 1Password or enter this secret manually:
</div>
<div className="mono" style={{ fontSize: 12, background: 'var(--bg-2)', padding: '6px 10px', borderRadius: 5, wordBreak: 'break-all', marginBottom: 12 }}>{enroll.secret}</div>
<div className="field">
<label className="field-label">Enter the 6-digit code to confirm</label>
<input className="field-input mono" value={code} autoFocus
onChange={e => setCode(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter' && code.trim()) confirmEnable(); }}
placeholder="123456" style={{ width: 140 }} />
</div>
<div style={{ marginTop: 10 }}>
<button className="btn primary sm" disabled={busy || !code.trim()} onClick={confirmEnable}>Enable</button>
<button className="btn ghost sm" style={{ marginLeft: 8 }} onClick={() => { setPhase('idle'); setEnroll(null); setCode(''); }}>Cancel</button>
</div>
</div>
</div>
)}
{/* Recovery codes — shown exactly once */}
{phase === 'recovery' && recovery && (
<div>
<div style={{ fontSize: 12, color: 'var(--text-2)', marginBottom: 8 }}>
Save these recovery codes somewhere safe. Each works once if you lose your authenticator. They won't be shown again.
</div>
<div className="mono" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6, background: 'var(--bg-2)', padding: 12, borderRadius: 6, fontSize: 13, marginBottom: 10 }}>
{recovery.map(c => <div key={c}>{c}</div>)}
</div>
<button className="btn sm" onClick={() => navigator.clipboard && navigator.clipboard.writeText(recovery.join('\n'))}>Copy codes</button>
<button className="btn primary sm" style={{ marginLeft: 8 }} onClick={() => { setPhase('idle'); setRecovery(null); }}>Done</button>
</div>
)}
{/* Disable confirmation */}
{showDisable && (
<div style={{ marginTop: 12, display: 'grid', gridTemplateColumns: '160px 1fr auto auto', gap: 8, alignItems: 'end' }}>
<div className="field" style={{ marginBottom: 0, gridColumn: '1 / 3' }}>
<label className="field-label">Confirm your password to disable</label>
<input className="field-input" type="password" value={disablePw} autoComplete="current-password"
onChange={e => setDisablePw(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter' && disablePw) disable(); }} />
</div>
<button className="btn danger sm" disabled={busy || !disablePw} onClick={disable}>Disable 2FA</button>
<button className="btn ghost sm" onClick={() => { setShowDisable(false); setDisablePw(''); }}>Cancel</button>
</div>
)}
{msg && <div style={{ marginTop: 10, fontSize: 11.5, color: msg.kind === 'ok' ? 'var(--success)' : 'var(--danger)' }}>{msg.text}</div>}
</section>
);
}
function ApiTokensSection() {
const [tokens, setTokens] = React.useState([]);
const [name, setName] = React.useState('');
const [justCreated, setJustCreated] = React.useState(null); // { token, prefix, name }
const [busy, setBusy] = React.useState(false);
const load = React.useCallback(async () => {
const r = await fetch('/api/v1/auth/tokens', { credentials: 'include' });
if (r.status === 200) setTokens(await r.json());
}, []);
React.useEffect(() => { load(); }, [load]);
const create = async () => {
if (!name.trim()) return;
setBusy(true);
try {
const r = await fetch('/api/v1/auth/tokens', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'dragonflight-ui' },
body: JSON.stringify({ name: name.trim() }),
});
if (r.status === 201) {
const created = await r.json();
setJustCreated(created);
setName('');
await load();
}
} finally { setBusy(false); }
};
const revoke = async (id) => {
await fetch('/api/v1/auth/tokens/' + id, {
method: 'DELETE',
credentials: 'include',
headers: { 'X-Requested-With': 'dragonflight-ui' },
});
await load();
};
return (
<section className="panel" style={{ padding: 16, marginBottom: 16 }}>
<h3 style={{ fontSize: 10.5, fontWeight: 600, color: 'var(--text-3)', letterSpacing: '0.08em', textTransform: 'uppercase', marginBottom: 12 }}>API Tokens</h3>
{justCreated && (
<div style={{ marginBottom: 12, padding: 10, border: '1px solid var(--accent)', background: 'var(--accent-soft)', borderRadius: 6 }}>
<div style={{ fontSize: 11, fontWeight: 600, color: 'var(--accent-text)', marginBottom: 6 }}>
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
Save this token now: it will not be shown again
</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--text-1)', wordBreak: 'break-all', marginBottom: 6 }}>{justCreated.token}</div>
<button className="btn sm" onClick={() => navigator.clipboard.writeText(justCreated.token)}>Copy</button>
<button className="btn sm" style={{ marginLeft: 8 }} onClick={() => setJustCreated(null)}>Dismiss</button>
</div>
)}
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
<input className="field-input" placeholder="Token name (e.g. Premiere panel)" value={name} onChange={e => setName(e.target.value)} style={{ flex: 1 }} />
<button className="btn primary sm" disabled={busy || !name.trim()} onClick={create}>New token</button>
</div>
<div className="token-list">
{tokens.length === 0 && <div style={{ fontSize: 11.5, color: 'var(--text-3)' }}>No tokens yet.</div>}
{tokens.map(t => (
<div key={t.id} className="token-row" style={{ display: 'grid', gridTemplateColumns: '1fr 120px 140px 80px', gap: 10, alignItems: 'center', padding: '8px 0', borderBottom: '1px solid var(--border)' }}>
<div>{t.name}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text-2)' }}>{t.prefix}</div>
<div style={{ fontSize: 11, color: 'var(--text-3)' }}>{t.last_used_at ? new Date(t.last_used_at).toLocaleString() : 'never used'}</div>
<button className="btn sm" onClick={() => revoke(t.id)}>Revoke</button>
</div>
))}
</div>
</section>
);
}
function Settings() {
const [section, setSection] = React.useState('account');
const SECTIONS = [
{ id: 'account', label: 'Account', icon: 'user' },
{ id: 'storage', label: 'Storage', icon: 'hdd' },
{ id: 'proxy', label: 'Proxy encoding', icon: 'gpu' },
{ id: 'sdk', label: 'Capture SDKs', icon: 'video' },
{ id: 'sdi', label: 'SDI capture', icon: 'video' },
];
return (
<div className="page">
<div className="page-header">
<h1>Settings</h1>
<span className="subtitle">System configuration · changes apply without restart</span>
</div>
<div className="page-body">
<div style={{ display: 'grid', gridTemplateColumns: '200px 1fr', gap: 24, alignItems: 'start' }}>
<nav className="settings-nav">
{SECTIONS.map(s => (
<a key={s.id}
className={`settings-nav-item ${section === s.id ? 'active' : ''}`}
onClick={() => setSection(s.id)}
style={{ cursor: 'pointer' }}>
<Icon name={s.icon} size={14} />{s.label}
</a>
))}
</nav>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{section === 'account' && (
<>
<AccountSection />
<TotpSection />
<ApiTokensSection />
</>
)}
{section === 'storage' && <StorageSection />}
{section === 'proxy' && <GpuSettingsCard />}
{section === 'sdk' && <SdkSettingsCard />}
{section === 'sdi' && <SdiSettingsCard />}
</div>
</div>
</div>
</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
// Storage - unified view: live mount/bucket health on top, then the two
// existing editors (S3 bucket + growing-files SMB landing zone) stacked.
// ────────────────────────────────────────────────────────────────────────────
function StorageSection() {
return (
<>
<MountHealthStrip />
<S3SettingsCard />
<GrowingSettingsCard />
</>
);
}
function formatBytes(n) {
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
if (n == null || isNaN(n)) return '·';
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
let v = n, i = 0;
while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; }
return `${v.toFixed(v >= 100 || i === 0 ? 0 : 1)} ${units[i]}`;
}
function HealthPill({ ok, label, detail }) {
const cls = ok ? 'badge success' : 'badge warning';
return (
<span className={cls} title={detail || ''} style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
<span style={{ width: 6, height: 6, borderRadius: 999, background: 'currentColor', display: 'inline-block' }} />
{label}
</span>
);
}
function MountHealthStrip() {
const [data, setData] = React.useState(null);
const [error, setError] = React.useState(null);
const [refreshing, setRefresh] = React.useState(false);
const load = React.useCallback(() => {
setRefresh(true);
window.ZAMPP_API.fetch('/storage/overview')
.then(d => { setData(d); setError(null); })
.catch(e => setError(e.message || String(e)))
.finally(() => setRefresh(false));
}, []);
React.useEffect(() => {
load();
// Light auto-refresh so free-space + reachability stay current while the
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
// operator is on the page. 15s is plenty - these are diagnostic, not real-time.
const t = setInterval(load, 15_000);
return () => clearInterval(t);
}, [load]);
if (error) {
return (
<SettingsCard icon="hdd" title="Mount health" sub="Live diagnostics for the storage subsystem"
tag={<span className="badge warning">unavailable</span>}>
<SettingsMsg msg={{ ok: false, text: 'Could not load /storage/overview: ' + error }} />
</SettingsCard>
);
}
if (!data) {
return (
<SettingsCard icon="hdd" title="Mount health" sub="Live diagnostics for the storage subsystem">
<div style={{ color: 'var(--text-3)', fontSize: 12.5 }}>Probing</div>
</SettingsCard>
);
}
const g = data.growing;
const s = data.s3;
const growingHealthy = g.enabled ? (g.exists && g.writable) : true;
return (
<SettingsCard icon="hdd" title="Mount health" sub="Live diagnostics for the storage subsystem"
tag={
<button className="btn ghost sm" onClick={load} disabled={refreshing} title="Re-probe now"
style={{ padding: '2px 8px' }}>
{refreshing ? '…' : 'Refresh'}
</button>
}>
{/* ── Growing-files row ─────────────────────────────────────────────── */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
<strong style={{ fontSize: 12.5 }}>Growing files</strong>
{g.enabled
? <HealthPill ok={growingHealthy} label={growingHealthy ? 'mounted' : 'unreachable'} detail={g.error || ''} />
: <span className="badge neutral">disabled</span>}
{g.enabled && g.exists && (
<HealthPill ok={g.writable} label={g.writable ? 'writable' : 'read-only'} />
)}
{g.free_bytes != null && (
<span className="badge neutral" title={g.total_bytes ? `of ${formatBytes(g.total_bytes)} total` : ''}>
{formatBytes(g.free_bytes)} free
</span>
)}
</div>
<div style={{ fontSize: 11.5, color: 'var(--text-3)', display: 'grid', gridTemplateColumns: 'auto 1fr', columnGap: 10, rowGap: 2 }}>
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>Container</span><span className="mono">{g.container_path || '·'}</span>
<span>Host</span><span className="mono">{g.host_path || '·'}</span>
<span>SMB</span><span className="mono">{g.smb_url || '·'}</span>
<span>Promote idle</span><span className="mono">{g.promote_after_seconds}s</span>
{g.error && <><span>Error</span><span style={{ color: 'var(--danger)' }}>{g.error}</span></>}
</div>
</div>
<div style={{ height: 1, background: 'var(--border-1, rgba(255,255,255,0.06))', margin: '10px 0' }} />
{/* ── S3 bucket row ─────────────────────────────────────────────────── */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
<strong style={{ fontSize: 12.5 }}>S3 bucket</strong>
<HealthPill ok={s.reachable} label={s.reachable ? 'reachable' : 'unreachable'} detail={s.error || ''} />
{s.head_latency_ms != null && (
<span className="badge neutral">{s.head_latency_ms} ms</span>
)}
{s.probe_method && <span className="badge neutral">{s.probe_method}</span>}
</div>
<div style={{ fontSize: 11.5, color: 'var(--text-3)', display: 'grid', gridTemplateColumns: 'auto 1fr', columnGap: 10, rowGap: 2 }}>
<span>Endpoint</span><span className="mono">{s.endpoint || '(AWS default)'}</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>Bucket</span><span className="mono">{s.bucket || '·'}</span>
<span>Region</span><span className="mono">{s.region || '·'}</span>
{s.error && <><span>Error</span><span style={{ color: 'var(--danger)' }}>{s.error}</span></>}
</div>
</div>
</SettingsCard>
);
}
function S3SettingsCard() {
const [s3, setS3] = React.useState({ s3_endpoint: '', s3_bucket: '', s3_access_key: '', s3_secret_key: '', s3_region: 'us-east-1' });
const [loading, setLoading] = React.useState(true);
const [saving, setSaving] = React.useState(false);
const [testing, setTesting] = React.useState(false);
const [msg, setMsg] = React.useState(null);
const [secretExists, setSecretExists] = React.useState(false);
React.useEffect(() => {
window.ZAMPP_API.fetch('/settings/s3')
.then(data => {
// Diagnostic: previous reports of "endpoint always blank" were
// hard to chase without seeing the raw payload. Log it once on
// load so the next user can verify quickly.
try { console.debug('[settings] /settings/s3 →', data); } catch (_) {}
setS3({
s3_endpoint: data.s3_endpoint || '',
s3_bucket: data.s3_bucket || '',
s3_access_key: data.s3_access_key || '',
s3_secret_key: '',
s3_region: data.s3_region || 'us-east-1',
});
setSecretExists(!!data.s3_secret_key_exists);
setLoading(false);
})
.catch(err => {
console.error('[settings] /settings/s3 failed:', err);
setMsg({ ok: false, text: 'Could not load S3 settings: ' + (err.message || err) });
setLoading(false);
});
}, []);
const save = () => {
setSaving(true); setMsg(null);
window.ZAMPP_API.fetch('/settings/s3', { method: 'PUT', body: JSON.stringify(s3) })
.then(() => { setSaving(false); setMsg({ ok: true, text: 'Saved and applied.' }); if (s3.s3_secret_key) setSecretExists(true); })
.catch(e => { setSaving(false); setMsg({ ok: false, text: e.message }); });
};
const test = () => {
setTesting(true); setMsg(null);
window.ZAMPP_API.fetch('/settings/s3/test', { method: 'POST', body: JSON.stringify(s3) })
.then(r => { setTesting(false); setMsg({ ok: r.ok !== false, text: r.message || 'Connection OK' }); })
.catch(e => { setTesting(false); setMsg({ ok: false, text: e.message }); });
};
return (
<SettingsCard icon="hdd" title="S3 / Object Storage" sub="S3-compatible bucket for media asset storage"
tag={secretExists ? <span className="badge success">connected</span> : <span className="badge warning">not configured</span>}>
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
{loading ? <div style={{ color: 'var(--text-3)', fontSize: 12.5 }}>Loading</div> : (
<form onSubmit={e => { e.preventDefault(); save(); }} autoComplete="off">
<SField label="Endpoint URL">
<input className="field-input mono" type="url" required value={s3.s3_endpoint} onChange={e => setS3(p => ({...p, s3_endpoint: e.target.value}))} placeholder="https://s3.example.com" />
</SField>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<SField label="Region"><input className="field-input mono" required value={s3.s3_region} onChange={e => setS3(p => ({...p, s3_region: e.target.value}))} placeholder="us-east-1" /></SField>
<SField label="Bucket"><input className="field-input mono" required value={s3.s3_bucket} onChange={e => setS3(p => ({...p, s3_bucket: e.target.value}))} placeholder="my-bucket" /></SField>
</div>
<SField label="Access key ID"><input className="field-input mono" required value={s3.s3_access_key} onChange={e => setS3(p => ({...p, s3_access_key: e.target.value}))} placeholder="Access key ID" autoComplete="off" /></SField>
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
<SField label="Secret access key"><input className="field-input mono" type="password" value={s3.s3_secret_key} onChange={e => setS3(p => ({...p, s3_secret_key: e.target.value}))} placeholder={secretExists ? '(saved: type to replace)' : 'Secret key'} autoComplete="new-password" /></SField>
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
<SettingsMsg msg={msg} />
<div style={{ display: 'flex', gap: 6, marginTop: 8 }}>
<button type="submit" className="btn primary sm" disabled={saving}>{saving ? 'Saving…' : 'Save & apply'}</button>
<button type="button" className="btn ghost sm" onClick={test} disabled={testing}>{testing ? 'Testing…' : 'Test connection'}</button>
</div>
</form>
)}
</SettingsCard>
);
}
function GpuSettingsCard() {
const [cfg, setCfg] = React.useState(null);
const [saving, setSaving] = React.useState(false);
const [msg, setMsg] = React.useState(null);
React.useEffect(() => {
window.ZAMPP_API.fetch('/settings/transcoding').then(setCfg).catch(() => setCfg({}));
}, []);
const save = () => {
setSaving(true); setMsg(null);
window.ZAMPP_API.fetch('/settings/transcoding', { method: 'PUT', body: JSON.stringify(cfg) })
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
.then(() => { setSaving(false); setMsg({ ok: true, text: 'Saved: new settings apply to the next proxy job.' }); })
.catch(e => { setSaving(false); setMsg({ ok: false, text: e.message }); });
};
if (!cfg) return <SettingsCard icon="gpu" title="Proxy encoding" sub="Global proxy encoder applied to every ingested file"><div style={{ color: 'var(--text-3)', fontSize: 12.5 }}>Loading</div></SettingsCard>;
const set = (k, v) => setCfg(c => ({ ...c, [k]: v }));
const gpuEnabled = cfg.gpu_transcode_enabled === 'true' || cfg.gpu_transcode_enabled === true;
return (
<SettingsCard icon="gpu" title="Proxy encoding" sub="Global proxy encoder applied to every ingested file"
tag={gpuEnabled ? <span className="badge success">GPU mode</span> : <span className="badge neutral">CPU mode</span>}>
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
<form onSubmit={e => { e.preventDefault(); save(); }} autoComplete="off">
<div style={{ fontSize: 12, color: 'var(--text-3)', lineHeight: 1.55, marginBottom: 4 }}>
These settings drive the proxy worker for <strong style={{ color: 'var(--text-2)' }}>every</strong> ingested asset (SDI, SRT, RTMP, upload). GPU mode uses NVENC / VAAPI when hardware is available; CPU mode uses libx264.
</div>
<SField label="Hardware acceleration">
<label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12.5 }}>
<input type="checkbox" checked={gpuEnabled} onChange={e => set('gpu_transcode_enabled', String(e.target.checked))} />
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 style={{ color: 'var(--text-2)' }}>Use GPU encoders (NVENC / VAAPI) when available: falls back to CPU on missing hardware</span>
</label>
</SField>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<SField label={gpuEnabled ? 'GPU codec' : 'CPU codec'}>
<select className="field-input" value={cfg.gpu_codec || 'h264_nvenc'} onChange={e => set('gpu_codec', e.target.value)} style={{ appearance: 'auto' }}>
{gpuEnabled ? (<>
<option value="h264_nvenc">h264_nvenc (NVIDIA)</option>
<option value="hevc_nvenc">hevc_nvenc (NVIDIA HEVC)</option>
<option value="h264_vaapi">h264_vaapi (Intel/AMD)</option>
<option value="hevc_vaapi">hevc_vaapi (Intel/AMD HEVC)</option>
</>) : (<>
<option value="libx264">libx264 (H.264, recommended)</option>
<option value="libx265">libx265 (HEVC, slower)</option>
</>)}
</select>
</SField>
<SField label="Preset">
<select className="field-input" value={cfg.gpu_preset || (gpuEnabled ? 'p4' : 'fast')} onChange={e => set('gpu_preset', e.target.value)} style={{ appearance: 'auto' }}>
{gpuEnabled
? ['p1','p2','p3','p4','p5','p6','p7'].map(p => <option key={p} value={p}>{p}</option>)
: ['ultrafast','superfast','veryfast','faster','fast','medium','slow','slower'].map(p => <option key={p} value={p}>{p}</option>)}
</select>
</SField>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<SField label="Target bitrate (Mbps)">
<input className="field-input mono" type="number" value={cfg.gpu_bitrate_mbps || ''} onChange={e => set('gpu_bitrate_mbps', e.target.value)} placeholder="10" />
</SField>
<SField label="Rate control">
<select className="field-input" value={cfg.gpu_rc_mode || 'cbr'} onChange={e => set('gpu_rc_mode', e.target.value)} style={{ appearance: 'auto' }}>
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
<option value="cbr">CBR: constant bitrate</option>
<option value="vbr">VBR: variable bitrate</option>
<option value="cqp">CQP / CRF: constant quality</option>
</select>
</SField>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<SField label="Audio codec">
<select className="field-input" value={cfg.gpu_audio_codec || 'aac'} onChange={e => set('gpu_audio_codec', e.target.value)} style={{ appearance: 'auto' }}>
<option value="aac">aac</option>
<option value="opus">opus</option>
<option value="mp3">mp3</option>
</select>
</SField>
<SField label="Audio bitrate (kbps)">
<input className="field-input mono" type="number" value={cfg.gpu_audio_bitrate_kbps || ''} onChange={e => set('gpu_audio_bitrate_kbps', e.target.value)} placeholder="192" />
</SField>
</div>
<SettingsMsg msg={msg} />
<div style={{ display: 'flex', gap: 6, marginTop: 8 }}>
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 type="submit" className="btn primary sm" disabled={saving}>{saving ? 'Saving…' : 'Save'}</button>
</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
</form>
</SettingsCard>
);
}
function GrowingSettingsCard() {
const [cfg, setCfg] = React.useState(null);
const [saving, setSaving] = React.useState(false);
const [msg, setMsg] = React.useState(null);
React.useEffect(() => {
window.ZAMPP_API.fetch('/settings/growing').then(setCfg).catch(() => setCfg({
growing_enabled: 'false', growing_path: '/growing', growing_smb_url: '', growing_promote_after_seconds: '8',
}));
}, []);
const save = () => {
setSaving(true); setMsg(null);
window.ZAMPP_API.fetch('/settings/growing', { method: 'PUT', body: JSON.stringify(cfg) })
.then(() => { setSaving(false); setMsg({ ok: true, text: 'Saved.' }); })
.catch(e => { setSaving(false); setMsg({ ok: false, text: e.message }); });
};
if (!cfg) return <SettingsCard icon="hdd" title="Growing files (SMB)" sub="Loading…"><div style={{color:'var(--text-3)'}}></div></SettingsCard>;
const set = (k, v) => setCfg(c => ({ ...c, [k]: v }));
const enabled = cfg.growing_enabled === 'true' || cfg.growing_enabled === true;
return (
<SettingsCard icon="hdd" title="Growing files (SMB)" sub="High-speed local landing zone; promote to S3 on stop"
tag={enabled ? <span className="badge success">enabled</span> : <span className="badge neutral">disabled</span>}>
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
<form onSubmit={e => { e.preventDefault(); save(); }} autoComplete="off">
<SField label="Enable growing-file capture">
<label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12.5 }}>
<input type="checkbox" checked={enabled} onChange={e => set('growing_enabled', String(e.target.checked))} />
<span style={{ color: 'var(--text-2)' }}>Capture writes to the local SMB share first; Premier can edit while it's still growing.</span>
</label>
</SField>
<SField label="Container mount path">
<input className="field-input mono" value={cfg.growing_path || ''} onChange={e => set('growing_path', e.target.value)} placeholder="/growing" />
</SField>
<SField label="SMB share URL (for editors)">
<input className="field-input mono" value={cfg.growing_smb_url || ''} onChange={e => set('growing_smb_url', e.target.value)} placeholder="smb://10.0.0.25/mam-growing" />
</SField>
<SField label="Promote-to-S3 idle threshold (seconds)">
<input className="field-input mono" type="number" value={cfg.growing_promote_after_seconds || ''} onChange={e => set('growing_promote_after_seconds', e.target.value)} placeholder="8" />
</SField>
<SettingsMsg msg={msg} />
<div style={{ display: 'flex', gap: 6, marginTop: 8 }}>
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 type="submit" className="btn primary sm" disabled={saving}>{saving ? 'Saving…' : 'Save'}</button>
</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
</form>
</SettingsCard>
);
}
function SdiSettingsCard() {
return (
<SettingsCard icon="video" title="SDI capture" sub="DeckLink device routing and defaults"
tag={<span className="badge neutral">per-recorder</span>}>
<div style={{ color: 'var(--text-3)', fontSize: 12.5, lineHeight: 1.6 }}>
SDI settings are configured per-recorder. Use{' '}
<strong style={{ color: 'var(--text-2)' }}>Ingest Recorders New recorder</strong>{' '}
to pick the DeckLink port, codec, and audio routing.
</div>
<div style={{ marginTop: 12 }}>
<a className="btn ghost sm" href="#" onClick={e => { e.preventDefault(); window.dispatchEvent(new CustomEvent('dragonflight-navigate', { detail: 'capture' })); }}>
<Icon name="video" />Open Capture dashboard
</a>
</div>
</SettingsCard>
);
}
// ────────────────────────────────────────────────────────────────────────────
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
// Capture SDK deployment - Blackmagic / AJA / Deltacast
// ────────────────────────────────────────────────────────────────────────────
const SDK_VENDORS = [
{
id: 'blackmagic',
name: 'Blackmagic DeckLink',
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
sub: 'DeckLink SDK 16.x: required for SDI capture via DeckLink cards',
expect: 'DeckLinkAPI.h, DeckLinkAPIDispatch.cpp, LinuxCOM.h, libDeckLinkAPI.so',
docs: 'https://www.blackmagicdesign.com/developer/product/capture',
buildHint: 'docker compose build --no-cache capture',
status: 'wired',
},
{
id: 'aja',
name: 'AJA NTV2',
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
sub: 'NTV2 SDK: for Kona / Io / U-Tap / T-Tap cards',
expect: 'libajantv2.so, ntv2card.h, ntv2enums.h',
docs: 'https://sdksupport.aja.com/',
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
buildHint: 'FFmpeg patch + Dockerfile update pending: files will be staged for the next capture image build',
status: 'staging-only',
},
{
id: 'deltacast',
name: 'Deltacast VideoMaster',
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
sub: 'VideoMasterHD SDK: for FLEX / DELTA-h4k2 / etc.',
expect: 'VideoMasterHD_Core.h, libVideoMasterHD_Core.so',
docs: 'https://www.deltacast.tv/products/sdk',
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
buildHint: 'FFmpeg patch + Dockerfile update pending: files will be staged for the next capture image build',
status: 'staging-only',
},
];
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
// Premiere panel releases - single source of truth lives on `window.PREMIERE_RELEASES`
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
// (see data.jsx). Local alias for readability.
const PREMIERE_RELEASES = window.PREMIERE_RELEASES;
function SdkSettingsCard() {
const [statuses, setStatuses] = React.useState(null);
const [msg, setMsg] = React.useState(null);
const load = React.useCallback(() => {
window.ZAMPP_API.fetch('/sdk').then(setStatuses).catch(() => setStatuses({}));
}, []);
React.useEffect(() => { load(); }, [load]);
return (
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
<SettingsCard icon="video" title="Capture SDKs" sub="Vendor SDKs are licensed: upload them here so the capture container can build with hardware support"
tag={<span className="badge neutral">{SDK_VENDORS.length} vendors</span>}>
{/* ── Premiere Panel download section ── */}
<div style={{ marginBottom: 18 }}>
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-2)', marginBottom: 8, textTransform: 'uppercase', letterSpacing: '0.06em' }}>
Premiere Pro Panel
</div>
<div style={{ fontSize: 12, color: 'var(--text-3)', lineHeight: 1.55, marginBottom: 10 }}>
The Dragonflight UXP plugin enables growing-file editing, batch trim, and one-click hi-res relink directly inside Premiere Pro.
Install the <strong style={{ color: 'var(--text-2)' }}>.ccx</strong> via the <a href="https://developer.adobe.com/photoshop/uxp/2022/guides/devtool/installation/" target="_blank" rel="noreferrer" style={{ color: 'var(--accent)' }}>Adobe UXP Developer Tool</a>,
or double-click it with Creative Cloud installed.
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{PREMIERE_RELEASES.map(r => (
<div key={r.version} style={{ border: '1px solid var(--border)', borderRadius: 6, padding: '10px 12px', background: 'var(--bg-2)', display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<strong style={{ fontSize: 13 }}>v{r.version}</strong>
{r.latest && <span className="badge success">latest</span>}
</div>
<div style={{ fontSize: 11.5, color: 'var(--text-3)', marginTop: 2 }}>{r.notes}</div>
</div>
{r.ccx && (
<a href={r.ccx} download style={{ textDecoration: 'none' }}>
<button className="btn ghost sm">UXP (.ccx)</button>
</a>
)}
{r.installer && (
<a href={r.installer} download style={{ textDecoration: 'none' }}>
<button className="btn ghost sm">Win Installer</button>
</a>
)}
</div>
))}
</div>
</div>
<div style={{ borderTop: '1px solid var(--border)', marginBottom: 14 }} />
{/* ── Capture SDK upload section ── */}
<div style={{ fontSize: 12, color: 'var(--text-3)', lineHeight: 1.55, marginBottom: 4 }}>
Each SDK archive should be a <strong style={{ color: 'var(--text-2)' }}>.zip</strong> or <strong style={{ color: 'var(--text-2)' }}>.tar.gz</strong> containing the vendor's Linux SDK contents. After uploading, rebuild the capture container on the host with a DeckLink/AJA/Deltacast card. The SDK files are staged under <code className="mono" style={{ fontSize: 11.5 }}>/sdk/&lt;vendor&gt;/</code> inside mam-api.
</div>
<SettingsMsg msg={msg} />
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
{SDK_VENDORS.map(v => (
<SdkVendorRow
key={v.id}
vendor={v}
status={(statuses && statuses[v.id]) || null}
onDone={(text, ok = true) => { setMsg({ ok, text }); load(); }}
/>
))}
</div>
</SettingsCard>
);
}
function SdkVendorRow({ vendor, status, onDone }) {
const fileRef = React.useRef(null);
const [uploading, setUploading] = React.useState(false);
const [progress, setProgress] = React.useState(0);
const deployed = status && status.file_count > 0;
const lastUpload = status?.uploaded_at
? new Date(status.uploaded_at).toLocaleString()
: null;
const handleFile = async (file) => {
if (!file) return;
setUploading(true); setProgress(0);
const fd = new FormData();
fd.append('archive', file);
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
// Use XHR so we can report progress to the user - fetch's stream API is fiddly.
await new Promise((resolve) => {
const xhr = new XMLHttpRequest();
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
xhr.open('POST', (window.ZAMPP_API_PREFIX || '/api/v1') + '/sdk/' + vendor.id);
xhr.setRequestHeader('X-Requested-With', 'dragonflight-ui');
xhr.withCredentials = true;
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) setProgress(Math.round((e.loaded / e.total) * 100));
};
xhr.onload = () => {
setUploading(false); setProgress(0);
if (xhr.status >= 200 && xhr.status < 300) {
onDone(vendor.name + ': SDK staged.', true);
} else {
let txt = xhr.responseText;
try { txt = JSON.parse(xhr.responseText).error || txt; } catch {}
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
onDone(vendor.name + ': upload failed: ' + txt, false);
}
resolve();
};
xhr.onerror = () => {
setUploading(false); setProgress(0);
onDone(vendor.name + ': network error', false);
resolve();
};
xhr.send(fd);
});
};
const clear = () => {
if (!confirm('Remove staged ' + vendor.name + ' SDK files?')) return;
window.ZAMPP_API.fetch('/sdk/' + vendor.id, { method: 'DELETE' })
.then(() => onDone(vendor.name + ': cleared.', true))
.catch(e => onDone(vendor.name + ': ' + e.message, false));
};
return (
<div style={{ border: '1px solid var(--border)', borderRadius: 6, padding: 12, background: 'var(--bg-2)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<strong style={{ fontSize: 13 }}>{vendor.name}</strong>
{deployed
? <span className="badge success">deployed · {status.file_count} files</span>
: <span className="badge neutral">not deployed</span>}
{vendor.status === 'staging-only' && <span className="badge warning" title={vendor.buildHint}>build pipeline pending</span>}
<div style={{ flex: 1 }} />
{deployed && <button className="btn ghost sm" onClick={clear}>Remove</button>}
<button className="btn primary sm" onClick={() => fileRef.current?.click()} disabled={uploading}>
{uploading ? `Uploading ${progress}%` : (deployed ? 'Replace' : 'Upload SDK')}
</button>
<input ref={fileRef} type="file" accept=".zip,.tar.gz,.tgz,.tar"
style={{ display: 'none' }}
onChange={e => handleFile(e.target.files?.[0])} />
</div>
<div style={{ fontSize: 11.5, color: 'var(--text-3)', lineHeight: 1.55 }}>
{vendor.sub}<br />
<span className="mono" style={{ fontSize: 11 }}>expects: {vendor.expect}</span>
{lastUpload && <><br /><span style={{ color: 'var(--text-3)' }}>uploaded: {lastUpload}</span></>}
{deployed && <><br /><span className="mono" style={{ fontSize: 11, color: 'var(--text-3)' }}>on host: rebuild with {vendor.buildHint}</span></>}
</div>
</div>
);
}
function AmppSettingsCard() {
const [cfg, setCfg] = React.useState(null);
const [saving, setSaving] = React.useState(false);
const [testing, setTesting] = React.useState(false);
const [msg, setMsg] = React.useState(null);
const [tokenExists, setTokenExists] = React.useState(false);
React.useEffect(() => {
window.ZAMPP_API.fetch('/settings/ampp').then(d => {
setCfg({ ampp_base_url: d.ampp_base_url || '', ampp_token: '' });
setTokenExists(!!d.ampp_token_exists);
}).catch(() => setCfg({ ampp_base_url: '', ampp_token: '' }));
}, []);
const save = () => {
setSaving(true); setMsg(null);
window.ZAMPP_API.fetch('/settings/ampp', { method: 'PUT', body: JSON.stringify(cfg) })
.then(() => { setSaving(false); setMsg({ ok: true, text: 'Saved.' }); if (cfg.ampp_token) setTokenExists(true); })
.catch(e => { setSaving(false); setMsg({ ok: false, text: e.message }); });
};
const test = () => {
setTesting(true); setMsg(null);
window.ZAMPP_API.fetch('/settings/ampp/test', { method: 'POST', body: JSON.stringify(cfg) })
.then(r => { setTesting(false); setMsg({ ok: true, text: r.message || 'Connection OK' }); })
.catch(e => { setTesting(false); setMsg({ ok: false, text: e.message }); });
};
if (!cfg) return <SettingsCard icon="link" title="AMPP integration" sub="Loading…"><div style={{color:'var(--text-3)'}}></div></SettingsCard>;
return (
<SettingsCard icon="link" title="AMPP integration" sub="Migrate assets and metadata from Grass Valley AMPP"
tag={tokenExists ? <span className="badge success">connected</span> : <span className="badge neutral">not configured</span>}>
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
<form onSubmit={e => { e.preventDefault(); save(); }} autoComplete="off">
<SField label="AMPP base URL">
<input className="field-input mono" type="url" required value={cfg.ampp_base_url} onChange={e => setCfg(p => ({...p, ampp_base_url: e.target.value}))} placeholder="https://my-org.gvampp.tv" />
</SField>
<SField label="API token">
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
<input className="field-input mono" type="password" value={cfg.ampp_token} onChange={e => setCfg(p => ({...p, ampp_token: e.target.value}))} placeholder={tokenExists ? '(saved: type to replace)' : 'AMPP API token'} autoComplete="new-password" />
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
</SField>
<SettingsMsg msg={msg} />
<div style={{ display: 'flex', gap: 6, marginTop: 8 }}>
<button type="submit" className="btn primary sm" disabled={saving}>{saving ? 'Saving…' : 'Save'}</button>
<button type="button" className="btn ghost sm" onClick={test} disabled={testing}>{testing ? 'Testing…' : 'Test connection'}</button>
</div>
</form>
</SettingsCard>
);
}
function SettingsMsg({ msg }) {
if (!msg) return null;
return (
<div style={{ fontSize: 12, padding: '6px 10px', borderRadius: 5, border: '1px solid',
background: msg.ok ? 'var(--success-soft)' : 'var(--danger-soft)',
borderColor: msg.ok ? 'var(--success)' : 'var(--danger)',
color: msg.ok ? 'var(--success)' : 'var(--danger)' }}>
{msg.text}
</div>
);
}
function SField({ label, children }) {
return (
<div className="field">
<label className="field-label">{label}</label>
{children}
</div>
);
}
function SettingsCard({ icon, title, sub, tag, children }) {
return (
<div className="settings-card">
<div className="settings-card-head">
<div className="settings-card-icon"><Icon name={icon} size={16} /></div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontWeight: 600, fontSize: 14 }}>{title}</div>
<div style={{ fontSize: 12, color: "var(--text-3)", marginTop: 2 }}>{sub}</div>
</div>
{tag}
</div>
<div className="settings-card-body">{children}</div>
</div>
);
}
Object.assign(window, { Users, Tokens, Containers, Cluster, Settings });