# Procedural Editor Redesign Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Restructure the procedural/maintenance flow editor with collapsible sections, fixed-height layout, drag-to-reorder steps, and maintenance schedule management.

**Architecture:** Convert ProceduralEditorPage from a scrolling document to a fixed-height editor. Details and Intake Form become collapsible one-liners. Step list gets all remaining height with independent scrolling. Add @dnd-kit drag-to-reorder on steps. Maintenance flows get an inline schedule section.

**Tech Stack:** React 19, TypeScript, Tailwind CSS, Zustand (proceduralEditorStore), @dnd-kit/core + @dnd-kit/sortable (already installed), existing maintenance schedule APIs.

---

## Phase 1: CollapsibleEditorSection Component

### Task 1: Create CollapsibleEditorSection

**Files:**
- Create: `frontend/src/components/procedural-editor/CollapsibleEditorSection.tsx`

**Context:** This is a reusable wrapper that shows a one-line summary when collapsed and reveals full content when expanded. Used by Details, Intake Form, and Maintenance Schedule sections. See the design doc at `docs/plans/2026-02-19-procedural-editor-redesign-design.md` for the spec.

**Step 1: Create the component**

```tsx
// frontend/src/components/procedural-editor/CollapsibleEditorSection.tsx
import { useState, type ReactNode } from 'react'
import { ChevronRight } from 'lucide-react'
import { cn } from '@/lib/utils'

interface CollapsibleEditorSectionProps {
  title: string
  icon: ReactNode
  summary: string
  expanded?: boolean
  onToggle?: () => void
  defaultExpanded?: boolean
  children: ReactNode
}

export function CollapsibleEditorSection({
  title,
  icon,
  summary,
  expanded: controlledExpanded,
  onToggle,
  defaultExpanded = false,
  children,
}: CollapsibleEditorSectionProps) {
  const [internalExpanded, setInternalExpanded] = useState(defaultExpanded)
  const isExpanded = controlledExpanded ?? internalExpanded
  const sectionId = `section-${title.toLowerCase().replace(/\s+/g, '-')}`

  const handleToggle = () => {
    if (onToggle) {
      onToggle()
    } else {
      setInternalExpanded(!internalExpanded)
    }
  }

  return (
    <div className="border-b border-border">
      {/* Collapsed header — always visible */}
      <button
        type="button"
        onClick={handleToggle}
        aria-expanded={isExpanded}
        aria-controls={sectionId}
        className="flex w-full items-center gap-3 px-4 py-2.5 text-left transition-colors hover:bg-accent/50"
      >
        <ChevronRight
          className={cn(
            'h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200',
            isExpanded && 'rotate-90'
          )}
        />
        <span className="shrink-0 text-muted-foreground">{icon}</span>
        <span className="text-sm font-medium text-foreground">{title}</span>
        {!isExpanded && (
          <span className="min-w-0 truncate text-sm text-muted-foreground">
            — {summary}
          </span>
        )}
      </button>

      {/* Expanded content */}
      {isExpanded && (
        <div id={sectionId} className="px-4 pb-4 pt-1">
          {children}
        </div>
      )}
    </div>
  )
}
```

**Step 2: Verify it builds**

Run: `cd frontend && npm run build 2>&1 | tail -5`
Expected: `built in` success message (component is created but not imported anywhere yet)

**Step 3: Commit**

```bash
git add frontend/src/components/procedural-editor/CollapsibleEditorSection.tsx
git commit -m "feat: add CollapsibleEditorSection component for procedural editor"
```

---

## Phase 2: Layout Restructure

### Task 2: Convert ProceduralEditorPage to Fixed-Height Editor

**Files:**
- Modify: `frontend/src/pages/ProceduralEditorPage.tsx`

**Context:** The page currently uses `container mx-auto px-4 py-6` with vertical scrolling. We need to convert it to `flex flex-col h-full overflow-hidden` so the step list can scroll independently. The toolbar becomes a sticky header, and Details + Intake Form become collapsible sections.

Reference the existing troubleshooting editor pattern: `frontend/src/pages/TreeEditorPage.tsx` lines 409-410 for the `flex h-full flex-col overflow-hidden` pattern.

**Step 1: Read the current file**

Read: `frontend/src/pages/ProceduralEditorPage.tsx`
Understand the full structure before making changes.

**Step 2: Restructure the layout**

Replace the entire render return (from `return (` to the closing `)`) with the new fixed-height layout. The key changes:

1. Outer wrapper: `<div className="flex h-full flex-col overflow-hidden">` (replaces `container mx-auto`)
2. Toolbar: sticky `<div className="flex items-center justify-between border-b border-border bg-card px-4 py-2">` containing the back button, title, and save/publish buttons
3. Collapsible sections zone: `<div className="shrink-0">` containing CollapsibleEditorSection wrappers for Details and IntakeFormBuilder
4. Step list zone: `<div className="flex-1 min-h-0 overflow-y-auto px-4 py-4">` containing StepList

Import `CollapsibleEditorSection` at the top:
```tsx
import { CollapsibleEditorSection } from '@/components/procedural-editor/CollapsibleEditorSection'
```

The Details collapsible section needs a summary string. Build it from store state:
```tsx
const detailsSummary = [
  name ? `"${name}"` : '"Untitled"',
  tags.length > 0 ? `${tags.length} tag${tags.length !== 1 ? 's' : ''}` : 'No tags',
  isPublic ? 'Public' : 'Private',
].join(' · ')
```

The IntakeFormBuilder collapsible section needs a summary too. Read the `intakeForm` array from the store (add it to destructuring if not already there) and build:
```tsx
const intakeSummary = intakeForm.length === 0
  ? 'No fields defined'
  : `${intakeForm.length} field${intakeForm.length !== 1 ? 's' : ''}: ${intakeForm.map(f => f.label || f.variable_name).slice(0, 4).join(', ')}${intakeForm.length > 4 ? ', ...' : ''}`
```

For the Details section content inside CollapsibleEditorSection, move the existing form fields (name input, description textarea, tags input, public checkbox) directly as children.

The Details section should `defaultExpanded={!isEditMode}` so new flows start with Details expanded (name is required), while existing flows start collapsed.

The Intake Form section should `defaultExpanded={false}` always.

**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: restructure procedural editor to fixed-height layout with collapsible sections"
```

---

## Phase 3: Step List Improvements

### Task 3: Add Empty State and Step Count Header

**Files:**
- Modify: `frontend/src/components/procedural-editor/StepList.tsx`

**Context:** The step list needs: (1) a step count + total estimated time in the header, (2) auto-scroll to new steps. Note: the store enforces minimum one `procedure_step`, so remove any "0 steps" UI paths. The step list's outer card wrapper should be removed since ProceduralEditorPage now provides the scrolling container — StepList should just render its header and step items directly.

**Step 1: Read the current file**

Read: `frontend/src/components/procedural-editor/StepList.tsx`

**Step 2: Modify the header and add empty state**

Remove the outer `<div className="bg-card border border-border rounded-2xl p-4 sm:p-6">` card wrapper. The step list now renders directly in the scrolling container.

Update the header to show step count + total estimated time:
```tsx
const totalMinutes = steps
  .filter(s => s.type === 'procedure_step' && s.estimated_minutes)
  .reduce((sum, s) => sum + (s.estimated_minutes || 0), 0)

// In the header:
<span className="text-sm text-muted-foreground">
  ({procedureSteps.length} step{procedureSteps.length !== 1 ? 's' : ''}
  {totalMinutes > 0 ? ` · ~${totalMinutes} min` : ''})
</span>
```

Add empty state after the header, before the step map:
```tsx
{procedureSteps.length === 0 && (
  <div className="flex flex-col items-center justify-center py-16 text-center">
    <Shield className="mb-3 h-10 w-10 text-muted-foreground/50" />
    <h3 className="mb-1 text-sm font-medium text-foreground">Add your first step</h3>
    <p className="mb-4 max-w-xs text-xs text-muted-foreground">
      Steps define the actions engineers follow during this procedure.
    </p>
    <div className="flex items-center gap-2">
      <button
        onClick={() => addStep()}
        className="flex items-center gap-1.5 rounded-md bg-gradient-brand px-3 py-1.5 text-sm font-medium text-white shadow-lg shadow-primary/20 hover:opacity-90"
      >
        <Plus className="h-3.5 w-3.5" />
        Add Step
      </button>
      <button
        onClick={() => addSectionHeader()}
        className="flex items-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
      >
        <SeparatorHorizontal className="h-3.5 w-3.5" />
        Add Section
      </button>
    </div>
  </div>
)}
```

**Step 3: Add auto-scroll to new step**

When a new step is added, the list should scroll to show it. Add a ref and useEffect:

```tsx
import { useRef, useEffect } from 'react'

const listEndRef = useRef<HTMLDivElement>(null)
const prevStepCount = useRef(steps.length)

useEffect(() => {
  if (steps.length > prevStepCount.current) {
    listEndRef.current?.scrollIntoView({ behavior: 'smooth' })
  }
  prevStepCount.current = steps.length
}, [steps.length])

// At the bottom of the step list, before the Add Step button:
<div ref={listEndRef} />
```

**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 empty state, step count header, and auto-scroll to step list"
```

---

## Phase 4: Drag-to-Reorder Steps

### Task 4: Add @dnd-kit Drag-to-Reorder to StepList

**Files:**
- Modify: `frontend/src/components/procedural-editor/StepList.tsx`

**Context:** @dnd-kit is already installed (`@dnd-kit/core@^6.3.1`, `@dnd-kit/sortable@^10.0.0`). There's an existing pattern in `frontend/src/components/step-library/CategoryRow.tsx` using `useSortable` and in `frontend/src/pages/admin/AdminCategoriesPage.tsx` using `DndContext` + `SortableContext`. The store already has `moveStep(fromIndex, toIndex)`. Reorder is array-index based only — do NOT recalculate `display_order`. `procedure_end` must remain non-draggable and always last. Drag handles must have accessible labels and keyboard support (Enter/Space to pick up, arrow keys to move, Escape to cancel).

**Step 1: Read the existing @dnd-kit pattern**

Read: `frontend/src/components/step-library/CategoryRow.tsx` (lines 1-50 for the useSortable pattern)
Read: `frontend/src/pages/admin/AdminCategoriesPage.tsx` (lines 1-10 for imports, lines 110-140 for handleDragEnd)

**Step 2: Add DndContext to StepList**

Add imports at the top of StepList.tsx:
```tsx
import {
  DndContext,
  closestCenter,
  KeyboardSensor,
  PointerSensor,
  useSensor,
  useSensors,
  type DragEndEvent,
} from '@dnd-kit/core'
import {
  SortableContext,
  sortableKeyboardCoordinates,
  verticalListSortingStrategy,
  useSortable,
} from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
```

Add sensors and drag handler before the return:
```tsx
const sensors = useSensors(
  useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
  useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
)

const handleDragEnd = useCallback((event: DragEndEvent) => {
  const { active, over } = event
  if (!over || active.id === over.id) return

  const oldIndex = steps.findIndex(s => s.id === active.id)
  const newIndex = steps.findIndex(s => s.id === over.id)
  if (oldIndex === -1 || newIndex === -1) return

  // Don't allow moving past the procedure_end step
  const endIndex = steps.findIndex(s => s.type === 'procedure_end')
  if (newIndex >= endIndex) return

  moveStep(oldIndex, newIndex)
}, [steps, moveStep])
```

Wrap the step list `<div className="space-y-2">` with DndContext and SortableContext:
```tsx
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
  <SortableContext items={steps.filter(s => s.type !== 'procedure_end').map(s => s.id)} strategy={verticalListSortingStrategy}>
    <div className="space-y-2">
      {steps.map((step) => {
        // ... existing rendering logic
      })}
    </div>
  </SortableContext>
</DndContext>
```

**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 (
    <div ref={setNodeRef} style={style} className={cn(isDragging && 'opacity-50 z-10')}>
      {/* Pass drag handle props to children via render prop or context */}
      {typeof children === 'function'
        ? children({ dragHandleProps: { ...attributes, ...listeners } })
        : children}
    </div>
  )
}
```

Actually, a simpler approach: apply useSortable directly to each card's GripVertical button. Wrap each step's outer `<div>` with `ref={setNodeRef}` and pass `{...attributes} {...listeners}` to the GripVertical button.

For collapsed procedure_step cards (the main case), the existing `<GripVertical>` button at line 148 gets the sortable props:
```tsx
// Replace the GripVertical span/button:
<button
  type="button"
  className="shrink-0 cursor-grab active:cursor-grabbing text-muted-foreground"
  {...attributes}
  {...listeners}
>
  <GripVertical className="h-4 w-4" />
</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. Schedule is NOT part of `tree_structure` — it's persisted through separate maintenance schedule API endpoints. Two-stage save: tree first, then schedule. If schedule save fails, tree save remains successful — show actionable error and preserve schedule draft state. 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`. Schedule draft state should be local UI state, not stored in proceduralEditorStore.

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<MaintenanceSchedule | null>(null)
  const [targetLists, setTargetLists] = useState<TargetList[]>([])
  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<string>('')

  // 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 (
      <div className="px-4 py-3 text-sm text-muted-foreground">Loading schedule...</div>
    )
  }

  return (
    <div className="space-y-4">
      {/* Frequency */}
      <div>
        <label className="mb-1 block text-sm font-medium text-muted-foreground">Frequency</label>
        <select
          value={frequency}
          onChange={(e) => 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 => (
            <option key={opt.value} value={opt.value}>{opt.label}</option>
          ))}
        </select>
      </div>

      {/* Time */}
      <div className="grid grid-cols-2 gap-4">
        <div>
          <label className="mb-1 block text-sm font-medium text-muted-foreground">Hour</label>
          <input
            type="number"
            min={0}
            max={23}
            value={hour}
            onChange={(e) => setHour(Number(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"
          />
        </div>
        <div>
          <label className="mb-1 block text-sm font-medium text-muted-foreground">Minute</label>
          <input
            type="number"
            min={0}
            max={59}
            value={minute}
            onChange={(e) => setMinute(Number(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"
          />
        </div>
      </div>

      {/* Timezone */}
      <div>
        <label className="mb-1 block text-sm font-medium text-muted-foreground">Timezone</label>
        <select
          value={timezone}
          onChange={(e) => 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 => (
            <option key={tz} value={tz}>{tz}</option>
          ))}
        </select>
      </div>

      {/* Target List */}
      {targetLists.length > 0 && (
        <div>
          <label className="mb-1 block text-sm font-medium text-muted-foreground">Target List (optional)</label>
          <select
            value={selectedTargetListId}
            onChange={(e) => 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"
          >
            <option value="">None — manual targets only</option>
            {targetLists.map(tl => (
              <option key={tl.id} value={tl.id}>{tl.name} ({tl.targets.length} targets)</option>
            ))}
          </select>
        </div>
      )}

      {/* Save button */}
      <button
        onClick={handleSaveSchedule}
        disabled={isSaving || !treeId}
        className={cn(
          'flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium',
          treeId
            ? 'bg-gradient-brand text-white shadow-lg shadow-primary/20 hover:opacity-90'
            : 'bg-card border border-border text-muted-foreground cursor-not-allowed opacity-50'
        )}
      >
        <Clock className="h-4 w-4" />
        {isSaving ? 'Saving...' : schedule ? 'Update Schedule' : 'Create Schedule'}
      </button>
      {!treeId && (
        <p className="text-xs text-muted-foreground">Save the flow first to configure a schedule.</p>
      )}
    </div>
  )
}
```

**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<MaintenanceSchedule | null>(null)
const [scheduleTargetList, setScheduleTargetList] = useState<TargetList | null>(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 && (
  <CollapsibleEditorSection
    title="Schedule"
    icon={<Calendar className="h-4 w-4" />}
    summary={scheduleSummary}
    defaultExpanded={!isEditMode || !schedule}
  >
    <MaintenanceScheduleSection treeId={treeId} />
  </CollapsibleEditorSection>
)}
```

**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.
