fix(jobs): cancel running + delete failed jobs to unstick the queue
The Jobs page only exposed a delete button for queued + done jobs, so a
stalled-active job (worker died holding a BullMQ concurrency slot) had
no way out from the UI. Operators were watching the queue back up
behind a single stuck thumbnail job with no kill switch.
- Running jobs now show a "Cancel" button (red text). Confirm copy
spells out that the worker may run a few seconds longer in the
background but the queue slot frees up immediately.
- Failed jobs now show the X icon for delete in addition to the
existing Retry button.
- Both routes hit the same DELETE /jobs/:id endpoint; BullMQ's
job.remove() works on any state including stalled-active.
- handleDelete takes an optional mode ('cancel' | 'delete') only to
customise the confirm prompt and error toast wording.
Right-aligned the action cell so the Retry/Cancel/Delete buttons sit
flush right like the rest of the table's actions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6322b61a04
commit
eb6c723713
1 changed files with 24 additions and 6 deletions
|
|
@ -82,11 +82,19 @@ function Jobs({ navigate }) {
|
|||
.catch(e => alert('Retry failed: ' + e.message));
|
||||
}, [refresh]);
|
||||
|
||||
const handleDelete = React.useCallback((job) => {
|
||||
if (!window.confirm('Remove this job from the queue?')) return;
|
||||
// One handler covers cancel (running) AND delete (queued / done / failed).
|
||||
// BullMQ's job.remove() — what the API calls — works on any state, so a
|
||||
// stalled-active job (worker died mid-process, holding a concurrency slot)
|
||||
// gets yanked and the next queued job runs. mode just changes the prompt
|
||||
// copy so the operator knows what they're doing.
|
||||
const handleDelete = React.useCallback((job, mode) => {
|
||||
const msg = mode === 'cancel'
|
||||
? 'Cancel this running ' + job.kind + ' job?\n\nThe worker may run a few seconds longer in the background, but its result will be discarded and the queue slot frees up immediately.'
|
||||
: 'Remove this ' + job.status + ' ' + job.kind + ' job from the queue?';
|
||||
if (!window.confirm(msg)) 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));
|
||||
.catch(e => alert((mode === 'cancel' ? 'Cancel' : 'Delete') + ' failed: ' + e.message));
|
||||
}, []);
|
||||
|
||||
// Retry every failed job at once. Useful after a transient infra issue
|
||||
|
|
@ -221,12 +229,22 @@ function JobRow({ job, onRetry, onDelete }) {
|
|||
})()}
|
||||
</div>
|
||||
<div><span className={'badge ' + (job.priority === 'high' ? 'warning' : 'outline')}>{job.priority}</span></div>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<div style={{ display: 'flex', gap: 4, justifyContent: 'flex-end' }}>
|
||||
{job.status === 'failed' && (
|
||||
<button className="btn ghost sm" onClick={() => onRetry(job)}><Icon name="refresh" />Retry</button>
|
||||
)}
|
||||
{(job.status === 'queued' || job.status === 'done') && (
|
||||
<button className="icon-btn" title="Remove job" onClick={() => onDelete(job)}><Icon name="x" /></button>
|
||||
{job.status === 'running' && (
|
||||
/* Cancel a stalled-active job — frees the BullMQ concurrency slot
|
||||
so anything queued behind it can run. The worker may finish in
|
||||
the background but its result is discarded. */
|
||||
<button className="btn ghost sm" onClick={() => onDelete(job, 'cancel')}
|
||||
style={{ color: 'var(--danger)' }} title="Cancel this running job and free its queue slot">
|
||||
<Icon name="x" />Cancel
|
||||
</button>
|
||||
)}
|
||||
{(job.status === 'queued' || job.status === 'done' || job.status === 'failed') && (
|
||||
<button className="icon-btn" title="Remove job from the queue"
|
||||
onClick={() => onDelete(job, 'delete')}><Icon name="x" /></button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue