mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-03-28 03:29:25 +01:00
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>
65 lines
2.5 KiB
TypeScript
65 lines
2.5 KiB
TypeScript
import { inject } from '@adonisjs/core'
|
|
import { QueueService } from './queue_service.js'
|
|
import { RunDownloadJob } from '#jobs/run_download_job'
|
|
import { DownloadModelJob } from '#jobs/download_model_job'
|
|
import { DownloadJobWithProgress } from '../../types/downloads.js'
|
|
import { normalize } from 'path'
|
|
|
|
@inject()
|
|
export class DownloadService {
|
|
constructor(private queueService: QueueService) {}
|
|
|
|
async listDownloadJobs(filetype?: string): Promise<DownloadJobWithProgress[]> {
|
|
// Get regular file download jobs (zim, map, etc.)
|
|
const queue = this.queueService.getQueue(RunDownloadJob.queue)
|
|
const fileJobs = await queue.getJobs(['waiting', 'active', 'delayed', 'failed'])
|
|
|
|
const fileDownloads = fileJobs.map((job) => ({
|
|
jobId: job.id!.toString(),
|
|
url: job.data.url,
|
|
progress: parseInt(job.progress.toString(), 10),
|
|
filepath: normalize(job.data.filepath),
|
|
filetype: job.data.filetype,
|
|
status: (job.failedReason ? 'failed' : 'active') as 'active' | 'failed',
|
|
failedReason: job.failedReason || undefined,
|
|
}))
|
|
|
|
// Get Ollama model download jobs
|
|
const modelQueue = this.queueService.getQueue(DownloadModelJob.queue)
|
|
const modelJobs = await modelQueue.getJobs(['waiting', 'active', 'delayed', 'failed'])
|
|
|
|
const modelDownloads = modelJobs.map((job) => ({
|
|
jobId: job.id!.toString(),
|
|
url: job.data.modelName || 'Unknown Model', // Use model name as url
|
|
progress: parseInt(job.progress.toString(), 10),
|
|
filepath: job.data.modelName || 'Unknown Model', // Use model name as filepath
|
|
filetype: 'model',
|
|
status: (job.failedReason ? 'failed' : 'active') as 'active' | 'failed',
|
|
failedReason: job.failedReason || undefined,
|
|
}))
|
|
|
|
const allDownloads = [...fileDownloads, ...modelDownloads]
|
|
|
|
// Filter by filetype if specified
|
|
const filtered = allDownloads.filter((job) => !filetype || job.filetype === filetype)
|
|
|
|
// Sort: active downloads first (by progress desc), then failed at the bottom
|
|
return filtered.sort((a, b) => {
|
|
if (a.status === 'failed' && b.status !== 'failed') return 1
|
|
if (a.status !== 'failed' && b.status === 'failed') return -1
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|