From 749333332e65ae1a9775c727624a6d6b9f40fa84 Mon Sep 17 00:00:00 2001
From: Chris Sherwood
Date: Fri, 20 Mar 2026 07:53:55 -0700
Subject: [PATCH] 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)
---
admin/app/controllers/downloads_controller.ts | 5 +++++
admin/app/services/download_service.ts | 11 +++++++++++
admin/inertia/components/ActiveDownloads.tsx | 17 +++++++++++++++--
admin/inertia/lib/api.ts | 6 ++++++
admin/start/routes.ts | 1 +
5 files changed, 38 insertions(+), 2 deletions(-)
diff --git a/admin/app/controllers/downloads_controller.ts b/admin/app/controllers/downloads_controller.ts
index bd58790..023806b 100644
--- a/admin/app/controllers/downloads_controller.ts
+++ b/admin/app/controllers/downloads_controller.ts
@@ -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 }
+ }
}
diff --git a/admin/app/services/download_service.ts b/admin/app/services/download_service.ts
index 63c7ecd..a2b7faf 100644
--- a/admin/app/services/download_service.ts
+++ b/admin/app/services/download_service.ts
@@ -50,4 +50,15 @@ export class DownloadService {
return b.progress - a.progress
})
}
+
+ async removeFailedJob(jobId: string): Promise {
+ 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
+ }
+ }
+ }
}
diff --git a/admin/inertia/components/ActiveDownloads.tsx b/admin/inertia/components/ActiveDownloads.tsx
index 69bbb8f..9661f22 100644
--- a/admin/inertia/components/ActiveDownloads.tsx
+++ b/admin/inertia/components/ActiveDownloads.tsx
@@ -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}` : ''}
+
) : (
{
+ 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(
diff --git a/admin/start/routes.ts b/admin/start/routes.ts
index 2045b27..631c528 100644
--- a/admin/start/routes.ts
+++ b/admin/start/routes.ts
@@ -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')