Add Z-AMPP UI: screens-home + screens-library: screens-home.jsx
This commit is contained in:
parent
001533fdf0
commit
100fc054cc
1 changed files with 182 additions and 0 deletions
182
services/web-ui/public/screens-home.jsx
Normal file
182
services/web-ui/public/screens-home.jsx
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
// screens-home.jsx - home dashboard
|
||||||
|
|
||||||
|
const { ACTIVITY, RECORDERS, JOBS, ASSETS } = window.ZAMPP_DATA;
|
||||||
|
|
||||||
|
function Home({ navigate }) {
|
||||||
|
const sparkAssets = [12, 14, 13, 18, 16, 22, 25, 24, 28, 30, 32, 34, 38];
|
||||||
|
const sparkIngest = [4, 6, 5, 8, 9, 12, 10, 14, 13, 18, 20, 22, 24];
|
||||||
|
const sparkJobs = [8, 7, 10, 9, 14, 12, 16, 13, 18, 15, 12, 10, 11];
|
||||||
|
const sparkStorage = [42, 44, 46, 47, 48, 50, 52, 54, 56, 58, 60, 62, 64];
|
||||||
|
|
||||||
|
const liveRecorders = RECORDERS.filter(r => r.status === "recording").slice(0, 4);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page">
|
||||||
|
<div className="home-greeting">
|
||||||
|
<h1>Good evening, Zach</h1>
|
||||||
|
<p>4 recorders live · 3 jobs running · cluster healthy across 4 nodes</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="stat-row">
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="label"><Icon name="library" size={12} /> Library</div>
|
||||||
|
<div className="value">1,553</div>
|
||||||
|
<div className="delta up">+38 today</div>
|
||||||
|
<Sparkline data={sparkAssets} color="#5B7CFA" />
|
||||||
|
</div>
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="label"><Icon name="upload" size={12} /> Ingest (24h)</div>
|
||||||
|
<div className="value">412 GB</div>
|
||||||
|
<div className="delta up">+82 GB last hr</div>
|
||||||
|
<Sparkline data={sparkIngest} color="#2DD4A8" />
|
||||||
|
</div>
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="label"><Icon name="jobs" size={12} /> Jobs queued</div>
|
||||||
|
<div className="value">3<span style={{ color: "var(--text-3)", fontWeight: 400, fontSize: 16 }}> / 247 done</span></div>
|
||||||
|
<div className="delta">avg 4.2 min</div>
|
||||||
|
<Sparkline data={sparkJobs} color="#B57CFA" />
|
||||||
|
</div>
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="label"><Icon name="hdd" size={12} /> Object store</div>
|
||||||
|
<div className="value">64%<span style={{ color: "var(--text-3)", fontWeight: 400, fontSize: 16 }}> of 18 TB</span></div>
|
||||||
|
<div className="delta">11.5 TB used</div>
|
||||||
|
<Sparkline data={sparkStorage} color="#F5A623" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="home-grid">
|
||||||
|
<div>
|
||||||
|
<SectionHead title="Live now" onMore={() => navigate("recorders")} moreLabel="All recorders" />
|
||||||
|
<div className="live-feed-grid">
|
||||||
|
{liveRecorders.map((r, i) => (
|
||||||
|
<div key={r.id} className="live-feed-tile" onClick={() => navigate("recorders")}>
|
||||||
|
<div className="live-feed-tile-badge">
|
||||||
|
<span className="badge live">REC</span>
|
||||||
|
</div>
|
||||||
|
<FauxFrame seed={i + 1} />
|
||||||
|
<div className="scanlines" />
|
||||||
|
<div className="live-feed-tile-label">
|
||||||
|
<span className="name">{r.name}</span>
|
||||||
|
<span className="time">{r.elapsed}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ height: 16 }} />
|
||||||
|
|
||||||
|
<SectionHead title="Recent activity" />
|
||||||
|
<div className="activity-feed">
|
||||||
|
{ACTIVITY.map(a => (
|
||||||
|
<div key={a.id} className="activity-row">
|
||||||
|
<div className={`activity-icon ${a.kind}`}>
|
||||||
|
<Icon name={iconForKind(a.kind)} size={12} />
|
||||||
|
</div>
|
||||||
|
<div className="activity-text">
|
||||||
|
<strong>{a.who}</strong> {a.what} <span className="target">{a.target}</span>
|
||||||
|
</div>
|
||||||
|
<div className="activity-time">{a.time}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<SectionHead title="Job queue" onMore={() => navigate("jobs")} moreLabel="View all" />
|
||||||
|
<div className="panel" style={{ padding: 4 }}>
|
||||||
|
{JOBS.slice(0, 5).map(j => (
|
||||||
|
<MiniJobRow key={j.id} job={j} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ height: 16 }} />
|
||||||
|
<SectionHead title="Storage" />
|
||||||
|
<div className="panel" style={{ padding: 16 }}>
|
||||||
|
<StorageBar />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SectionHead({ title, onMore, moreLabel = "View all" }) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", alignItems: "center", padding: "16px 0 10px", gap: 12 }}>
|
||||||
|
<div style={{ fontSize: 13, fontWeight: 600, letterSpacing: "-0.01em", whiteSpace: "nowrap" }}>{title}</div>
|
||||||
|
{onMore && (
|
||||||
|
<button className="btn ghost sm" style={{ marginLeft: "auto", whiteSpace: "nowrap" }} onClick={onMore}>
|
||||||
|
{moreLabel}<Icon name="arrowRight" size={11} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MiniJobRow({ job }) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: "10px 12px", display: "flex", alignItems: "center", gap: 10, borderBottom: "1px solid var(--border)" }}>
|
||||||
|
<StatusDot status={job.status} />
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<div style={{ fontSize: 12, display: "flex", gap: 6, alignItems: "center" }}>
|
||||||
|
<span style={{ color: "var(--text-2)" }}>{job.kind}</span>
|
||||||
|
<span className="muted" style={{ fontFamily: "var(--font-mono)", fontSize: 10.5 }}>·</span>
|
||||||
|
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{job.asset}</span>
|
||||||
|
</div>
|
||||||
|
{job.status === "running" && (
|
||||||
|
<div style={{ marginTop: 5, height: 3, background: "var(--bg-3)", borderRadius: 99, overflow: "hidden" }}>
|
||||||
|
<div style={{ width: `${job.progress}%`, height: "100%", background: "var(--accent)", transition: "width 300ms" }} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{job.status === "failed" && (
|
||||||
|
<div style={{ marginTop: 3, fontSize: 11, color: "var(--danger)" }}>{job.error}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontFamily: "var(--font-mono)", fontSize: 10.5, color: "var(--text-3)", textAlign: "right", minWidth: 40 }}>
|
||||||
|
{job.status === "running" ? job.eta : job.status}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StorageBar() {
|
||||||
|
const segments = [
|
||||||
|
{ label: "Master files", color: "#5B7CFA", value: 38 },
|
||||||
|
{ label: "Proxies", color: "#2DD4A8", value: 12 },
|
||||||
|
{ label: "Live captures", color: "#FF3B30", value: 8 },
|
||||||
|
{ label: "Audio", color: "#B57CFA", value: 3 },
|
||||||
|
{ label: "Other", color: "#6B7280", value: 3 },
|
||||||
|
];
|
||||||
|
const total = segments.reduce((a, b) => a + b.value, 0);
|
||||||
|
const cap = 100;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", marginBottom: 12 }}>
|
||||||
|
<div style={{ fontSize: 22, fontWeight: 600, letterSpacing: "-0.02em" }}>
|
||||||
|
11.5 <span style={{ color: "var(--text-3)", fontSize: 14, fontWeight: 400 }}>TB / 18 TB</span>
|
||||||
|
</div>
|
||||||
|
<span className="badge accent">64% used</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", height: 10, borderRadius: 99, overflow: "hidden", background: "var(--bg-3)" }}>
|
||||||
|
{segments.map((s, i) => (
|
||||||
|
<div key={i} data-tip={`${s.label} — ${s.value}%`} style={{ width: `${(s.value / cap) * 100}%`, background: s.color }} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 12, display: "grid", gridTemplateColumns: "1fr 1fr", gap: 6 }}>
|
||||||
|
{segments.map((s, i) => (
|
||||||
|
<div key={i} style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 11.5 }}>
|
||||||
|
<span style={{ width: 8, height: 8, borderRadius: 2, background: s.color }} />
|
||||||
|
<span style={{ color: "var(--text-2)" }}>{s.label}</span>
|
||||||
|
<span style={{ marginLeft: "auto", color: "var(--text-3)", fontFamily: "var(--font-mono)", fontSize: 10.5 }}>{s.value}%</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function iconForKind(kind) {
|
||||||
|
return { comment: "comment", record: "record", job: "jobs", upload: "upload", sync: "refresh", approve: "check", error: "alert" }[kind] || "clock";
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Home = Home;
|
||||||
Loading…
Reference in a new issue