Merge branch 'main' into feat/l1-workspace
Some checks failed
Mirror to GitHub / mirror (push) Successful in 4s
CI / frontend (pull_request) Failing after 1m52s
CI / e2e (pull_request) Failing after 6m6s
CI / backend (pull_request) Successful in 12m15s

# Conflicts:
#	frontend/src/router.tsx
This commit is contained in:
2026-05-29 00:24:54 -04:00
43 changed files with 1246 additions and 412 deletions

View File

@@ -7,6 +7,7 @@ import { RouteError } from '@/components/common/RouteError'
import { ErrorBoundary } from '@/components/common/ErrorBoundary'
import { PageLoader } from '@/components/common/PageLoader'
import { lazyWithRetry } from '@/lib/lazyWithRetry'
import { useAuthStore } from '@/store/authStore'
import { L1RouteGuard } from '@/components/layout/L1RouteGuard'
const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV7(createBrowserRouter)
@@ -125,10 +126,27 @@ function page(Component: React.LazyExoticComponent<React.ComponentType>) {
)
}
/**
* Public `/` wrapper — sends authenticated users to /home before LandingPage
* mounts, so they never see marketing-frame flicker.
*/
// eslint-disable-next-line react-refresh/only-export-components -- router.tsx exports a router instance, not a component
function PublicLanding() {
const isAuthed = useAuthStore((s) => s.isAuthenticated)
if (isAuthed) return <Navigate to="/home" replace />
return page(LandingPage)
}
export const router = sentryCreateBrowserRouter([
{
path: '/',
element: <PublicLanding />,
errorElement: <RouteError />,
},
// Stale-bookmark redirect — keep one release, delete in a follow-up.
{
path: '/landing',
element: page(LandingPage),
element: <Navigate to="/" replace />,
errorElement: <RouteError />,
},
{
@@ -236,7 +254,6 @@ export const router = sentryCreateBrowserRouter([
errorElement: <RouteError />,
},
{
path: '/',
element: (
<ProtectedRoute>
<AppLayout />
@@ -244,61 +261,61 @@ export const router = sentryCreateBrowserRouter([
),
errorElement: <RouteError />,
children: [
{ index: true, element: page(QuickStartPage) },
{ path: 'trees', element: page(TreeLibraryPage) },
{ path: 'my-trees', element: page(MyTreesPage) },
{ path: 'trees/new', element: page(TreeEditorPage) },
{ path: 'trees/:id/edit', element: page(TreeEditorPage) },
{ path: 'flows/new', element: page(ProceduralEditorPage) },
{ path: 'flows/:id/edit', element: page(ProceduralEditorPage) },
{ path: 'flows/:id/navigate', element: page(ProceduralNavigationPage) },
{ path: 'flows/:id/maintenance', element: page(MaintenanceFlowDetailPage) },
{ path: 'flows/:id/batches/:batchId', element: page(BatchStatusPage) },
{ path: 'trees/:id/navigate', element: page(TreeNavigationPage) },
{ path: 'sessions', element: page(SessionHistoryPage) },
{ path: 'sessions/:id', element: page(SessionDetailPage) },
{ path: 'tickets', element: page(TicketsPage) },
{ path: 'shares', element: page(MySharesPage) },
{ path: 'analytics', element: page(TeamAnalyticsPage) },
{ path: 'analytics/me', element: page(MyAnalyticsPage) },
{ path: 'feedback', element: page(FeedbackPage) },
{ path: 'step-library', element: page(StepLibraryPage) },
{ path: 'scripts', element: page(ScriptLibraryPage) },
{ path: 'scripts/manage', element: page(ScriptManagePage) },
{ path: 'script-builder', element: page(ScriptBuilderPage) },
{ path: 'network-diagrams', element: page(NetworkDiagramsPage) },
{ path: 'network-diagrams/new', element: page(DiagramEditorPage) },
{ path: 'network-diagrams/:id', element: page(DiagramEditorPage) },
{ path: 'kb-accelerator', element: page(KBAcceleratorPage) },
{ path: '/home', element: page(QuickStartPage) },
{ path: '/trees', element: page(TreeLibraryPage) },
{ path: '/my-trees', element: page(MyTreesPage) },
{ path: '/trees/new', element: page(TreeEditorPage) },
{ path: '/trees/:id/edit', element: page(TreeEditorPage) },
{ path: '/flows/new', element: page(ProceduralEditorPage) },
{ path: '/flows/:id/edit', element: page(ProceduralEditorPage) },
{ path: '/flows/:id/navigate', element: page(ProceduralNavigationPage) },
{ path: '/flows/:id/maintenance', element: page(MaintenanceFlowDetailPage) },
{ path: '/flows/:id/batches/:batchId', element: page(BatchStatusPage) },
{ path: '/trees/:id/navigate', element: page(TreeNavigationPage) },
{ path: '/sessions', element: page(SessionHistoryPage) },
{ path: '/sessions/:id', element: page(SessionDetailPage) },
{ path: '/tickets', element: page(TicketsPage) },
{ path: '/shares', element: page(MySharesPage) },
{ path: '/analytics', element: page(TeamAnalyticsPage) },
{ path: '/analytics/me', element: page(MyAnalyticsPage) },
{ path: '/feedback', element: page(FeedbackPage) },
{ path: '/step-library', element: page(StepLibraryPage) },
{ path: '/scripts', element: page(ScriptLibraryPage) },
{ path: '/scripts/manage', element: page(ScriptManagePage) },
{ path: '/script-builder', element: page(ScriptBuilderPage) },
{ path: '/network-diagrams', element: page(NetworkDiagramsPage) },
{ path: '/network-diagrams/new', element: page(DiagramEditorPage) },
{ path: '/network-diagrams/:id', element: page(DiagramEditorPage) },
{ path: '/kb-accelerator', element: page(KBAcceleratorPage) },
// Phase 1 — FlowPilot migration. The unified chat-primary surface lives at
// /pilot; /assistant permanently redirects. FlowPilotSessionPage (old
// guided surface) is no longer mounted.
{ path: 'pilot', element: page(AssistantChatPage) },
{ path: 'pilot/:sessionId', element: page(AssistantChatPage) },
{ path: 'assistant', element: <Navigate to="/pilot" replace /> },
{ path: 'assistant/:sessionId', element: <AssistantSessionRedirect /> },
{ path: 'flow-assist', element: page(FlowAssistPage) },
{ path: 'escalations', element: page(EscalationQueuePage) },
{ path: 'queue', element: page(SessionQueuePage) },
{ path: 'review-queue', element: page(ReviewQueuePage) },
{ path: 'analytics/flowpilot', element: page(FlowPilotAnalyticsPage) },
{ path: 'dev/branching', element: page(DevBranchingPage) },
{ path: 'guides', element: page(GuidesHubPage) },
{ path: 'guides/:slug', element: page(GuideDetailPage) },
{ path: '/pilot', element: page(AssistantChatPage) },
{ path: '/pilot/:sessionId', element: page(AssistantChatPage) },
{ path: '/assistant', element: <Navigate to="/pilot" replace /> },
{ path: '/assistant/:sessionId', element: <AssistantSessionRedirect /> },
{ path: '/flow-assist', element: page(FlowAssistPage) },
{ path: '/escalations', element: page(EscalationQueuePage) },
{ path: '/queue', element: page(SessionQueuePage) },
{ path: '/review-queue', element: page(ReviewQueuePage) },
{ path: '/analytics/flowpilot', element: page(FlowPilotAnalyticsPage) },
{ path: '/dev/branching', element: page(DevBranchingPage) },
{ path: '/guides', element: page(GuidesHubPage) },
{ path: '/guides/:slug', element: page(GuideDetailPage) },
// Welcome wizard (Phase 2). Mounted inside AppLayout so the email-
// verification banner persists above each step.
{ path: 'welcome', element: page(WelcomeRouter) },
{ path: 'welcome/step-1', element: page(WelcomeStep1) },
{ path: 'welcome/step-2', element: page(WelcomeStep2) },
{ path: 'welcome/step-3', element: page(WelcomeStep3) },
{ path: '/welcome', element: page(WelcomeRouter) },
{ path: '/welcome/step-1', element: page(WelcomeStep1) },
{ path: '/welcome/step-2', element: page(WelcomeStep2) },
{ path: '/welcome/step-3', element: page(WelcomeStep3) },
// L1 workspace routes — gated by canUseL1Surface
{ path: 'l1', element: <L1RouteGuard>{page(L1Dashboard)}</L1RouteGuard> },
{ path: 'l1/walk/:sessionId', element: <L1RouteGuard>{page(L1WalkPage)}</L1RouteGuard> },
{ path: 'l1/drafts', element: <L1RouteGuard>{page(L1DraftsPage)}</L1RouteGuard> },
{ path: 'l1/tickets', element: <L1RouteGuard>{page(L1TicketsPage)}</L1RouteGuard> },
{ path: '/l1', element: <L1RouteGuard>{page(L1Dashboard)}</L1RouteGuard> },
{ path: '/l1/walk/:sessionId', element: <L1RouteGuard>{page(L1WalkPage)}</L1RouteGuard> },
{ path: '/l1/drafts', element: <L1RouteGuard>{page(L1DraftsPage)}</L1RouteGuard> },
{ path: '/l1/tickets', element: <L1RouteGuard>{page(L1TicketsPage)}</L1RouteGuard> },
// Admin routes
{
path: 'admin',
path: '/admin',
element: (
<ErrorBoundary>
<Suspense fallback={<PageLoader />}>
@@ -327,7 +344,7 @@ export const router = sentryCreateBrowserRouter([
},
// Account routes
{
path: 'account',
path: '/account',
element: (
<ErrorBoundary>
<Suspense fallback={<PageLoader />}>