diff --git a/services/web-ui/public/screens-jobs.jsx b/services/web-ui/public/screens-jobs.jsx
index 20b9da4..5c84b9c 100644
--- a/services/web-ui/public/screens-jobs.jsx
+++ b/services/web-ui/public/screens-jobs.jsx
@@ -1,98 +1,106 @@
-// screens-jobs.jsx — Jobs queue with real-time progress visualization
-
-const { JOBS: ALL_JOBS } = window.ZAMPP_DATA;
+// screens-jobs.jsx
function Jobs({ navigate }) {
- const [tab, setTab] = React.useState("all");
- const [jobs, setJobs] = React.useState(ALL_JOBS);
+ const [tab, setTab] = React.useState('all');
+ const [jobs, setJobs] = React.useState(window.ZAMPP_DATA.JOBS);
+ const [lastFetch, setLastFetch] = React.useState(Date.now());
+ // Poll for job updates every 5s
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);
+ const poll = () => {
+ window.ZAMPP_API.fetch('/jobs')
+ .then(raw => {
+ const normalized = (raw || []).map(j => {
+ const statusMap = { waiting: 'queued', active: 'running', completed: 'done', failed: 'failed' };
+ const kindMap = { proxy: 'Proxy', thumbnail: 'Thumbnail', conform: 'Conform', transcode: 'Transcode' };
+ const meta = j.metadata || {};
+ return {
+ ...j,
+ status: statusMap[j.status] || j.status,
+ kind: kindMap[j.type] || j.type || 'Job',
+ asset: j.asset_name || meta.filename || '—',
+ eta: '—',
+ node: meta.node || '—',
+ priority: meta.priority || 'normal',
+ error: j.error || null,
+ progress: j.progress || 0,
+ };
+ });
+ window.ZAMPP_DATA.JOBS = normalized;
+ setJobs(normalized);
+ setLastFetch(Date.now());
+ })
+ .catch(() => {});
+ };
+ const i = setInterval(poll, 5000);
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,
+ 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];
+ const filtered = tab === 'all' ? jobs : jobs.filter(j => j.status === tab);
return (
Jobs
-
Proxy generation, transcoding, AMPP sync — live across worker pool
+
Proxy generation, transcoding, and processing queue
-
-
+
-
Throughput
-
42 / min
-
+18% vs last hour
-
+
Running
+
{counts.running}
+
{counts.queued} queued
-
Avg duration
-
4.2 min
-
Proxy: 6.1m · Transcode: 2.4m
+
Completed
+
{counts.done}
+
Total done
-
Worker pool
-
3 / 4 active
-
GPU util: 67%
-
-
-
Failed (24h)
+
Failed
{counts.failed}
-
0 ? "var(--warning)" : "var(--text-3)" }}>
- {counts.failed > 0 ? "1 needs attention" : "All good"}
+
0 ? 'var(--warning)' : '' }}>
+ {counts.failed > 0 ? 'Needs attention' : 'All clear'}
+
+
Total jobs
+
{counts.all}
+
Updated {Math.round((Date.now()-lastFetch)/1000)}s ago
+
-
+
{[
- { 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}` },
+ { 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
-
+
Job
Asset
Node
Progress
ETA
Priority
- {filtered.map(j =>
)}
+ {filtered.length === 0
+ ?
No jobs in this category.
+ : filtered.map(j =>
)}
@@ -100,59 +108,35 @@ function Jobs({ navigate }) {
}
function JobRow({ job }) {
+ const iconMap = { Proxy: 'proxy', Transcode: 'film', Thumbnail: 'image', Conform: 'layers' };
return (
-
-
+
+
{job.kind}
-
- {job.asset}
-
-
{job.node}
+
{job.asset}
+
{job.node}
- {job.status === "running" && (
+ {job.status === 'running' && (
-
-
- {Math.round(job.progress)}%
-
+
+
{Math.round(job.progress)}%
)}
- {job.status === "done" && (
-
- Complete
-
- )}
- {job.status === "queued" && (
-
Waiting…
- )}
- {job.status === "failed" && (
-
- {job.error}
-
- )}
+ {job.status === 'done' &&
Complete}
+ {job.status === 'queued' &&
Waiting…}
+ {job.status === 'failed' &&
{job.error || 'Failed'}}
-
{job.eta}
+
{job.eta}
+
{job.priority}
-
- {job.priority}
-
-
-
- {job.status === "running" && }
- {job.status === "failed" && }
- {(job.status === "queued" || job.status === "done") && }
+ {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;