From 55cd481e48c15e9c60f822ed4bea533403a562ea Mon Sep 17 00:00:00 2001 From: chihlasm Date: Tue, 17 Feb 2026 14:11:20 -0500 Subject: [PATCH] feat: BatchLaunchModal and MaintenanceFlowDetailPage with schedule panel and run history Co-Authored-By: Claude Sonnet 4.5 --- .../maintenance/BatchLaunchModal.tsx | 202 ++++++++++++++++++ .../src/pages/MaintenanceFlowDetailPage.tsx | 198 +++++++++++++++++ frontend/src/router.tsx | 9 + 3 files changed, 409 insertions(+) create mode 100644 frontend/src/components/maintenance/BatchLaunchModal.tsx create mode 100644 frontend/src/pages/MaintenanceFlowDetailPage.tsx diff --git a/frontend/src/components/maintenance/BatchLaunchModal.tsx b/frontend/src/components/maintenance/BatchLaunchModal.tsx new file mode 100644 index 00000000..46c091c9 --- /dev/null +++ b/frontend/src/components/maintenance/BatchLaunchModal.tsx @@ -0,0 +1,202 @@ +import { useState, useEffect } from 'react' +import { X, List, Clock, PenLine, ExternalLink } from 'lucide-react' +import { cn } from '@/lib/utils' +import { toast } from '@/lib/toast' +import { targetListsApi, batchLaunchApi } from '@/api' +import type { TargetList, TargetEntry } from '@/types' + +interface BatchLaunchModalProps { + treeId: string + treeName: string + onClose: () => void + onLaunched: (batchId: string, count: number) => void +} + +type TabId = 'manual' | 'saved' | 'previous' | 'psa' + +export function BatchLaunchModal({ treeId, treeName, onClose, onLaunched }: BatchLaunchModalProps) { + const [activeTab, setActiveTab] = useState('manual') + const [savedLists, setSavedLists] = useState(null) + const [selectedListId, setSelectedListId] = useState(null) + const [manualInput, setManualInput] = useState('') + const [isLaunching, setIsLaunching] = useState(false) + + useEffect(() => { + if (activeTab === 'saved' && savedLists === null) { + targetListsApi.list() + .then(setSavedLists) + .catch(() => toast.error('Failed to load saved lists')) + } + }, [activeTab, savedLists]) + + const getTargets = (): TargetEntry[] => { + if (activeTab === 'saved' && selectedListId && savedLists) { + const list = savedLists.find(l => l.id === selectedListId) + return list?.targets ?? [] + } + if (activeTab === 'manual') { + return manualInput + .split('\n') + .map(l => l.trim()) + .filter(Boolean) + .map(label => ({ label })) + } + return [] + } + + const targets = getTargets() + + const handleLaunch = async () => { + if (targets.length === 0) { + toast.error('Add at least one target before launching') + return + } + if (targets.length > 100) { + toast.error('Maximum 100 targets per batch') + return + } + setIsLaunching(true) + try { + const result = await batchLaunchApi.launch({ tree_id: treeId, targets }) + toast.success(`${result.count} sessions created`) + onLaunched(result.batch_id, result.count) + } catch { + toast.error('Failed to launch batch') + } finally { + setIsLaunching(false) + } + } + + const tabs: { id: TabId; label: string; icon: React.ReactNode }[] = [ + { id: 'manual', label: 'Manual Entry', icon: }, + { id: 'saved', label: 'Saved List', icon: }, + { id: 'previous', label: 'Previous Run', icon: }, + { id: 'psa', label: 'PSA / RMM', icon: }, + ] + + return ( +
+
+ {/* Header */} +
+
+

Batch Launch

+

{treeName}

+
+ +
+ + {/* Tabs */} +
+ {tabs.map(tab => ( + + ))} +
+ + {/* Content */} +
+ {activeTab === 'manual' && ( +
+ +