admin QOL features
Build Images and Deploy / Update-PROD-Stack (push) Successful in 1m24s

This commit is contained in:
2026-03-19 16:13:53 -04:00
parent aa7a80c3e7
commit 873b86f85e
4 changed files with 69 additions and 5 deletions
+15 -3
View File
@@ -1,6 +1,7 @@
import { priceUpdateQueue, maintenanceQueue, schedulerQueue } from '@/lib/queue'
import { priceUpdateQueue, maintenanceQueue, schedulerQueue, fundNavSnapshotQueue } from '@/lib/queue'
import { formatDistanceToNow } from 'date-fns'
import RetryFailedButton from '@/components/admin/RetryFailedButton'
import TriggerJobButton from '@/components/admin/TriggerJobButton'
export const dynamic = 'force-dynamic'
@@ -69,13 +70,21 @@ async function getQueueSummary(queue: typeof priceUpdateQueue): Promise<QueueSum
}
export default async function AdminQueuePage() {
const [priceSummary, maintenanceSummary, schedulerSummary] = await Promise.all([
const [priceSummary, maintenanceSummary, schedulerSummary, fundNavSummary] = await Promise.all([
getQueueSummary(priceUpdateQueue),
getQueueSummary(maintenanceQueue),
getQueueSummary(schedulerQueue),
getQueueSummary(fundNavSnapshotQueue),
])
const queues = [priceSummary, maintenanceSummary, schedulerSummary]
const queues = [priceSummary, maintenanceSummary, schedulerSummary, fundNavSummary]
// Queues that support a manual trigger, and the label to show
const triggerLabels: Record<string, string> = {
'hashex-scheduler': 'Trigger sweep',
'hashex-maintenance': 'Run maintenance',
'hashex-fund-nav-snapshot': 'Snapshot NAVs',
}
return (
<div className="space-y-6">
@@ -90,6 +99,9 @@ export default async function AdminQueuePage() {
<div className="flex items-center justify-between px-4 py-3 border-b border-surface-border">
<h3 className="font-medium text-sm">{q.name}</h3>
<div className="flex items-center gap-3 text-xs">
{triggerLabels[q.name] && (
<TriggerJobButton queueName={q.name} label={triggerLabels[q.name]} />
)}
<RetryFailedButton queueName={q.name} count={q.failed} />
<Badge label="waiting" count={q.waiting} color="slate" />
<Badge label="active" count={q.active} color="indigo" />
+16 -1
View File
@@ -1,13 +1,21 @@
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { priceUpdateQueue, maintenanceQueue, schedulerQueue } from '@/lib/queue'
import { priceUpdateQueue, maintenanceQueue, schedulerQueue, fundNavSnapshotQueue } from '@/lib/queue'
import { Queue } from 'bullmq'
const QUEUES: Record<string, Queue> = {
'hashex-price-updates': priceUpdateQueue,
'hashex-maintenance': maintenanceQueue,
'hashex-scheduler': schedulerQueue,
'hashex-fund-nav-snapshot': fundNavSnapshotQueue,
}
// Job name to add when manually triggering each queue
const TRIGGER_JOB: Record<string, string> = {
'hashex-scheduler': 'trigger-sweep',
'hashex-maintenance': 'daily-maintenance',
'hashex-fund-nav-snapshot': 'fund-nav-snapshot',
}
export async function POST(
@@ -36,5 +44,12 @@ export async function POST(
return NextResponse.json({ ok: true })
}
if (action === 'trigger') {
const jobName = TRIGGER_JOB[params.name]
if (!jobName) return NextResponse.json({ error: 'Queue does not support manual trigger' }, { status: 400 })
await queue.add(jobName, {}, { jobId: `manual-${Date.now()}` })
return NextResponse.json({ ok: true })
}
return NextResponse.json({ error: 'Unknown action' }, { status: 400 })
}
+37
View File
@@ -0,0 +1,37 @@
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
export default function TriggerJobButton({ queueName, label }: { queueName: string; label: string }) {
const [loading, setLoading] = useState(false)
const [done, setDone] = useState(false)
const router = useRouter()
async function handleTrigger() {
setLoading(true)
setDone(false)
try {
await fetch(`/api/admin/queues/${encodeURIComponent(queueName)}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'trigger' }),
})
setDone(true)
setTimeout(() => setDone(false), 3000)
} finally {
router.refresh()
setLoading(false)
}
}
return (
<button
onClick={handleTrigger}
disabled={loading}
className="text-xs px-2 py-1 rounded bg-indigo-500/20 text-indigo-400 hover:bg-indigo-500/30 disabled:opacity-50 transition-colors"
>
{loading ? 'Triggering…' : done ? '✓ Triggered' : label}
</button>
)
}
+1 -1
View File
File diff suppressed because one or more lines are too long