Files
resolutionflow/frontend/src/router.tsx
Michael Chihlas 4c83cebfca
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
Merge branch 'main' into feat/l1-workspace
# Conflicts:
#	frontend/src/router.tsx
2026-05-29 00:24:54 -04:00

408 lines
18 KiB
TypeScript

import { createBrowserRouter, Navigate } from 'react-router-dom'
import { AssistantSessionRedirect } from '@/components/routing/AssistantSessionRedirect'
import * as Sentry from '@sentry/react'
import { Suspense } from 'react'
import { AppLayout, ProtectedRoute } from '@/components/layout'
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)
import {
LoginPage,
RegisterPage,
} from '@/pages'
// Public pages
const LandingPage = lazyWithRetry(() => import('@/pages/LandingPage'))
const PublicTemplatesPage = lazyWithRetry(() => import('@/pages/PublicTemplatesPage'))
const SharedSessionPage = lazyWithRetry(() => import('@/pages/SharedSessionPage'))
const SurveyPage = lazyWithRetry(() => import('@/pages/SurveyPage'))
const SurveyThankYouPage = lazyWithRetry(() => import('@/pages/SurveyThankYouPage'))
const PrivacyPage = lazyWithRetry(() => import('@/pages/PrivacyPage'))
const TermsPage = lazyWithRetry(() => import('@/pages/TermsPage'))
const PricingPage = lazyWithRetry(() => import('@/pages/PricingPage'))
const ContactSalesPage = lazyWithRetry(() => import('@/pages/ContactSalesPage'))
const ContactPage = lazyWithRetry(() => import('@/pages/ContactPage'))
const PoliciesPage = lazyWithRetry(() => import('@/pages/PoliciesPage'))
const PromotionsPage = lazyWithRetry(() => import('@/pages/PromotionsPage'))
// Standalone auth pages
const VerifyEmailPage = lazyWithRetry(() => import('@/pages/VerifyEmailPage'))
const OAuthCallbackPage = lazyWithRetry(() => import('@/pages/OAuthCallbackPage'))
const AcceptInvitePage = lazyWithRetry(() => import('@/pages/AcceptInvitePage'))
const ChangePasswordPage = lazyWithRetry(() => import('@/pages/ChangePasswordPage'))
const ForgotPasswordPage = lazyWithRetry(() => import('@/pages/ForgotPasswordPage'))
const ResetPasswordPage = lazyWithRetry(() => import('@/pages/ResetPasswordPage'))
// Lazy load heavy pages for code splitting
const QuickStartPage = lazyWithRetry(() => import('@/pages/QuickStartPage'))
const TreeLibraryPage = lazyWithRetry(() => import('@/pages/TreeLibraryPage'))
const MyTreesPage = lazyWithRetry(() => import('@/pages/MyTreesPage'))
const TreeNavigationPage = lazyWithRetry(() => import('@/pages/TreeNavigationPage'))
const TreeEditorPage = lazyWithRetry(() => import('@/pages/TreeEditorPage'))
const ProceduralEditorPage = lazyWithRetry(() => import('@/pages/ProceduralEditorPage'))
const ProceduralNavigationPage = lazyWithRetry(() => import('@/pages/ProceduralNavigationPage'))
const MaintenanceFlowDetailPage = lazyWithRetry(() => import('@/pages/MaintenanceFlowDetailPage'))
const BatchStatusPage = lazyWithRetry(() => import('@/pages/BatchStatusPage'))
const SessionHistoryPage = lazyWithRetry(() => import('@/pages/SessionHistoryPage'))
const SessionDetailPage = lazyWithRetry(() => import('@/pages/SessionDetailPage'))
const MySharesPage = lazyWithRetry(() => import('@/pages/MySharesPage'))
const TeamAnalyticsPage = lazyWithRetry(() => import('@/pages/TeamAnalyticsPage'))
const MyAnalyticsPage = lazyWithRetry(() => import('@/pages/MyAnalyticsPage'))
const FeedbackPage = lazyWithRetry(() => import('@/pages/FeedbackPage'))
const StepLibraryPage = lazyWithRetry(() => import('@/pages/StepLibraryPage'))
const ScriptLibraryPage = lazyWithRetry(() => import('@/pages/ScriptLibraryPage'))
const ScriptManagePage = lazyWithRetry(() => import('@/pages/ScriptManagePage'))
const AssistantChatPage = lazyWithRetry(() => import('@/pages/AssistantChatPage'))
const FlowAssistPage = lazyWithRetry(() => import('@/pages/FlowAssistPage'))
// FlowPilotSessionPage (the old guided-mode surface) was removed from active
// routing in Phase 1 of the FlowPilot migration. The file is retained on disk
// for reference but not mounted — the unified chat-primary surface now serves
// /pilot. Delete the file when nothing in the tree references it anymore.
const EscalationQueuePage = lazyWithRetry(() => import('@/pages/EscalationQueuePage'))
const ReviewQueuePage = lazyWithRetry(() => import('@/pages/ReviewQueuePage'))
const FlowPilotAnalyticsPage = lazyWithRetry(() => import('@/pages/FlowPilotAnalyticsPage'))
const ScriptBuilderPage = lazyWithRetry(() => import('@/pages/ScriptBuilderPage'))
const KBAcceleratorPage = lazyWithRetry(() => import('@/pages/KBAcceleratorPage'))
const SessionQueuePage = lazyWithRetry(() => import('@/pages/SessionQueuePage'))
const TicketsPage = lazyWithRetry(() => import('@/pages/TicketsPage'))
const DevBranchingPage = lazyWithRetry(() => import('@/pages/DevBranchingPage'))
const GuidesHubPage = lazyWithRetry(() => import('@/pages/GuidesHubPage'))
const GuideDetailPage = lazyWithRetry(() => import('@/pages/GuideDetailPage'))
const AccountSettingsPage = lazyWithRetry(() => import('@/pages/AccountSettingsPage'))
// Welcome wizard (Phase 2)
const WelcomeRouter = lazyWithRetry(() => import('@/pages/welcome/WelcomeRouter'))
const WelcomeStep1 = lazyWithRetry(() => import('@/pages/welcome/WelcomeStep1'))
const WelcomeStep2 = lazyWithRetry(() => import('@/pages/welcome/WelcomeStep2'))
const WelcomeStep3 = lazyWithRetry(() => import('@/pages/welcome/WelcomeStep3'))
const NetworkDiagramsPage = lazyWithRetry(() => import('@/pages/NetworkDiagrams'))
const DiagramEditorPage = lazyWithRetry(() => import('@/pages/NetworkDiagrams/DiagramEditor'))
// Admin pages
const AdminLayout = lazyWithRetry(() => import('@/components/admin/AdminLayout'))
const AdminDashboardPage = lazyWithRetry(() => import('@/pages/admin/DashboardPage'))
const AdminAccountsPage = lazyWithRetry(() => import('@/pages/admin/AccountsPage'))
const AdminAccountDetailPage = lazyWithRetry(() => import('@/pages/admin/AccountDetailPage'))
const AdminUserDetailPage = lazyWithRetry(() => import('@/pages/admin/UserDetailPage'))
const AdminInviteCodesPage = lazyWithRetry(() => import('@/pages/admin/InviteCodesPage'))
const AdminAuditLogsPage = lazyWithRetry(() => import('@/pages/admin/AuditLogsPage'))
const AdminPlanLimitsPage = lazyWithRetry(() => import('@/pages/admin/PlanLimitsPage'))
const AdminFeatureFlagsPage = lazyWithRetry(() => import('@/pages/admin/FeatureFlagsPage'))
const AdminSettingsPage = lazyWithRetry(() => import('@/pages/admin/SettingsPage'))
const AdminGlobalCategoriesPage = lazyWithRetry(() => import('@/pages/admin/GlobalCategoriesPage'))
const AdminSurveyInvitesPage = lazyWithRetry(() => import('@/pages/admin/SurveyInvitesPage'))
const AdminSurveyResponsesPage = lazyWithRetry(() => import('@/pages/admin/SurveyResponsesPage'))
const AdminGalleryManagementPage = lazyWithRetry(() => import('@/pages/admin/GalleryManagementPage'))
// L1 workspace pages
const L1Dashboard = lazyWithRetry(() => import('@/pages/l1/L1Dashboard'))
const L1WalkPage = lazyWithRetry(() => import('@/pages/l1/L1WalkPage'))
const L1DraftsPage = lazyWithRetry(() => import('@/pages/l1/L1DraftsPage'))
const L1TicketsPage = lazyWithRetry(() => import('@/pages/l1/L1TicketsPage'))
// Account pages
const AccountLayout = lazyWithRetry(() => import('@/components/account/AccountLayout'))
const ProfileSettingsPage = lazyWithRetry(() => import('@/pages/account/ProfileSettingsPage'))
const TeamCategoriesPage = lazyWithRetry(() => import('@/pages/account/TeamCategoriesPage'))
const TargetListsPage = lazyWithRetry(() => import('@/pages/account/TargetListsPage'))
const ChatRetentionSettingsPage = lazyWithRetry(() => import('@/pages/account/ChatRetentionSettingsPage'))
const AccountSecuritySettingsPage = lazyWithRetry(() => import('@/pages/account/AccountSecuritySettingsPage'))
const IntegrationsPage = lazyWithRetry(() => import('@/pages/account/IntegrationsPage'))
const BrandingSettingsPage = lazyWithRetry(() => import('@/pages/account/BrandingSettingsPage'))
const BillingPage = lazyWithRetry(() => import('@/pages/account/BillingPage'))
const SelectPlanPage = lazyWithRetry(() => import('@/pages/account/SelectPlanPage'))
/** Wraps a lazy-loaded page with Suspense + ErrorBoundary */
function page(Component: React.LazyExoticComponent<React.ComponentType>) {
return (
<ErrorBoundary>
<Suspense fallback={<PageLoader />}>
<Component />
</Suspense>
</ErrorBoundary>
)
}
/**
* 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: <Navigate to="/" replace />,
errorElement: <RouteError />,
},
{
path: '/templates',
element: page(PublicTemplatesPage),
errorElement: <RouteError />,
},
{
path: '/privacy',
element: page(PrivacyPage),
errorElement: <RouteError />,
},
{
path: '/terms',
element: page(TermsPage),
errorElement: <RouteError />,
},
{
path: '/pricing',
element: page(PricingPage),
errorElement: <RouteError />,
},
{
path: '/contact-sales',
element: page(ContactSalesPage),
errorElement: <RouteError />,
},
{
path: '/contact',
element: page(ContactPage),
errorElement: <RouteError />,
},
{
path: '/policies',
element: page(PoliciesPage),
errorElement: <RouteError />,
},
{
path: '/promotions',
element: page(PromotionsPage),
errorElement: <RouteError />,
},
{
path: '/login',
element: <LoginPage />,
errorElement: <RouteError />,
},
{
path: '/register',
element: <RegisterPage />,
errorElement: <RouteError />,
},
{
path: '/forgot-password',
element: page(ForgotPasswordPage),
errorElement: <RouteError />,
},
{
path: '/reset-password',
element: page(ResetPasswordPage),
errorElement: <RouteError />,
},
{
path: '/verify-email',
element: page(VerifyEmailPage),
errorElement: <RouteError />,
},
{
path: '/accept-invite',
element: page(AcceptInvitePage),
errorElement: <RouteError />,
},
{
path: '/auth/google/callback',
element: page(OAuthCallbackPage),
errorElement: <RouteError />,
},
{
path: '/auth/microsoft/callback',
element: page(OAuthCallbackPage),
errorElement: <RouteError />,
},
{
path: '/survey',
element: page(SurveyPage),
errorElement: <RouteError />,
},
{
path: '/survey/thank-you',
element: page(SurveyThankYouPage),
errorElement: <RouteError />,
},
{
path: '/share/:shareToken',
element: page(SharedSessionPage),
errorElement: <RouteError />,
},
{
path: '/change-password',
element: (
<ProtectedRoute>
{page(ChangePasswordPage)}
</ProtectedRoute>
),
errorElement: <RouteError />,
},
{
element: (
<ProtectedRoute>
<AppLayout />
</ProtectedRoute>
),
errorElement: <RouteError />,
children: [
{ 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) },
// 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) },
// 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> },
// Admin routes
{
path: '/admin',
element: (
<ErrorBoundary>
<Suspense fallback={<PageLoader />}>
<ProtectedRoute requiredRole="super_admin">
<AdminLayout />
</ProtectedRoute>
</Suspense>
</ErrorBoundary>
),
children: [
{ index: true, element: page(AdminDashboardPage) },
{ path: 'accounts', element: page(AdminAccountsPage) },
{ path: 'accounts/:accountId', element: page(AdminAccountDetailPage) },
{ path: 'users', element: page(AdminAccountsPage) },
{ path: 'users/:userId', element: page(AdminUserDetailPage) },
{ path: 'invite-codes', element: page(AdminInviteCodesPage) },
{ path: 'audit-logs', element: page(AdminAuditLogsPage) },
{ path: 'plan-limits', element: page(AdminPlanLimitsPage) },
{ path: 'feature-flags', element: page(AdminFeatureFlagsPage) },
{ path: 'settings', element: page(AdminSettingsPage) },
{ path: 'categories', element: page(AdminGlobalCategoriesPage) },
{ path: 'survey-invites', element: page(AdminSurveyInvitesPage) },
{ path: 'survey-responses', element: page(AdminSurveyResponsesPage) },
{ path: 'gallery', element: page(AdminGalleryManagementPage) },
],
},
// Account routes
{
path: '/account',
element: (
<ErrorBoundary>
<Suspense fallback={<PageLoader />}>
<AccountLayout />
</Suspense>
</ErrorBoundary>
),
children: [
{ index: true, element: page(AccountSettingsPage) },
{ path: 'profile', element: page(ProfileSettingsPage) },
{
path: 'categories',
element: (
<ProtectedRoute requiredRole="owner">
{page(TeamCategoriesPage)}
</ProtectedRoute>
),
},
{
path: 'chat-retention',
element: (
<ProtectedRoute requiredRole="owner">
{page(ChatRetentionSettingsPage)}
</ProtectedRoute>
),
},
{
path: 'security',
element: (
<ProtectedRoute requiredRole="owner">
{page(AccountSecuritySettingsPage)}
</ProtectedRoute>
),
},
{ path: 'target-lists', element: page(TargetListsPage) },
{
path: 'integrations',
element: (
<ProtectedRoute requiredRole="owner">
{page(IntegrationsPage)}
</ProtectedRoute>
),
},
{
path: 'branding',
element: (
<ProtectedRoute requiredRole="owner">
{page(BrandingSettingsPage)}
</ProtectedRoute>
),
},
{ path: 'billing', element: page(BillingPage) },
{ path: 'billing/select-plan', element: page(SelectPlanPage) },
],
},
],
},
])
export default router