From 60e52763a7ff8abaf59bc21d534c0664acf0905d Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Sat, 14 Feb 2026 22:04:29 -0500 Subject: [PATCH] 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 --- backend/app/schemas/tree.py | 2 +- frontend/src/lib/routing.ts | 31 +++++++++++++ .../src/pages/ProceduralNavigationPage.tsx | 46 ++++++++++++++++++- frontend/src/pages/QuickStartPage.tsx | 14 +++--- frontend/src/pages/SessionHistoryPage.tsx | 3 +- frontend/src/pages/TreeLibraryPage.tsx | 3 +- frontend/src/pages/TreeNavigationPage.tsx | 10 ++++ frontend/src/types/session.ts | 2 + 8 files changed, 101 insertions(+), 10 deletions(-) create mode 100644 frontend/src/lib/routing.ts diff --git a/backend/app/schemas/tree.py b/backend/app/schemas/tree.py index 11e6b746..0da6c79b 100644 --- a/backend/app/schemas/tree.py +++ b/backend/app/schemas/tree.py @@ -12,7 +12,7 @@ TreeType = Literal['troubleshooting', 'procedural'] # --- Intake Form Schemas --- FIELD_TYPES = Literal[ - 'text', 'textarea', 'number', 'ip_address', 'email', + 'text', 'textarea', 'number', 'ip_address', 'email', 'url', 'select', 'multi_select', 'checkbox', 'password' ] diff --git a/frontend/src/lib/routing.ts b/frontend/src/lib/routing.ts new file mode 100644 index 00000000..42f73e5a --- /dev/null +++ b/frontend/src/lib/routing.ts @@ -0,0 +1,31 @@ +/** + * Shared routing helpers for tree/session navigation. + * Centralizes the logic for determining the correct navigation path + * based on tree type (troubleshooting vs procedural). + */ + +/** + * Get the navigation path for starting or resuming a tree/session. + */ +export function getTreeNavigatePath( + treeId: string, + treeType?: string +): string { + if (treeType === 'procedural') { + return `/flows/${treeId}/navigate` + } + return `/trees/${treeId}/navigate` +} + +/** + * Get the editor path for a tree. + */ +export function getTreeEditorPath( + treeId: string, + treeType?: string +): string { + if (treeType === 'procedural') { + return `/flows/${treeId}/edit` + } + return `/trees/${treeId}/edit` +} diff --git a/frontend/src/pages/ProceduralNavigationPage.tsx b/frontend/src/pages/ProceduralNavigationPage.tsx index 420753e9..754ec68e 100644 --- a/frontend/src/pages/ProceduralNavigationPage.tsx +++ b/frontend/src/pages/ProceduralNavigationPage.tsx @@ -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(null) const [session, setSession] = useState(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() + 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 || [] diff --git a/frontend/src/pages/QuickStartPage.tsx b/frontend/src/pages/QuickStartPage.tsx index b930e88d..e7dcd751 100644 --- a/frontend/src/pages/QuickStartPage.tsx +++ b/frontend/src/pages/QuickStartPage.tsx @@ -5,6 +5,7 @@ import { treesApi } from '@/api/trees' import { sessionsApi } from '@/api/sessions' import type { TreeListItem } from '@/types' import type { Session } from '@/types/session' +import { getTreeNavigatePath } from '@/lib/routing' function timeAgo(dateStr: string): string { @@ -27,7 +28,7 @@ export function QuickStartPage() { const [isSearching, setIsSearching] = useState(false) const [showResults, setShowResults] = useState(false) const [activeSessions, setActiveSessions] = useState([]) - const [recentTrees, setRecentTrees] = useState<{ tree_id: string; name: string; lastUsed: string }[]>([]) + const [recentTrees, setRecentTrees] = useState<{ tree_id: string; name: string; lastUsed: string; tree_type?: string }[]>([]) const [isLoading, setIsLoading] = useState(true) const searchRef = useRef(null) const debounceRef = useRef | null>(null) @@ -44,7 +45,7 @@ export function QuickStartPage() { // Deduplicate recent sessions by tree_id, max 5 const seen = new Set() - const deduped: { tree_id: string; name: string; lastUsed: string }[] = [] + const deduped: { tree_id: string; name: string; lastUsed: string; tree_type?: string }[] = [] for (const s of recent) { if (!seen.has(s.tree_id) && deduped.length < 5) { seen.add(s.tree_id) @@ -52,6 +53,7 @@ export function QuickStartPage() { tree_id: s.tree_id, name: s.tree_snapshot?.name || 'Unnamed Tree', lastUsed: s.started_at, + tree_type: s.tree_snapshot?.tree_type, }) } } @@ -164,7 +166,7 @@ export function QuickStartPage() { {searchResults.map((tree) => (
  • {!session.completed_at && (