diff --git a/src/app/admin/queue/page.tsx b/src/app/admin/queue/page.tsx index 8c57b0f..c51d3d5 100644 --- a/src/app/admin/queue/page.tsx +++ b/src/app/admin/queue/page.tsx @@ -1,5 +1,6 @@ import { priceUpdateQueue, maintenanceQueue, schedulerQueue } from '@/lib/queue' import { formatDistanceToNow } from 'date-fns' +import RetryFailedButton from '@/components/admin/RetryFailedButton' export const dynamic = 'force-dynamic' @@ -89,6 +90,7 @@ export default async function AdminQueuePage() {

{q.name}

+ diff --git a/src/app/api/admin/queues/[name]/route.ts b/src/app/api/admin/queues/[name]/route.ts new file mode 100644 index 0000000..0210766 --- /dev/null +++ b/src/app/api/admin/queues/[name]/route.ts @@ -0,0 +1,40 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getServerSession } from 'next-auth' +import { authOptions } from '@/lib/auth' +import { priceUpdateQueue, maintenanceQueue, schedulerQueue } from '@/lib/queue' +import { Queue } from 'bullmq' + +const QUEUES: Record = { + 'hashex-price-updates': priceUpdateQueue, + 'hashex-maintenance': maintenanceQueue, + 'hashex-scheduler': schedulerQueue, +} + +export async function POST( + req: NextRequest, + { params }: { params: { name: string } } +) { + const session = await getServerSession(authOptions) + if (!session?.user?.isAdmin) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + + const queue = QUEUES[params.name] + if (!queue) { + return NextResponse.json({ error: 'Queue not found' }, { status: 404 }) + } + + const { action } = await req.json() as { action: string } + + if (action === 'retry-failed') { + await queue.retryJobs({ count: 100, state: 'failed' }) + return NextResponse.json({ ok: true }) + } + + if (action === 'clean-failed') { + await queue.clean(0, 100, 'failed') + return NextResponse.json({ ok: true }) + } + + return NextResponse.json({ error: 'Unknown action' }, { status: 400 }) +} diff --git a/src/components/admin/RetryFailedButton.tsx b/src/components/admin/RetryFailedButton.tsx new file mode 100644 index 0000000..b7bac34 --- /dev/null +++ b/src/components/admin/RetryFailedButton.tsx @@ -0,0 +1,41 @@ +'use client' + +import { useState } from 'react' +import { useRouter } from 'next/navigation' + +export default function RetryFailedButton({ + queueName, + count, +}: { + queueName: string + count: number +}) { + const [loading, setLoading] = useState(false) + const router = useRouter() + + if (count === 0) return null + + async function handleRetry() { + setLoading(true) + try { + await fetch(`/api/admin/queues/${encodeURIComponent(queueName)}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'retry-failed' }), + }) + } finally { + router.refresh() + setLoading(false) + } + } + + return ( + + ) +}