fix(downloads): allow users to dismiss failed downloads

Failed download jobs persist in BullMQ forever with no way to clear
them, leaving stale error notifications in Content Explorer and Easy
Setup. Adds a dismiss button (X) on failed download cards that removes
the job from the queue via a new DELETE endpoint.

- Backend: DELETE /api/downloads/jobs/:jobId endpoint
- Frontend: X button on failed download cards with immediate refresh

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Sherwood 2026-03-20 07:53:55 -07:00 committed by Jake Turner
parent d6c6cb66fa
commit 023e3f30af
5 changed files with 38 additions and 2 deletions

View File

@ -15,4 +15,9 @@ export default class DownloadsController {
const payload = await request.validateUsing(downloadJobsByFiletypeSchema)
return this.downloadService.listDownloadJobs(payload.params.filetype)
}
async removeJob({ params }: HttpContext) {
await this.downloadService.removeFailedJob(params.jobId)
return { success: true }
}
}

View File

@ -50,4 +50,15 @@ export class DownloadService {
return b.progress - a.progress
})
}
async removeFailedJob(jobId: string): Promise<void> {
for (const queueName of [RunDownloadJob.queue, DownloadModelJob.queue]) {
const queue = this.queueService.getQueue(queueName)
const job = await queue.getJob(jobId)
if (job) {
await job.remove()
return
}
}
}
}

View File

@ -2,7 +2,8 @@ import useDownloads, { useDownloadsProps } from '~/hooks/useDownloads'
import HorizontalBarChart from './HorizontalBarChart'
import { extractFileName } from '~/lib/util'
import StyledSectionHeader from './StyledSectionHeader'
import { IconAlertTriangle } from '@tabler/icons-react'
import { IconAlertTriangle, IconX } from '@tabler/icons-react'
import api from '~/lib/api'
interface ActiveDownloadProps {
filetype?: useDownloadsProps['filetype']
@ -10,7 +11,12 @@ interface ActiveDownloadProps {
}
const ActiveDownloads = ({ filetype, withHeader = false }: ActiveDownloadProps) => {
const { data: downloads } = useDownloads({ filetype })
const { data: downloads, invalidate } = useDownloads({ filetype })
const handleDismiss = async (jobId: string) => {
await api.removeDownloadJob(jobId)
invalidate()
}
return (
<>
@ -37,6 +43,13 @@ const ActiveDownloads = ({ filetype, withHeader = false }: ActiveDownloadProps)
Download failed{download.failedReason ? `: ${download.failedReason}` : ''}
</p>
</div>
<button
onClick={() => handleDismiss(download.jobId)}
className="flex-shrink-0 p-1 rounded hover:bg-red-100 transition-colors"
title="Dismiss failed download"
>
<IconX className="w-4 h-4 text-red-400 hover:text-red-600" />
</button>
</div>
) : (
<HorizontalBarChart

View File

@ -532,6 +532,12 @@ class API {
})()
}
async removeDownloadJob(jobId: string): Promise<void> {
return catchInternal(async () => {
await this.client.delete(`/downloads/jobs/${jobId}`)
})()
}
async runBenchmark(type: BenchmarkType, sync: boolean = false) {
return catchInternal(async () => {
const response = await this.client.post<RunBenchmarkResponse>(

View File

@ -92,6 +92,7 @@ router
.group(() => {
router.get('/jobs', [DownloadsController, 'index'])
router.get('/jobs/:filetype', [DownloadsController, 'filetype'])
router.delete('/jobs/:jobId', [DownloadsController, 'removeJob'])
})
.prefix('/api/downloads')