import { useEffect, useState, useRef, useCallback } from 'react' import { useParams, useNavigate } from 'react-router-dom' import { ChevronLeft, RefreshCw, Wrench } from 'lucide-react' import { treesApi } from '@/api/trees' import { sessionsApi } from '@/api/sessions' import { BatchStatusCard } from '@/components/maintenance/BatchStatusCard' import { Spinner } from '@/components/common/Spinner' import { cn } from '@/lib/utils' import type { Tree, Session } from '@/types' // Batch sessions are created with a shared batch_id and individual target_labels. // Some targets may not have a session yet if created via a pre-populated target list // where sessions were created all at once — in practice all sessions exist at batch // launch time, so we group by target_label from the sessions we get back. export default function BatchStatusPage() { const { id: treeId, batchId } = useParams<{ id: string; batchId: string }>() const navigate = useNavigate() const [tree, setTree] = useState(null) const [sessions, setSessions] = useState([]) const [isLoading, setIsLoading] = useState(true) const [isRefreshing, setIsRefreshing] = useState(false) const [loadError, setLoadError] = useState(null) const [batchDate, setBatchDate] = useState(null) const pollRef = useRef | null>(null) const loadSessions = useCallback(async (showRefreshing = false) => { if (!batchId) return if (showRefreshing) setIsRefreshing(true) try { const data = await sessionsApi.list({ batch_id: batchId, size: 100 }) setSessions(Array.isArray(data) ? data : []) setLoadError(null) if (data.length > 0 && data[0].started_at) { setBatchDate(new Date(data[0].started_at)) } } catch { setLoadError('Failed to load batch sessions') } finally { if (showRefreshing) setIsRefreshing(false) } }, [batchId]) // Initial load useEffect(() => { if (!treeId || !batchId) return const load = async () => { try { const [treeData] = await Promise.all([ treesApi.get(treeId), loadSessions(), ]) setTree(treeData) } catch { setLoadError('Failed to load batch data') } finally { setIsLoading(false) } } load() }, [treeId, batchId, loadSessions]) // Polling: refresh every 5s while any session is in-progress useEffect(() => { const hasInProgress = sessions.some(s => s.started_at && !s.completed_at) if (hasInProgress) { pollRef.current = setInterval(() => loadSessions(), 5000) } else { if (pollRef.current) { clearInterval(pollRef.current) pollRef.current = null } } return () => { if (pollRef.current) clearInterval(pollRef.current) } }, [sessions, loadSessions]) if (isLoading) { return (
) } const total = sessions.length const completed = sessions.filter(s => s.completed_at).length const inProgress = sessions.filter(s => s.started_at && !s.completed_at).length const allDone = total > 0 && completed === total // Outcome summary for completion const outcomeCounts = sessions.reduce>((acc, s) => { if (s.outcome) acc[s.outcome] = (acc[s.outcome] ?? 0) + 1 return acc }, {}) const progressPercent = total > 0 ? Math.round((completed / total) * 100) : 0 return (
{/* Breadcrumb */}
{/* Header */}

Batch Run

{batchDate && (

{batchDate.toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}

)}
{/* Progress bar */} {total > 0 && (
{completed} of {total} complete 0 ? 'text-amber-400 bg-amber-500/10' : 'text-muted-foreground bg-muted' )}> {allDone ? 'Complete' : inProgress > 0 ? `${inProgress} in progress` : 'Not started'}
{/* Completion summary */} {allDone && Object.keys(outcomeCounts).length > 0 && (
{outcomeCounts.resolved && ( {outcomeCounts.resolved} resolved )} {outcomeCounts.escalated && ( {outcomeCounts.escalated} escalated )} {outcomeCounts.workaround && ( {outcomeCounts.workaround} workaround )} {outcomeCounts.unresolved && ( {outcomeCounts.unresolved} unresolved )}
)}
)} {/* Target cards */}
{loadError ? (

{loadError}

) : sessions.length === 0 ? (

No sessions found for this batch.

) : ( sessions .sort((a, b) => { // Sort: in-progress first, then not-started, then complete const rank = (s: Session) => { if (s.started_at && !s.completed_at) return 0 if (!s.started_at) return 1 return 2 } return rank(a) - rank(b) }) .map((session) => ( )) )}
) }