import React, { useState, useEffect, useRef } from 'react'; import { Box, Flex, Text, Progress, Button, VStack, HStack, Badge, useDisclosure, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalCloseButton, Table, Thead, Tbody, Tr, Th, Td, Spinner, } from '@chakra-ui/react'; import { api } from '../services/api'; interface JobProgress { jobId: string; type: 'storage-sync' | 'song-matching'; status: 'running' | 'completed' | 'failed'; progress: number; current: number; total: number; message: string; startTime: Date; endTime?: Date; result?: any; error?: string; } interface BackgroundJobProgressProps { jobId?: string; onJobComplete?: (result: any) => void; onJobError?: (error: string) => void; } export const BackgroundJobProgress: React.FC = ({ jobId, onJobComplete, onJobError, }) => { const [jobs, setJobs] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const { isOpen, onClose } = useDisclosure(); const intervalRef = useRef(null); // Load all jobs const loadJobs = async () => { try { setLoading(true); const jobsData = await api.getAllJobs(); setJobs(jobsData); setError(null); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load jobs'); } finally { setLoading(false); } }; // Update specific job progress const updateJobProgress = async (jobId: string) => { try { const progress = await api.getJobProgress(jobId); setJobs(prev => prev.map(job => job.jobId === jobId ? progress : job )); // Handle job completion if (progress.status === 'completed' && onJobComplete) { onJobComplete(progress.result); } else if (progress.status === 'failed' && onJobError) { onJobError(progress.error || 'Job failed'); } } catch (err) { console.error('Error updating job progress:', err); } }; // Start polling for jobs and update progress const startPolling = () => { if (intervalRef.current) { clearInterval(intervalRef.current); } const tick = async () => { try { // Always reload job list to detect newly started jobs const jobsData = await api.getAllJobs(); setJobs(jobsData); // Update progress for active jobs const activeJobIds = jobsData.filter((j: JobProgress) => j.status === 'running').map((j: JobProgress) => j.jobId); for (const id of activeJobIds) { await updateJobProgress(id); } if (jobId) { await updateJobProgress(jobId); } } catch (err) { // ignore transient polling errors } }; // Adaptive interval: 2s if active jobs, else 10s const schedule = async () => { await tick(); const hasActive = (jobs || []).some(j => j.status === 'running'); const delay = hasActive ? 2000 : 10000; intervalRef.current = setTimeout(schedule, delay) as any; }; schedule(); }; // Stop polling const stopPolling = () => { if (intervalRef.current) { clearTimeout(intervalRef.current as any); intervalRef.current = null; } }; // Start polling on mount and stop on unmount useEffect(() => { loadJobs(); startPolling(); return () => stopPolling(); }, []); // Cleanup on unmount useEffect(() => { return () => { stopPolling(); }; }, []); const getStatusColor = (status: string) => { switch (status) { case 'running': return 'blue'; case 'completed': return 'green'; case 'failed': return 'red'; default: return 'gray'; } }; const getStatusIcon = (status: string) => { switch (status) { case 'running': return ; case 'completed': return ; case 'failed': return ; default: return ⏸️; } }; const formatDuration = (startTime: Date, endTime?: Date) => { const start = new Date(startTime).getTime(); const end = endTime ? new Date(endTime).getTime() : Date.now(); const duration = Math.floor((end - start) / 1000); if (duration < 60) return `${duration}s`; if (duration < 3600) return `${Math.floor(duration / 60)}m ${duration % 60}s`; return `${Math.floor(duration / 3600)}h ${Math.floor((duration % 3600) / 60)}m`; }; const activeJobs = jobs.filter(job => job.status === 'running'); // const completedJobs = jobs.filter(job => job.status === 'completed' || job.status === 'failed'); return ( <> {/* Active Jobs Summary */} {activeJobs.length > 0 && ( Background Jobs ({activeJobs.length}) {activeJobs.map(job => ( {job.type === 'storage-sync' ? 'Storage Sync' : 'Song Matching'} {job.status} {job.message} {job.progress}% {formatDuration(job.startTime)} ))} )} {/* All Jobs Modal */} Background Jobs All Jobs {error && ( {error} )} {jobs.map(job => ( ))}
Type Status Progress Duration Message
{job.type === 'storage-sync' ? 'Storage Sync' : 'Song Matching'} {getStatusIcon(job.status)} {job.status} {job.progress}% {formatDuration(job.startTime, job.endTime)} {job.message}
{jobs.length === 0 && !loading && ( No background jobs found )}
); };