2026-05-22 10:02:55 -04:00
|
|
|
// app.jsx — main shell
|
2026-05-22 08:13:03 -04:00
|
|
|
|
2026-05-22 10:02:55 -04:00
|
|
|
const ACCENT = '#5B7CFA';
|
2026-05-22 08:13:03 -04:00
|
|
|
|
|
|
|
|
function App() {
|
2026-05-22 10:02:55 -04:00
|
|
|
const [route, setRoute] = React.useState('home');
|
2026-05-22 08:13:03 -04:00
|
|
|
const [openAsset, setOpenAsset] = React.useState(null);
|
|
|
|
|
const [openProject, setOpenProject] = React.useState(null);
|
|
|
|
|
const [showNewRecorder, setShowNewRecorder] = React.useState(false);
|
2026-05-22 10:02:55 -04:00
|
|
|
const [dataReady, setDataReady] = React.useState(false);
|
2026-05-22 08:13:03 -04:00
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
2026-05-22 10:02:55 -04:00
|
|
|
document.documentElement.style.setProperty('--accent', ACCENT);
|
|
|
|
|
document.documentElement.style.setProperty('--accent-soft', hexToRgba(ACCENT, 0.14));
|
|
|
|
|
document.documentElement.style.setProperty('--accent-soft-2', hexToRgba(ACCENT, 0.22));
|
|
|
|
|
document.documentElement.style.setProperty('--accent-text', lighten(ACCENT, 0.25));
|
|
|
|
|
document.documentElement.style.setProperty('--accent-hover', lighten(ACCENT, 0.08));
|
|
|
|
|
}, []);
|
2026-05-22 08:13:03 -04:00
|
|
|
|
2026-05-22 10:02:55 -04:00
|
|
|
React.useEffect(() => {
|
|
|
|
|
window.ZAMPP_API.loadData()
|
|
|
|
|
.then(() => setDataReady(true))
|
|
|
|
|
.catch(err => { console.error('[Dragonflight] load failed:', err); setDataReady(true); });
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const navigate = (id) => { setOpenAsset(null); setRoute(id); };
|
2026-05-22 08:13:03 -04:00
|
|
|
|
|
|
|
|
const crumbs = React.useMemo(() => {
|
|
|
|
|
if (openAsset) return [
|
2026-05-22 10:02:55 -04:00
|
|
|
{ label: 'Library', to: 'library' },
|
|
|
|
|
{ label: openAsset.project || 'Library', to: 'library' },
|
2026-05-22 08:13:03 -04:00
|
|
|
{ label: openAsset.name },
|
|
|
|
|
];
|
|
|
|
|
if (openProject) return [
|
2026-05-22 10:02:55 -04:00
|
|
|
{ label: 'Projects', to: 'projects' },
|
2026-05-22 08:13:03 -04:00
|
|
|
{ label: openProject.name },
|
|
|
|
|
];
|
|
|
|
|
const labels = {
|
2026-05-22 10:02:55 -04:00
|
|
|
home: ['Home'], library: ['Library'], projects: ['Projects'],
|
|
|
|
|
upload: ['Ingest', 'Upload'], recorders: ['Ingest', 'Recorders'],
|
feat(scheduler): recorder scheduling — UI, CRUD, tick loop, recurrence
- New Ingest → Schedule page: upcoming/past/all tabs, status badges
(pending / recording / completed / cancelled / failed), 10s
auto-refresh, cancel/delete actions
- New Schedule modal: name, recorder dropdown, datetime-local start/end,
recurrence (one-shot / daily / weekly), sensible defaults (+5min / +35min)
- Backend: migration 009 (recorder_schedules), routes/schedules.js
(list/create/edit/cancel/delete), scheduler.js tick loop polling every
15s; transitions trigger /recorders/:id/start and /stop via in-process
HTTP so we reuse the full container orchestration path
- Recurring schedules: tick loop auto-queues the next occurrence on
completion (daily = +24h, weekly = +7d)
- Sidebar + app.jsx route wired in, schedule-row table style added
2026-05-22 23:19:24 -04:00
|
|
|
schedule: ['Ingest', 'Schedule'],
|
2026-05-22 10:02:55 -04:00
|
|
|
capture: ['Ingest', 'Capture'], monitors: ['Ingest', 'Monitors'],
|
|
|
|
|
jobs: ['Jobs'], editor: ['Editor'],
|
|
|
|
|
users: ['Admin', 'Users & Groups'], tokens: ['Admin', 'Tokens'],
|
|
|
|
|
containers: ['Admin', 'Containers'], cluster: ['Admin', 'Cluster'],
|
|
|
|
|
settings: ['Admin', 'Settings'],
|
2026-05-22 08:13:03 -04:00
|
|
|
};
|
2026-05-22 10:02:55 -04:00
|
|
|
return (labels[route] || ['Home']).map(label => ({ label }));
|
2026-05-22 08:13:03 -04:00
|
|
|
}, [route, openAsset, openProject]);
|
|
|
|
|
|
2026-05-22 10:02:55 -04:00
|
|
|
if (!dataReady) {
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<style>{'@keyframes _df_spin{to{transform:rotate(360deg)}}'}</style>
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100vh', flexDirection: 'column', gap: 14, background: 'var(--bg-0)' }}>
|
|
|
|
|
<div style={{ width: 28, height: 28, borderRadius: '50%', border: '2px solid var(--border)', borderTopColor: ACCENT, animation: '_df_spin 0.8s linear infinite' }} />
|
|
|
|
|
<div style={{ fontSize: 12, color: 'var(--text-3)', fontFamily: 'var(--font-mono)' }}>Loading Dragonflight…</div>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-22 08:13:03 -04:00
|
|
|
let content;
|
|
|
|
|
if (openAsset) {
|
|
|
|
|
content = <AssetDetail asset={openAsset} onClose={() => setOpenAsset(null)} />;
|
|
|
|
|
} else {
|
|
|
|
|
switch (route) {
|
2026-05-22 10:02:55 -04:00
|
|
|
case 'home': content = <Home navigate={navigate} />; break;
|
|
|
|
|
case 'library': content = <Library navigate={navigate} onOpenAsset={setOpenAsset} openProject={openProject} />; break;
|
|
|
|
|
case 'projects': content = <Projects navigate={navigate} onOpenProject={(p) => { setOpenProject(p); setRoute('library'); }} />; break;
|
|
|
|
|
case 'upload': content = <Upload navigate={navigate} />; break;
|
|
|
|
|
case 'recorders': content = <Recorders navigate={navigate} onNew={() => setShowNewRecorder(true)} />; break;
|
feat(scheduler): recorder scheduling — UI, CRUD, tick loop, recurrence
- New Ingest → Schedule page: upcoming/past/all tabs, status badges
(pending / recording / completed / cancelled / failed), 10s
auto-refresh, cancel/delete actions
- New Schedule modal: name, recorder dropdown, datetime-local start/end,
recurrence (one-shot / daily / weekly), sensible defaults (+5min / +35min)
- Backend: migration 009 (recorder_schedules), routes/schedules.js
(list/create/edit/cancel/delete), scheduler.js tick loop polling every
15s; transitions trigger /recorders/:id/start and /stop via in-process
HTTP so we reuse the full container orchestration path
- Recurring schedules: tick loop auto-queues the next occurrence on
completion (daily = +24h, weekly = +7d)
- Sidebar + app.jsx route wired in, schedule-row table style added
2026-05-22 23:19:24 -04:00
|
|
|
case 'schedule': content = <Schedule navigate={navigate} />; break;
|
2026-05-22 10:02:55 -04:00
|
|
|
case 'capture': content = <Capture navigate={navigate} />; break;
|
|
|
|
|
case 'monitors': content = <Monitors navigate={navigate} />; break;
|
|
|
|
|
case 'jobs': content = <Jobs navigate={navigate} />; break;
|
|
|
|
|
case 'editor': content = <Editor />; break;
|
|
|
|
|
case 'users': content = <Users />; break;
|
|
|
|
|
case 'tokens': content = <Tokens />; break;
|
|
|
|
|
case 'containers':content = <Containers />; break;
|
|
|
|
|
case 'cluster': content = <Cluster />; break;
|
|
|
|
|
case 'settings': content = <Settings />; break;
|
|
|
|
|
default: content = <Home navigate={navigate} />;
|
2026-05-22 08:13:03 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2026-05-22 10:02:55 -04:00
|
|
|
<div className="app" data-density="comfortable" data-grid-size="md" data-sidebar="expanded">
|
|
|
|
|
<Sidebar active={openAsset ? 'library' : route} onNavigate={navigate} />
|
2026-05-22 08:13:03 -04:00
|
|
|
<div className="main">
|
|
|
|
|
{!openAsset && <Topbar crumbs={crumbs} onNavigate={navigate} />}
|
|
|
|
|
{content}
|
|
|
|
|
</div>
|
|
|
|
|
{showNewRecorder && <NewRecorderModal open={showNewRecorder} onClose={() => setShowNewRecorder(false)} />}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hexToRgba(hex, a) {
|
2026-05-22 10:02:55 -04:00
|
|
|
const h = hex.replace('#', '');
|
2026-05-22 08:13:03 -04:00
|
|
|
const r = parseInt(h.slice(0, 2), 16);
|
|
|
|
|
const g = parseInt(h.slice(2, 4), 16);
|
|
|
|
|
const b = parseInt(h.slice(4, 6), 16);
|
2026-05-22 10:02:55 -04:00
|
|
|
return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';
|
2026-05-22 08:13:03 -04:00
|
|
|
}
|
|
|
|
|
function lighten(hex, amt) {
|
2026-05-22 10:02:55 -04:00
|
|
|
const h = hex.replace('#', '');
|
2026-05-22 08:13:03 -04:00
|
|
|
const r = Math.min(255, parseInt(h.slice(0, 2), 16) + Math.round(amt * 255));
|
|
|
|
|
const g = Math.min(255, parseInt(h.slice(2, 4), 16) + Math.round(amt * 255));
|
|
|
|
|
const b = Math.min(255, parseInt(h.slice(4, 6), 16) + Math.round(amt * 255));
|
2026-05-22 10:02:55 -04:00
|
|
|
return 'rgb(' + r + ',' + g + ',' + b + ')';
|
2026-05-22 08:13:03 -04:00
|
|
|
}
|
|
|
|
|
|
2026-05-22 10:02:55 -04:00
|
|
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
2026-05-22 08:13:03 -04:00
|
|
|
root.render(<App />);
|