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 (
+
+ )
+}