import { IconCircleCheck as CheckCircle, IconClock as Clock, IconLoader2 as Loader2, IconPlayerPlay as Play, IconCircleX as XCircle, } from '@tabler/icons-react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; import { PageHeader } from '@/components/PageHeader'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { apiJson } from '@/lib/api'; import { ContentPanel } from '../../components/ContentPanel'; interface JobSummary { name: string; lastRun: string | null; lastStatus: string; lastResult: string | null; lastError: string | null; runCount: number; } interface JobHistory { id: string; job_name: string; status: string; started_at: string; finished_at: string | null; duration_ms: number | null; result: string | null; error: string | null; } const jobsQueryKey = ['admin', 'jobs']; function formatDate(dateString: string): string { return new Date(dateString).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }); } function StatusBadge({ status }: { status: string }) { if (status === 'succeeded') { return ( Success ); } if (status === 'failed') { return ( Failed ); } return ( {status} ); } export default function AdminJobs() { const queryClient = useQueryClient(); const { data, isLoading } = useQuery<{ jobs: JobSummary[]; history: JobHistory[] }>({ queryKey: jobsQueryKey, queryFn: () => apiJson<{ jobs: JobSummary[]; history: JobHistory[] }>( '/api/v1/admin/jobs', {}, 'Failed to fetch jobs' ), }); const { data: historyData } = useQuery<{ history: JobHistory[] }>({ queryKey: [...jobsQueryKey, 'history'], queryFn: () => apiJson<{ history: JobHistory[] }>( '/api/v1/admin/jobs/history?limit=50', {}, 'Failed to fetch job history' ), }); const triggerMutation = useMutation({ mutationFn: (jobName: string) => apiJson( '/api/v1/admin/jobs/trigger', { method: 'POST', body: { jobName } }, 'Failed to trigger job' ), onSuccess: () => { queryClient.invalidateQueries({ queryKey: jobsQueryKey }); toast.success('Job triggered successfully'); }, onError: (err) => { toast.error(err.message); }, }); const jobs = data?.jobs ?? []; const history = historyData?.history ?? data?.history ?? []; return ( <> {/* Scheduled Jobs */} Scheduled Jobs Jobs are managed via pg_cron in PostgreSQL. Edit the migration file to add or modify schedules. {isLoading ? ( ) : jobs.length === 0 ? ( No job history yet. Jobs will appear here after their first execution. ) : ( {jobs.map((job) => ( {job.name} {job.lastRun ? `Last run: ${formatDate(job.lastRun)}` : 'Never run'} {job.runCount > 0 && ` (${job.runCount} total runs)`} {job.lastResult && ( {job.lastResult} )} {job.lastError && ( {job.lastError} )} triggerMutation.mutate(job.name)} disabled={triggerMutation.isPending} > {triggerMutation.isPending && triggerMutation.variables === job.name ? ( ) : ( )} Run Now ))} )} {/* Execution History */} Execution History Recent job executions (last 50) {history.length === 0 ? ( No execution history yet. ) : ( {history.map((entry) => ( {entry.job_name} {entry.result && {entry.result}} {entry.error && ( {entry.error} )} {formatDate(entry.started_at)} ))} )} > ); }
No job history yet. Jobs will appear here after their first execution.
{job.lastResult}
{job.lastError}
No execution history yet.