import { useState, useEffect, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { useAuth } from '../contexts/AuthContext'; import { getPyPICacheStatus, getPyPICacheFailedTasks, getPyPICacheActiveTasks, retryPyPICacheTask, retryAllPyPICacheTasks, } from '../api'; import { PyPICacheStatus, PyPICacheTask, PyPICacheActiveTask } from '../types'; import './AdminJobsPage.css'; function AdminJobsPage() { const { user, loading: authLoading } = useAuth(); const navigate = useNavigate(); // PyPI cache status const [cacheStatus, setCacheStatus] = useState(null); const [failedTasks, setFailedTasks] = useState([]); const [activeTasks, setActiveTasks] = useState([]); const [loadingStatus, setLoadingStatus] = useState(true); const [statusError, setStatusError] = useState(null); // Action states const [retryingPackage, setRetryingPackage] = useState(null); const [retryingAll, setRetryingAll] = useState(false); const [successMessage, setSuccessMessage] = useState(null); // Auto-refresh const [autoRefresh, setAutoRefresh] = useState(true); useEffect(() => { if (!authLoading && !user) { navigate('/login', { state: { from: '/admin/jobs' } }); } }, [user, authLoading, navigate]); const loadData = useCallback(async () => { if (!user?.is_admin) return; setStatusError(null); try { const [status, failed, active] = await Promise.all([ getPyPICacheStatus(), getPyPICacheFailedTasks(100), getPyPICacheActiveTasks(50), ]); setCacheStatus(status); setFailedTasks(failed); setActiveTasks(active); } catch (err) { setStatusError(err instanceof Error ? err.message : 'Failed to load status'); } finally { setLoadingStatus(false); } }, [user]); useEffect(() => { if (user?.is_admin) { loadData(); } }, [user, loadData]); // Auto-refresh every 5 seconds when enabled useEffect(() => { if (!autoRefresh || !user?.is_admin) return; const interval = setInterval(() => { loadData(); }, 5000); return () => clearInterval(interval); }, [autoRefresh, user, loadData]); useEffect(() => { if (successMessage) { const timer = setTimeout(() => setSuccessMessage(null), 3000); return () => clearTimeout(timer); } }, [successMessage]); async function handleRetryPackage(packageName: string) { setRetryingPackage(packageName); try { const result = await retryPyPICacheTask(packageName); setSuccessMessage(result.message); await loadData(); } catch (err) { setStatusError(err instanceof Error ? err.message : 'Failed to retry'); } finally { setRetryingPackage(null); } } async function handleRetryAll() { if (!window.confirm('Retry all failed tasks? This will re-queue all failed packages.')) { return; } setRetryingAll(true); try { const result = await retryAllPyPICacheTasks(); setSuccessMessage(result.message); await loadData(); } catch (err) { setStatusError(err instanceof Error ? err.message : 'Failed to retry all'); } finally { setRetryingAll(false); } } if (authLoading) { return
Loading...
; } if (!user?.is_admin) { return (
Access denied. Admin privileges required.
); } const totalJobs = cacheStatus ? cacheStatus.pending + cacheStatus.in_progress + cacheStatus.completed + cacheStatus.failed : 0; const completedPercent = totalJobs > 0 ? Math.round((cacheStatus?.completed ?? 0) / totalJobs * 100) : 0; // Combine all jobs into unified list with type column type UnifiedJob = { id: string; type: 'pypi'; package: string; status: 'pending' | 'in_progress' | 'completed' | 'failed'; depth: number; attempts: number; error?: string | null; started_at?: string | null; failed_at?: string | null; version_constraint?: string | null; }; const allJobs: UnifiedJob[] = [ // Active tasks first ...activeTasks.map((t): UnifiedJob => ({ id: t.id, type: 'pypi', package: t.package, status: 'in_progress', depth: t.depth, attempts: t.attempts, started_at: t.started_at, version_constraint: t.version_constraint, })), // Then failed tasks ...failedTasks.map((t): UnifiedJob => ({ id: t.id, type: 'pypi', package: t.package, status: 'failed', depth: t.depth, attempts: t.attempts, error: t.error, failed_at: t.failed_at, })), ]; return (

Background Jobs

{successMessage &&
{successMessage}
} {statusError &&
{statusError}
} {/* Overall Progress Bar */} {totalJobs > 0 && (
Overall Progress {cacheStatus?.completed ?? 0} / {totalJobs} completed {(cacheStatus?.in_progress ?? 0) > 0 && ` · ${cacheStatus?.in_progress} active`} {(cacheStatus?.failed ?? 0) > 0 && ` · ${cacheStatus?.failed} failed`}
{(cacheStatus?.in_progress ?? 0) > 0 && (
)}
)} {/* Unified Jobs Section */}

All Jobs

{failedTasks.length > 0 && ( )}
{loadingStatus && !cacheStatus ? (

Loading job status...

) : totalJobs === 0 ? (

No jobs yet. Jobs are created when packages are downloaded through the PyPI proxy.

) : allJobs.length === 0 ? (

All {cacheStatus?.completed ?? 0} jobs completed successfully!

) : ( {allJobs.map((job) => { return ( ); })}
Type Package Status Depth Attempts Details Actions
PyPI {job.status === 'in_progress' && } {job.package} {job.status === 'in_progress' ? ( Working ) : job.status === 'failed' ? ( Failed ) : ( Pending )} {job.depth} {job.attempts} {job.status === 'failed' && job.error && ( {job.error.length > 40 ? job.error.substring(0, 40) + '...' : job.error} )} {job.status === 'in_progress' && job.version_constraint && ( {job.version_constraint} )} {job.status === 'failed' && job.failed_at && ( {new Date(job.failed_at).toLocaleTimeString()} )} {job.status === 'failed' && ( )}
)}
); } export default AdminJobsPage;