feat(home): restore launcher home page; move current home to Dashboard
The original first-version home page (big-button launcher with the
Dragonflight wordmark) is back at /. The Frame.io-style metrics +
recent-activity layout we've been treating as "home" is now the
Dashboard, reachable from the sidebar and from the launcher's
"Open dashboard" button.
- Renames existing Home → Dashboard (all the cards, sparklines, live
feed, job-queue, cluster mini-list are unchanged).
- New Home component: hero with the dragon-coiled-D logo (existing
img/dragon-logo.png), wordmark "DRAGONFLIGHT", a tag line, and 5
big tiles (Library, Recorders, Editor, Jobs, Settings) plus a
smaller Dashboard tile. Live cluster + recorder status pip at the
bottom mirrors what's in the topbar.
- The launcher pulls /metrics/home so the tile counts ("34 assets",
"0 live", "0 running") reflect reality.
This commit is contained in:
parent
7a6296585c
commit
72fc9cb755
1 changed files with 160 additions and 2 deletions
|
|
@ -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 (
|
||||
<div className="launcher">
|
||||
<div className="launcher-inner">
|
||||
<div className="launcher-hero">
|
||||
<img
|
||||
className="launcher-logo"
|
||||
src="img/dragon-logo.png"
|
||||
alt="Dragonflight"
|
||||
draggable="false"
|
||||
/>
|
||||
<h1 className="launcher-wordmark">DRAGONFLIGHT</h1>
|
||||
<p className="launcher-tagline">
|
||||
Self-hosted broadcast media-asset management
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="launcher-grid">
|
||||
{tiles.map(t => (
|
||||
<button
|
||||
key={t.id}
|
||||
className={'launcher-tile tone-' + t.tone}
|
||||
onClick={() => navigate(t.id)}
|
||||
>
|
||||
<span className="launcher-tile-icon">
|
||||
<Icon name={t.icon} size={26} />
|
||||
</span>
|
||||
<span className="launcher-tile-label">{t.label}</span>
|
||||
<span className="launcher-tile-sub">{t.sub}</span>
|
||||
<span className="launcher-tile-desc">{t.desc}</span>
|
||||
<span className="launcher-tile-arrow">
|
||||
<Icon name="arrowRight" size={14} />
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
|
||||
<button
|
||||
className="launcher-tile tone-ghost launcher-tile-secondary"
|
||||
onClick={() => navigate('dashboard')}
|
||||
>
|
||||
<span className="launcher-tile-icon">
|
||||
<Icon name="home" size={22} />
|
||||
</span>
|
||||
<span className="launcher-tile-label">Dashboard</span>
|
||||
<span className="launcher-tile-sub">Operations view</span>
|
||||
<span className="launcher-tile-desc">
|
||||
Recent activity, job queue, cluster health.
|
||||
</span>
|
||||
<span className="launcher-tile-arrow">
|
||||
<Icon name="arrowRight" size={14} />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="launcher-status">
|
||||
<span className="launcher-status-pip">
|
||||
<span
|
||||
className="dot"
|
||||
style={{ background: clusterHealthy ? 'var(--success)' : 'var(--warning)' }}
|
||||
/>
|
||||
{clusterHealthy ? 'cluster healthy' : 'cluster degraded'}
|
||||
<span className="muted">· {nodesOnline}/{nodesTotal || 0} node{nodesTotal === 1 ? '' : 's'}</span>
|
||||
</span>
|
||||
{liveCount > 0 && (
|
||||
<span className="launcher-status-pip live">
|
||||
<span className="dot" style={{ background: 'var(--live)' }} />
|
||||
{liveCount} recorder{liveCount === 1 ? '' : 's'} live
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Dashboard({ navigate }) {
|
||||
const { RECORDERS, JOBS, ASSETS, NODES } = window.ZAMPP_DATA;
|
||||
|
||||
// Live (current-state) data from the boot-time data load
|
||||
|
|
@ -38,13 +196,12 @@ function Home({ navigate }) {
|
|||
|
||||
// Sum the most recent hour of each bucketed series for the delta line so
|
||||
// the "+N this hour" hint always reflects the latest bucket.
|
||||
const lastBucket = (series) => (Array.isArray(series) && series.length ? series[series.length - 1].v : 0);
|
||||
const sumWindow = (series) => (Array.isArray(series) ? series.reduce((a, p) => a + p.v, 0) : 0);
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<div className="home-greeting">
|
||||
<h1>Dragonflight</h1>
|
||||
<h1>Dashboard</h1>
|
||||
<p>
|
||||
{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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue