feat(l1): proposal L1-source block + engineer L1-escalations section
Some checks failed
Mirror to GitHub / mirror (push) Successful in 4s
CI / frontend (pull_request) Successful in 6m59s
CI / e2e (pull_request) Failing after 5m13s
CI / backend (pull_request) Successful in 12m39s

- flow-proposal.ts: source_session_id nullable + add l1_session_id (matches backend
  FlowProposalSummary).
- ProposalDetail.tsx: render an 'AI L1 walk (outcome-validated)' note when
  l1_session_id is set instead of the /pilot/{source_session_id} link; fall back to
  the link for ai_session-sourced proposals.
- New L1EscalationsSection.tsx (GET /l1/escalations) — expandable rows with walked-path
  summary; renders nothing if empty. Mounted below the FlowPilot queue on
  EscalationQueuePage. tsc -b + eslint clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-30 20:48:30 -04:00
parent 1b7aedb204
commit 8ce6bc80fa
2 changed files with 81 additions and 1 deletions

View File

@@ -0,0 +1,77 @@
import { useEffect, useState } from 'react'
import { l1Api } from '@/api/l1'
import type { WalkSession } from '@/types/l1'
/**
* Engineer-visible list of escalated L1 sessions (the Phase 2A handoff queue).
* Backed by GET /l1/escalations (engineer-or-above). Pollable, dependency-free —
* each row expands to show the walked path summary. Renders nothing if empty.
*/
export function L1EscalationsSection() {
const [rows, setRows] = useState<WalkSession[]>([])
const [loaded, setLoaded] = useState(false)
const [expanded, setExpanded] = useState<string | null>(null)
useEffect(() => {
l1Api
.escalations()
.then(setRows)
.catch(() => setRows([]))
.finally(() => setLoaded(true))
}, [])
if (!loaded || rows.length === 0) return null
return (
<section className="space-y-3">
<div>
<h2 className="font-heading text-lg font-bold text-heading">L1 escalations</h2>
<p className="text-sm text-text-muted">
Tickets an L1 tech escalated mid-walk pick one up to continue.
</p>
</div>
<div className="rounded-lg border border-default bg-card overflow-hidden">
{rows.map((s) => {
const isOpen = expanded === s.id
return (
<div key={s.id} className="border-b border-default last:border-b-0">
<button
type="button"
onClick={() => setExpanded(isOpen ? null : s.id)}
className="w-full px-4 py-3 flex items-center justify-between text-left hover:bg-elevated transition-colors"
>
<div className="flex items-center gap-3 min-w-0">
<span className="font-mono text-xs text-text-muted">#{s.id.slice(0, 8)}</span>
<span className="text-sm text-text-primary truncate">
{s.walked_path.length} step{s.walked_path.length === 1 ? '' : 's'} walked
</span>
</div>
<span className="text-xs text-text-muted whitespace-nowrap">
{new Date(s.last_step_at).toLocaleString()}
</span>
</button>
{isOpen && (
<div className="px-4 pb-3 space-y-1.5">
{s.walked_path.length === 0 ? (
<p className="text-xs text-text-muted">No steps recorded.</p>
) : (
<ol className="space-y-1.5 text-sm">
{s.walked_path.map((step, i) => (
<li key={i} className="flex flex-col">
<span className="text-text-muted text-xs">{step.question}</span>
{step.answer && (
<span className="font-medium text-text-primary"> {step.answer}</span>
)}
</li>
))}
</ol>
)}
</div>
)}
</div>
)
})}
</div>
</section>
)
}

View File

@@ -10,7 +10,10 @@ export interface FlowProposalSummary {
supporting_session_count: number
status: 'pending' | 'approved' | 'modified' | 'rejected' | 'dismissed' | 'auto_reinforced'
target_flow_id: string | null
source_session_id: string
// Exactly one source is set: source_session_id (FlowPilot ai_session) XOR
// l1_session_id (L1 ai_build walk). Both nullable on the backend (Phase 2A).
source_session_id: string | null
l1_session_id: string | null
created_at: string
}