import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import {
Play,
Pause,
XCircle,
CheckCircle,
Clock,
AlertCircle,
RefreshCw,
Leaf,
Download,
XOctagon,
} from 'lucide-react'
import { jobsApi, Job } from '../api/client'
export default function Jobs() {
const queryClient = useQueryClient()
const { data, isLoading, refetch } = useQuery({
queryKey: ['jobs'],
queryFn: () => jobsApi.list({ limit: 100 }).then((res) => res.data),
refetchInterval: 1000, // Faster refresh for live updates
})
const pauseMutation = useMutation({
mutationFn: (id: number) => jobsApi.pause(id),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['jobs'] }),
})
const resumeMutation = useMutation({
mutationFn: (id: number) => jobsApi.resume(id),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['jobs'] }),
})
const cancelMutation = useMutation({
mutationFn: (id: number) => jobsApi.cancel(id),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['jobs'] }),
})
const getStatusIcon = (status: string) => {
switch (status) {
case 'running':
return
case 'pending':
return
case 'paused':
return
case 'completed':
return
case 'failed':
return
default:
return null
}
}
const getStatusClass = (status: string) => {
switch (status) {
case 'running':
return 'bg-blue-100 text-blue-700'
case 'pending':
return 'bg-yellow-100 text-yellow-700'
case 'paused':
return 'bg-gray-100 text-gray-700'
case 'completed':
return 'bg-green-100 text-green-700'
case 'failed':
return 'bg-red-100 text-red-700'
default:
return 'bg-gray-100 text-gray-700'
}
}
// Separate running jobs from others
const runningJobs = data?.items.filter((j) => j.status === 'running') || []
const otherJobs = data?.items.filter((j) => j.status !== 'running') || []
return (
Jobs
{isLoading ? (
) : data?.items.length === 0 ? (
No jobs yet
Select species and start a scrape job to get started
) : (
{/* Running Jobs - More prominent display */}
{runningJobs.length > 0 && (
Active Jobs ({runningJobs.length})
{runningJobs.map((job) => (
pauseMutation.mutate(job.id)}
onCancel={() => cancelMutation.mutate(job.id)}
/>
))}
)}
{/* Other Jobs */}
{otherJobs.length > 0 && (
{runningJobs.length > 0 && (
Other Jobs
)}
{otherJobs.map((job) => (
{getStatusIcon(job.status)}
{job.name}
{job.status}
Source: {job.source}
Downloaded: {job.images_downloaded}
Rejected: {job.images_rejected}
{/* Progress bar for paused jobs */}
{job.status === 'paused' && job.progress_total > 0 && (
{job.progress_current} / {job.progress_total} species
{Math.round(
(job.progress_current / job.progress_total) * 100
)}
%
)}
{job.error_message && (
Error: {job.error_message}
)}
{job.started_at && (
Started: {new Date(job.started_at).toLocaleString()}
)}
{job.completed_at && (
Completed: {new Date(job.completed_at).toLocaleString()}
)}
{/* Actions */}
{job.status === 'paused' && (
)}
{(job.status === 'paused' || job.status === 'pending') && (
)}
))}
)}
)}
)
}
function RunningJobCard({
job,
onPause,
onCancel,
}: {
job: Job
onPause: () => void
onCancel: () => void
}) {
// Fetch real-time progress for this job
const { data: progress } = useQuery({
queryKey: ['job-progress', job.id],
queryFn: () => jobsApi.progress(job.id).then((res) => res.data),
refetchInterval: 500, // Very fast updates for live feel
enabled: job.status === 'running',
})
const currentSpecies = progress?.current_species || ''
const progressCurrent = progress?.progress_current ?? job.progress_current
const progressTotal = progress?.progress_total ?? job.progress_total
const percentage = progressTotal > 0 ? Math.round((progressCurrent / progressTotal) * 100) : 0
return (
{job.name}
running
{/* Live Stats */}
Species Progress
{progressCurrent} / {progressTotal}
Downloaded
{job.images_downloaded}
Rejected
{job.images_rejected}
{/* Current Species */}
{currentSpecies && (
Currently scraping:
{currentSpecies}
)}
{/* Progress bar */}
{progressTotal > 0 && (
)}
Source: {job.source} • Started: {job.started_at ? new Date(job.started_at).toLocaleString() : 'N/A'}
{/* Actions */}
)
}