diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 04bec2a5..3f6dd659 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -13,3 +13,5 @@ export { default as adminApi } from './admin' export { treeMarkdownApi } from './treeMarkdown' export { default as pinnedFlowsApi } from './pinnedFlows' export { default as analyticsApi } from './analytics' +export { targetListsApi } from './targetLists' +export { maintenanceSchedulesApi, batchLaunchApi } from './maintenanceSchedules' diff --git a/frontend/src/api/maintenanceSchedules.ts b/frontend/src/api/maintenanceSchedules.ts new file mode 100644 index 00000000..e63ad693 --- /dev/null +++ b/frontend/src/api/maintenanceSchedules.ts @@ -0,0 +1,24 @@ +import { apiClient } from './client' +import type { + MaintenanceSchedule, + MaintenanceScheduleCreate, + MaintenanceScheduleUpdate, + BatchLaunchRequest, + BatchLaunchResponse, +} from '@/types' + +export const maintenanceSchedulesApi = { + getForTree: (treeId: string): Promise => + apiClient.get(`/maintenance-schedules/tree/${treeId}`).then(r => r.data), + + create: (data: MaintenanceScheduleCreate): Promise => + apiClient.post('/maintenance-schedules', data).then(r => r.data), + + update: (id: string, data: MaintenanceScheduleUpdate): Promise => + apiClient.patch(`/maintenance-schedules/${id}`, data).then(r => r.data), +} + +export const batchLaunchApi = { + launch: (data: BatchLaunchRequest): Promise => + apiClient.post('/sessions/batch', data).then(r => r.data), +} diff --git a/frontend/src/api/targetLists.ts b/frontend/src/api/targetLists.ts new file mode 100644 index 00000000..28508b3a --- /dev/null +++ b/frontend/src/api/targetLists.ts @@ -0,0 +1,19 @@ +import { apiClient } from './client' +import type { TargetList, TargetListCreate } from '@/types' + +export const targetListsApi = { + list: (): Promise => + apiClient.get('/target-lists/').then(r => r.data), + + get: (id: string): Promise => + apiClient.get(`/target-lists/${id}`).then(r => r.data), + + create: (data: TargetListCreate): Promise => + apiClient.post('/target-lists/', data).then(r => r.data), + + update: (id: string, data: Partial): Promise => + apiClient.put(`/target-lists/${id}`, data).then(r => r.data), + + delete: (id: string): Promise => + apiClient.delete(`/target-lists/${id}`).then(() => undefined), +} diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index cd00f5b2..c092601f 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -29,7 +29,7 @@ export function Sidebar() { const [activeTags, setActiveTags] = useState([]) const [activeSessionCount, setActiveSessionCount] = useState(0) const [pinnedFlows, setPinnedFlows] = useState([]) - const [treeCounts, setTreeCounts] = useState({ total: 0, troubleshooting: 0, procedural: 0 }) + const [treeCounts, setTreeCounts] = useState({ total: 0, troubleshooting: 0, procedural: 0, maintenance: 0 }) // Fetch sidebar data on mount useEffect(() => { @@ -55,7 +55,8 @@ export function Sidebar() { const total = allTrees.length const troubleshooting = allTrees.filter(t => t.tree_type === 'troubleshooting').length const procedural = allTrees.filter(t => t.tree_type === 'procedural').length - setTreeCounts({ total, troubleshooting, procedural }) + const maintenance = allTrees.filter(t => t.tree_type === 'maintenance').length + setTreeCounts({ total, troubleshooting, procedural, maintenance }) } catch { // Silently handle errors } @@ -145,6 +146,7 @@ export function Sidebar() { children={[ { href: '/trees?type=troubleshooting', label: 'Troubleshooting', count: treeCounts.troubleshooting || undefined }, { href: '/trees?type=procedural', label: 'Projects', count: treeCounts.procedural || undefined }, + { href: '/trees?type=maintenance', label: 'Maintenance', count: treeCounts.maintenance || undefined }, ]} /> diff --git a/frontend/src/components/library/TreeGridView.tsx b/frontend/src/components/library/TreeGridView.tsx index a6d8cb8b..a633a862 100644 --- a/frontend/src/components/library/TreeGridView.tsx +++ b/frontend/src/components/library/TreeGridView.tsx @@ -1,5 +1,5 @@ import { Link } from 'react-router-dom' -import { Pencil, Globe, Lock, Trash2, GitBranch, FileText } from 'lucide-react' +import { Pencil, Globe, Lock, Trash2, GitBranch, FileText, Wrench } from 'lucide-react' import type { TreeListItem } from '@/types' import { TagBadges } from '@/components/common/TagBadges' import { AddToFolderMenu } from './AddToFolderMenu' @@ -41,6 +41,12 @@ export function TreeGridView({ Draft )} + {tree.tree_type === 'maintenance' && ( + + + Maintenance + + )}
{tree.is_public ? ( diff --git a/frontend/src/components/library/TreeListView.tsx b/frontend/src/components/library/TreeListView.tsx index 82227b39..7485d164 100644 --- a/frontend/src/components/library/TreeListView.tsx +++ b/frontend/src/components/library/TreeListView.tsx @@ -1,5 +1,5 @@ import { Link } from 'react-router-dom' -import { Pencil, Globe, Lock, GitBranch, FileText, Trash2 } from 'lucide-react' +import { Pencil, Globe, Lock, GitBranch, FileText, Trash2, Wrench } from 'lucide-react' import type { TreeListItem } from '@/types' import { TagBadges } from '@/components/common/TagBadges' import { AddToFolderMenu } from './AddToFolderMenu' @@ -42,6 +42,12 @@ export function TreeListView({ Draft )} + {tree.tree_type === 'maintenance' && ( + + + Maintenance + + )} {tree.is_public ? ( diff --git a/frontend/src/components/library/TreeTableView.tsx b/frontend/src/components/library/TreeTableView.tsx index bd4ea7ae..c09101ed 100644 --- a/frontend/src/components/library/TreeTableView.tsx +++ b/frontend/src/components/library/TreeTableView.tsx @@ -1,6 +1,6 @@ import { useState } from 'react' import { Link } from 'react-router-dom' -import { Pencil, Globe, Lock, ChevronUp, ChevronDown, GitBranch, FileText, Trash2 } from 'lucide-react' +import { Pencil, Globe, Lock, ChevronUp, ChevronDown, GitBranch, FileText, Trash2, Wrench } from 'lucide-react' import type { TreeListItem } from '@/types' import { TagBadges } from '@/components/common/TagBadges' import { AddToFolderMenu } from './AddToFolderMenu' @@ -144,6 +144,12 @@ export function TreeTableView({ Draft )} + {tree.tree_type === 'maintenance' && ( + + + Maintenance + + )} {tree.is_public ? ( diff --git a/frontend/src/lib/routing.ts b/frontend/src/lib/routing.ts index 42f73e5a..608a2436 100644 --- a/frontend/src/lib/routing.ts +++ b/frontend/src/lib/routing.ts @@ -1,7 +1,7 @@ /** * Shared routing helpers for tree/session navigation. * Centralizes the logic for determining the correct navigation path - * based on tree type (troubleshooting vs procedural). + * based on tree type (troubleshooting vs procedural vs maintenance). */ /** @@ -14,6 +14,9 @@ export function getTreeNavigatePath( if (treeType === 'procedural') { return `/flows/${treeId}/navigate` } + if (treeType === 'maintenance') { + return `/flows/${treeId}/maintenance` + } return `/trees/${treeId}/navigate` } @@ -24,7 +27,7 @@ export function getTreeEditorPath( treeId: string, treeType?: string ): string { - if (treeType === 'procedural') { + if (treeType === 'procedural' || treeType === 'maintenance') { return `/flows/${treeId}/edit` } return `/trees/${treeId}/edit` diff --git a/frontend/src/pages/TreeLibraryPage.tsx b/frontend/src/pages/TreeLibraryPage.tsx index 1df82437..6988b5a4 100644 --- a/frontend/src/pages/TreeLibraryPage.tsx +++ b/frontend/src/pages/TreeLibraryPage.tsx @@ -37,14 +37,14 @@ export function TreeLibraryPage() { // Read type filter from URL query params (e.g. /trees?type=procedural) const urlType = searchParams.get('type') - const [typeFilter, setTypeFilter] = useState<'all' | 'troubleshooting' | 'procedural'>( - urlType === 'troubleshooting' || urlType === 'procedural' ? urlType : 'all' + const [typeFilter, setTypeFilter] = useState<'all' | 'troubleshooting' | 'procedural' | 'maintenance'>( + urlType === 'troubleshooting' || urlType === 'procedural' || urlType === 'maintenance' ? urlType : 'all' ) // Sync filters when URL changes (e.g. clicking sidebar categories/tags or nav sub-items) useEffect(() => { const t = searchParams.get('type') - if (t === 'troubleshooting' || t === 'procedural') { + if (t === 'troubleshooting' || t === 'procedural' || t === 'maintenance') { setTypeFilter(t) } else { setTypeFilter('all') @@ -253,14 +253,16 @@ export function TreeLibraryPage() {

- {typeFilter === 'procedural' ? 'Projects' : typeFilter === 'troubleshooting' ? 'Troubleshooting Flows' : 'Flow Library'} + {typeFilter === 'procedural' ? 'Projects' : typeFilter === 'troubleshooting' ? 'Troubleshooting Flows' : typeFilter === 'maintenance' ? 'Maintenance Flows' : 'Flow Library'}

{typeFilter === 'procedural' ? 'Step-by-step projects and runbooks' : typeFilter === 'troubleshooting' ? 'Branching decision flows for troubleshooting' - : 'Browse and start troubleshooting flows and projects'} + : typeFilter === 'maintenance' + ? 'Scheduled maintenance procedures run across targets' + : 'Browse and start troubleshooting flows and projects'}

{canCreateTrees && ( @@ -326,7 +328,7 @@ export function TreeLibraryPage() {
- {(['all', 'troubleshooting', 'procedural'] as const).map((t) => ( + {(['all', 'troubleshooting', 'procedural', 'maintenance'] as const).map((t) => ( ))}
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index b6ff745d..901cfbea 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -23,3 +23,14 @@ export interface PaginatedResponse { export interface ApiError { detail: string } + +export type { + TargetEntry, + TargetList, + TargetListCreate, + MaintenanceSchedule, + MaintenanceScheduleCreate, + MaintenanceScheduleUpdate, + BatchLaunchRequest, + BatchLaunchResponse, +} from './maintenance' diff --git a/frontend/src/types/maintenance.ts b/frontend/src/types/maintenance.ts new file mode 100644 index 00000000..511e6f4f --- /dev/null +++ b/frontend/src/types/maintenance.ts @@ -0,0 +1,65 @@ +export interface TargetEntry { + label: string + notes?: string +} + +export interface TargetList { + id: string + team_id: string + created_by?: string + name: string + description?: string + targets: TargetEntry[] + created_at: string + updated_at: string +} + +export interface TargetListCreate { + name: string + description?: string + targets: TargetEntry[] +} + +export interface MaintenanceSchedule { + id: string + tree_id: string + created_by?: string + cron_expression: string + timezone: string + target_list_id?: string + is_active: boolean + next_run_at?: string + last_run_at?: string + created_at: string + updated_at: string +} + +export interface MaintenanceScheduleCreate { + tree_id: string + cron_expression: string + timezone: string + target_list_id?: string +} + +export interface MaintenanceScheduleUpdate { + cron_expression?: string + timezone?: string + target_list_id?: string + is_active?: boolean +} + +export interface BatchLaunchRequest { + tree_id: string + targets: TargetEntry[] +} + +export interface BatchLaunchResponse { + batch_id: string + count: number + sessions: Array<{ + id: string + batch_id: string + target_label: string + tree_id: string + }> +} diff --git a/frontend/src/types/tree.ts b/frontend/src/types/tree.ts index a7654211..1dae8a5a 100644 --- a/frontend/src/types/tree.ts +++ b/frontend/src/types/tree.ts @@ -58,7 +58,7 @@ export interface TreeStructure { // --- Procedural Flow Types --- -export type TreeType = 'troubleshooting' | 'procedural' +export type TreeType = 'troubleshooting' | 'procedural' | 'maintenance' export type IntakeFieldType = | 'text' | 'textarea' | 'number' | 'ip_address' | 'email' | 'url'