diff --git a/services/web-ui/public/screens-home.jsx b/services/web-ui/public/screens-home.jsx index 03de81e..badc885 100644 --- a/services/web-ui/public/screens-home.jsx +++ b/services/web-ui/public/screens-home.jsx @@ -1,6 +1,164 @@ // screens-home.jsx +// +// Two routes share this file: +// +// • Home — the launcher. Big-button entry into each section of the MAM. +// This is what you see when you log in / hit /. Resembles the original +// first-version landing page. +// +// • Dashboard — the operations view (recent activity, job queue, cluster, +// live recorders). Reachable from the sidebar or from the Home launcher. +// This is the React component that used to be `Home`. function Home({ navigate }) { + // Pull live counts so the tile subtitles ("34 assets", "0 live", "3 running") + // reflect what's actually in the DB right now, not a stale boot-time cache. + const [cards, setCards] = React.useState({}); + React.useEffect(() => { + let cancelled = false; + const load = () => { + window.ZAMPP_API.fetch('/metrics/home?hours=1') + .then(d => { if (!cancelled) setCards(d?.cards || {}); }) + .catch(() => {}); + }; + load(); + const t = setInterval(load, 30_000); + return () => { cancelled = true; clearInterval(t); }; + }, []); + + const { ASSETS = [], RECORDERS = [], JOBS = [], NODES = [] } = window.ZAMPP_DATA || {}; + const assetsTotal = cards.assets?.total ?? ASSETS.length; + const liveCount = cards.recorders?.live ?? RECORDERS.filter(r => r.status === 'recording').length; + const totalRecs = cards.recorders?.total ?? RECORDERS.length; + const runningJobs = cards.jobs?.running ?? JOBS.filter(j => j.status === 'running' || j.status === 'queued').length; + const failedJobs = cards.jobs?.failed_total ?? JOBS.filter(j => j.status === 'failed').length; + const nodesOnline = cards.cluster?.online ?? NODES.filter(n => n.status === 'online' || n.online === true).length; + const nodesTotal = cards.cluster?.total ?? NODES.length; + + const tiles = [ + { + id: 'library', + label: 'Library', + icon: 'library', + tone: 'accent', + sub: assetsTotal > 0 ? assetsTotal.toLocaleString() + ' assets' : 'No assets yet', + desc: 'Browse projects, bins, and assets. Hover-scrub previews.', + }, + { + id: 'recorders', + label: 'Recorders', + icon: 'record', + tone: 'live', + sub: liveCount > 0 + ? liveCount + ' live · ' + totalRecs + ' configured' + : totalRecs + ' configured', + desc: 'SDI · SRT · RTMP ingest. Start, stop, schedule.', + }, + { + id: 'editor', + label: 'Editor', + icon: 'editor', + tone: 'purple', + sub: 'Beta', + desc: 'Timeline editor with cross-clip preview and render queue.', + }, + { + id: 'jobs', + label: 'Jobs', + icon: 'jobs', + tone: failedJobs > 0 ? 'warn' : 'success', + sub: runningJobs > 0 + ? runningJobs + ' running' + (failedJobs > 0 ? ' · ' + failedJobs + ' failed' : '') + : (failedJobs > 0 ? failedJobs + ' failed' : 'All clear'), + desc: 'Proxy + thumbnail queue. Retry failed jobs.', + }, + { + id: 'settings', + label: 'Settings', + icon: 'settings', + tone: 'neutral', + sub: 'S3 · Encoder · Growing files', + desc: 'Storage, proxy encoder, capture SDK, growing-file mode.', + }, + ]; + + const clusterHealthy = !nodesTotal || nodesOnline >= nodesTotal; + + return ( +
+ + Self-hosted broadcast media-asset management +
+{liveCount > 0 ? liveCount + ' live · ' : ''} {runningCount > 0 ? runningCount + ' job' + (runningCount > 1 ? 's running' : ' running') + ' · ' : ''} @@ -204,3 +361,4 @@ function MiniJobRow({ job }) { } window.Home = Home; +window.Dashboard = Dashboard;