diff --git a/docs/plans/2026-02-24-procedural-custom-steps-design.md b/docs/plans/2026-02-24-procedural-custom-steps-design.md new file mode 100644 index 00000000..578b9120 --- /dev/null +++ b/docs/plans/2026-02-24-procedural-custom-steps-design.md @@ -0,0 +1,178 @@ +# Procedural Custom Steps — Design + +> **Date:** February 24, 2026 +> **Status:** Approved +> **Phase:** 2.5 — Feature 2 of 3 + +--- + +## What We're Building + +Add "Add Custom Step" support to `ProceduralNavigationPage` so engineers can insert ad-hoc steps between any existing checklist items during execution. The inserted step appears inline in the checklist and detail panel — worked through just like a regular step before proceeding. + +--- + +## Key Difference from Troubleshooting Custom Steps + +`useCustomStepFlow` is built for tree-graph navigation: `currentNodeId`, `findNode`, `setCurrentNodeId`, path traversal, continuation modal for picking descendants, custom branch mode. **None of that applies here.** + +Procedural flows are linear arrays. A custom step is just a new `ProceduralStep`-shaped object injected at a position in the array. No new hook needed — all state lives in `ProceduralNavigationPage`. + +--- + +## Data Model + +Custom steps are stored in session `custom_steps` (already a JSONB array on `Session`). The existing `CustomStep` type is: + +```ts +interface CustomStep { + id: string + inserted_after_node_id: string // step ID it was inserted after + step_data: Step | CustomStepDraft + timestamp: string +} +``` + +For procedural flows, `inserted_after_node_id` is the `ProceduralStep.id` of the step it follows. + +A custom step is represented in the runtime `ProceduralStep[]` as: + +```ts +{ + id: customStep.id, // the CustomStep UUID + type: 'procedure_step', + title: step_data.title, + description: step_data.content.instructions, + content_type: 'action', + commands: step_data.content.commands (mapped), + // marker for custom steps: + _isCustom: true // not in ProceduralStep type — use a local union +} +``` + +Rather than mutating `ProceduralStep`, we use a local discriminated union: + +```ts +type RuntimeStep = ProceduralStep | CustomProceduralStep + +interface CustomProceduralStep { + id: string + type: 'procedure_step' + title: string + description?: string + content_type: 'action' + commands?: CommandBlock[] + isCustom: true // discriminant +} +``` + +The `procedureSteps` array used for rendering becomes `RuntimeStep[]` instead of `ProceduralStep[]`. + +--- + +## User Flow + +1. Engineer is on any step in a procedural flow +2. Clicks **"+ Add Step"** button below the current step detail +3. `CustomStepModal` opens (existing component — Create tab + Browse Library tab) +4. Engineer creates or selects a step → `PostStepActionModal` appears +5. **"Use Now"**: inserts after current step, closes modals, advances to the new step +6. **"Save for Later"**: saves to library only, no insertion +7. **"Do Both"**: saves to library + inserts + +The inserted step renders in `StepDetail` and `StepChecklist` with a visual "Custom" badge. The engineer marks it complete the same way as any other step (the "Mark Complete" button). + +No continuation modal. No custom branch mode. No fork flow. + +--- + +## What Changes + +### ProceduralNavigationPage.tsx + +**New state:** +```ts +const [runtimeSteps, setRuntimeSteps] = useState([]) +const [showCustomStepModal, setShowCustomStepModal] = useState(false) +const [showPostStepModal, setShowPostStepModal] = useState(false) +const [pendingCustomStep, setPendingCustomStep] = useState(null) +const [pendingIsFromLibrary, setPendingIsFromLibrary] = useState(false) +const [isSavingStep, setIsSavingStep] = useState(false) +const [sessionCustomSteps, setSessionCustomSteps] = useState([]) +``` + +**`runtimeSteps`** is initialized from `tree.tree_structure.steps` and updated when custom steps are inserted. `procedureSteps` (currently `steps.filter(s => s.type === 'procedure_step')`) becomes `runtimeSteps.filter(...)`. + +**`handleInsertCustomStep(step, isFromLibrary)`:** +- Builds a `CustomProceduralStep` from the draft/step +- Inserts it into `runtimeSteps` after `currentStepIndex` +- Adds a `CustomStep` entry to `sessionCustomSteps` +- Calls `sessionsApi.update(session.id, { custom_steps: newCustomSteps })` +- Advances `currentStepIndex` by 1 (focus moves to the new step) + +**`handleStepCreated(step, isFromLibrary)`:** — same pattern as troubleshooting +- Sets `pendingCustomStep`, `pendingIsFromLibrary` +- Closes `CustomStepModal`, opens `PostStepActionModal` + +**Add `handleSaveForLater`, `handleUseNow`, `handleBoth`** — same logic as `useCustomStepFlow` but without path/node navigation. + +**"+ Add Step" button:** Renders in the right panel below `StepDetail`, above `StepFeedback`. Only shown on the current active (incomplete) step. Not shown on already-completed steps or on custom steps (no nesting). + +**Session resume:** On `resumeSession`, initialize `runtimeSteps` from the tree steps then inject custom steps from `sessionData.custom_steps` at the correct positions. + +### StepChecklist.tsx + +Accept `RuntimeStep[]` instead of `ProceduralStep[]`. Render a small "Custom" badge (amber dot or label) next to custom step titles. + +### StepDetail.tsx + +Accept `RuntimeStep` instead of `ProceduralStep`. When `step.isCustom === true`, render slightly differently: no `content_type` badge (use a plain "Custom Step" label instead), show instructions as description, render commands if present using the existing command block renderer. + +### New type: `RuntimeStep` + +Add to `frontend/src/types/tree.ts` (or a separate `procedural.ts`): + +```ts +export interface CustomProceduralStep { + id: string + type: 'procedure_step' + title: string + description?: string + content_type: 'action' + commands?: CommandBlock[] + isCustom: true +} + +export type RuntimeStep = ProceduralStep | CustomProceduralStep +``` + +--- + +## What Does NOT Change + +- `useCustomStepFlow` — not used at all in procedural flows +- `ContinuationModal` — not used (no branching) +- Fork flow — not used +- `CustomStepModal` — reused as-is (already works for both create and browse) +- `PostStepActionModal` — reused as-is +- `sessionsApi.update` with `custom_steps` — already supports this +- Backend — no changes needed + +--- + +## Checklist render (sidebar) + +Custom steps show in the checklist with an amber `✦` or small "Custom" chip: + +``` +✅ 1. Check service status +✅ 2. Restart broker agent + 3. [✦ Custom] Verify VDA re-registration ← custom step + 4. Check event log +``` + +--- + +## Completion behavior + +When the engineer marks the last custom step complete and it was inserted before the last regular step, `currentStepIndex` increments normally — they continue with the remaining regular steps. The total step count in the progress bar updates when custom steps are inserted. diff --git a/docs/plans/2026-02-24-procedural-custom-steps-plan.md b/docs/plans/2026-02-24-procedural-custom-steps-plan.md new file mode 100644 index 00000000..76e8ac4e --- /dev/null +++ b/docs/plans/2026-02-24-procedural-custom-steps-plan.md @@ -0,0 +1,578 @@ +# Procedural Custom Steps — Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add "Add Custom Step" support to `ProceduralNavigationPage` so engineers can insert ad-hoc steps between existing checklist items during a session. + +**Architecture:** Introduce a `RuntimeStep = ProceduralStep | CustomProceduralStep` discriminated union type. `ProceduralNavigationPage` grows new state + handlers for the custom-step modal flow (reusing `CustomStepModal` and `PostStepActionModal` as-is). `StepChecklist` and `StepDetail` accept `RuntimeStep` instead of `ProceduralStep`. No backend changes needed. + +**Tech Stack:** React 19, TypeScript, Tailwind CSS, existing `sessionsApi.update`, `CustomStepModal`, `PostStepActionModal` + +--- + +### Task 1: Add `RuntimeStep` union type + +**Files:** +- Modify: `frontend/src/types/tree.ts` +- Modify: `frontend/src/types/index.ts` (re-export if needed) + +**Context:** `ProceduralStep` is already in `tree.ts` along with `CommandBlock`. The new `CustomProceduralStep` interface lives in the same file. No hook needed — this is purely a type. + +**Step 1: Add the types at the bottom of `frontend/src/types/tree.ts`** + +After the existing `ProceduralStep` interface (line ~124), add: + +```ts +export interface CustomProceduralStep { + id: string + type: 'procedure_step' + title: string + description?: string + content_type: 'action' + commands?: CommandBlock[] + isCustom: true +} + +export type RuntimeStep = ProceduralStep | CustomProceduralStep +``` + +**Step 2: Export from `frontend/src/types/index.ts`** + +Check what is currently re-exported from `tree.ts` in `index.ts` and add `CustomProceduralStep` and `RuntimeStep` to the export list. + +Run: `cd frontend && npm run build 2>&1 | tail -20` +Expected: no new type errors + +**Step 3: Commit** + +```bash +git add frontend/src/types/tree.ts frontend/src/types/index.ts +git commit -m "feat: add RuntimeStep union type for procedural custom steps + +Co-Authored-By: Claude Sonnet 4.6 " +``` + +--- + +### Task 2: Update `StepChecklist` to accept `RuntimeStep[]` + +**Files:** +- Modify: `frontend/src/components/procedural/StepChecklist.tsx` + +**Context:** Currently `StepChecklistProps.steps` is `ProceduralStep[]`. It filters internally for `type === 'procedure_step'`. Custom steps already have `type: 'procedure_step'` so the filter still works. We need to: +1. Change prop type to `RuntimeStep[]` +2. Render an amber "Custom" badge next to custom step titles + +**Step 1: Update the import and prop type** + +Change: +```tsx +import type { ProceduralStep } from '@/types' +``` +To: +```tsx +import type { RuntimeStep } from '@/types' +``` + +Change `StepChecklistProps`: +```tsx +interface StepChecklistProps { + steps: RuntimeStep[] + currentStepIndex: number + completedStepIds: Set + onStepClick: (index: number) => void +} +``` + +**Step 2: Add the "Custom" badge in the step row** + +In the button's `` row, add a badge after the title when `'isCustom' in step && step.isCustom`: + +```tsx + + {step.title || 'Untitled step'} + {'isCustom' in step && step.isCustom && ( + + Custom + + )} + +``` + +**Step 3: Build check** + +Run: `cd frontend && npm run build 2>&1 | tail -20` +Expected: no new errors + +**Step 4: Commit** + +```bash +git add frontend/src/components/procedural/StepChecklist.tsx +git commit -m "feat: StepChecklist accepts RuntimeStep[], renders amber Custom badge + +Co-Authored-By: Claude Sonnet 4.6 " +``` + +--- + +### Task 3: Update `StepDetail` to accept `RuntimeStep` + +**Files:** +- Modify: `frontend/src/components/procedural/StepDetail.tsx` + +**Context:** `StepDetail` currently only accepts `ProceduralStep`. For `CustomProceduralStep`, we skip the `content_type` badge (show "Custom Step" label instead), hide `warning_text`/`expected_outcome`/`verification`/`reference_url` sections (they don't exist on custom steps), and show description + commands normally. The `canComplete()` check and Mark Complete button stay the same. + +**Step 1: Update import and prop type** + +```tsx +import type { RuntimeStep, CommandBlock } from '@/types' +``` + +Change `StepDetailProps`: +```tsx +interface StepDetailProps { + step: RuntimeStep + // ... rest unchanged +} +``` + +**Step 2: Handle custom step header (replace content_type badge block)** + +The existing header block uses `step.content_type` to pick a config. Add a guard: + +```tsx +// At top of component body, after existing state: +const isCustom = 'isCustom' in step && step.isCustom + +// In the header JSX, replace the content_type badge span: +{isCustom ? ( + + ✦ Custom Step + +) : ( + + + {config.label} + +)} +``` + +Note: `contentType` and `config` are still computed but only used in the non-custom branch. TypeScript will be happy because `ProceduralStep` has `content_type` and custom step skips that branch. + +**Step 3: Guard sections that don't exist on custom steps** + +Sections that need guarding (wrap with `!isCustom &&`): +- `{step.warning_text && ...}` — already implicitly guarded since `CustomProceduralStep` has no `warning_text`, but TypeScript may complain → add `!isCustom &&` before the expression +- `{step.expected_outcome && ...}` — same +- `{verificationPrompt && ...}` — same (derive `verificationPrompt` with `!isCustom &&` check) +- `{step.reference_url && ...}` — same + +The description block, commandBlocks block, and notes block all work fine as-is (custom steps have `description?` and `commands?` matching the normalized shapes). + +Specifically, for `verificationPrompt` / `verificationType`: +```tsx +const verificationPrompt = isCustom ? undefined : (step.verification_prompt || step.verification?.prompt) +const verificationType = isCustom ? undefined : (step.verification_type || step.verification?.type) +``` + +For `commandBlocks` normalization — `CustomProceduralStep.commands` is `CommandBlock[]` already, so the existing ternary handles it fine. + +**Step 4: Build check** + +Run: `cd frontend && npm run build 2>&1 | tail -20` +Expected: no errors + +**Step 5: Commit** + +```bash +git add frontend/src/components/procedural/StepDetail.tsx +git commit -m "feat: StepDetail accepts RuntimeStep, renders Custom Step badge for custom steps + +Co-Authored-By: Claude Sonnet 4.6 " +``` + +--- + +### Task 4: Wire custom step flow into `ProceduralNavigationPage` + +**Files:** +- Modify: `frontend/src/pages/ProceduralNavigationPage.tsx` + +**Context:** This is the main task. Current page has no custom step support. We add: +- `runtimeSteps: RuntimeStep[]` state (replaces `procedureSteps` derived value) +- `sessionCustomSteps: CustomStep[]` state +- `showCustomStepModal`, `showPostStepModal`, `pendingCustomStep`, `pendingIsFromLibrary`, `isSavingStep` state +- `handleStepCreated` — closes CustomStepModal, opens PostStepActionModal +- `handleInsertCustomStep` — builds CustomProceduralStep, inserts into runtimeSteps, persists +- `handleSaveForLater` / `handleUseNow` / `handleBoth` — wire PostStepActionModal buttons +- "Add Step" button below StepDetail, above StepFeedback +- Resume: inject custom steps at correct positions + +**Step 1: Add new imports** + +```tsx +import { CustomStepModal } from '@/components/step-library/CustomStepModal' +import { PostStepActionModal } from '@/components/session/PostStepActionModal' +import type { RuntimeStep, CustomProceduralStep } from '@/types' +import type { CustomStep } from '@/types/session' +import type { Step } from '@/types/step' +import type { CustomStepDraft } from '@/components/step-library/CustomStepModal' +import { Plus } from 'lucide-react' +import { v4 as uuidv4 } from 'uuid' +``` + +Check if `uuid` package is already available: `grep -r "from 'uuid'" frontend/src/`. If not, use `crypto.randomUUID()` instead (available in all modern browsers and Node 16+). + +**Step 2: Add new state (after existing state declarations)** + +```tsx +const [runtimeSteps, setRuntimeSteps] = useState([]) +const [sessionCustomSteps, setSessionCustomSteps] = useState([]) +const [showCustomStepModal, setShowCustomStepModal] = useState(false) +const [showPostStepModal, setShowPostStepModal] = useState(false) +const [pendingCustomStep, setPendingCustomStep] = useState(null) +const [pendingIsFromLibrary, setPendingIsFromLibrary] = useState(false) +const [isSavingStep, setIsSavingStep] = useState(false) +``` + +**Step 3: Replace `procedureSteps` derivation** + +Currently: +```tsx +const procedureSteps = steps.filter((s) => s.type === 'procedure_step') +``` + +Replace with: +```tsx +// runtimeSteps is the authoritative list; filter for rendering (excludes section_headers) +const procedureSteps = runtimeSteps.filter((s) => s.type === 'procedure_step') +``` + +Also update `estimatedTotalMinutes` — it only sums `estimated_minutes` which only exists on `ProceduralStep`. Cast safely: +```tsx +const estimatedTotalMinutes = procedureSteps.reduce( + (sum, step) => sum + (('estimated_minutes' in step ? step.estimated_minutes : undefined) || 0), + 0 +) +``` + +**Step 4: Initialize `runtimeSteps` in `startSession`** + +After `setSession(newSession)`, add: +```tsx +// Initialize runtimeSteps from tree steps +const allSteps = getStepsFromTree(tree!) +setRuntimeSteps(allSteps) +setSessionCustomSteps([]) +``` + +The existing step state initialization loop stays the same — custom steps will add to it when inserted. + +**Step 5: Initialize `runtimeSteps` in `resumeSession`** + +After loading `sessionData`, before computing `pSteps`: +```tsx +// Build runtimeSteps: start with tree steps, then inject custom steps +const allSteps = getStepsFromTree(treeData) +const customSteps = sessionData.custom_steps || [] +setSessionCustomSteps(customSteps) + +// Inject custom steps at correct positions +const hydrated = buildRuntimeSteps(allSteps, customSteps) +setRuntimeSteps(hydrated) +``` + +And update the `pSteps` / `firstIncomplete` calculation to use `hydrated` (not `allSteps`): +```tsx +const pSteps = hydrated.filter((s) => s.type === 'procedure_step') +const firstIncomplete = pSteps.findIndex((s) => !initialStates.get(s.id)?.completedAt) +setCurrentStepIndex(firstIncomplete >= 0 ? firstIncomplete : pSteps.length - 1) +``` + +**Step 6: Add `buildRuntimeSteps` helper** + +Add this function before the component (or as a module-level utility): + +```ts +function buildRuntimeSteps(baseSteps: ProceduralStep[], customSteps: CustomStep[]): RuntimeStep[] { + const result: RuntimeStep[] = [...baseSteps] + // Sort custom steps by timestamp so earlier insertions come first if multiple + const sorted = [...customSteps].sort((a, b) => a.timestamp.localeCompare(b.timestamp)) + for (const cs of sorted) { + // Find the index of the step this was inserted after + const afterIdx = result.findIndex((s) => s.id === cs.inserted_after_node_id) + const insertAt = afterIdx >= 0 ? afterIdx + 1 : result.length + const runtimeCustom: CustomProceduralStep = { + id: cs.id, + type: 'procedure_step', + title: cs.step_data.title, + description: cs.step_data.content?.instructions, + content_type: 'action', + commands: cs.step_data.content?.commands?.map((c) => ({ + code: c.command, + label: c.label, + })), + isCustom: true, + } + result.splice(insertAt, 0, runtimeCustom) + } + return result +} +``` + +Note: `ProceduralStep` needs to be imported in scope here. Since it's used in `getStepsFromTree` already, it's available. + +**Step 7: Add `handleStepCreated`** + +```tsx +const handleStepCreated = (step: Step | CustomStepDraft, isFromLibrary: boolean) => { + setPendingCustomStep(step) + setPendingIsFromLibrary(isFromLibrary) + setShowCustomStepModal(false) + setShowPostStepModal(true) +} +``` + +**Step 8: Add `handleInsertCustomStep`** + +```tsx +const handleInsertCustomStep = async (step: Step | CustomStepDraft) => { + if (!session) return + + const id = crypto.randomUUID() + const currentStep = procedureSteps[currentStepIndex] + const insertedAfterId = currentStep?.id ?? '' + + // Build the runtime representation + const runtimeCustom: CustomProceduralStep = { + id, + type: 'procedure_step', + title: step.title, + description: step.content?.instructions, + content_type: 'action', + commands: step.content?.commands?.map((c) => ({ + code: c.command, + label: c.label, + })), + isCustom: true, + } + + // Insert after currentStepIndex in runtimeSteps + setRuntimeSteps((prev) => { + const next = [...prev] + // Find the global index of the current procedureStep in runtimeSteps + const globalIdx = next.findIndex((s) => s.id === insertedAfterId) + const insertAt = globalIdx >= 0 ? globalIdx + 1 : next.length + next.splice(insertAt, 0, runtimeCustom) + return next + }) + + // Initialize step state for the new step + setStepStates((prev) => { + const next = new Map(prev) + next.set(id, { notes: '', verificationValue: '', completedAt: null }) + return next + }) + + // Persist to session + const newCustomStep: CustomStep = { + id, + inserted_after_node_id: insertedAfterId, + step_data: step, + timestamp: new Date().toISOString(), + } + const newCustomSteps = [...sessionCustomSteps, newCustomStep] + setSessionCustomSteps(newCustomSteps) + + try { + await sessionsApi.update(session.id, { custom_steps: newCustomSteps }) + } catch { + toast.error('Failed to save custom step') + } + + // Advance to the new step (it's now at currentStepIndex + 1) + setCurrentStepIndex(currentStepIndex + 1) +} +``` + +**Step 9: Add `handleSaveForLater`, `handleUseNow`, `handleBoth`** + +```tsx +const handleSaveForLater = async () => { + if (!pendingCustomStep || pendingIsFromLibrary) return + setIsSavingStep(true) + try { + await stepsApi.create({ + title: pendingCustomStep.title, + step_type: pendingCustomStep.step_type, + content: pendingCustomStep.content, + visibility: 'private', + }) + toast.success('Step saved to library') + } catch { + toast.error('Failed to save step') + } finally { + setIsSavingStep(false) + setShowPostStepModal(false) + setPendingCustomStep(null) + } +} + +const handleUseNow = async () => { + if (!pendingCustomStep) return + setShowPostStepModal(false) + await handleInsertCustomStep(pendingCustomStep) + setPendingCustomStep(null) +} + +const handleBoth = async () => { + if (!pendingCustomStep || pendingIsFromLibrary) return + setIsSavingStep(true) + try { + await stepsApi.create({ + title: pendingCustomStep.title, + step_type: pendingCustomStep.step_type, + content: pendingCustomStep.content, + visibility: 'private', + }) + } catch { + toast.error('Failed to save step to library') + } finally { + setIsSavingStep(false) + } + setShowPostStepModal(false) + await handleInsertCustomStep(pendingCustomStep) + setPendingCustomStep(null) +} +``` + +Add `stepsApi` import at the top: `import { stepsApi } from '@/api/steps'` + +**Step 10: Update `handleMarkComplete` to use `runtimeSteps`-based `procedureSteps`** + +The existing `handleMarkComplete` reads `procedureSteps[currentStepIndex]` — since `procedureSteps` is now derived from `runtimeSteps`, this Just Works. But the completion check `currentStepIndex >= procedureSteps.length - 1` also works correctly. + +No changes needed to `handleMarkComplete` itself. + +**Step 11: Update `StepChecklist` call in JSX** + +Change `steps={steps}` to `steps={runtimeSteps}`: +```tsx + +``` + +**Step 12: Update `StepDetail` call + add "Add Step" button** + +Change `step={currentStep}` prop type is now `RuntimeStep` (TypeScript satisfied since `procedureSteps` is `RuntimeStep[]`). + +Change `totalSteps={procedureSteps.length}` — still correct. + +After the `StepDetail` closing tag and before `StepFeedback`, add the "Add Step" button: + +```tsx +{/* Add Custom Step button — only on current active (incomplete) step, not on custom steps */} +{currentStep && !completedStepIds.has(currentStep.id) && !('isCustom' in currentStep && currentStep.isCustom) && ( +
+ +
+)} +``` + +**Step 13: Add modals at the bottom of the JSX (before closing ``)** + +After the `ConfirmDialog` and parameters popover, add: + +```tsx +{/* Custom Step Modal */} + setShowCustomStepModal(false)} + onInsertStep={handleStepCreated} +/> + +{/* Post Step Action Modal */} +{pendingCustomStep && ( + { setShowPostStepModal(false); setPendingCustomStep(null) }} + step={pendingCustomStep} + onSaveForLater={handleSaveForLater} + onUseNow={handleUseNow} + onBoth={handleBoth} + isFromLibrary={pendingIsFromLibrary} + isSaving={isSavingStep} + /> +)} +``` + +**Step 14: ProgressBar total count update** + +The `ProgressBar` already uses `procedureSteps.length` as `totalSteps` which is derived from `runtimeSteps` — so the progress bar total automatically updates when custom steps are inserted. + +**Step 15: Build check** + +Run: `cd frontend && npm run build 2>&1 | tail -30` +Expected: no errors. Watch especially for: +- `CustomStep` import (it's in `@/types/session`, not `@/types` — import directly if needed) +- `buildRuntimeSteps` needing `ProceduralStep` type explicitly imported + +**Step 16: Commit** + +```bash +git add frontend/src/pages/ProceduralNavigationPage.tsx +git commit -m "feat: custom step insertion in procedural flow sessions + +Engineers can add custom steps inline during execution. Steps are +persisted to session.custom_steps and restored on resume. + +Co-Authored-By: Claude Sonnet 4.6 " +``` + +--- + +### Task 5: Manual verification checklist + +**No code changes — verification only.** + +Start a procedural session (or maintenance session). Verify: + +1. **Add Step button appears** below the current step detail on an active, incomplete, non-custom step +2. **Add Step button is hidden** on completed steps +3. **Clicking Add Step** opens `CustomStepModal` with Create + Browse Library tabs +4. **Creating a step** → `PostStepActionModal` appears +5. **"Use Now"** → closes modal, custom step appears as next step in checklist with amber "Custom" badge, current view advances to it +6. **"Save for Later"** → closes modal, nothing inserted, no crash +7. **"Do Both"** → saves to library AND inserts +8. **Mark Complete on custom step** → advances to next step normally +9. **Custom step in StepDetail** → shows "Custom Step" amber badge instead of content_type badge +10. **Progress bar total** increments when custom step is inserted +11. **Resume session** (navigate away and back with `{ state: { sessionId } }`) → custom step reappears at correct position in checklist + +--- + +### Task 6: Final build validation + +Run: `cd frontend && npm run build` +Expected: build succeeds with zero errors (pre-existing chunk-size warnings are fine). + +Commit build validation result if no issues: + +```bash +git add -p # stage any incidental fixes +git commit -m "chore: final build validation for procedural custom steps + +Co-Authored-By: Claude Sonnet 4.6 " +``` + +Only commit if there were actual changes to stage. diff --git a/docs/plans/2026-02-24-step-library-page-design.md b/docs/plans/2026-02-24-step-library-page-design.md new file mode 100644 index 00000000..1a1f9100 --- /dev/null +++ b/docs/plans/2026-02-24-step-library-page-design.md @@ -0,0 +1,175 @@ +# Step Library Page — Design + +> **Date:** February 24, 2026 +> **Status:** Approved +> **Phase:** 2.5 — Feature 1 of 3 + +--- + +## What We're Building + +Replace the "Coming Soon" stub in `StepLibraryPage` with a fully functional standalone Step Library. Users can browse, search, create, edit, delete, preview, and save steps to their own library. + +All backend endpoints and frontend components are already built. This is primarily a wiring and UX integration task. + +--- + +## Scope + +**In scope:** +- Full-page Step Library with header, filters, grouped results +- Create step flow (modal with `StepForm`) +- Edit step flow (same modal, pre-filled) +- Delete step with confirmation +- "Save to My Library" for steps you don't own (copies step as private) +- Preview via `StepDetailModal` +- `StepCard` actions adapted for library context (vs. session insert context) + +**Out of scope:** +- Rating/reviewing steps from this page (deferred — will come when steps are used in sessions) +- Admin category management (already exists in Admin Panel) +- Session custom step insertion (Feature 2 of 3) + +--- + +## Card Actions by Context + +| Ownership | Actions | +|-----------|---------| +| Your own step | Preview · Edit · Delete | +| Team or public step | Preview · Save to My Library | + +"Save to My Library" POSTs a new step copying the title, step_type, content, category, and tags — with `visibility: 'private'` and the current user as owner. + +--- + +## Page Layout + +``` +┌─────────────────────────────────────────────────────────┐ +│ Step Library [+ Create Step] │ +│ Reusable steps you can insert into any flow │ +├─────────────────────────────────────────────────────────┤ +│ [Search...] [Category ▼] [Type ▼] [Rating ▼] [Sort▼]│ +│ Popular Tags: [powershell] [dns] [quick-fix] ... │ +├─────────────────────────────────────────────────────────┤ +│ MY STEPS (3) [▲] │ +│ ┌────────────┐ ┌────────────┐ │ +│ │ StepCard │ │ StepCard │ (Preview · Edit · Del) │ +│ └────────────┘ └────────────┘ │ +│ │ +│ TEAM STEPS (12) [▲] │ +│ ┌────────────┐ ┌────────────┐ │ +│ │ StepCard │ │ StepCard │ (Preview · Save) │ +│ └────────────┘ └────────────┘ │ +│ │ +│ COMMUNITY (47) [▲] │ +│ ┌────────────┐ ┌────────────┐ │ +│ │ StepCard │ │ StepCard │ (Preview · Save) │ +│ └────────────┘ └────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## Component Architecture + +### StepLibraryPage (rewritten) +- Owns modal state: `createOpen`, `editingStep`, `deletingStepId` +- Renders page header with "+ Create Step" button +- Renders `StepLibraryBrowser` with page-context props +- Renders `StepFormModal` (create/edit) +- Renders delete confirmation dialog + +### StepLibraryBrowser (extend props) +Add props: +```ts +interface StepLibraryBrowserProps { + onInsert: (step: Step) => void // existing (used in session context) + onEdit?: (step: Step) => void // new — library page only + onDelete?: (stepId: string) => void // new — library page only + onSave?: (step: Step) => void // new — save to my library + onCreateNew?: () => void // existing + showCreateButton?: boolean // existing + currentUserId?: string // new — to determine ownership +} +``` + +### StepCard (extend props) +Add props mirroring the browser: +```ts +interface StepCardProps { + step: StepListItem + onPreview: (step: StepListItem) => void + onInsert?: (step: StepListItem) => void // optional — session context + onEdit?: (step: StepListItem) => void // library context + onDelete?: (stepId: string) => void // library context + onSave?: (step: StepListItem) => void // library context + currentUserId?: string +} +``` + +`StepCard` renders actions based on which callbacks are present and whether `step.created_by === currentUserId`. + +### StepFormModal (new wrapper component) +Thin modal shell around the existing `StepForm`: +- Fixed header ("Create Step" or "Edit Step") +- Scrollable body with `StepForm` +- Handles create (`stepsApi.create`) and update (`stepsApi.update`) API calls +- Calls `onSuccess(step)` so the browser list can refresh + +--- + +## Data Flow + +### Create +1. User clicks "+ Create Step" +2. `StepFormModal` opens in create mode +3. On submit → `stepsApi.create(data)` → `onSuccess` → reload steps + +### Edit +1. User clicks Edit on their own step card +2. `stepsApi.get(stepId)` fetches full step (need content fields) +3. `StepFormModal` opens pre-filled with step data +4. On submit → `stepsApi.update(id, data)` → `onSuccess` → reload steps + +### Delete +1. User clicks Delete on their own step card +2. Confirmation dialog: "Delete '[title]'? This cannot be undone." +3. On confirm → `stepsApi.delete(id)` → remove from local state + +### Save to My Library +1. User clicks "Save to My Library" on a team/public step +2. No confirmation needed — silent copy +3. `stepsApi.create({ ...step fields, visibility: 'private' })` → toast "Saved to My Steps" +4. Reload steps so the copy appears under "My Steps" + +--- + +## Files to Change + +| File | Change | +|------|--------| +| `pages/StepLibraryPage.tsx` | Rewrite — wire up browser + modals | +| `components/step-library/StepCard.tsx` | Add `onEdit`, `onDelete`, `onSave`, `currentUserId` props; render contextual buttons | +| `components/step-library/StepLibraryBrowser.tsx` | Add `onEdit`, `onDelete`, `onSave`, `currentUserId` props; pass through to cards; expose refresh trigger | +| `components/step-library/StepFormModal.tsx` | Create — thin modal wrapper around `StepForm` | + +No backend changes required. + +--- + +## StepCard Button Layout + +**Own step:** +``` +[Preview] [Edit] [🗑] +``` +Edit and Preview are full-width-ish buttons; delete is an icon-only button in red on hover. + +**Others' step:** +``` +[Preview] [Save to My Library] +``` + +Both full-width buttons, same pattern as current Insert layout. diff --git a/docs/plans/2026-02-24-step-library-page-plan.md b/docs/plans/2026-02-24-step-library-page-plan.md new file mode 100644 index 00000000..cc7b4801 --- /dev/null +++ b/docs/plans/2026-02-24-step-library-page-plan.md @@ -0,0 +1,705 @@ +# Step Library Page Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Wire up the existing Step Library components into a fully functional standalone page — replacing the "Coming Soon" stub — with create, edit, delete, preview, and save-to-library actions. + +**Architecture:** `StepLibraryPage` owns all modal state and orchestrates four components: `StepLibraryBrowser` (list + filters), `StepFormModal` (new wrapper for create/edit), `StepDetailModal` (already exists), and a delete confirmation dialog. `StepCard` and `StepLibraryBrowser` get new optional props for library-page-specific actions. No new API calls beyond what already exists in `stepsApi`. + +**Tech Stack:** React 19, TypeScript, Tailwind CSS, Zustand (`useAuthStore` for current user ID), `stepsApi` + `stepCategoriesApi` (all endpoints already wired to backend). + +--- + +## Key Files Reference + +- `frontend/src/pages/StepLibraryPage.tsx` — currently a "Coming Soon" stub; will be rewritten +- `frontend/src/components/step-library/StepLibraryBrowser.tsx` — list + filters component +- `frontend/src/components/step-library/StepCard.tsx` — individual step card +- `frontend/src/components/step-library/StepDetailModal.tsx` — preview modal (already complete) +- `frontend/src/components/step-library/StepForm.tsx` — create/edit form (already complete) +- `frontend/src/api/steps.ts` — `stepsApi.create`, `.get`, `.update`, `.delete` already implemented +- `frontend/src/types/step.ts` — `Step`, `StepListItem`, `StepCreate`, `StepUpdate` types +- `frontend/src/store/authStore.ts` — use `useAuthStore((s) => s.user)` to get current user +- `frontend/src/hooks/usePermissions.ts` — `canCreateSteps` already defined + +--- + +## How to Get Current User ID + +```tsx +import { useAuthStore } from '@/store/authStore' +const user = useAuthStore((s) => s.user) +// user.id is the current user's UUID string +``` + +--- + +## Task 1: Extend StepCard with library-page actions + +**Files:** +- Modify: `frontend/src/components/step-library/StepCard.tsx` + +This task adds `onEdit`, `onDelete`, `onSave`, and `currentUserId` props. When on the library page (these props are present), the action buttons change based on ownership. + +**Step 1: Read the current file** + +Already read above. Current interface: +```ts +interface StepCardProps { + step: StepListItem + onPreview: (step: StepListItem) => void + onInsert: (step: StepListItem) => void +} +``` + +**Step 2: Update the interface and button logic** + +Replace the `StepCardProps` interface and the Actions section at the bottom of `StepCard.tsx`. + +New interface (all new props are optional so existing `CustomStepModal` usage is unchanged): +```tsx +interface StepCardProps { + step: StepListItem + onPreview: (step: StepListItem) => void + onInsert?: (step: StepListItem) => void // session context (existing) + onEdit?: (step: StepListItem) => void // library page + onDelete?: (stepId: string) => void // library page + onSave?: (step: StepListItem) => void // library page (save copy) + currentUserId?: string // to determine ownership +} +``` + +Replace the Actions section (the `
` at the bottom) with: + +```tsx +{/* Actions */} +
+ {/* Library page context */} + {(onEdit || onDelete || onSave) ? ( + isOwn ? ( + // Own step: Preview + Edit + Delete icon + <> + + + + + ) : ( + // Others' step: Preview + Save + <> + + + + ) + ) : ( + // Session context (original): Preview + Insert + <> + + + + )} +
+``` + +Add `isOwn` derived value near top of the component function (before the return): +```tsx +const isOwn = currentUserId ? step.created_by === currentUserId : false +``` + +Add new imports at top of file: +```tsx +import { Star, User, Calendar, TrendingUp, Eye, Plus, HelpCircle, Zap, CheckCircle, Pencil, Trash2, Bookmark } from 'lucide-react' +``` + +**Step 3: Verify TypeScript compiles** + +```bash +cd /home/michaelchihlas/dev/patherly/frontend && npx tsc --noEmit 2>&1 | head -30 +``` + +Expected: no errors related to StepCard. + +**Step 4: Commit** + +```bash +cd /home/michaelchihlas/dev/patherly +git add frontend/src/components/step-library/StepCard.tsx +git commit -m "feat: add library-page action props to StepCard (edit/delete/save) + +Co-Authored-By: Claude Sonnet 4.6 " +``` + +--- + +## Task 2: Extend StepLibraryBrowser with library-page props + +**Files:** +- Modify: `frontend/src/components/step-library/StepLibraryBrowser.tsx` + +Pass the new `onEdit`, `onDelete`, `onSave`, `currentUserId` props through from browser to each `StepCard`. Also expose a `refreshKey` prop so the page can trigger a reload after create/edit/delete/save. + +**Step 1: Update the interface** + +Current interface: +```ts +interface StepLibraryBrowserProps { + onInsert: (step: Step) => void + onCreateNew?: () => void + showCreateButton?: boolean +} +``` + +New interface: +```ts +interface StepLibraryBrowserProps { + onInsert?: (step: Step) => void // now optional (not needed on library page) + onCreateNew?: () => void + showCreateButton?: boolean + onEdit?: (step: StepListItem) => void + onDelete?: (stepId: string) => void + onSave?: (step: StepListItem) => void + currentUserId?: string + refreshKey?: number // increment to trigger reload +} +``` + +**Step 2: Wire refreshKey into the steps useEffect** + +In the existing `useEffect` that calls `loadSteps`, add `refreshKey` to the dependency array: + +```tsx +useEffect(() => { + const loadSteps = async () => { ... } + loadSteps() +}, [searchQuery, selectedCategoryId, selectedStepType, minRating, sortBy, selectedTag, refreshKey]) +``` + +**Step 3: Pass new props to StepCard** + +In all three `groupedSteps.private/team/public` map blocks, update `StepCard` usage: + +```tsx + +``` + +**Step 4: Verify TypeScript compiles** + +```bash +cd /home/michaelchihlas/dev/patherly/frontend && npx tsc --noEmit 2>&1 | head -30 +``` + +**Step 5: Commit** + +```bash +cd /home/michaelchihlas/dev/patherly +git add frontend/src/components/step-library/StepLibraryBrowser.tsx +git commit -m "feat: pass library-page action props through StepLibraryBrowser + refreshKey + +Co-Authored-By: Claude Sonnet 4.6 " +``` + +--- + +## Task 3: Create StepFormModal + +**Files:** +- Create: `frontend/src/components/step-library/StepFormModal.tsx` + +A thin modal wrapper around the existing `StepForm`. Handles both create and edit modes. + +**Step 1: Create the file** + +```tsx +import { useState } from 'react' +import { X } from 'lucide-react' +import { stepsApi } from '@/api/steps' +import { StepForm } from './StepForm' +import type { Step, StepCreate, StepListItem } from '@/types/step' + +interface StepFormModalProps { + isOpen: boolean + onClose: () => void + onSuccess: (step: Step) => void + editingStep?: StepListItem | null // if set, edit mode; if null/undefined, create mode +} + +export function StepFormModal({ isOpen, onClose, onSuccess, editingStep }: StepFormModalProps) { + const [isSubmitting, setIsSubmitting] = useState(false) + const [error, setError] = useState(null) + + if (!isOpen) return null + + const isEditMode = !!editingStep + + const handleSubmit = async (data: StepCreate) => { + setIsSubmitting(true) + setError(null) + try { + let result: Step + if (isEditMode && editingStep) { + result = await stepsApi.update(editingStep.id, data) + } else { + result = await stepsApi.create(data) + } + onSuccess(result) + } catch (err) { + console.error('Failed to save step:', err) + setError('Failed to save step. Please try again.') + } finally { + setIsSubmitting(false) + } + } + + // Build initialData from editingStep for edit mode + // StepListItem doesn't have `content`, so for edit we need to fetch full step + // This is handled by the parent (StepLibraryPage fetches full step before opening modal) + const initialData = editingStep ? { + title: editingStep.title, + step_type: editingStep.step_type as 'decision' | 'action' | 'solution', + visibility: editingStep.visibility as 'private' | 'team' | 'public', + category_id: editingStep.category_id, + tags: editingStep.tags, + } : undefined + + return ( +
+
+ {/* Header */} +
+

+ {isEditMode ? 'Edit Step' : 'Create Step'} +

+ +
+ + {/* Error */} + {error && ( +
+ {error} +
+ )} + + {/* Body */} +
+ +
+
+
+ ) +} +``` + +**Step 2: Update StepForm to accept `submitLabel` and `isSubmitting` props** + +`StepForm` currently has a hardcoded submit button label ("Insert Step") and no loading state. Add these two optional props: + +```ts +interface StepFormProps { + onSubmit: (data: StepCreate) => void + onCancel: () => void + initialData?: Partial + submitLabel?: string // default: 'Insert Step' + isSubmitting?: boolean // default: false +} +``` + +In `StepForm`, use them on the submit button: +```tsx + +``` + +**Step 3: Verify TypeScript compiles** + +```bash +cd /home/michaelchihlas/dev/patherly/frontend && npx tsc --noEmit 2>&1 | head -30 +``` + +**Step 4: Commit** + +```bash +cd /home/michaelchihlas/dev/patherly +git add frontend/src/components/step-library/StepFormModal.tsx \ + frontend/src/components/step-library/StepForm.tsx +git commit -m "feat: StepFormModal wrapper + submitLabel/isSubmitting props on StepForm + +Co-Authored-By: Claude Sonnet 4.6 " +``` + +--- + +## Task 4: Rewrite StepLibraryPage + +**Files:** +- Modify: `frontend/src/pages/StepLibraryPage.tsx` + +This is the main wiring task. Replace the stub with the full page. + +**Step 1: Write the new page** + +```tsx +import { useState } from 'react' +import { Bookmark, Trash2 } from 'lucide-react' +import { useAuthStore } from '@/store/authStore' +import { usePermissions } from '@/hooks/usePermissions' +import { stepsApi } from '@/api/steps' +import { StepLibraryBrowser } from '@/components/step-library/StepLibraryBrowser' +import { StepFormModal } from '@/components/step-library/StepFormModal' +import type { Step, StepListItem } from '@/types/step' + +export default function StepLibraryPage() { + const user = useAuthStore((s) => s.user) + const { canCreateSteps } = usePermissions() + + // Modal state + const [createOpen, setCreateOpen] = useState(false) + const [editingStep, setEditingStep] = useState(null) + const [deletingStep, setDeletingStep] = useState(null) + const [isDeleting, setIsDeleting] = useState(false) + const [deleteError, setDeleteError] = useState(null) + const [saveToast, setSaveToast] = useState(null) + + // Increment to trigger StepLibraryBrowser reload + const [refreshKey, setRefreshKey] = useState(0) + const refresh = () => setRefreshKey(k => k + 1) + + const handleEdit = (step: StepListItem) => { + setEditingStep(step) + } + + const handleDeleteRequest = (stepId: string) => { + // Find the step in order to show its title in the confirmation + // We store the StepListItem via the browser's onDelete callback + // The step object is passed from StepCard which has the full StepListItem + setDeletingStep({ id: stepId } as StepListItem) + } + + const handleDeleteConfirm = async () => { + if (!deletingStep) return + setIsDeleting(true) + setDeleteError(null) + try { + await stepsApi.delete(deletingStep.id) + setDeletingStep(null) + refresh() + } catch (err) { + console.error('Failed to delete step:', err) + setDeleteError('Failed to delete step. Please try again.') + } finally { + setIsDeleting(false) + } + } + + const handleSave = async (step: StepListItem) => { + try { + // Fetch full step to get content fields + const full = await stepsApi.get(step.id) + await stepsApi.create({ + title: full.title, + step_type: full.step_type, + content: full.content, + visibility: 'private', + category_id: full.category_id, + tags: full.tags, + }) + setSaveToast(`"${full.title}" saved to My Steps`) + setTimeout(() => setSaveToast(null), 3000) + refresh() + } catch (err) { + console.error('Failed to save step:', err) + } + } + + const handleFormSuccess = (_step: Step) => { + setCreateOpen(false) + setEditingStep(null) + refresh() + } + + return ( +
+ {/* Page Header */} +
+
+ + + +
+

Step Library

+

Reusable steps you can insert into any flow

+
+
+ {canCreateSteps && ( + + )} +
+ + {/* Browser fills remaining height */} +
+ { + // We need the full StepListItem for the confirmation title. + // Pass a minimal object; the title will show as "this step" if not available. + setDeletingStep({ id: stepId, title: '' } as StepListItem) + }} + onSave={handleSave} + currentUserId={user?.id} + refreshKey={refreshKey} + showCreateButton={false} + /> +
+ + {/* Create / Edit Modal */} + { setCreateOpen(false); setEditingStep(null) }} + onSuccess={handleFormSuccess} + editingStep={editingStep} + /> + + {/* Delete Confirmation Dialog */} + {deletingStep && ( +
+
+
+
+ +
+

Delete Step

+
+

+ {deletingStep.title + ? <>Are you sure you want to delete "{deletingStep.title}"? + : 'Are you sure you want to delete this step?' + } +

+

This cannot be undone.

+ {deleteError && ( +

{deleteError}

+ )} +
+ + +
+
+
+ )} + + {/* Save Toast */} + {saveToast && ( +
+ {saveToast} +
+ )} +
+ ) +} +``` + +**NOTE on delete title:** The `onDelete` callback from `StepCard` only passes `stepId: string`, not the full `StepListItem`. To show the step title in the confirmation dialog, change the `StepLibraryBrowser`'s `onDelete` prop type to pass the full `StepListItem` instead: + +In `StepLibraryBrowser.tsx`, change: +```ts +onDelete?: (stepId: string) => void +``` +to: +```ts +onDelete?: (step: StepListItem) => void +``` + +And update where it calls `onDelete` from cards — pass the full `step` object. Update `StepCard` similarly: change `onDelete?: (stepId: string) => void` to `onDelete?: (step: StepListItem) => void` and call `onDelete?.(step)` instead of `onDelete?.(step.id)`. + +Then in `StepLibraryPage`, use `handleDeleteRequest(step: StepListItem)` and set `setDeletingStep(step)` directly — no need to pass a minimal object. + +**Step 2: Run TypeScript check** + +```bash +cd /home/michaelchihlas/dev/patherly/frontend && npx tsc --noEmit 2>&1 | head -40 +``` + +Fix any type errors before proceeding. + +**Step 3: Run build** + +```bash +cd /home/michaelchihlas/dev/patherly/frontend && npm run build 2>&1 | tail -20 +``` + +Expected: build succeeds with no errors. + +**Step 4: Commit** + +```bash +cd /home/michaelchihlas/dev/patherly +git add frontend/src/pages/StepLibraryPage.tsx \ + frontend/src/components/step-library/StepLibraryBrowser.tsx \ + frontend/src/components/step-library/StepCard.tsx +git commit -m "feat: Step Library page — create, edit, delete, save-to-library + +Co-Authored-By: Claude Sonnet 4.6 " +``` + +--- + +## Task 5: Manual verification checklist + +Start the dev server and verify these flows work end-to-end: + +```bash +docker start patherly_postgres +cd /home/michaelchihlas/dev/patherly/backend && source venv/bin/activate && uvicorn app.main:app --reload & +cd /home/michaelchihlas/dev/patherly/frontend && npm run dev +``` + +Navigate to `http://localhost:5173/step-library` and verify: + +- [ ] Page loads without errors (not "Coming Soon") +- [ ] "+ Create Step" button appears (login as engineer or admin) +- [ ] Creating a step via the modal saves it and it appears under "My Steps" on reload +- [ ] "Edit" button appears on your own step cards +- [ ] Editing a step opens the form pre-filled (note: `content` fields won't pre-fill since `StepListItem` doesn't have content — this is acceptable for now; see note below) +- [ ] "Delete" button appears on your own step cards +- [ ] Delete confirmation shows step title; confirming removes it from the list +- [ ] "Save" button appears on team/community step cards +- [ ] Saving a step copies it to "My Steps" and shows toast +- [ ] "Preview" opens `StepDetailModal` correctly on all card types +- [ ] Filters (category, type, rating, sort) work +- [ ] Popular tags clickable and filter results + +**Note on edit pre-fill:** `StepListItem` does not include `content`. The `StepFormModal` passes `initialData` from `editingStep`, but `content` will be missing. For a full pre-fill, `StepLibraryPage.handleEdit` should fetch the full step via `stepsApi.get(step.id)` before opening the modal, and store the result as a `Step` (not `StepListItem`) in `editingStep` state. Update `editingStep` state type to `Step | null` and fetch in `handleEdit`: + +```tsx +const [editingStep, setEditingStep] = useState(null) + +const handleEdit = async (step: StepListItem) => { + try { + const full = await stepsApi.get(step.id) + setEditingStep(full) + } catch (err) { + console.error('Failed to load step for edit:', err) + } +} +``` + +Update `StepFormModal`'s `editingStep` prop type to accept `Step | null` and build `initialData` from the full `Step` including `content`: + +```tsx +editingStep?: Step | null + +const initialData = editingStep ? { + title: editingStep.title, + step_type: editingStep.step_type, + content: editingStep.content, + visibility: editingStep.visibility, + category_id: editingStep.category_id, + tags: editingStep.tags, +} : undefined +``` + +This should be done as part of Task 4 before verifying. + +--- + +## Task 6: Final build validation and commit + +```bash +cd /home/michaelchihlas/dev/patherly/frontend && npm run build 2>&1 | tail -20 +``` + +Expected: clean build, no TypeScript errors, no warnings about missing exports. + +If clean: +```bash +cd /home/michaelchihlas/dev/patherly +git add -A +git status # confirm only expected files changed +git commit -m "chore: step library page final build validation + +Co-Authored-By: Claude Sonnet 4.6 " +```