From c5d79085cb0ffe8361897aa608e4002564d72542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Thu, 23 Jul 2026 16:14:49 +0000 Subject: [PATCH] jobs: Stop now cancels queued jobs; add remove/stop button on list rows Co-Authored-By: Claude Opus 4.8 --- .../Screens/Dashboard/Jobs/JobsPage.tsx | 69 ++++++++++++------- src/servers/api/tasks/pipeline-job-manager.ts | 11 +++ src/servers/api/tasks/pipeline-jobs-routes.ts | 6 +- 3 files changed, 59 insertions(+), 27 deletions(-) diff --git a/src/apps/officer-web/Screens/Dashboard/Jobs/JobsPage.tsx b/src/apps/officer-web/Screens/Dashboard/Jobs/JobsPage.tsx index 888fd074..6e261bd9 100644 --- a/src/apps/officer-web/Screens/Dashboard/Jobs/JobsPage.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Jobs/JobsPage.tsx @@ -1,6 +1,6 @@ -import { useState, useEffect, type ReactNode } from 'react'; +import { useState, useEffect, useCallback, type ReactNode, type MouseEvent } from 'react'; import { useParams, useNavigate } from 'react-router'; -import { Search, CheckCircle2, AlertCircle, Clock, Loader2, StopCircle, AlertTriangle, Inbox } from 'lucide-react'; +import { Search, CheckCircle2, AlertCircle, Clock, Loader2, StopCircle, AlertTriangle, Inbox, X } from 'lucide-react'; import { WorkspaceLayout } from 'officerdev'; import type { LayoutNode, PanelComponents } from 'officerdev'; import { useClient } from 'hooks/useClient'; @@ -49,14 +49,22 @@ const JobsListPanel = () => { const [search, setSearch] = useState(''); const [isLoading, setIsLoading] = useState(true); + const load = useCallback( + () => client.get('/jobs').then((data) => { setJobs(data); setIsLoading(false); }).catch(() => setIsLoading(false)), + [client], + ); + useEffect(() => { - let alive = true; - const load = () => - client.get('/jobs').then((data) => { if (alive) { setJobs(data); setIsLoading(false); } }).catch(() => alive && setIsLoading(false)); load(); const timer = setInterval(load, 2500); - return () => { alive = false; clearInterval(timer); }; - }, []); + return () => clearInterval(timer); + }, [load]); + + // Stop a running job / remove a queued one, then refresh immediately. + const cancel = (ev: MouseEvent, id: string) => { + ev.stopPropagation(); + client.post(`/jobs/${id}/stop`, {}).then(() => load()).catch(() => {}); + }; const filtered = search ? jobs.filter((j) => { @@ -73,24 +81,37 @@ const JobsListPanel = () => { ]; const history = filtered.filter((j) => j.status !== 'running' && j.status !== 'pending'); - const renderRow = (job: JobSummary) => ( - + {cancellable && ( + + )} - - ); + ); + }; return (
diff --git a/src/servers/api/tasks/pipeline-job-manager.ts b/src/servers/api/tasks/pipeline-job-manager.ts index c4d2513f..4380ff65 100644 --- a/src/servers/api/tasks/pipeline-job-manager.ts +++ b/src/servers/api/tasks/pipeline-job-manager.ts @@ -288,6 +288,17 @@ export function stopJob(jobId: string): boolean { return true; } +// Stop a running job (cooperative abort) OR cancel a queued one (mark stopped so it won't promote). +export async function requestStop(jobId: string): Promise<'stopped' | 'cancelled' | 'noop'> { + if (stopJob(jobId)) return 'stopped'; + const job = await getPipelineJob(jobId); + if (job && job.status === 'pending') { + await updatePipelineJob(jobId, { status: 'stopped', completedAt: new Date() }); + return 'cancelled'; + } + return 'noop'; +} + export function isJobLive(jobId: string): boolean { return liveJobs.has(jobId); } diff --git a/src/servers/api/tasks/pipeline-jobs-routes.ts b/src/servers/api/tasks/pipeline-jobs-routes.ts index 67b85145..18973e2d 100644 --- a/src/servers/api/tasks/pipeline-jobs-routes.ts +++ b/src/servers/api/tasks/pipeline-jobs-routes.ts @@ -98,13 +98,13 @@ pipelineJobsRouter.get('/:id/log', async (c) => { } }); -// POST /:id/stop — request cancellation (cooperative abort). +// POST /:id/stop — stop a running job or cancel a queued one. pipelineJobsRouter.post('/:id/stop', async (c) => { const user = c.get('user'); const job = await getPipelineJob(c.req.param('id')); if (!job || job.userId !== user.id) return c.json({ error: 'Not found' }, 404); - const stopped = jobManager.stopJob(job.id); - return c.json({ ok: true, wasLive: stopped }); + const result = await jobManager.requestStop(job.id); + return c.json({ ok: true, result }); }); // GET /:id — single job detail (full row).