` with DndContext and SortableContext:
+```tsx
+
+ s.type !== 'procedure_end').map(s => s.id)} strategy={verticalListSortingStrategy}>
+
+ {steps.map((step) => {
+ // ... existing rendering logic
+ })}
+
+
+
+```
+
+**Step 3: Make each step card sortable**
+
+For each step card (procedure_step collapsed, section_header collapsed), extract the card div into a `SortableStepCard` wrapper or apply `useSortable` inline.
+
+The simplest approach: create a small wrapper component inside StepList.tsx:
+
+```tsx
+function SortableStepWrapper({ id, children, disabled }: { id: string; children: ReactNode; disabled?: boolean }) {
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id, disabled })
+
+ const style = {
+ transform: CSS.Transform.toString(transform),
+ transition,
+ }
+
+ return (
+
+ {/* Pass drag handle props to children via render prop or context */}
+ {typeof children === 'function'
+ ? children({ dragHandleProps: { ...attributes, ...listeners } })
+ : children}
+
+ )
+}
+```
+
+Actually, a simpler approach: apply useSortable directly to each card's GripVertical button. Wrap each step's outer `
` with `ref={setNodeRef}` and pass `{...attributes} {...listeners}` to the GripVertical button.
+
+For collapsed procedure_step cards (the main case), the existing `
` button at line 148 gets the sortable props:
+```tsx
+// Replace the GripVertical span/button:
+
+
+
+```
+
+For expanded steps (StepEditor), disable sorting (`disabled: true` in useSortable) since the user is editing.
+
+The `procedure_end` step should NOT be in the SortableContext items array (already excluded above) and should not have useSortable applied.
+
+**Step 4: Verify it builds**
+
+Run: `cd frontend && npm run build 2>&1 | tail -5`
+Expected: Clean build
+
+**Step 5: Commit**
+
+```bash
+git add frontend/src/components/procedural-editor/StepList.tsx
+git commit -m "feat: add drag-to-reorder steps with @dnd-kit"
+```
+
+---
+
+## Phase 5: Maintenance Schedule Section
+
+### Task 5: Create MaintenanceScheduleSection
+
+**Files:**
+- Create: `frontend/src/components/procedural-editor/MaintenanceScheduleSection.tsx`
+
+**Context:** This component renders only for maintenance flows. It shows a schedule builder for new flows (or flows without a schedule) and a collapsed summary for existing scheduled flows. It uses the existing `maintenanceSchedulesApi` from `frontend/src/api/maintenanceSchedules.ts` and `targetListsApi` from `frontend/src/api/targetLists.ts`. Types are in `frontend/src/types/maintenance.ts`.
+
+Read these files first:
+- `frontend/src/api/maintenanceSchedules.ts`
+- `frontend/src/api/targetLists.ts`
+- `frontend/src/types/maintenance.ts`
+
+**Step 1: Create the component**
+
+```tsx
+// frontend/src/components/procedural-editor/MaintenanceScheduleSection.tsx
+import { useState, useEffect } from 'react'
+import { Calendar, Clock } from 'lucide-react'
+import { maintenanceSchedulesApi } from '@/api/maintenanceSchedules'
+import { targetListsApi } from '@/api/targetLists'
+import type { MaintenanceSchedule, TargetList } from '@/types'
+import { cn } from '@/lib/utils'
+import { toast } from '@/lib/toast'
+
+interface MaintenanceScheduleSectionProps {
+ treeId: string | null // null for new flows
+}
+
+const FREQUENCY_OPTIONS = [
+ { value: 'daily', label: 'Daily', cron: (hour: number, min: number) => `${min} ${hour} * * *` },
+ { value: 'weekly-mon', label: 'Weekly (Monday)', cron: (hour: number, min: number) => `${min} ${hour} * * 1` },
+ { value: 'weekly-fri', label: 'Weekly (Friday)', cron: (hour: number, min: number) => `${min} ${hour} * * 5` },
+ { value: 'monthly', label: 'Monthly (1st)', cron: (hour: number, min: number) => `${min} ${hour} 1 * *` },
+] as const
+
+const TIMEZONE_OPTIONS = [
+ 'UTC',
+ 'America/New_York',
+ 'America/Chicago',
+ 'America/Denver',
+ 'America/Los_Angeles',
+ 'Europe/London',
+ 'Europe/Berlin',
+ 'Asia/Tokyo',
+ 'Australia/Sydney',
+]
+
+export function MaintenanceScheduleSection({ treeId }: MaintenanceScheduleSectionProps) {
+ const [schedule, setSchedule] = useState(null)
+ const [targetLists, setTargetLists] = useState([])
+ const [isLoading, setIsLoading] = useState(false)
+ const [isSaving, setIsSaving] = useState(false)
+
+ // Form state
+ const [frequency, setFrequency] = useState('weekly-mon')
+ const [hour, setHour] = useState(9)
+ const [minute, setMinute] = useState(0)
+ const [timezone, setTimezone] = useState('UTC')
+ const [selectedTargetListId, setSelectedTargetListId] = useState('')
+
+ // Load existing schedule and target lists
+ useEffect(() => {
+ const load = async () => {
+ setIsLoading(true)
+ try {
+ const lists = await targetListsApi.list()
+ setTargetLists(lists)
+
+ if (treeId) {
+ try {
+ const existing = await maintenanceSchedulesApi.getForTree(treeId)
+ setSchedule(existing)
+ if (existing.target_list_id) {
+ setSelectedTargetListId(existing.target_list_id)
+ }
+ } catch {
+ // No schedule yet — that's fine
+ }
+ }
+ } catch {
+ // Target lists may not load — non-critical
+ } finally {
+ setIsLoading(false)
+ }
+ }
+ load()
+ }, [treeId])
+
+ const handleSaveSchedule = async () => {
+ if (!treeId) {
+ toast.error('Save the flow first before configuring a schedule')
+ return
+ }
+
+ setIsSaving(true)
+ try {
+ const freqOption = FREQUENCY_OPTIONS.find(f => f.value === frequency)
+ const cronExpression = freqOption?.cron(hour, minute) ?? `${minute} ${hour} * * 1`
+
+ if (schedule) {
+ const updated = await maintenanceSchedulesApi.update(schedule.id, {
+ cron_expression: cronExpression,
+ timezone,
+ target_list_id: selectedTargetListId || undefined,
+ })
+ setSchedule(updated)
+ toast.success('Schedule updated')
+ } else {
+ const created = await maintenanceSchedulesApi.create({
+ tree_id: treeId,
+ cron_expression: cronExpression,
+ timezone,
+ target_list_id: selectedTargetListId || undefined,
+ })
+ setSchedule(created)
+ toast.success('Schedule created')
+ }
+ } catch {
+ toast.error('Failed to save schedule')
+ } finally {
+ setIsSaving(false)
+ }
+ }
+
+ const selectedTargetList = targetLists.find(tl => tl.id === selectedTargetListId)
+
+ if (isLoading) {
+ return (
+ Loading schedule...
+ )
+ }
+
+ return (
+
+ {/* Frequency */}
+
+ Frequency
+ setFrequency(e.target.value)}
+ className="w-full rounded-lg border border-border bg-card px-3 py-2 text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
+ >
+ {FREQUENCY_OPTIONS.map(opt => (
+ {opt.label}
+ ))}
+
+
+
+ {/* Time */}
+
+
+ {/* Timezone */}
+
+ Timezone
+ setTimezone(e.target.value)}
+ className="w-full rounded-lg border border-border bg-card px-3 py-2 text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
+ >
+ {TIMEZONE_OPTIONS.map(tz => (
+ {tz}
+ ))}
+
+
+
+ {/* Target List */}
+ {targetLists.length > 0 && (
+
+ Target List (optional)
+ setSelectedTargetListId(e.target.value)}
+ className="w-full rounded-lg border border-border bg-card px-3 py-2 text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
+ >
+ None — manual targets only
+ {targetLists.map(tl => (
+ {tl.name} ({tl.targets.length} targets)
+ ))}
+
+
+ )}
+
+ {/* Save button */}
+
+
+ {isSaving ? 'Saving...' : schedule ? 'Update Schedule' : 'Create Schedule'}
+
+ {!treeId && (
+
Save the flow first to configure a schedule.
+ )}
+
+ )
+}
+```
+
+**Step 2: Build a summary string helper**
+
+Add this exported function at the bottom of the file (used by ProceduralEditorPage for the collapsible section summary):
+
+```tsx
+export function getScheduleSummary(schedule: MaintenanceSchedule | null, targetList?: TargetList | null): string {
+ if (!schedule) return 'No schedule configured'
+
+ // Parse cron for human-readable display
+ const parts = schedule.cron_expression.split(' ')
+ const min = parts[0] ?? '0'
+ const hour = parts[1] ?? '0'
+ const timeStr = `${hour.padStart(2, '0')}:${min.padStart(2, '0')}`
+
+ const dayOfWeek = parts[4]
+ let freqStr = 'Custom'
+ if (parts[2] === '*' && parts[3] === '*') {
+ if (dayOfWeek === '*') freqStr = 'Daily'
+ else if (dayOfWeek === '1') freqStr = 'Every Monday'
+ else if (dayOfWeek === '5') freqStr = 'Every Friday'
+ } else if (parts[2] === '1') {
+ freqStr = 'Monthly (1st)'
+ }
+
+ const targetStr = targetList ? ` · ${targetList.targets.length} targets` : ''
+ return `${freqStr} at ${timeStr} ${schedule.timezone}${targetStr}`
+}
+```
+
+**Step 3: Verify it builds**
+
+Run: `cd frontend && npm run build 2>&1 | tail -5`
+Expected: Clean build
+
+**Step 4: Commit**
+
+```bash
+git add frontend/src/components/procedural-editor/MaintenanceScheduleSection.tsx
+git commit -m "feat: add MaintenanceScheduleSection with schedule builder and summary"
+```
+
+---
+
+## Phase 6: Wire Maintenance Schedule into ProceduralEditorPage
+
+### Task 6: Add Schedule Section to ProceduralEditorPage
+
+**Files:**
+- Modify: `frontend/src/pages/ProceduralEditorPage.tsx`
+
+**Context:** The MaintenanceScheduleSection should appear as a third collapsible section, only for maintenance flows. It needs access to `treeId` (from store) and renders between Intake Form and the step list.
+
+**Step 1: Read the current ProceduralEditorPage**
+
+Read: `frontend/src/pages/ProceduralEditorPage.tsx`
+
+**Step 2: Add imports and the schedule section**
+
+Add imports:
+```tsx
+import { MaintenanceScheduleSection, getScheduleSummary } from '@/components/procedural-editor/MaintenanceScheduleSection'
+import { Calendar } from 'lucide-react'
+```
+
+Add the schedule state (load existing schedule for summary):
+```tsx
+import { useState, useEffect } from 'react'
+import { maintenanceSchedulesApi } from '@/api/maintenanceSchedules'
+import { targetListsApi } from '@/api/targetLists'
+import type { MaintenanceSchedule, TargetList } from '@/types'
+
+// Inside the component:
+const [schedule, setSchedule] = useState(null)
+const [scheduleTargetList, setScheduleTargetList] = useState(null)
+
+useEffect(() => {
+ if (!isMaintenance || !treeId) return
+ const loadSchedule = async () => {
+ try {
+ const s = await maintenanceSchedulesApi.getForTree(treeId)
+ setSchedule(s)
+ if (s.target_list_id) {
+ const tl = await targetListsApi.get(s.target_list_id)
+ setScheduleTargetList(tl)
+ }
+ } catch {
+ // No schedule — fine
+ }
+ }
+ loadSchedule()
+}, [isMaintenance, treeId])
+
+const scheduleSummary = getScheduleSummary(schedule, scheduleTargetList)
+```
+
+Add the collapsible schedule section after the Intake Form section and before the step list:
+```tsx
+{isMaintenance && (
+ }
+ summary={scheduleSummary}
+ defaultExpanded={!isEditMode || !schedule}
+ >
+
+
+)}
+```
+
+**Step 3: Verify it builds**
+
+Run: `cd frontend && npm run build 2>&1 | tail -5`
+Expected: Clean build
+
+**Step 4: Commit**
+
+```bash
+git add frontend/src/pages/ProceduralEditorPage.tsx
+git commit -m "feat: wire maintenance schedule section into procedural editor"
+```
+
+---
+
+## Phase 7: Final Verification
+
+### Task 7: Build and Manual Testing Checklist
+
+**Step 1: Run full build**
+
+Run: `cd frontend && npm run build 2>&1 | tail -10`
+Expected: Clean build with no TypeScript or lint errors
+
+**Step 2: Manual testing checklist**
+
+Start the dev server: `cd frontend && npm run dev`
+
+Test each scenario:
+
+**Procedural flow — new:**
+- [ ] Navigate to `/flows/new?type=procedural`
+- [ ] Page uses fixed-height layout (no page scrolling)
+- [ ] Details section is expanded by default (name field visible)
+- [ ] Intake Form section is collapsed, shows "No fields defined"
+- [ ] No Schedule section visible (procedural, not maintenance)
+- [ ] Step list shows empty state with "Add your first step"
+- [ ] Click "Add Step" — new step appears and auto-expands
+- [ ] Step list scrolls independently from the toolbar
+- [ ] Fill in name, collapse Details — summary shows `"Name" · No tags · Private`
+
+**Procedural flow — existing:**
+- [ ] Edit an existing procedural flow
+- [ ] Details section is collapsed with rich summary
+- [ ] Intake Form section is collapsed with field names
+- [ ] Steps visible immediately with count and estimated time
+- [ ] Drag a step by its grip handle — reorders correctly
+- [ ] Expanded step cannot be dragged
+- [ ] Section headers can be dragged
+- [ ] procedure_end step stays at the bottom (cannot be dragged above it)
+
+**Maintenance flow — new:**
+- [ ] Navigate to `/flows/new?type=maintenance`
+- [ ] Schedule section is visible and expanded
+- [ ] Can select frequency, time, timezone
+- [ ] "Save the flow first" message shown (no treeId yet)
+- [ ] After saving, schedule can be created
+
+**Maintenance flow — existing with schedule:**
+- [ ] Schedule section shows collapsed summary: "Every Monday at 09:00 UTC · 5 targets"
+- [ ] Expand to modify schedule
+- [ ] Update saves correctly
+
+**Step 3: Commit any fixes found during testing**
+
+```bash
+git add -A
+git commit -m "fix: address issues found during manual testing"
+```
+
+**Step 4: Final commit message for the complete feature**
+
+If all tests pass and no fixes needed, the branch is ready for PR.