Add Z-AMPP UI: screens-jobs + screens-editor + modal-new-recorder: screens-jobs.jsx

This commit is contained in:
Zac Gaetano 2026-05-22 08:19:01 -04:00
parent 90d2c1cf82
commit f8bd80e38e

View file

@ -0,0 +1,158 @@
// screens-jobs.jsx Jobs queue with real-time progress visualization
const { JOBS: ALL_JOBS } = window.ZAMPP_DATA;
function Jobs({ navigate }) {
const [tab, setTab] = React.useState("all");
const [jobs, setJobs] = React.useState(ALL_JOBS);
React.useEffect(() => {
const i = setInterval(() => {
setJobs(js => js.map(j => {
if (j.status !== "running") return j;
const next = Math.min(100, j.progress + Math.random() * 3);
if (next >= 100) return { ...j, progress: 100, status: "done", eta: "—" };
const [m, s] = j.eta.split(":").map(Number);
const total = m * 60 + s - 1;
const newEta = total > 0 ? `${String(Math.floor(total / 60)).padStart(2, "0")}:${String(total % 60).padStart(2, "0")}` : "00:00";
return { ...j, progress: next, eta: newEta };
}));
}, 1200);
return () => clearInterval(i);
}, []);
const counts = {
all: jobs.length,
running: jobs.filter(j => j.status === "running").length,
queued: jobs.filter(j => j.status === "queued").length,
done: jobs.filter(j => j.status === "done").length,
failed: jobs.filter(j => j.status === "failed").length,
};
const filtered = tab === "all" ? jobs : jobs.filter(j => j.status === tab);
const throughput = [12, 14, 13, 16, 15, 18, 22, 24, 22, 28, 32, 38, 42, 40];
return (
<div className="page">
<div className="page-header">
<h1>Jobs</h1>
<span className="subtitle">Proxy generation, transcoding, AMPP sync live across worker pool</span>
<div className="spacer" />
<button className="btn ghost sm"><Icon name="filter" />Filter</button>
<button className="btn ghost sm"><Icon name="refresh" />Refresh</button>
</div>
<div className="page-body">
<div className="jobs-stats">
<div className="stat-card">
<div className="label">Throughput</div>
<div className="value">42<span style={{ fontSize: 14, color: "var(--text-3)", fontWeight: 400 }}> / min</span></div>
<div className="delta up">+18% vs last hour</div>
<Sparkline data={throughput} color="#5B7CFA" />
</div>
<div className="stat-card">
<div className="label">Avg duration</div>
<div className="value">4.2<span style={{ fontSize: 14, color: "var(--text-3)", fontWeight: 400 }}> min</span></div>
<div className="delta">Proxy: 6.1m · Transcode: 2.4m</div>
</div>
<div className="stat-card">
<div className="label">Worker pool</div>
<div className="value">3<span style={{ fontSize: 14, color: "var(--text-3)", fontWeight: 400 }}> / 4 active</span></div>
<div className="delta">GPU util: 67%</div>
</div>
<div className="stat-card">
<div className="label">Failed (24h)</div>
<div className="value">{counts.failed}</div>
<div className="delta" style={{ color: counts.failed > 0 ? "var(--warning)" : "var(--text-3)" }}>
{counts.failed > 0 ? "1 needs attention" : "All good"}
</div>
</div>
</div>
<div className="tab-group" style={{ marginTop: 20, width: "fit-content" }}>
{[
{ id: "all", label: `All · ${counts.all}` },
{ id: "running", label: `Running · ${counts.running}` },
{ id: "queued", label: `Queued · ${counts.queued}` },
{ id: "done", label: `Done · ${counts.done}` },
{ id: "failed", label: `Failed · ${counts.failed}` },
].map(t => (
<button key={t.id} className={tab === t.id ? "active" : ""} onClick={() => setTab(t.id)}>{t.label}</button>
))}
</div>
<div className="panel" style={{ marginTop: 12 }}>
<div className="job-row head">
<div></div>
<div>Job</div>
<div>Asset</div>
<div>Node</div>
<div>Progress</div>
<div>ETA</div>
<div>Priority</div>
<div></div>
</div>
{filtered.map(j => <JobRow key={j.id} job={j} />)}
</div>
</div>
</div>
);
}
function JobRow({ job }) {
return (
<div className="job-row">
<div><StatusDot status={job.status} /></div>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<Icon name={iconForJob(job.kind)} size={13} style={{ color: "var(--text-3)" }} />
<span style={{ fontWeight: 500 }}>{job.kind}</span>
</div>
<div style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", color: "var(--text-2)" }}>
{job.asset}
</div>
<div className="mono" style={{ fontSize: 11.5, color: "var(--text-3)" }}>{job.node}</div>
<div>
{job.status === "running" && (
<div className="job-progress-wrap">
<div className="job-progress-bar">
<div className="job-progress-fill" style={{ width: `${job.progress}%` }} />
</div>
<span className="mono" style={{ fontSize: 10.5, color: "var(--text-3)", minWidth: 32, textAlign: "right" }}>
{Math.round(job.progress)}%
</span>
</div>
)}
{job.status === "done" && (
<span className="badge success" style={{ background: "transparent", padding: 0 }}>
<Icon name="check" size={12} /> Complete
</span>
)}
{job.status === "queued" && (
<span style={{ fontSize: 12, color: "var(--text-3)" }}>Waiting</span>
)}
{job.status === "failed" && (
<span style={{ fontSize: 12, color: "var(--danger)", display: "flex", alignItems: "center", gap: 4 }}>
<Icon name="alert" size={12} /> {job.error}
</span>
)}
</div>
<div className="mono" style={{ fontSize: 12, color: "var(--text-3)" }}>{job.eta}</div>
<div>
<span className={`badge ${job.priority === "high" ? "warning" : job.priority === "low" ? "neutral" : "outline"}`}>
{job.priority}
</span>
</div>
<div>
{job.status === "running" && <button className="btn ghost sm">Cancel</button>}
{job.status === "failed" && <button className="btn ghost sm"><Icon name="refresh" />Retry</button>}
{(job.status === "queued" || job.status === "done") && <button className="icon-btn"><Icon name="more" /></button>}
</div>
</div>
);
}
function iconForJob(kind) {
return { Proxy: "proxy", Transcode: "film", Thumbnail: "image", "AMPP Sync": "refresh" }[kind] || "jobs";
}
window.Jobs = Jobs;