diff --git a/services/web-ui/public/screens-jobs.jsx b/services/web-ui/public/screens-jobs.jsx
new file mode 100644
index 0000000..20b9da4
--- /dev/null
+++ b/services/web-ui/public/screens-jobs.jsx
@@ -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 (
+
+
+
Jobs
+
Proxy generation, transcoding, AMPP sync — live across worker pool
+
+
+
+
+
+
+
+
+
Throughput
+
42 / min
+
+18% vs last hour
+
+
+
+
Avg duration
+
4.2 min
+
Proxy: 6.1m · Transcode: 2.4m
+
+
+
Worker pool
+
3 / 4 active
+
GPU util: 67%
+
+
+
Failed (24h)
+
{counts.failed}
+
0 ? "var(--warning)" : "var(--text-3)" }}>
+ {counts.failed > 0 ? "1 needs attention" : "All good"}
+
+
+
+
+
+ {[
+ { 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 => (
+
+ ))}
+
+
+
+
+
+
Job
+
Asset
+
Node
+
Progress
+
ETA
+
Priority
+
+
+ {filtered.map(j =>
)}
+
+
+
+ );
+}
+
+function JobRow({ job }) {
+ return (
+
+
+
+
+ {job.kind}
+
+
+ {job.asset}
+
+
{job.node}
+
+ {job.status === "running" && (
+
+
+
+ {Math.round(job.progress)}%
+
+
+ )}
+ {job.status === "done" && (
+
+ Complete
+
+ )}
+ {job.status === "queued" && (
+
Waiting…
+ )}
+ {job.status === "failed" && (
+
+ {job.error}
+
+ )}
+
+
{job.eta}
+
+
+ {job.priority}
+
+
+
+ {job.status === "running" && }
+ {job.status === "failed" && }
+ {(job.status === "queued" || job.status === "done") && }
+
+
+ );
+}
+
+function iconForJob(kind) {
+ return { Proxy: "proxy", Transcode: "film", Thumbnail: "image", "AMPP Sync": "refresh" }[kind] || "jobs";
+}
+
+window.Jobs = Jobs;