import { useQuery } from '@tanstack/react-query'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { Activity, Play, Pause, RotateCcw, Terminal, CheckCircle, AlertCircle, Clock, ChevronDown, ChevronRight, Copy, Filter, X, RefreshCw, } from 'lucide-react'; import { jobsService, JobHistoryEntry, JobStatus, JOB_LABELS, getJobDataSummary, getJobDataDetails, getJobStatus, } from '../services/jobsService'; import { formatDistanceToNow, format } from 'date-fns'; /* ─────────────────────────────────────────── Filter state ─────────────────────────────────────────── */ interface Filters { status: JobStatus | 'all'; type: string; } const INITIAL_FILTERS: Filters = { status: 'all', type: 'all' }; /* ─────────────────────────────────────────── Helpers ─────────────────────────────────────────────── */ const JOB_TYPES = Object.keys(JOB_LABELS); function statusBgColor(status: JobStatus): string { switch (status) { case 'running': return 'bg-accent/10 border-accent/25'; case 'failed': return 'bg-red-400/10 border-red-400/25'; case 'completed': return 'bg-green-400/10 border-green-400/25'; } } function formatDuration(ms: number): string { const seconds = Math.floor(ms / 1000); if (seconds < 60) return `${seconds}s`; const minutes = Math.floor(seconds / 60); const remainSec = seconds % 60; return `${minutes}m ${remainSec}s`; } /* ─────────────────────────────────────────── Sub-components ──────────────────────────────────────── */ function StatCard({ icon: Icon, label, value, color }: { icon: React.ComponentType<{ className?: string; style?: React.CSSProperties }>; label: string; value: number; color: string }) { return (

{label}

{value.toLocaleString()}

); } function StatusBadge({ status }: { status: JobStatus }) { const dotColor = status === 'running' ? 'bg-accent' : status === 'failed' ? 'bg-red-400' : 'bg-green-400'; const label = status === 'running' ? 'Running' : status === 'failed' ? 'Failed' : 'Completed'; return ( {label} ); } function ExpandedDetail({ label, value }: { label: string; value: string }) { return (
{label} {value || '—'}
); } /* ─────────────────────────────────────────── Job row ─────────────────────────────────────────────── */ function JobRow({ job, isExpanded, onToggle, }: { job: JobHistoryEntry; isExpanded: boolean; onToggle: () => void; }) { const status = getJobStatus(job); const summary = getJobDataSummary(job.name, job.data); const details = useMemo(() => getJobDataDetails(job.name, job.data), [job.name, job.data]); const duration = job.finishedOn && job.timestamp ? job.finishedOn - job.timestamp : null; const hasReturnValue = job.returnvalue !== undefined && job.returnvalue !== null; return (
{/* ── collapsed row ── */} {/* ── expanded detail ── */} {isExpanded && (
{/* Job metadata */}
{job.finishedOn && ( )} {duration !== null && ( )}
{/* Job-specific payload */} {details.length > 0 && (

Payload

{details.map((d) => ( ))}
)} {/* Raw data (collapsible) */} {Object.keys(job.data).length > 0 && ( )} {/* Return value */} {hasReturnValue && } {/* Failed reason */} {job.failedReason && (

Error

{job.failedReason}
)}
)}
); } /* ── Raw JSON expandable block ── */ function RawDataBlock({ label, data }: { label: string; data: unknown }) { const [open, setOpen] = useState(false); const text = JSON.stringify(data, null, 2); return (
{open && (
            {text}
          
)}
); } /* ─────────────────────────────────────────── Filter bar ──────────────────────────────────────────── */ function FilterBar({ filters, onChange, totalCount, filteredCount, }: { filters: Filters; onChange: (f: Filters) => void; totalCount: number; filteredCount: number; }) { const setStatus = (status: Filters['status']) => onChange({ ...filters, status }); const setType = (type: string) => onChange({ ...filters, type }); const hasActiveFilters = filters.status !== 'all' || filters.type !== 'all'; const statusOptions: { value: Filters['status']; label: string }[] = [ { value: 'all', label: 'All' }, { value: 'completed', label: 'Completed' }, { value: 'failed', label: 'Failed' }, { value: 'running', label: 'Running' }, ]; const typeOptions = JOB_TYPES; return (
{/* Status pills */}
{statusOptions.map((opt) => ( ))}
{/* Type dropdown */}
{/* Result count */} {filteredCount} / {totalCount} jobs {/* Clear filters */} {hasActiveFilters && ( )}
); } /* ─────────────────────────────────────────── Page ────────────────────────────────────────────────── */ export default function JobsPage() { const [autoRefresh, setAutoRefresh] = useState(true); const [selectedTab, setSelectedTab] = useState<'overview' | 'history'>('overview'); const [filters, setFilters] = useState(INITIAL_FILTERS); const [expandedIds, setExpandedIds] = useState>(new Set()); const { data: stats, refetch: refetchStats } = useQuery({ queryKey: ['queueStats'], queryFn: () => jobsService.getQueueStats(), refetchInterval: autoRefresh ? 3000 : false, staleTime: 1000, }); const { data: history, refetch: refetchHistory } = useQuery({ queryKey: ['jobHistory'], queryFn: () => jobsService.getJobHistory(200), refetchInterval: autoRefresh ? 5000 : false, staleTime: 2000, }); useEffect(() => { if (selectedTab === 'overview') refetchStats(); else refetchHistory(); }, [selectedTab, refetchStats, refetchHistory]); const toggleExpanded = useCallback((id: string) => { setExpandedIds((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }, []); // Filtered & sorted history const filteredHistory = useMemo(() => { if (!history) return []; return history .filter((job) => { if (filters.status !== 'all' && getJobStatus(job) !== filters.status) return false; if (filters.type !== 'all' && job.name !== filters.type) return false; return true; }); }, [history, filters]); return (
{/* ── Header ── */}

Jobs

Background task queue monitoring

{/* ── Tabs ── */}
{/* ── Content ── */}
{/* ── Overview tab ── */} {selectedTab === 'overview' && stats && (
)} {/* ── History tab ── */} {selectedTab === 'history' && ( <> {/* Filter bar */} {history && history.length > 0 && ( )} {/* Job list */} {filteredHistory.length === 0 ? (

{history && history.length > 0 ? 'No jobs match the current filters' : 'No job history yet'}

{(history && history.length > 0) && ( )}
) : (
{filteredHistory.map((job) => ( toggleExpanded(job.id)} /> ))}
)} )}
); }