import { useEffect, useState } from 'react' import { useParams, useNavigate } from 'react-router-dom' import { Wrench, Calendar, Play, Settings, Clock, CheckCircle, AlertCircle } from 'lucide-react' import { treesApi } from '@/api/trees' import { sessionsApi } from '@/api/sessions' import { maintenanceSchedulesApi } from '@/api/maintenanceSchedules' import { BatchLaunchModal } from '@/components/maintenance/BatchLaunchModal' import { ActiveBatchBanner } from '@/components/maintenance/ActiveBatchBanner' import { Spinner } from '@/components/common/Spinner' import { EmptyState } from '@/components/common/EmptyState' import { PageHeader } from '@/components/common/PageHeader' import { toast } from '@/lib/toast' import { cn } from '@/lib/utils' import type { Tree, MaintenanceSchedule, Session } from '@/types' const OUTCOME_LABELS: Record = { resolved: 'resolved', escalated: 'escalated', workaround: 'workaround', unresolved: 'unresolved', } export default function MaintenanceFlowDetailPage() { const { id } = useParams<{ id: string }>() const navigate = useNavigate() const [tree, setTree] = useState(null) const [schedule, setSchedule] = useState(null) const [recentSessions, setRecentSessions] = useState([]) const [showBatchModal, setShowBatchModal] = useState(false) const [isLoading, setIsLoading] = useState(true) const [isRunning, setIsRunning] = useState(false) const [bannerDismissed, setBannerDismissed] = useState(false) useEffect(() => { if (!id) return const load = async () => { try { const treeData = await treesApi.get(id) if (treeData.tree_type !== 'maintenance') { toast.error('This page is only for maintenance flows') navigate('/trees?type=maintenance') return } setTree(treeData) try { const sessionData = await sessionsApi.list({ tree_id: id, size: 30 }) setRecentSessions(Array.isArray(sessionData) ? sessionData : []) } catch { // Sessions load is optional } try { const sched = await maintenanceSchedulesApi.getForTree(id) setSchedule(sched) } catch { // No schedule yet is fine } } catch { toast.error('Failed to load maintenance flow') navigate('/trees?type=maintenance') } finally { setIsLoading(false) } } load() }, [id, navigate]) const handleLaunched = (batchId: string, _count: number) => { setShowBatchModal(false) setBannerDismissed(false) // Reload sessions so banner picks up the new batch if (id) { sessionsApi.list({ tree_id: id, size: 30 }) .then(data => setRecentSessions(Array.isArray(data) ? data : [])) .catch(() => {}) } navigate(`/flows/${id}/batches/${batchId}`) } const handleRun = () => { setIsRunning(true) navigate(`/flows/${id}/navigate`) } if (isLoading) { return (
) } if (!tree) { return (
navigate('/trees?type=maintenance')} className="rounded-md border border-border px-4 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-foreground" > Back to Maintenance Flows )} />
) } // Group sessions by batch_id for run history const batchMap = new Map() for (const s of recentSessions) { const key = s.batch_id ?? s.id const existing = batchMap.get(key) ?? [] batchMap.set(key, [...existing, s]) } const batches = Array.from(batchMap.entries()).slice(0, 10) // Show banner only if there are in-progress batch sessions and it hasn't been dismissed const hasActiveBatch = recentSessions.some(s => s.started_at && !s.completed_at && s.batch_id) return (
{/* Active batch banner */} {hasActiveBatch && !bannerDismissed && ( setBannerDismissed(true)} /> )} {/* Header */}
)} titleClassName="text-xl font-semibold" action={(
)} /> {/* Schedule Panel */}

Schedule

{schedule ? (
{schedule.is_active ? : } {schedule.is_active ? 'Active' : 'Paused'} {schedule.cron_expression} ({schedule.timezone})
{schedule.next_run_at && (

Next run: {new Date(schedule.next_run_at).toLocaleString()}

)}
) : (

No schedule configured. Sessions can still be launched manually via Batch Launch.

)}
{/* Run History */}

Run History

{batches.length === 0 ? (

No runs yet. Launch a batch to get started.

) : (
{batches.map(([batchKey, batchSessions]) => { const completed = batchSessions.filter(s => s.completed_at).length const total = batchSessions.length const isActive = batchSessions.some(s => s.started_at && !s.completed_at) const date = batchSessions[0]?.started_at const isSingleRun = !batchSessions[0]?.batch_id // Outcome summary const outcomeCounts = batchSessions.reduce>((acc, s) => { if (s.outcome) acc[s.outcome] = (acc[s.outcome] ?? 0) + 1 return acc }, {}) const outcomeParts = Object.entries(outcomeCounts) .map(([k, v]) => `${v} ${OUTCOME_LABELS[k] ?? k}`) // Mini progress dots (up to 8 shown) const dotsToShow = Math.min(total, 8) const dots = Array.from({ length: dotsToShow }, (_, i) => i < completed) const extraDots = total > 8 ? total - 8 : 0 const handleRowClick = () => { if (isSingleRun && batchSessions[0]) { navigate(`/sessions/${batchSessions[0].id}`) } else { navigate(`/flows/${id}/batches/${batchKey}`) } } return ( ) })}
)}
{showBatchModal && ( setShowBatchModal(false)} onLaunched={handleLaunched} /> )} ) }