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 && (
Progress {percentage}%
)}
Source: {job.source} • Started: {job.started_at ? new Date(job.started_at).toLocaleString() : 'N/A'}
{/* Actions */}
) }