diff --git a/services/web-ui/public/screens-jobs.jsx b/services/web-ui/public/screens-jobs.jsx index 5c84b9c..e643eec 100644 --- a/services/web-ui/public/screens-jobs.jsx +++ b/services/web-ui/public/screens-jobs.jsx @@ -5,35 +5,50 @@ function Jobs({ navigate }) { 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 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 normalizeJob = (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, }; - const i = setInterval(poll, 5000); + }; + + const refresh = React.useCallback(() => { + window.ZAMPP_API.fetch('/jobs') + .then(raw => { + const norm = (raw || []).map(normalizeJob); + window.ZAMPP_DATA.JOBS = norm; + setJobs(norm); + setLastFetch(Date.now()); + }) + .catch(() => {}); + }, []); + + React.useEffect(() => { + const i = setInterval(refresh, 5000); return () => clearInterval(i); + }, [refresh]); + + const handleRetry = React.useCallback((job) => { + window.ZAMPP_API.fetch('/jobs/' + job.id + '/retry', { method: 'POST' }) + .then(() => refresh()) + .catch(e => alert('Retry failed: ' + e.message)); + }, [refresh]); + + const handleDelete = React.useCallback((job) => { + if (!window.confirm('Remove this job from the queue?')) return; + window.ZAMPP_API.fetch('/jobs/' + job.id, { method: 'DELETE' }) + .then(() => setJobs(prev => prev.filter(j => j.id !== job.id))) + .catch(e => alert('Delete failed: ' + e.message)); }, []); const counts = { @@ -51,7 +66,7 @@ function Jobs({ navigate }) {

Jobs

Proxy generation, transcoding, and processing queue
-
@@ -100,14 +115,14 @@ function Jobs({ navigate }) { {filtered.length === 0 ?
No jobs in this category.
- : filtered.map(j => )} + : filtered.map(j => )} ); } -function JobRow({ job }) { +function JobRow({ job, onRetry, onDelete }) { const iconMap = { Proxy: 'proxy', Transcode: 'film', Thumbnail: 'image', Conform: 'layers' }; return (
@@ -131,9 +146,13 @@ function JobRow({ job }) {
{job.eta}
{job.priority}
-
- {job.status === 'failed' && } - {(job.status === 'queued' || job.status === 'done') && } +
+ {job.status === 'failed' && ( + + )} + {(job.status === 'queued' || job.status === 'done') && ( + + )}
);