fix: add type-aware routing for procedural flows

Centralizes tree navigation routing via getTreeNavigatePath helper.
Fixes all pages to route procedural sessions to /flows/:id/navigate
instead of /trees/:id/navigate. Adds safety redirect in troubleshooting
navigator and resume support in procedural navigator.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-02-14 22:04:29 -05:00
parent 505b1c8246
commit 60e52763a7
8 changed files with 101 additions and 10 deletions

View File

@@ -1,5 +1,5 @@
import { useEffect, useState, useRef } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { useParams, useNavigate, useLocation } from 'react-router-dom'
import { ChevronLeft, ChevronRight, ListOrdered, Settings2, X } from 'lucide-react'
import { treesApi } from '@/api/trees'
import { sessionsApi } from '@/api/sessions'
@@ -21,6 +21,8 @@ interface StepState {
export function ProceduralNavigationPage() {
const { id: treeId } = useParams<{ id: string }>()
const navigate = useNavigate()
const location = useLocation()
const locationState = location.state as { sessionId?: string } | undefined
const [tree, setTree] = useState<Tree | null>(null)
const [session, setSession] = useState<Session | null>(null)
@@ -98,6 +100,12 @@ export function ProceduralNavigationPage() {
}
setTree(treeData)
// If resuming an existing session
if (locationState?.sessionId) {
await resumeSession(treeData, locationState.sessionId)
return
}
// Check if intake form exists
if (treeData.intake_form && treeData.intake_form.length > 0) {
setShowIntakeForm(true)
@@ -134,6 +142,42 @@ export function ProceduralNavigationPage() {
}
}
const resumeSession = async (treeData: Tree, sessionId: string) => {
try {
const sessionData = await sessionsApi.get(sessionId)
setSession(sessionData)
setSessionVariables(sessionData.session_variables || {})
setShowIntakeForm(false)
// Initialize step states from session decisions
const allSteps = getStepsFromTree(treeData)
const initialStates = new Map<string, StepState>()
for (const step of allSteps) {
initialStates.set(step.id, { notes: '', verificationValue: '', completedAt: null })
}
// Hydrate completed steps from decisions
for (const decision of sessionData.decisions || []) {
if (decision.answer === 'completed' && initialStates.has(decision.node_id)) {
initialStates.set(decision.node_id, {
notes: decision.notes || '',
verificationValue: decision.command_output || '',
completedAt: decision.exited_at || decision.timestamp,
})
}
}
setStepStates(initialStates)
// Set current step to first incomplete step
const pSteps = allSteps.filter((s) => s.type === 'procedure_step')
const firstIncomplete = pSteps.findIndex((s) => !initialStates.get(s.id)?.completedAt)
setCurrentStepIndex(firstIncomplete >= 0 ? firstIncomplete : pSteps.length - 1)
} catch {
toast.error('Failed to resume session')
navigate('/my-trees')
}
}
const getStepsFromTree = (t: Tree): ProceduralStep[] => {
const structure = t.tree_structure as unknown as { steps?: ProceduralStep[] }
return structure.steps || []