From 778270e0b9f8812cb94b46e29dc532d6b357b844 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Thu, 19 Feb 2026 02:49:33 -0500 Subject: [PATCH 01/13] docs: add procedural/maintenance editor redesign design Collapsible sections, fixed-height layout, drag-to-reorder steps, maintenance schedule section, and step list UX improvements. Co-Authored-By: Claude Opus 4.6 --- ...02-19-procedural-editor-redesign-design.md | 232 ++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 docs/plans/2026-02-19-procedural-editor-redesign-design.md diff --git a/docs/plans/2026-02-19-procedural-editor-redesign-design.md b/docs/plans/2026-02-19-procedural-editor-redesign-design.md new file mode 100644 index 00000000..30555b4b --- /dev/null +++ b/docs/plans/2026-02-19-procedural-editor-redesign-design.md @@ -0,0 +1,232 @@ +# Procedural & Maintenance Editor Redesign — Design + +> **Date:** 2026-02-19 +> **Scope:** Restructure the procedural/maintenance flow editor for better space utilization, collapsible sections, drag-to-reorder steps, and maintenance-specific schedule management + +## Overview + +The current procedural editor (`ProceduralEditorPage.tsx`) uses a scrolling document layout where Details, Intake Form, and Steps stack vertically. The Details and Intake Form sections consume significant screen space, pushing the step list — the core editing surface — to the bottom. This redesign converts the page to a fixed-height editor with collapsible sections and drag-to-reorder steps. + +## Problems Solved + +1. **Steps buried at the bottom** — Details and Intake Form sections are "screen space goblins" that push the step list down, especially on smaller screens +2. **No drag reorder** — grip handles are visible but non-functional; reordering requires deleting and re-creating steps +3. **Adding steps is tedious** — new steps append at the bottom but don't auto-expand, requiring an extra click +4. **No overview at a glance** — collapsed sections don't show useful summaries; users expand just to check what's there +5. **Maintenance flows lack inline schedule management** — schedule/targets are only configurable on the detail page, not during initial creation + +## Layout Architecture + +### Current Layout (Scrolling Document) + +``` +┌─────────────────────────────────────┐ +│ Header (back, title, save, publish) │ ← scrolls with page +├─────────────────────────────────────┤ +│ Details Card (~200px) │ ← always expanded +│ Name, Description, Tags, Public │ +├─────────────────────────────────────┤ +│ Intake Form Card (~150-400px) │ ← always expanded +│ Field editors... │ +├─────────────────────────────────────┤ +│ Steps Card (whatever's left) │ ← pushed to bottom +│ Step list... │ +└─────────────────────────────────────┘ + ↕ entire page scrolls +``` + +### New Layout (Fixed-Height Editor) + +``` +┌─────────────────────────────────────┐ +│ Toolbar (sticky) │ ← fixed, never scrolls +├─────────────────────────────────────┤ +│ ▶ Details: "DC Build" · 3 tags · … │ ← collapsed one-liner +│ ▶ Intake Form: 3 fields: Host, … │ ← collapsed one-liner +│ ▶ Schedule: Mon 2:00 AM · 5 targets│ ← maintenance only +├─────────────────────────────────────┤ +│ │ +│ Steps (flex-1, scrolls alone) │ ← gets all remaining height +│ ┌─ 1. Check prerequisites ───────┐│ +│ ├─ 2. Install AD DS role ────────┤│ +│ ├─ 3. Promote to DC ────────────┤│ +│ ├─ 4. Verify replication ───────┤│ +│ └─ + Add Step ───────────────────┘│ +│ │ +└─────────────────────────────────────┘ +``` + +### Key Layout Changes + +- **Page:** `container mx-auto` scrolling → `flex flex-col h-full overflow-hidden` +- **Toolbar:** Scrolls with page → sticky at top (matches troubleshooting editor pattern) +- **Sections:** Always-expanded cards → collapsible one-liners with rich summaries +- **Step list:** Stacked in scroll flow → `flex-1 overflow-y-auto` (independent scrolling) + +## Collapsible Sections + +### Shared Wrapper: `CollapsibleEditorSection` + +A reusable component used by Details, Intake Form, and Maintenance Schedule. + +**Props:** +- `title` — section label ("Details", "Intake Form", "Schedule") +- `icon` — Lucide icon component +- `summary` — rich one-line summary string shown when collapsed +- `defaultExpanded` — whether to start expanded (default: `false`) +- `children` — expanded content +- `onEdit` — optional callback for the Edit button (alternative to expanding) + +**Collapsed state:** Single row with icon, title, summary text, and expand chevron. Entire row is clickable. + +**Expanded state:** Full content slides down with a subtle animation. Collapse chevron rotates. + +### Details Section — Collapsed Summary + +Format: `"Flow Name" · N tags · Public/Private` + +Examples: +- `"Domain Controller Build" · 3 tags · Public` +- `"New Procedure" · No tags · Private` (new flow, default expanded) + +**New flow behavior:** Details section starts **expanded** when creating a new flow (name is required), collapses after the user provides a name and clicks away or expands another section. + +### Intake Form Section — Collapsed Summary + +Format: `N fields: Field1, Field2, Field3` (field names truncated if many) + +Examples: +- `3 fields: Hostname, Domain Name, IP Address` +- `6 fields: Hostname, Domain, IP, DNS, Gateway, …` (truncated with ellipsis) +- `No fields defined` (empty state) + +### Maintenance Schedule Section + +Only renders when `treeType === 'maintenance'`. + +**New flow (no schedule):** Section starts expanded with: +- Cron expression builder (frequency picker: daily/weekly/monthly + time + timezone) +- Target list selector (dropdown of saved target lists, or create new inline) +- These fields write to the store and get saved with the flow + +**Existing flow (has schedule):** Collapsed summary: +- Format: `"Every Monday at 2:00 AM UTC · 5 targets"` +- Expand to modify schedule and targets + +**Existing flow (no schedule yet):** Shows collapsed with summary `"No schedule configured"` + "Set Up" button that expands the section. + +## Step List Improvements + +### Empty State + +When the step list has 0 steps, show a centered empty state instead of a blank area: + +``` + ┌──────────────────────────┐ + │ 📋 │ + │ Add your first step │ + │ │ + │ Steps define the actions │ + │ engineers follow during │ + │ this procedure. │ + │ │ + │ [+ Add Step] [+ Section]│ + └──────────────────────────┘ +``` + +When 1-2 steps exist, the list renders normally — no special treatment needed since the steps themselves fill the space adequately. + +### Step Count + Time in Header + +The Steps section header shows aggregate info: + +- `Steps (4 steps · ~25 min estimated)` — when steps have time estimates +- `Steps (4 steps)` — when no time estimates set +- `Steps (0 steps)` — empty, triggers the empty state below + +### Auto-Expand New Steps + +When `addStep()` is called, the new step is automatically expanded (`setExpandedStepId(newStep.id)`) and the step list scrolls to the bottom to show it. This eliminates the extra click to start editing. + +Same for `addSectionHeader()` — auto-expand for immediate title editing. + +### Drag-to-Reorder + +**Library:** `@dnd-kit/core` + `@dnd-kit/sortable` + +**Behavior:** +- Drag via the `GripVertical` handle on each step card +- Dragged card lifts with `shadow-lg` and slight scale +- Drop target: blue insertion line between steps +- Section headers are draggable — moving a section header moves it independently (steps below stay in place) +- On drop: update the store's step array order and recalculate `display_order` values +- Keyboard accessible: focus grip handle, Enter to pick up, arrow keys to move, Enter to drop + +**Implementation:** +- Wrap step list in `` + `` +- Each step card wrapped in `useSortable()` hook +- Drag overlay shows a simplified card (just step number + title) +- `onDragEnd` handler reorders the `steps` array in the procedural editor store + +### Collapsed Step Cards + +Current cards are already compact. Minor tightening: +- Step number badge + content type icon + title + time estimate + chevron + delete (on hover) +- No changes to the collapsed card layout — it's already well-designed + +## Toolbar + +Matches the troubleshooting editor's toolbar pattern: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ ← Back Edit Procedure — DC Build [Unsaved] │ Save │ Publish │ +└─────────────────────────────────────────────────────────────┘ +``` + +- **Left:** Back button (→ `/my-trees`), flow type icon, title with flow name +- **Right:** Unsaved indicator, Save Draft button, Publish button (gradient) +- **Sticky:** `sticky top-0 z-10` within the editor area (not the app shell) + +Maintenance flows show `Wrench` icon + "Edit Maintenance Flow" title. + +## File Changes + +### Modified Files + +| File | Changes | +|------|---------| +| `ProceduralEditorPage.tsx` | Layout restructure: scrolling → fixed-height, collapsible sections, toolbar refactor | +| `StepList.tsx` | Drag reorder with @dnd-kit, auto-expand on add, empty state, step count header | +| `IntakeFormBuilder.tsx` | Wrap in collapsible section with field-name summary | +| `proceduralEditorStore.ts` | `reorderSteps(fromIndex, toIndex)` action, auto-expand on add | + +### New Files + +| File | Purpose | +|------|---------| +| `components/procedural-editor/CollapsibleEditorSection.tsx` | Shared collapsible wrapper with summary display | +| `components/procedural-editor/MaintenanceScheduleSection.tsx` | Schedule builder + collapsed summary for maintenance flows | + +### New Dependencies + +- `@dnd-kit/core` — drag-and-drop framework +- `@dnd-kit/sortable` — sortable preset for ordered lists +- `@dnd-kit/utilities` — CSS utilities for transforms + +### Unchanged + +- `StepEditor.tsx` — inline step editing form, no changes +- `IntakeFieldEditor.tsx` — field editor, no changes +- `proceduralEditorStore.ts` steps/intakeForm data model — no schema changes +- Backend — no API changes needed +- Troubleshooting tree editor — completely separate, unaffected + +## Not Included (YAGNI) + +- No drag-to-reorder intake form fields (low value, fields rarely reordered) +- No inline cron expression text input (use friendly frequency picker instead) +- No step templates or presets +- No bulk step operations (select multiple, delete, move) +- No step preview/dry-run from the editor +- No undo/redo for the procedural editor (separate effort) -- 2.49.1 From 96b8818b364b7afc79066187ac551e97f3b49d8e Mon Sep 17 00:00:00 2001 From: chihlasm Date: Thu, 19 Feb 2026 02:53:59 -0500 Subject: [PATCH 02/13] docs: add procedural editor redesign implementation plan 7 tasks across 7 phases: collapsible sections, fixed-height layout, step list improvements, drag-to-reorder, maintenance schedule section. Co-Authored-By: Claude Opus 4.6 --- ...6-02-19-procedural-editor-redesign-impl.md | 795 ++++++++++++++++++ 1 file changed, 795 insertions(+) create mode 100644 docs/plans/2026-02-19-procedural-editor-redesign-impl.md diff --git a/docs/plans/2026-02-19-procedural-editor-redesign-impl.md b/docs/plans/2026-02-19-procedural-editor-redesign-impl.md new file mode 100644 index 00000000..a4f794cf --- /dev/null +++ b/docs/plans/2026-02-19-procedural-editor-redesign-impl.md @@ -0,0 +1,795 @@ +# 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 + defaultExpanded?: boolean + children: ReactNode +} + +export function CollapsibleEditorSection({ + title, + icon, + summary, + defaultExpanded = false, + children, +}: CollapsibleEditorSectionProps) { + const [expanded, setExpanded] = useState(defaultExpanded) + + return ( +
+ {/* Collapsed header — always visible */} + + + {/* Expanded content */} + {expanded && ( +
+ {children} +
+ )} +
+ ) +} +``` + +**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: `
` (replaces `container mx-auto`) +2. Toolbar: sticky `
` containing the back button, title, and save/publish buttons +3. Collapsible sections zone: `
` containing CollapsibleEditorSection wrappers for Details and IntakeFormBuilder +4. Step list zone: `
` 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) an empty state when no steps exist. 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 `
` 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: + + ({procedureSteps.length} step{procedureSteps.length !== 1 ? 's' : ''} + {totalMinutes > 0 ? ` · ~${totalMinutes} min` : ''}) + +``` + +Add empty state after the header, before the step map: +```tsx +{procedureSteps.length === 0 && ( +
+ +

Add your first step

+

+ Steps define the actions engineers follow during this procedure. +

+
+ + +
+
+)} +``` + +**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(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: +
+``` + +**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)`. + +**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 `
` 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 */} +
+ + +
+ + {/* Time */} +
+
+ + 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" + /> +
+
+ + 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" + /> +
+
+ + {/* Timezone */} +
+ + +
+ + {/* Target List */} + {targetLists.length > 0 && ( +
+ + +
+ )} + + {/* Save button */} + + {!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. -- 2.49.1 From dff53e55bb531898f5b1d241d6a67fb99357ce47 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Thu, 19 Feb 2026 03:15:00 -0500 Subject: [PATCH 03/13] feat: restructure procedural editor with collapsible sections and fixed-height layout Convert scrolling document layout to fixed-height editor with accordion-mode collapsible sections for Details and Intake Form. Step list now gets all remaining height with independent scrolling. Add CollapsibleEditorSection component with ARIA attributes (aria-expanded, aria-controls). Co-Authored-By: Claude Opus 4.6 --- ...02-19-procedural-editor-redesign-design.md | 31 +++++--- ...6-02-19-procedural-editor-redesign-impl.md | 34 ++++++--- .../CollapsibleEditorSection.tsx | 68 ++++++++++++++++++ .../procedural-editor/IntakeFormBuilder.tsx | 11 +-- frontend/src/pages/ProceduralEditorPage.tsx | 72 ++++++++++++++----- 5 files changed, 174 insertions(+), 42 deletions(-) create mode 100644 frontend/src/components/procedural-editor/CollapsibleEditorSection.tsx diff --git a/docs/plans/2026-02-19-procedural-editor-redesign-design.md b/docs/plans/2026-02-19-procedural-editor-redesign-design.md index 30555b4b..2aa83ae1 100644 --- a/docs/plans/2026-02-19-procedural-editor-redesign-design.md +++ b/docs/plans/2026-02-19-procedural-editor-redesign-design.md @@ -81,6 +81,14 @@ A reusable component used by Details, Intake Form, and Maintenance Schedule. **Expanded state:** Full content slides down with a subtle animation. Collapse chevron rotates. +**Accordion mode:** Single-open by default — expanding one section collapses others. Controlled by parent page component. + +**Accessibility:** +- Toggle button has `aria-expanded` and `aria-controls` pointing to section content `id` +- Content region has matching `id` +- Keyboard operable (Enter/Space to toggle) +- Focus remains stable after toggle + ### Details Section — Collapsed Summary Format: `"Flow Name" · N tags · Public/Private` @@ -107,7 +115,9 @@ Only renders when `treeType === 'maintenance'`. **New flow (no schedule):** Section starts expanded with: - Cron expression builder (frequency picker: daily/weekly/monthly + time + timezone) - Target list selector (dropdown of saved target lists, or create new inline) -- These fields write to the store and get saved with the flow +- These fields write to local UI draft state (NOT tree_structure) +- On save: tree saved first, then schedule created via `maintenanceSchedulesApi.create` with resulting `tree_id` +- If schedule create fails: tree save remains successful, show actionable error, preserve draft **Existing flow (has schedule):** Collapsed summary: - Format: `"Every Monday at 2:00 AM UTC · 5 targets"` @@ -142,7 +152,7 @@ The Steps section header shows aggregate info: - `Steps (4 steps · ~25 min estimated)` — when steps have time estimates - `Steps (4 steps)` — when no time estimates set -- `Steps (0 steps)` — empty, triggers the empty state below +- `Steps (0 steps)` — empty state (note: store currently enforces minimum one `procedure_step`, so 0-step state only appears if invariant is intentionally changed) ### Auto-Expand New Steps @@ -159,8 +169,8 @@ Same for `addSectionHeader()` — auto-expand for immediate title editing. - Dragged card lifts with `shadow-lg` and slight scale - Drop target: blue insertion line between steps - Section headers are draggable — moving a section header moves it independently (steps below stay in place) -- On drop: update the store's step array order and recalculate `display_order` values -- Keyboard accessible: focus grip handle, Enter to pick up, arrow keys to move, Enter to drop +- On drop: update the store's step array order (array-index based only, no `display_order` recalculation) +- Keyboard accessible: focus grip handle, Enter/Space to pick up, arrow keys to move, Enter to drop, Escape to cancel **Implementation:** - Wrap step list in `` + `` @@ -208,11 +218,16 @@ Maintenance flows show `Wrench` icon + "Edit Maintenance Flow" title. | `components/procedural-editor/CollapsibleEditorSection.tsx` | Shared collapsible wrapper with summary display | | `components/procedural-editor/MaintenanceScheduleSection.tsx` | Schedule builder + collapsed summary for maintenance flows | -### New Dependencies +### Existing Dependencies Used -- `@dnd-kit/core` — drag-and-drop framework -- `@dnd-kit/sortable` — sortable preset for ordered lists -- `@dnd-kit/utilities` — CSS utilities for transforms +- `@dnd-kit/core` — drag-and-drop framework (already installed) +- `@dnd-kit/sortable` — sortable preset for ordered lists (already installed) +- `@dnd-kit/utilities` — CSS utilities for transforms (already installed) + +### Existing APIs Used + +- `frontend/src/api/maintenanceSchedules.ts` — schedule CRUD via separate endpoints (NOT tree_structure) +- `frontend/src/api/targetLists.ts` — target list selection for schedules ### Unchanged diff --git a/docs/plans/2026-02-19-procedural-editor-redesign-impl.md b/docs/plans/2026-02-19-procedural-editor-redesign-impl.md index a4f794cf..1c6002bf 100644 --- a/docs/plans/2026-02-19-procedural-editor-redesign-impl.md +++ b/docs/plans/2026-02-19-procedural-editor-redesign-impl.md @@ -31,6 +31,8 @@ interface CollapsibleEditorSectionProps { title: string icon: ReactNode summary: string + expanded?: boolean + onToggle?: () => void defaultExpanded?: boolean children: ReactNode } @@ -39,28 +41,42 @@ export function CollapsibleEditorSection({ title, icon, summary, + expanded: controlledExpanded, + onToggle, defaultExpanded = false, children, }: CollapsibleEditorSectionProps) { - const [expanded, setExpanded] = useState(defaultExpanded) + 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 (
{/* Collapsed header — always visible */} {/* Expanded content */} - {expanded && ( -
+ {isExpanded && ( +
{children}
)} @@ -165,7 +181,7 @@ git commit -m "feat: restructure procedural editor to fixed-height layout with c **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) an empty state when no steps exist. 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. +**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** @@ -259,7 +275,7 @@ git commit -m "feat: add empty state, step count header, and auto-scroll to step **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)`. +**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** @@ -390,7 +406,7 @@ git commit -m "feat: add drag-to-reorder steps with @dnd-kit" **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`. +**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` diff --git a/frontend/src/components/procedural-editor/CollapsibleEditorSection.tsx b/frontend/src/components/procedural-editor/CollapsibleEditorSection.tsx new file mode 100644 index 00000000..2325e4ff --- /dev/null +++ b/frontend/src/components/procedural-editor/CollapsibleEditorSection.tsx @@ -0,0 +1,68 @@ +import { useState, useId, 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 generatedId = useId() + const sectionId = `section-${generatedId}` + + const handleToggle = () => { + if (onToggle) { + onToggle() + } else { + setInternalExpanded(!internalExpanded) + } + } + + return ( +
+ + + {isExpanded && ( +
+ {children} +
+ )} +
+ ) +} diff --git a/frontend/src/components/procedural-editor/IntakeFormBuilder.tsx b/frontend/src/components/procedural-editor/IntakeFormBuilder.tsx index eb3969fa..bc28e34b 100644 --- a/frontend/src/components/procedural-editor/IntakeFormBuilder.tsx +++ b/frontend/src/components/procedural-editor/IntakeFormBuilder.tsx @@ -6,15 +6,8 @@ export function IntakeFormBuilder() { const { intakeForm, addField, removeField, updateField } = useProceduralEditorStore() return ( -
-
-
- -

Intake Form

- - ({intakeForm.length} field{intakeForm.length !== 1 ? 's' : ''}) - -
+
+
@@ -144,7 +171,7 @@ export function ProceduralEditorPage() {
- {/* Content */} -
- {/* Metadata */} -
-

Details

+ {/* Collapsible sections */} +
+ } + summary={detailsSummary} + expanded={expandedSection === 'details'} + onToggle={() => toggleSection('details')} + >
@@ -199,12 +230,21 @@ export function ProceduralEditorPage() {
-
+ - {/* Intake Form Builder */} - + } + summary={intakeSummary} + expanded={expandedSection === 'intake'} + onToggle={() => toggleSection('intake')} + > + + +
- {/* Step List */} + {/* Step List — flex-1, scrolls independently */} +
-- 2.49.1 From aa3651ebfe30bc693b47a631ce29e640be9cbf7a Mon Sep 17 00:00:00 2001 From: chihlasm Date: Thu, 19 Feb 2026 03:16:24 -0500 Subject: [PATCH 04/13] feat: add step count with time estimate header and auto-scroll to new steps Remove outer card wrapper from StepList (now rendered in scrolling container). Header shows total estimated minutes when steps have time estimates. Auto-scrolls to newly added steps using ref + scrollIntoView. Co-Authored-By: Claude Opus 4.6 --- .../components/procedural-editor/StepList.tsx | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/procedural-editor/StepList.tsx b/frontend/src/components/procedural-editor/StepList.tsx index 066843b0..f413fb48 100644 --- a/frontend/src/components/procedural-editor/StepList.tsx +++ b/frontend/src/components/procedural-editor/StepList.tsx @@ -1,3 +1,4 @@ +import { useRef, useEffect } from 'react' import { Plus, GripVertical, Trash2, ChevronDown, CheckCircle2, AlertTriangle, Info, Zap, Shield, SeparatorHorizontal } from 'lucide-react' import type { StepContentType } from '@/types' import { StepEditor } from './StepEditor' @@ -24,16 +25,35 @@ export function StepList() { } = useProceduralEditorStore() const procedureSteps = steps.filter((s) => s.type === 'procedure_step') + const totalMinutes = steps + .filter(s => s.type === 'procedure_step' && s.estimated_minutes) + .reduce((sum, s) => sum + (s.estimated_minutes || 0), 0) + + // Auto-scroll to new steps + const scrollTargetRef = useRef(null) + const prevStepCount = useRef(steps.length) + + useEffect(() => { + if (steps.length > prevStepCount.current) { + // Scroll the newly expanded step into view + setTimeout(() => { + scrollTargetRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) + }, 50) + } + prevStepCount.current = steps.length + }, [steps.length]) + let stepCounter = 0 return ( -
+

Steps

- ({procedureSteps.length} step{procedureSteps.length !== 1 ? 's' : ''}) + ({procedureSteps.length} step{procedureSteps.length !== 1 ? 's' : ''} + {totalMinutes > 0 ? ` \u00b7 ~${totalMinutes} min` : ''})
@@ -81,7 +101,7 @@ export function StepList() { if (isExpanded) { return ( -
+
+
Add Step + +
) } -- 2.49.1 From 80e7831e90e30acaff60844b3c053ac236a59fa1 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Thu, 19 Feb 2026 03:19:14 -0500 Subject: [PATCH 05/13] feat: add drag-to-reorder steps with @dnd-kit Wrap step list in DndContext + SortableContext. Each step/section header gets a SortableStepWrapper with useSortable. Drag handles have accessible labels and keyboard support. procedure_end stays non-draggable and always last. Expanded steps are disabled for dragging. Array-index reorder only. Co-Authored-By: Claude Opus 4.6 --- .../components/procedural-editor/StepList.tsx | 341 +++++++++++------- 1 file changed, 211 insertions(+), 130 deletions(-) diff --git a/frontend/src/components/procedural-editor/StepList.tsx b/frontend/src/components/procedural-editor/StepList.tsx index f413fb48..a9cb93f1 100644 --- a/frontend/src/components/procedural-editor/StepList.tsx +++ b/frontend/src/components/procedural-editor/StepList.tsx @@ -1,5 +1,9 @@ -import { useRef, useEffect } from 'react' +import { useRef, useEffect, useCallback, type ReactNode } from 'react' import { Plus, GripVertical, Trash2, ChevronDown, CheckCircle2, AlertTriangle, Info, Zap, Shield, SeparatorHorizontal } from 'lucide-react' +import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core' +import type { DragEndEvent } from '@dnd-kit/core' +import { SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' import type { StepContentType } from '@/types' import { StepEditor } from './StepEditor' import { useProceduralEditorStore } from '@/store/proceduralEditorStore' @@ -12,6 +16,29 @@ const contentTypeConfig: Record }) => ReactNode +}) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id, disabled }) + + const style = { + transform: CSS.Transform.toString(transform), + transition, + } + + return ( +
+ {children({ dragHandleProps: { ...attributes, ...listeners } })} +
+ ) +} + export function StepList() { const { steps, @@ -22,6 +49,7 @@ export function StepList() { addSectionHeader, removeStep, updateStep, + moveStep, } = useProceduralEditorStore() const procedureSteps = steps.filter((s) => s.type === 'procedure_step') @@ -35,7 +63,6 @@ export function StepList() { useEffect(() => { if (steps.length > prevStepCount.current) { - // Scroll the newly expanded step into view setTimeout(() => { scrollTargetRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) }, 50) @@ -43,6 +70,30 @@ export function StepList() { prevStepCount.current = steps.length }, [steps.length]) + // DnD setup + 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]) + + // Sortable items: everything except procedure_end + const sortableItems = steps.filter(s => s.type !== 'procedure_end').map(s => s.id) + let stepCounter = 0 return ( @@ -74,138 +125,168 @@ export function StepList() {
-
- {steps.map((step) => { - if (step.type === 'procedure_end') { - return ( -
- - updateStep(step.id, { title: e.target.value })} - className="flex-1 bg-transparent text-sm text-muted-foreground focus:outline-none" - placeholder="Procedure Complete" - /> - END -
- ) - } + + +
+ {steps.map((step) => { + if (step.type === 'procedure_end') { + // procedure_end: non-draggable, always last + return ( +
+ + updateStep(step.id, { title: e.target.value })} + className="flex-1 bg-transparent text-sm text-muted-foreground focus:outline-none" + placeholder="Procedure Complete" + /> + END +
+ ) + } - // Section header rendering - if (step.type === 'section_header') { - const isExpanded = expandedStepId === step.id + // Section header rendering + if (step.type === 'section_header') { + const isExpanded = expandedStepId === step.id + + if (isExpanded) { + return ( + + {() => ( +
+ updateStep(step.id, updates)} + onCollapse={() => setExpandedStepId(null)} + availableVariables={intakeForm} + /> +
+ )} +
+ ) + } + + return ( + + {({ dragHandleProps }) => ( +
+ + setExpandedStepId(step.id)} + > + {step.title || 'Untitled Section'} + + +
+ )} +
+ ) + } + + // Regular procedure step + stepCounter++ + const stepNumber = stepCounter + const isExpanded = expandedStepId === step.id + const contentType = step.content_type || 'action' + const config = contentTypeConfig[contentType] + const Icon = config.icon + + if (isExpanded) { + return ( + + {() => ( +
+ updateStep(step.id, updates)} + onCollapse={() => setExpandedStepId(null)} + availableVariables={intakeForm} + /> +
+ )} +
+ ) + } - if (isExpanded) { return ( -
- updateStep(step.id, updates)} - onCollapse={() => setExpandedStepId(null)} - availableVariables={intakeForm} - /> -
+ + {({ dragHandleProps }) => ( +
+ + + + {stepNumber} + + + + + + + setExpandedStepId(step.id)} + > + {step.title || 'Untitled step'} + + + {step.estimated_minutes && ( + + ~{step.estimated_minutes}m + + )} + + + + +
+ )} +
) - } - - return ( -
- - setExpandedStepId(step.id)} - > - {step.title || 'Untitled Section'} - - -
- ) - } - - // Regular procedure step - stepCounter++ - const stepNumber = stepCounter - const isExpanded = expandedStepId === step.id - const contentType = step.content_type || 'action' - const config = contentTypeConfig[contentType] - const Icon = config.icon - - if (isExpanded) { - return ( -
- updateStep(step.id, updates)} - onCollapse={() => setExpandedStepId(null)} - availableVariables={intakeForm} - /> -
- ) - } - - return ( -
-
- - - - {stepNumber} - - - - - - - setExpandedStepId(step.id)} - > - {step.title || 'Untitled step'} - - - {step.estimated_minutes && ( - - ~{step.estimated_minutes}m - - )} - - - - -
-
- ) - })} -
+ })} +
+ + {/* Add step button at bottom */} + {!treeId && ( +

Save the flow first to configure a schedule.

+ )} +
+ ) +} + +export function getScheduleSummary(schedule: MaintenanceSchedule | null, targetList?: TargetList | null): string { + if (!schedule) return 'No schedule configured' + + 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] + const dayOfMonth = parts[2] + let freqStr = 'Custom' + if (dayOfMonth === '1') { + freqStr = 'Monthly (1st)' + } else if (dayOfMonth === '*' && parts[3] === '*') { + if (dayOfWeek === '*') freqStr = 'Daily' + else if (dayOfWeek === '1') freqStr = 'Every Monday' + else if (dayOfWeek === '5') freqStr = 'Every Friday' + } + + const targetStr = targetList ? ` \u00b7 ${targetList.targets.length} target${targetList.targets.length !== 1 ? 's' : ''}` : '' + return `${freqStr} at ${timeStr} ${schedule.timezone}${targetStr}` +} -- 2.49.1 From 77ed04520418be5feba8ba8926804a23f02fb9a7 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Thu, 19 Feb 2026 03:22:13 -0500 Subject: [PATCH 07/13] feat: wire maintenance schedule section into procedural editor Add collapsible Schedule section for maintenance flows with accordion integration. Schedule summary shows frequency, time, and target count when collapsed. New maintenance flows default to schedule section expanded. Two-stage save preserved: tree saved first, schedule managed independently. Co-Authored-By: Claude Opus 4.6 --- frontend/src/pages/ProceduralEditorPage.tsx | 31 +++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/ProceduralEditorPage.tsx b/frontend/src/pages/ProceduralEditorPage.tsx index 8e5a2905..f17b4d8e 100644 --- a/frontend/src/pages/ProceduralEditorPage.tsx +++ b/frontend/src/pages/ProceduralEditorPage.tsx @@ -1,14 +1,15 @@ import { useEffect, useState, useCallback } from 'react' import { useParams, useNavigate, useSearchParams } from 'react-router-dom' -import { Save, ArrowLeft, ListOrdered, Wrench, Settings, FileText } from 'lucide-react' +import { Save, ArrowLeft, ListOrdered, Wrench, Settings, FileText, Calendar } from 'lucide-react' import { treesApi } from '@/api/trees' import { useProceduralEditorStore } from '@/store/proceduralEditorStore' import { CollapsibleEditorSection } from '@/components/procedural-editor/CollapsibleEditorSection' import { IntakeFormBuilder } from '@/components/procedural-editor/IntakeFormBuilder' +import { MaintenanceScheduleSection, getScheduleSummary } from '@/components/procedural-editor/MaintenanceScheduleSection' import { StepList } from '@/components/procedural-editor/StepList' import { TagInput } from '@/components/common/TagInput' import { toast } from '@/lib/toast' -import type { TreeType } from '@/types' +import type { TreeType, MaintenanceSchedule, TargetList } from '@/types' type SectionKey = 'details' | 'intake' | 'schedule' @@ -49,10 +50,19 @@ export function ProceduralEditorPage() { isEditMode ? null : 'details' ) + // Schedule state for collapsed summary + const [schedule, setSchedule] = useState(null) + const [scheduleTargetList, setScheduleTargetList] = useState(null) + const toggleSection = useCallback((key: SectionKey) => { setExpandedSection(prev => prev === key ? null : key) }, []) + const handleScheduleLoaded = useCallback((s: MaintenanceSchedule | null, tl: TargetList | null) => { + setSchedule(s) + setScheduleTargetList(tl) + }, []) + // Load tree or init new useEffect(() => { if (isEditMode && id) { @@ -123,6 +133,8 @@ export function ProceduralEditorPage() { isPublic ? 'Public' : 'Private', ].join(' \u00b7 ') + const scheduleSummary = getScheduleSummary(schedule, scheduleTargetList) + 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 ? ', \u2026' : ''}` @@ -241,6 +253,21 @@ export function ProceduralEditorPage() { > + + {isMaintenance && ( + } + summary={scheduleSummary} + expanded={expandedSection === 'schedule'} + onToggle={() => toggleSection('schedule')} + > + + + )}
{/* Step List — flex-1, scrolls independently */} -- 2.49.1 From ef699f3eaba7737a3bb44bc0b891ab3aedfb9523 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Thu, 19 Feb 2026 07:02:30 -0500 Subject: [PATCH 08/13] fix: resolve lint issues in maintenance schedule and editor page Move getScheduleSummary to scheduleUtils.ts to satisfy react-refresh only-export-components rule. Add onScheduleLoaded to useEffect deps. Co-Authored-By: Claude Opus 4.6 --- .../MaintenanceScheduleSection.tsx | 25 +------------------ .../procedural-editor/scheduleUtils.ts | 24 ++++++++++++++++++ frontend/src/pages/ProceduralEditorPage.tsx | 3 ++- 3 files changed, 27 insertions(+), 25 deletions(-) create mode 100644 frontend/src/components/procedural-editor/scheduleUtils.ts diff --git a/frontend/src/components/procedural-editor/MaintenanceScheduleSection.tsx b/frontend/src/components/procedural-editor/MaintenanceScheduleSection.tsx index d41fdbcb..0e513ac0 100644 --- a/frontend/src/components/procedural-editor/MaintenanceScheduleSection.tsx +++ b/frontend/src/components/procedural-editor/MaintenanceScheduleSection.tsx @@ -76,7 +76,7 @@ export function MaintenanceScheduleSection({ treeId, onScheduleLoaded }: Mainten } } load() - }, [treeId]) + }, [treeId, onScheduleLoaded]) const hydrateFromSchedule = (s: MaintenanceSchedule) => { const parts = s.cron_expression.split(' ') @@ -239,26 +239,3 @@ export function MaintenanceScheduleSection({ treeId, onScheduleLoaded }: Mainten
) } - -export function getScheduleSummary(schedule: MaintenanceSchedule | null, targetList?: TargetList | null): string { - if (!schedule) return 'No schedule configured' - - 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] - const dayOfMonth = parts[2] - let freqStr = 'Custom' - if (dayOfMonth === '1') { - freqStr = 'Monthly (1st)' - } else if (dayOfMonth === '*' && parts[3] === '*') { - if (dayOfWeek === '*') freqStr = 'Daily' - else if (dayOfWeek === '1') freqStr = 'Every Monday' - else if (dayOfWeek === '5') freqStr = 'Every Friday' - } - - const targetStr = targetList ? ` \u00b7 ${targetList.targets.length} target${targetList.targets.length !== 1 ? 's' : ''}` : '' - return `${freqStr} at ${timeStr} ${schedule.timezone}${targetStr}` -} diff --git a/frontend/src/components/procedural-editor/scheduleUtils.ts b/frontend/src/components/procedural-editor/scheduleUtils.ts new file mode 100644 index 00000000..501c99b2 --- /dev/null +++ b/frontend/src/components/procedural-editor/scheduleUtils.ts @@ -0,0 +1,24 @@ +import type { MaintenanceSchedule, TargetList } from '@/types' + +export function getScheduleSummary(schedule: MaintenanceSchedule | null, targetList?: TargetList | null): string { + if (!schedule) return 'No schedule configured' + + 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] + const dayOfMonth = parts[2] + let freqStr = 'Custom' + if (dayOfMonth === '1') { + freqStr = 'Monthly (1st)' + } else if (dayOfMonth === '*' && parts[3] === '*') { + if (dayOfWeek === '*') freqStr = 'Daily' + else if (dayOfWeek === '1') freqStr = 'Every Monday' + else if (dayOfWeek === '5') freqStr = 'Every Friday' + } + + const targetStr = targetList ? ` \u00b7 ${targetList.targets.length} target${targetList.targets.length !== 1 ? 's' : ''}` : '' + return `${freqStr} at ${timeStr} ${schedule.timezone}${targetStr}` +} diff --git a/frontend/src/pages/ProceduralEditorPage.tsx b/frontend/src/pages/ProceduralEditorPage.tsx index f17b4d8e..2bf6c9d2 100644 --- a/frontend/src/pages/ProceduralEditorPage.tsx +++ b/frontend/src/pages/ProceduralEditorPage.tsx @@ -5,7 +5,8 @@ import { treesApi } from '@/api/trees' import { useProceduralEditorStore } from '@/store/proceduralEditorStore' import { CollapsibleEditorSection } from '@/components/procedural-editor/CollapsibleEditorSection' import { IntakeFormBuilder } from '@/components/procedural-editor/IntakeFormBuilder' -import { MaintenanceScheduleSection, getScheduleSummary } from '@/components/procedural-editor/MaintenanceScheduleSection' +import { MaintenanceScheduleSection } from '@/components/procedural-editor/MaintenanceScheduleSection' +import { getScheduleSummary } from '@/components/procedural-editor/scheduleUtils' import { StepList } from '@/components/procedural-editor/StepList' import { TagInput } from '@/components/common/TagInput' import { toast } from '@/lib/toast' -- 2.49.1 From 7f520772ec80ca2e7dd1446c3c2154e80185c7cb Mon Sep 17 00:00:00 2001 From: chihlasm Date: Thu, 19 Feb 2026 07:02:47 -0500 Subject: [PATCH 09/13] docs: add design and implementation revision documents Revision docs correct original plans: schedule persistence via API endpoints (not tree_structure), array-index reorder (no display_order), store minimum-one-step invariant, accordion mode, ARIA requirements, and two-stage save orchestration with failure handling. Co-Authored-By: Claude Opus 4.6 --- ...edural-editor-redesign-design-revisions.md | 155 ++++++++++++ ...ocedural-editor-redesign-impl-revisions.md | 225 ++++++++++++++++++ 2 files changed, 380 insertions(+) create mode 100644 docs/plans/2026-02-19-procedural-editor-redesign-design-revisions.md create mode 100644 docs/plans/2026-02-19-procedural-editor-redesign-impl-revisions.md diff --git a/docs/plans/2026-02-19-procedural-editor-redesign-design-revisions.md b/docs/plans/2026-02-19-procedural-editor-redesign-design-revisions.md new file mode 100644 index 00000000..7dfe8488 --- /dev/null +++ b/docs/plans/2026-02-19-procedural-editor-redesign-design-revisions.md @@ -0,0 +1,155 @@ +# Procedural & Maintenance Editor Redesign - Design Revisions + +> **Date:** 2026-02-19 +> **Revises:** `docs/plans/2026-02-19-procedural-editor-redesign-design.md` +> **Purpose:** Resolve implementation gaps and make the design decision-complete for engineering handoff + +## Summary + +This revision tightens the original design to match current architecture and APIs. +It resolves contradictions around maintenance schedule persistence, aligns step-list behavior with store invariants, and defines concrete DnD/accessibility/test expectations. + +## Revision Decisions (Locked) + +1. Keep fixed-height editor layout with independent StepList scrolling. +2. Use collapsible sections for Details, Intake Form, and Maintenance Schedule. +3. Use existing installed `@dnd-kit/*` packages (no new dependency work). +4. Keep `procedure_end` as non-draggable and always last. +5. Keep schedule as optional for maintenance flows (manual batch launch remains valid). + +## Critical Corrections to Original Design + +1. **Maintenance schedule persistence** +- Schedule is not embedded in `tree_structure`. +- Schedule must be persisted through maintenance schedule endpoints. +- New unsaved flow requires two-stage save: + 1. Save tree (`treesApi.create` / `treesApi.update`) + 2. Create/update schedule (`maintenanceSchedulesApi.create` or `maintenanceSchedulesApi.update`) +- One schedule per tree (`uq_maintenance_schedules_tree_id`). + +2. **Step empty-state semantics** +- Original "0 steps" state conflicts with current store minimum step behavior. +- Revised behavior: + - If minimum-one-step invariant remains, remove "0 steps" UX language. + - Empty-state is only shown if invariant is intentionally changed in store. + +3. **DnD data model correction** +- Do not mention recalculating `display_order` for procedural steps. +- Reorder is array-index based only. +- Explicitly block dragging `procedure_end`. + +4. **Dependencies section correction** +- Replace "New Dependencies" with "Existing Dependencies Used" for `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`. + +5. **File impact correction** +- Add API integration surfaces: + - `frontend/src/api/maintenanceSchedules.ts` + - target-list integration file(s) if inline list creation is in scope +- Clarify interaction with `frontend/src/pages/MaintenanceFlowDetailPage.tsx` (retain schedule view/edit there or shift entirely to editor). + +6. **Collapsible behavior clarification** +- Use single-open accordion mode by default. +- Details defaults expanded for new flows. +- Intake defaults collapsed. +- Maintenance Schedule defaults: + - new maintenance flow: expanded + - existing with schedule: collapsed summary + - existing without schedule: collapsed with "Set Up" affordance + +7. **A11y + keyboard DnD acceptance** +- Include keyboard reorder acceptance criteria. +- Include focus order and ARIA labeling criteria for section toggles and drag handles. + +## Revised Maintenance Schedule Section Spec + +### New maintenance flow (tree not yet saved) + +1. Render schedule editor expanded. +2. Keep schedule input in local UI draft state. +3. On Save Draft / Publish: +- Save tree first. +- If tree save succeeds, create schedule with resulting `tree_id`. +4. If schedule create fails: +- Tree save remains successful. +- Show actionable error ("Schedule not saved. Retry."). +- Preserve schedule draft state. + +### Existing maintenance flow + +1. Load schedule via `maintenanceSchedulesApi.getForTree(treeId)`. +2. Edit and persist via `maintenanceSchedulesApi.update(scheduleId, data)`. +3. If no schedule exists, show collapsed "No schedule configured" summary + setup button. + +## Revised StepList Reorder Spec + +1. Draggable types: +- `procedure_step` +- `section_header` +2. Non-draggable: +- `procedure_end` +3. Reorder behavior: +- Move item by array index. +- Preserve all step payload fields. +- No implicit grouped movement under section headers. +4. Keyboard behavior: +- Drag handle focusable. +- Enter/Space pick up + drop. +- Arrow keys move while grabbed. +- Escape cancels. + +## Acceptance Criteria + +1. **Layout** +- Toolbar remains visible while StepList scrolls. +- Details/Intake/Schedule sections collapse without shrinking StepList usability. + +2. **Steps** +- New step auto-expands. +- New step scrolls into view. +- Reorder works by pointer and keyboard. +- `procedure_end` remains last and fixed. + +3. **Maintenance schedule** +- New unsaved maintenance flow can be saved without schedule. +- Schedule can be created immediately after first tree save. +- Existing schedule loads and updates in editor. +- Schedule failure does not roll back successful tree save. + +4. **Accessibility** +- Section toggles keyboard operable. +- Drag handles have accessible labels. +- Focus remains stable after reorder. + +5. **Build/Test** +- `npm run build` succeeds. +- Affected component/store tests pass. + +## Updated File Impact + +### Modified + +- `frontend/src/pages/ProceduralEditorPage.tsx` +- `frontend/src/components/procedural-editor/StepList.tsx` +- `frontend/src/components/procedural-editor/IntakeFormBuilder.tsx` +- `frontend/src/store/proceduralEditorStore.ts` +- `frontend/src/api/maintenanceSchedules.ts` +- `frontend/src/pages/MaintenanceFlowDetailPage.tsx` (if schedule UX ownership changes) + +### New + +- `frontend/src/components/procedural-editor/CollapsibleEditorSection.tsx` +- `frontend/src/components/procedural-editor/MaintenanceScheduleSection.tsx` + +### Existing dependencies used + +- `@dnd-kit/core` +- `@dnd-kit/sortable` +- `@dnd-kit/utilities` + +## Out of Scope (unchanged) + +1. Intake field DnD reorder. +2. Procedural undo/redo parity. +3. Step templates/presets. +4. Bulk step operations. +5. Backend schema/model changes for procedural steps. diff --git a/docs/plans/2026-02-19-procedural-editor-redesign-impl-revisions.md b/docs/plans/2026-02-19-procedural-editor-redesign-impl-revisions.md new file mode 100644 index 00000000..8f803a99 --- /dev/null +++ b/docs/plans/2026-02-19-procedural-editor-redesign-impl-revisions.md @@ -0,0 +1,225 @@ +# Procedural Editor Redesign - Implementation Revisions + +> **Date:** 2026-02-19 +> **Revises:** `docs/plans/2026-02-19-procedural-editor-redesign-impl.md` +> **Related Design Revision:** `docs/plans/2026-02-19-procedural-editor-redesign-design-revisions.md` + +## Goal + +Revise the implementation plan so it matches actual architecture and APIs, with explicit handling for maintenance schedule persistence, step-list invariants, DnD constraints, and accessibility/test requirements. + +## Critical Corrections from Original Impl Plan + +1. Do not treat maintenance schedule as part of `tree_structure`. +2. Do not use `display_order` for procedural step reorder. +3. Do not assume `0 steps` state unless store invariant is changed intentionally. +4. Do not list `@dnd-kit/*` as new dependency (already installed). +5. Add explicit save orchestration for unsaved maintenance flows. +6. Add explicit failure handling when tree save succeeds but schedule save fails. + +## Phase 0: Scope Lock + +### Task 0.1 - Confirm invariants and UX ownership + +**Decisions to lock in code before implementation:** +1. `procedure_end` remains fixed, non-draggable, last. +2. Minimum one `procedure_step` remains enforced (recommended). +3. Schedule editing in editor is source-of-truth for create/edit, with detail page as display/secondary entrypoint. + +**Files:** none (decision checkpoint) + +--- + +## Phase 1: Layout and Collapsible Sections + +### Task 1.1 - Add shared collapsible wrapper + +**Files:** +- Create: `frontend/src/components/procedural-editor/CollapsibleEditorSection.tsx` + +**Requirements:** +1. Single-row collapsed summary. +2. Keyboard-accessible toggle button. +3. `aria-expanded`, `aria-controls`, and section `id`. +4. Optional `defaultExpanded`. + +### Task 1.2 - Convert ProceduralEditorPage to fixed-height editor + +**Files:** +- Modify: `frontend/src/pages/ProceduralEditorPage.tsx` + +**Changes:** +1. Outer layout becomes `flex h-full flex-col overflow-hidden`. +2. Toolbar becomes sticky. +3. Details and Intake wrapped in `CollapsibleEditorSection`. +4. Steps area becomes `flex-1 min-h-0 overflow-y-auto`. +5. Accordion mode: only one section open at a time (explicit state in page component). + +**Summaries:** +1. Details: `"Name" - N tags - Public/Private`. +2. Intake: `N fields: label1, label2...` (truncate). + +--- + +## Phase 2: StepList Behavior and DnD + +### Task 2.1 - Align header/empty behavior with current store invariant + +**Files:** +- Modify: `frontend/src/components/procedural-editor/StepList.tsx` +- Optional invariant change (if desired): `frontend/src/store/proceduralEditorStore.ts` + +**Required behavior (recommended):** +1. Keep minimum one `procedure_step`. +2. Remove/unset any `0 steps` UI paths. +3. Header shows: +- `Steps (N steps - ~M min)` when estimates exist +- `Steps (N steps)` otherwise. + +### Task 2.2 - Ensure new step auto-expands + scrolls into view + +**Files:** +- Modify: `frontend/src/components/procedural-editor/StepList.tsx` +- Verify existing store behavior in `frontend/src/store/proceduralEditorStore.ts` + +**Behavior:** +1. On add step/section, expanded editor opens immediately. +2. Newly inserted row is scrolled into view via stable element refs (prefer scroll target by id over "scroll to bottom"). + +### Task 2.3 - Implement DnD with current model constraints + +**Files:** +- Modify: `frontend/src/components/procedural-editor/StepList.tsx` +- Modify: `frontend/src/store/proceduralEditorStore.ts` (reuse `moveStep`) + +**Rules:** +1. Draggable: `procedure_step`, `section_header`. +2. Non-draggable: `procedure_end`. +3. Reorder by array index only. +4. No `display_order` recalculation for steps. +5. Keyboard drag support and visible insertion indicator. + +--- + +## Phase 3: Maintenance Schedule Section (Correct API orchestration) + +### Task 3.1 - Add schedule section component + +**Files:** +- Create: `frontend/src/components/procedural-editor/MaintenanceScheduleSection.tsx` +- Modify: `frontend/src/pages/ProceduralEditorPage.tsx` + +**Behavior:** +1. Render only for `treeType === 'maintenance'`. +2. Capture: +- cron expression +- timezone +- target list id +3. Collapsed summary: +- configured: human-readable cadence + target list status +- unconfigured: `No schedule configured`. + +### Task 3.2 - Add schedule draft UI state and save orchestration + +**Files:** +- Modify: `frontend/src/store/proceduralEditorStore.ts` (UI draft state only) +- Modify: `frontend/src/pages/ProceduralEditorPage.tsx` +- Use: `frontend/src/api/maintenanceSchedules.ts` + +**Save flow:** +1. Save tree first (`create`/`update`). +2. If maintenance and schedule draft present: +- if existing schedule id: `maintenanceSchedulesApi.update` +- else: `maintenanceSchedulesApi.create` with saved tree id. +3. If schedule save fails: +- keep tree save success +- show actionable error toast/banner +- preserve schedule draft as dirty. + +### Task 3.3 - Existing flow load + +**Files:** +- Modify: `frontend/src/pages/ProceduralEditorPage.tsx` + +**Behavior:** +1. On edit maintenance flow, fetch schedule via `getForTree(treeId)`. +2. 404 = no schedule yet (valid state). +3. Hydrate schedule draft state for section UI. + +--- + +## Phase 4: Integration polish and consistency + +### Task 4.1 - Clarify MaintenanceFlowDetailPage role + +**Files:** +- Modify (if needed): `frontend/src/pages/MaintenanceFlowDetailPage.tsx` + +**Decision implementation:** +1. Keep schedule read-only there, with "Edit in Flow Editor" CTA. +2. Avoid split-brain schedule edits in two places unless explicitly desired. + +--- + +## Phase 5: Tests and verification + +### Task 5.1 - Automated tests + +**Files (new/updated):** +- `frontend/src/components/procedural-editor/StepList.test.tsx` +- `frontend/src/pages/ProceduralEditorPage.test.tsx` +- `frontend/src/store/proceduralEditorStore.test.ts` (if absent, add focused tests) + +**Minimum coverage:** +1. Reorder respects `procedure_end` constraints. +2. New steps auto-expand and scroll target call occurs. +3. Accordion open/close state and summaries. +4. Maintenance save orchestration: +- tree create/update then schedule create/update +- schedule failure does not revert tree success. + +### Task 5.2 - Manual acceptance checklist + +1. Steps list remains primary viewport focus in fixed-height layout. +2. Details/Intake/Schedule sections collapse and summarize correctly. +3. DnD works by mouse and keyboard. +4. End step never drags. +5. New maintenance flow: +- can save draft without schedule +- can save with schedule in one action (tree first, schedule second). +6. Existing maintenance flow loads schedule and saves edits. + +### Task 5.3 - Build and lint gates + +1. `cd frontend && npm run build` +2. `cd frontend && npm run test` +3. `cd frontend && npm run lint` + +--- + +## File Impact (Revised) + +### Create +1. `frontend/src/components/procedural-editor/CollapsibleEditorSection.tsx` +2. `frontend/src/components/procedural-editor/MaintenanceScheduleSection.tsx` + +### Modify +1. `frontend/src/pages/ProceduralEditorPage.tsx` +2. `frontend/src/components/procedural-editor/StepList.tsx` +3. `frontend/src/components/procedural-editor/IntakeFormBuilder.tsx` +4. `frontend/src/store/proceduralEditorStore.ts` +5. `frontend/src/pages/MaintenanceFlowDetailPage.tsx` (if ownership adjusted) + +### Existing APIs used +1. `frontend/src/api/maintenanceSchedules.ts` +2. target list API module(s) if inline list selection/creation is implemented + +--- + +## Out of Scope (unchanged) + +1. Intake field DnD reorder. +2. Procedural undo/redo parity. +3. Step templates/presets. +4. Bulk step operations. +5. Backend schema/model changes for procedural steps. -- 2.49.1 From 30ab5a5907ec7f6ec481383ee5f65c572e4e6c48 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Thu, 19 Feb 2026 07:59:59 -0500 Subject: [PATCH 10/13] feat: auto-seed PR environments with SEED_ON_DEPLOY flag Release command now runs migrations + seeds test users when SEED_ON_DEPLOY=true. Tree seeding runs as a background task on startup via HTTP API. Everything is idempotent and non-fatal. Co-Authored-By: Claude Opus 4.6 --- backend/app/core/config.py | 3 +++ backend/app/main.py | 46 +++++++++++++++++++++++++++++++++ backend/railway.toml | 2 +- backend/scripts/release.py | 53 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 backend/scripts/release.py diff --git a/backend/app/core/config.py b/backend/app/core/config.py index adc81277..eb073d08 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -72,6 +72,9 @@ class Settings(BaseSettings): """Check if Stripe is configured.""" return self.STRIPE_SECRET_KEY is not None and self.STRIPE_WEBHOOK_SECRET is not None + # Deployment – auto-seed test data on PR environments + SEED_ON_DEPLOY: bool = False + # CORS - set FRONTEND_URL in production (e.g., https://patherly.up.railway.app) CORS_ORIGINS: list[str] = ["http://localhost:3000", "http://localhost:5173", "http://localhost:5174"] FRONTEND_URL: Optional[str] = None diff --git a/backend/app/main.py b/backend/app/main.py index 5536e832..0c5da246 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,4 +1,6 @@ +import asyncio import logging +import os from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware @@ -18,6 +20,42 @@ setup_logging() logger = logging.getLogger(__name__) +async def _seed_trees_background() -> None: + """Background task: seed trees via HTTP API after server is ready.""" + await asyncio.sleep(5) # Wait for server to be fully ready + port = os.environ.get("PORT", "8000") + api_url = f"http://127.0.0.1:{port}/api/v1" + email = "admin@resolutionflow.example.com" + password = "TestPass123!" + + try: + import httpx + # Login to get token + async with httpx.AsyncClient(base_url=api_url, timeout=30) as client: + login_resp = await client.post("/auth/login/json", json={"email": email, "password": password}) + if login_resp.status_code != 200: + logger.warning("[seed] Could not login as admin — skipping tree seeding") + return + + token = login_resp.json()["access_token"] + # Check if trees already exist + trees_resp = await client.get("/trees", headers={"Authorization": f"Bearer {token}"}) + if trees_resp.status_code == 200 and len(trees_resp.json()) > 0: + logger.info(f"[seed] {len(trees_resp.json())} trees already exist — skipping tree seeding") + return + + # Trees don't exist yet — run the full seed script + logger.info("[seed] No trees found — running seed_trees...") + import scripts.seed_trees as seed_trees_mod + seed_trees_mod.API_BASE_URL = api_url + seed_trees_mod.ADMIN_EMAIL = email + seed_trees_mod.ADMIN_PASSWORD = password + await seed_trees_mod.seed_database() + logger.info("[seed] Tree seeding complete!") + except Exception as e: + logger.warning(f"[seed] Tree seeding failed (non-fatal): {e}") + + @asynccontextmanager async def lifespan(app: FastAPI): """Application lifespan handler.""" @@ -33,9 +71,17 @@ async def lifespan(app: FastAPI): async with async_session_maker() as db: await load_all_schedules(db) + # Auto-seed trees in background on PR environments + seed_task = None + if settings.SEED_ON_DEPLOY: + logger.info("[seed] SEED_ON_DEPLOY=true — scheduling background tree seeding") + seed_task = asyncio.create_task(_seed_trees_background()) + yield # Shutdown + if seed_task and not seed_task.done(): + seed_task.cancel() scheduler.shutdown(wait=False) logger.info("Shutting down ResolutionFlow API server...") diff --git a/backend/railway.toml b/backend/railway.toml index 2c439905..a2b6bd6f 100644 --- a/backend/railway.toml +++ b/backend/railway.toml @@ -7,4 +7,4 @@ healthcheckPath = "/health" healthcheckTimeout = 100 restartPolicyType = "on_failure" restartPolicyMaxRetries = 3 -releaseCommand = "alembic upgrade head" +releaseCommand = "python -m scripts.release" diff --git a/backend/scripts/release.py b/backend/scripts/release.py new file mode 100644 index 00000000..6118da0b --- /dev/null +++ b/backend/scripts/release.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +""" +Railway release command — runs migrations + optional seed data. + +Set SEED_ON_DEPLOY=true in Railway env vars to auto-seed test users +on PR environments. Seeding is idempotent (skips existing records). + +Usage (called by railway.toml releaseCommand): + python -m scripts.release +""" + +import asyncio +import subprocess +import sys + +from app.core.config import settings + + +def run_migrations() -> None: + """Run alembic upgrade head.""" + print("\n[release] Running database migrations...") + result = subprocess.run( + ["alembic", "upgrade", "head"], + capture_output=False, + ) + if result.returncode != 0: + print("[release] ERROR: Migrations failed!") + sys.exit(1) + print("[release] Migrations complete.") + + +async def seed_test_data() -> None: + """Seed test users (direct DB, no HTTP needed).""" + print("\n[release] Seeding test users...") + from scripts.seed_test_users import main as seed_users + await seed_users() + print("[release] Test users seeded.") + + +def main() -> None: + run_migrations() + + if settings.SEED_ON_DEPLOY: + print("[release] SEED_ON_DEPLOY=true — seeding test data...") + asyncio.run(seed_test_data()) + else: + print("[release] SEED_ON_DEPLOY not set — skipping seed data.") + + print("\n[release] Release complete!") + + +if __name__ == "__main__": + main() -- 2.49.1 From 0e8ca66831a25f489797dfa8e95cd7e94b9692fe Mon Sep 17 00:00:00 2001 From: chihlasm Date: Thu, 19 Feb 2026 08:03:55 -0500 Subject: [PATCH 11/13] fix: add httpx to requirements for PR environment seeding Co-Authored-By: Claude Opus 4.6 --- backend/requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/requirements.txt b/backend/requirements.txt index 686c481e..47d6eaf8 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -28,6 +28,9 @@ stripe==14.3.0 # Email resend==2.21.0 +# HTTP client (seed scripts, internal API calls) +httpx>=0.27.0 + # Utilities python-dotenv==1.0.1 croniter>=2.0.0 -- 2.49.1 From a7ebeb4f7238ea10888d84f830c22620a5022638 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Thu, 19 Feb 2026 08:08:24 -0500 Subject: [PATCH 12/13] feat: seed all flow types (v2, procedural, maintenance) on deploy Runs seed_trees, seed_trees_v2, seed_procedural_flows, and seed_maintenance_flows sequentially as background tasks when SEED_ON_DEPLOY=true. Each script failure is non-fatal. Co-Authored-By: Claude Opus 4.6 --- backend/app/main.py | 46 ++++++++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index 0c5da246..91b34c91 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -20,8 +20,15 @@ setup_logging() logger = logging.getLogger(__name__) +def _configure_seed_module(mod: object, api_url: str, email: str, password: str) -> None: + """Set globals on a seed script module.""" + mod.API_BASE_URL = api_url # type: ignore[attr-defined] + mod.ADMIN_EMAIL = email # type: ignore[attr-defined] + mod.ADMIN_PASSWORD = password # type: ignore[attr-defined] + + async def _seed_trees_background() -> None: - """Background task: seed trees via HTTP API after server is ready.""" + """Background task: seed all flows via HTTP API after server is ready.""" await asyncio.sleep(5) # Wait for server to be fully ready port = os.environ.get("PORT", "8000") api_url = f"http://127.0.0.1:{port}/api/v1" @@ -30,30 +37,43 @@ async def _seed_trees_background() -> None: try: import httpx - # Login to get token + # Login to verify admin user exists async with httpx.AsyncClient(base_url=api_url, timeout=30) as client: login_resp = await client.post("/auth/login/json", json={"email": email, "password": password}) if login_resp.status_code != 200: - logger.warning("[seed] Could not login as admin — skipping tree seeding") + logger.warning("[seed] Could not login as admin — skipping flow seeding") return token = login_resp.json()["access_token"] # Check if trees already exist trees_resp = await client.get("/trees", headers={"Authorization": f"Bearer {token}"}) if trees_resp.status_code == 200 and len(trees_resp.json()) > 0: - logger.info(f"[seed] {len(trees_resp.json())} trees already exist — skipping tree seeding") + logger.info(f"[seed] {len(trees_resp.json())} flows already exist — skipping flow seeding") return - # Trees don't exist yet — run the full seed script - logger.info("[seed] No trees found — running seed_trees...") - import scripts.seed_trees as seed_trees_mod - seed_trees_mod.API_BASE_URL = api_url - seed_trees_mod.ADMIN_EMAIL = email - seed_trees_mod.ADMIN_PASSWORD = password - await seed_trees_mod.seed_database() - logger.info("[seed] Tree seeding complete!") + # No flows yet — run all seed scripts + seed_scripts = [ + ("seed_trees (Tier 1)", "scripts.seed_trees", "seed_database"), + ("seed_trees_v2 (AD/M365/Networking)", "scripts.seed_trees_v2", "seed_database"), + ("seed_procedural_flows", "scripts.seed_procedural_flows", "seed_procedural_flows"), + ("seed_maintenance_flows", "scripts.seed_maintenance_flows", "seed_maintenance_flows"), + ] + + for label, module_path, func_name in seed_scripts: + try: + import importlib + mod = importlib.import_module(module_path) + _configure_seed_module(mod, api_url, email, password) + seed_fn = getattr(mod, func_name) + logger.info(f"[seed] Running {label}...") + await seed_fn() + logger.info(f"[seed] {label} complete!") + except Exception as e: + logger.warning(f"[seed] {label} failed (non-fatal): {e}") + + logger.info("[seed] All flow seeding complete!") except Exception as e: - logger.warning(f"[seed] Tree seeding failed (non-fatal): {e}") + logger.warning(f"[seed] Flow seeding failed (non-fatal): {e}") @asynccontextmanager -- 2.49.1 From b60a8b12969b9e337428437796a660bda77f791a Mon Sep 17 00:00:00 2001 From: chihlasm Date: Thu, 19 Feb 2026 08:13:47 -0500 Subject: [PATCH 13/13] chore: trigger redeploy for full seed Co-Authored-By: Claude Opus 4.6 -- 2.49.1