diff --git a/frontend/src/components/flowpilot/ProposalDetail.tsx b/frontend/src/components/flowpilot/ProposalDetail.tsx index 358310fd..530c9ae1 100644 --- a/frontend/src/components/flowpilot/ProposalDetail.tsx +++ b/frontend/src/components/flowpilot/ProposalDetail.tsx @@ -88,18 +88,35 @@ export function ProposalDetail({ proposal, onReview }: ProposalDetailProps) { {/* Content */}
- {/* Source session link */} -
-

Source Session

- - - View session that generated this proposal - -
+ {/* Source — exactly one of a FlowPilot session XOR an L1 walk is set + (DB CHECK). Never link to /pilot for an L1-sourced proposal: + source_session_id is NULL there, so the old unconditional link + rendered a broken /pilot/null. */} + {proposal.source_session_id ? ( +
+

Source Session

+ + + View session that generated this proposal + +
+ ) : proposal.l1_session_id ? ( +
+

Source — L1 AI walkthrough

+

+ Captured from an L1 technician's AI-guided walk and validated by a + successful resolution. The proposed flow is the path that resolved the ticket. +

+

+ + L1 session {proposal.l1_session_id.slice(0, 8)} +

+
+ ) : null} {/* Proposed diff (for enhancements) */} {proposal.proposed_diff && (() => { diff --git a/frontend/src/components/l1/L1EscalationsSection.tsx b/frontend/src/components/l1/L1EscalationsSection.tsx index 5640a7e8..97a7ee1f 100644 --- a/frontend/src/components/l1/L1EscalationsSection.tsx +++ b/frontend/src/components/l1/L1EscalationsSection.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react' import { l1Api } from '@/api/l1' +import { timeAgo } from '@/lib/timeAgo' import type { WalkSession } from '@/types/l1' /** @@ -43,11 +44,13 @@ export function L1EscalationsSection() {
#{s.id.slice(0, 8)} - {s.walked_path.length} step{s.walked_path.length === 1 ? '' : 's'} walked + {s.problem_text + ? s.problem_text + : `${s.walked_path.length} step${s.walked_path.length === 1 ? '' : 's'} walked`}
- {new Date(s.last_step_at).toLocaleString()} + {timeAgo(s.last_step_at)} {isOpen && ( @@ -58,7 +61,7 @@ export function L1EscalationsSection() {
    {s.walked_path.map((step, i) => (
  1. - {step.question} + {step.question ?? step.text} {step.answer && ( → {step.answer} )} diff --git a/frontend/src/components/l1/L1WalkTreeVariant.tsx b/frontend/src/components/l1/L1WalkTreeVariant.tsx index 7d464284..e887cea8 100644 --- a/frontend/src/components/l1/L1WalkTreeVariant.tsx +++ b/frontend/src/components/l1/L1WalkTreeVariant.tsx @@ -44,7 +44,7 @@ export function L1WalkTreeVariant({ session, onSessionUpdate, onDone }: Props) { }, [isAiBuild, session.id, session.status]) const advanceNode = useCallback( - async (body: { answer?: 'yes' | 'no'; acknowledged?: boolean }) => { + async (body: { answer?: 'yes' | 'no' }) => { if (!node) return setNodeLoading(true) setNodeError(null) @@ -183,7 +183,7 @@ export function L1WalkTreeVariant({ session, onSessionUpdate, onDone }: Props) { <>

    {node.text}

    + diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 61f07f00..c92bbbce 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -367,7 +367,7 @@ export const router = sentryCreateBrowserRouter([ { path: 'l1-categories', element: ( - + {page(L1CategoriesPage)} ), diff --git a/frontend/src/types/l1.ts b/frontend/src/types/l1.ts index ea74a577..69fa25e5 100644 --- a/frontend/src/types/l1.ts +++ b/frontend/src/types/l1.ts @@ -3,9 +3,15 @@ export type SessionStatus = 'active' | 'resolved' | 'escalated' | 'abandoned' export type TicketKind = 'psa' | 'internal' export interface WalkStep { - node_id: string - question: string - answer: string + // Two shapes coexist (segregated by session_kind): legacy flow/adhoc steps use + // node_id + question; ai_build steps use id + node_type + text. Render with + // `step.question ?? step.text`. + node_id?: string + id?: string + node_type?: string + question?: string + text?: string + answer: string | null l1_note: string | null } @@ -17,6 +23,8 @@ export interface AdhocNote { export interface WalkSession { id: string session_kind: SessionKind + category: string | null + problem_text: string | null flow_id: string | null flow_proposal_id: string | null current_node_id: string | null @@ -42,10 +50,11 @@ export interface IntakeRequest { customer_name?: string customer_contact?: string flow_id?: string + adhoc?: boolean force_build?: boolean } -export type IntakeOutcome = 'matched' | 'suggest' | 'out_of_scope' | 'build' +export type IntakeOutcome = 'matched' | 'suggest' | 'out_of_scope' | 'build' | 'adhoc' export interface NearMiss { flow_id: string @@ -77,8 +86,7 @@ export type TreeNode = export interface NextNodeRequest { node_id?: string node_text?: string // rendered text of the node being answered - answer?: 'yes' | 'no' - acknowledged?: boolean + answer?: 'yes' | 'no' // omit to acknowledge an instruction node note?: string } diff --git a/frontend/src/types/user.ts b/frontend/src/types/user.ts index df756e4e..e26e510a 100644 --- a/frontend/src/types/user.ts +++ b/frontend/src/types/user.ts @@ -9,7 +9,7 @@ export interface User { is_active: boolean must_change_password: boolean account_id: string | null - account_role: 'owner' | 'engineer' | 'l1_tech' | 'viewer' | null + account_role: 'owner' | 'admin' | 'engineer' | 'l1_tech' | 'viewer' | null can_cover_l1: boolean team_id: string | null created_at: string