From 05646465b86c04ba6e54155873f8fa185b2e6b2a Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Thu, 14 May 2026 01:58:10 -0400 Subject: [PATCH 1/4] feat(routing): serve public landing at / and move authed index to /home MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stripe's compliance crawler fetches the apex URL without executing JS and declined live-mode review when `https://resolutionflow.com/` returned the empty SPA shell that redirected to /landing client-side. Restructure the router so / serves LandingPage directly: - `/` → new `PublicLanding` wrapper (LandingPage for anon; Navigate to /home for authed users so there's no marketing-frame flicker). - Authed tree converted to a path-less layout route with absolute child paths. QuickStartPage moves to `/home`; all other children (`/trees`, `/pilot`, `/admin/*`, `/account/*`, etc.) keep their URLs. - `/landing` kept as a one-release stale-bookmark redirect to /. - `ProtectedRoute` unauth redirect flipped /landing → /; `state.from` preserved for post-login return. Reference updates: - Post-login / post-onboarding destinations → /home: OAuthCallbackPage (incl. `?welcome=teammate` query), WelcomeStep1/2/3 dismiss-rest, AssistantChatPage post-escalate, WelcomeRouter completion/dismiss redirects, VerifyEmailPage's three "Go to dashboard" links. - Authed chrome → /home: TopBar logo, AppLayout mobile nav + drawer logo, CommandPalette Dashboard entry. - Dashboard onboarding → /home: NextStepCard `ran_session.ctaPath`, SetupChecklist `ran_session.path`, SessionHistoryPage empty-state CTA. - Public back-links → /: TermsPage, PrivacyPage, PoliciesPage, ContactPage, PromotionsPage, PublicTemplatesPage (header + footer). SharedSessionPage's `to="/"` left as-is — now correctly lands anon visitors on the public landing. Crawlability: - New `frontend/public/robots.txt` allowlisting public pages and disallowing the authed app. - New `frontend/public/sitemap.xml` for /, /pricing, /contact-sales, /contact, /templates, /terms, /privacy, /policies, /promotions. - `PageMeta` gains an `og:url` (defaults to `window.location.href`) and flips `twitter:card` to `summary_large_image` when an `ogImage` is passed. Tests: - `AppLayout.test.tsx` updated to mount at `/home`. - New `ProtectedRoute.test.tsx` asserts unauthenticated `/home` redirects to `/` (not `/landing`) and preserves origin in `state.from`. If Stripe's crawler still cannot see the site after this (zero-JS crawler), the documented next escalation is server-side prerendering of public routes via `vite-plugin-ssg`. Out of scope here. Plan: docs/plans/2026-05-13-public-landing-routing-refactor.md Co-Authored-By: Claude Opus 4.7 --- frontend/public/robots.txt | 36 ++++++ frontend/public/sitemap.xml | 57 +++++++++ frontend/src/components/common/PageMeta.tsx | 9 +- .../src/components/dashboard/NextStepCard.tsx | 2 +- .../components/dashboard/SetupChecklist.tsx | 2 +- frontend/src/components/layout/AppLayout.tsx | 4 +- .../src/components/layout/CommandPalette.tsx | 2 +- .../src/components/layout/ProtectedRoute.tsx | 2 +- frontend/src/components/layout/TopBar.tsx | 2 +- .../layout/__tests__/AppLayout.test.tsx | 4 +- .../layout/__tests__/ProtectedRoute.test.tsx | 59 ++++++++++ frontend/src/pages/AssistantChatPage.tsx | 2 +- frontend/src/pages/ContactPage.tsx | 2 +- frontend/src/pages/OAuthCallbackPage.tsx | 6 +- frontend/src/pages/PoliciesPage.tsx | 2 +- frontend/src/pages/PrivacyPage.tsx | 2 +- frontend/src/pages/PromotionsPage.tsx | 2 +- frontend/src/pages/PublicTemplatesPage.tsx | 4 +- frontend/src/pages/SessionHistoryPage.tsx | 2 +- frontend/src/pages/TermsPage.tsx | 2 +- frontend/src/pages/VerifyEmailPage.tsx | 6 +- frontend/src/pages/welcome/WelcomeRouter.tsx | 8 +- frontend/src/pages/welcome/WelcomeStep1.tsx | 2 +- frontend/src/pages/welcome/WelcomeStep2.tsx | 2 +- frontend/src/pages/welcome/WelcomeStep3.tsx | 2 +- frontend/src/router.tsx | 109 ++++++++++-------- 26 files changed, 254 insertions(+), 78 deletions(-) create mode 100644 frontend/public/robots.txt create mode 100644 frontend/public/sitemap.xml create mode 100644 frontend/src/components/layout/__tests__/ProtectedRoute.test.tsx diff --git a/frontend/public/robots.txt b/frontend/public/robots.txt new file mode 100644 index 00000000..1c41f9ab --- /dev/null +++ b/frontend/public/robots.txt @@ -0,0 +1,36 @@ +User-agent: * +Allow: / +Allow: /terms +Allow: /policies +Allow: /privacy +Allow: /contact +Allow: /contact-sales +Allow: /pricing +Allow: /promotions +Allow: /templates +Disallow: /home +Disallow: /trees/ +Disallow: /my-trees +Disallow: /pilot/ +Disallow: /admin/ +Disallow: /account/ +Disallow: /script-builder +Disallow: /scripts +Disallow: /sessions +Disallow: /analytics +Disallow: /escalations +Disallow: /queue +Disallow: /review-queue +Disallow: /network-diagrams +Disallow: /kb-accelerator +Disallow: /step-library +Disallow: /tickets +Disallow: /shares +Disallow: /feedback +Disallow: /welcome +Disallow: /flow-assist +Disallow: /dev/ +Disallow: /flows/ +Disallow: /guides + +Sitemap: https://resolutionflow.com/sitemap.xml diff --git a/frontend/public/sitemap.xml b/frontend/public/sitemap.xml new file mode 100644 index 00000000..c0db68a5 --- /dev/null +++ b/frontend/public/sitemap.xml @@ -0,0 +1,57 @@ + + + + https://resolutionflow.com/ + 2026-05-13 + weekly + 1.0 + + + https://resolutionflow.com/pricing + 2026-05-13 + monthly + 0.9 + + + https://resolutionflow.com/contact-sales + 2026-05-13 + monthly + 0.8 + + + https://resolutionflow.com/contact + 2026-05-13 + monthly + 0.7 + + + https://resolutionflow.com/templates + 2026-05-13 + weekly + 0.7 + + + https://resolutionflow.com/terms + 2026-05-13 + yearly + 0.4 + + + https://resolutionflow.com/privacy + 2026-05-13 + yearly + 0.4 + + + https://resolutionflow.com/policies + 2026-05-13 + yearly + 0.4 + + + https://resolutionflow.com/promotions + 2026-05-13 + monthly + 0.4 + + diff --git a/frontend/src/components/common/PageMeta.tsx b/frontend/src/components/common/PageMeta.tsx index 342aad6b..f2d8b962 100644 --- a/frontend/src/components/common/PageMeta.tsx +++ b/frontend/src/components/common/PageMeta.tsx @@ -5,6 +5,8 @@ interface PageMetaProps { description?: string ogImage?: string ogType?: string + /** Canonical/Open Graph URL. Defaults to `window.location.href` in the browser. */ + url?: string } const SITE_NAME = 'ResolutionFlow' @@ -20,8 +22,12 @@ export function PageMeta({ description = DEFAULT_DESCRIPTION, ogImage, ogType = 'website', + url, }: PageMetaProps) { const fullTitle = title ? `${title} | ${SITE_NAME}` : `${SITE_NAME} — ${DEFAULT_TAGLINE}` + const resolvedUrl = + url ?? (typeof window !== 'undefined' ? window.location.href : undefined) + const twitterCard = ogImage ? 'summary_large_image' : 'summary' return ( @@ -33,10 +39,11 @@ export function PageMeta({ + {resolvedUrl && } {ogImage && } {/* Twitter */} - + {ogImage && } diff --git a/frontend/src/components/dashboard/NextStepCard.tsx b/frontend/src/components/dashboard/NextStepCard.tsx index 9b158d5a..9dc1fc7f 100644 --- a/frontend/src/components/dashboard/NextStepCard.tsx +++ b/frontend/src/components/dashboard/NextStepCard.tsx @@ -79,7 +79,7 @@ export function pickNextStep( title: 'Run your first FlowPilot session', description: 'Paste a ticket or pick a flow to see ResolutionFlow in action.', ctaLabel: 'Start a session', - ctaPath: '/', + ctaPath: '/home', } } if (!status.connected_psa) { diff --git a/frontend/src/components/dashboard/SetupChecklist.tsx b/frontend/src/components/dashboard/SetupChecklist.tsx index 13ddcb1f..5ee7d726 100644 --- a/frontend/src/components/dashboard/SetupChecklist.tsx +++ b/frontend/src/components/dashboard/SetupChecklist.tsx @@ -51,7 +51,7 @@ export function buildChecklistItems( { key: 'ran_session', label: 'Run your first FlowPilot session', - path: '/', + path: '/home', done: status.ran_session, }, { diff --git a/frontend/src/components/layout/AppLayout.tsx b/frontend/src/components/layout/AppLayout.tsx index 5eaaf104..8366c699 100644 --- a/frontend/src/components/layout/AppLayout.tsx +++ b/frontend/src/components/layout/AppLayout.tsx @@ -58,7 +58,7 @@ export function AppLayout() { } const mobileNavItems = [ - { path: '/', label: 'Dashboard', icon: LayoutGrid }, + { path: '/home', label: 'Dashboard', icon: LayoutGrid }, { path: '/sessions', label: 'Session History', icon: Clock }, { path: '/escalations', label: 'Escalations', icon: AlertTriangle }, { path: '/trees', label: 'Guided Flows', icon: GitBranch }, @@ -106,7 +106,7 @@ export function AppLayout() { style={{ background: 'var(--color-bg-sidebar)', borderRight: '1px solid var(--color-border-default)' }} >
- + ResolutionFlow diff --git a/frontend/src/components/layout/CommandPalette.tsx b/frontend/src/components/layout/CommandPalette.tsx index 7bac283f..de411391 100644 --- a/frontend/src/components/layout/CommandPalette.tsx +++ b/frontend/src/components/layout/CommandPalette.tsx @@ -40,7 +40,7 @@ interface Group { } const PAGES: PaletteItem[] = [ - { id: 'page-dashboard', group: 'pages', title: 'Dashboard', path: '/', icon: 'page' }, + { id: 'page-dashboard', group: 'pages', title: 'Dashboard', path: '/home', icon: 'page' }, { id: 'page-flows', group: 'pages', title: 'All Flows', subtitle: 'Browse your flow library', path: '/trees', icon: 'page' }, { id: 'page-sessions', group: 'pages', title: 'Sessions', subtitle: 'View session history', path: '/sessions', icon: 'page' }, { id: 'page-flowpilot', group: 'pages', title: 'FlowPilot', subtitle: 'AI troubleshooting', path: '/pilot', icon: 'page' }, diff --git a/frontend/src/components/layout/ProtectedRoute.tsx b/frontend/src/components/layout/ProtectedRoute.tsx index 1c5d6140..73e2bb10 100644 --- a/frontend/src/components/layout/ProtectedRoute.tsx +++ b/frontend/src/components/layout/ProtectedRoute.tsx @@ -22,7 +22,7 @@ export function ProtectedRoute({ requiredRole, children }: ProtectedRouteProps) } if (!isAuthenticated) { - return + return } // Enforce must_change_password — redirect unless already on /change-password diff --git a/frontend/src/components/layout/TopBar.tsx b/frontend/src/components/layout/TopBar.tsx index 42e107df..91c2bf5b 100644 --- a/frontend/src/components/layout/TopBar.tsx +++ b/frontend/src/components/layout/TopBar.tsx @@ -63,7 +63,7 @@ export function TopBar() { > {/* Logo area */} diff --git a/frontend/src/components/layout/__tests__/AppLayout.test.tsx b/frontend/src/components/layout/__tests__/AppLayout.test.tsx index 26bf3fcd..0ada0458 100644 --- a/frontend/src/components/layout/__tests__/AppLayout.test.tsx +++ b/frontend/src/components/layout/__tests__/AppLayout.test.tsx @@ -71,11 +71,11 @@ const FROZEN_NOW = new Date('2026-05-06T00:00:00Z') function renderAppLayout() { return render( - + }> child route
} /> diff --git a/frontend/src/components/layout/__tests__/ProtectedRoute.test.tsx b/frontend/src/components/layout/__tests__/ProtectedRoute.test.tsx new file mode 100644 index 00000000..a87f3bf9 --- /dev/null +++ b/frontend/src/components/layout/__tests__/ProtectedRoute.test.tsx @@ -0,0 +1,59 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { render, screen } from '@testing-library/react' +import { MemoryRouter, Routes, Route, useLocation } from 'react-router-dom' + +import { ProtectedRoute } from '../ProtectedRoute' +import { useAuthStore } from '@/store/authStore' + +/** + * Probe component: surfaces the current pathname and `location.state.from` so + * the test can assert both the redirect target and that the original + * destination is preserved for post-login return. + */ +function LocationProbe() { + const loc = useLocation() + const from = + (loc.state as { from?: { pathname?: string } } | null)?.from?.pathname ?? '' + return ( + <> +
{loc.pathname}
+
{from}
+ + ) +} + +describe('ProtectedRoute — unauthenticated redirect', () => { + beforeEach(() => { + useAuthStore.setState({ + user: null, + token: null, + isAuthenticated: false, + isLoading: false, + }) + }) + + it('redirects unauthenticated visits to /home → / and preserves origin in state.from', () => { + render( + + + +
home
+ + } + /> + } /> +
+
, + ) + + // The protected page should not render. + expect(screen.queryByTestId('home-content')).not.toBeInTheDocument() + + // We landed on / (the public landing route), not /landing. + expect(screen.getByTestId('probe-pathname')).toHaveTextContent('/') + expect(screen.getByTestId('probe-from')).toHaveTextContent('/home') + }) +}) diff --git a/frontend/src/pages/AssistantChatPage.tsx b/frontend/src/pages/AssistantChatPage.tsx index 4e6f6fca..bcd214de 100644 --- a/frontend/src/pages/AssistantChatPage.tsx +++ b/frontend/src/pages/AssistantChatPage.tsx @@ -2416,7 +2416,7 @@ export default function AssistantChatPage() { setShowConclude(false) if (activeSessionStatus === 'escalated') { toast.info('Session escalated. Heading back to your dashboard.') - navigate('/') + navigate('/home') } }} onConclude={handleConclude} diff --git a/frontend/src/pages/ContactPage.tsx b/frontend/src/pages/ContactPage.tsx index e7d68adf..dade0f1a 100644 --- a/frontend/src/pages/ContactPage.tsx +++ b/frontend/src/pages/ContactPage.tsx @@ -7,7 +7,7 @@ export default function ContactPage() {
- ← Back to home + ← Back to home

Contact ResolutionFlow

We respond to customer inquiries Monday through Friday during U.S. business hours, excluding federal holidays. Email is the fastest path to a response. diff --git a/frontend/src/pages/OAuthCallbackPage.tsx b/frontend/src/pages/OAuthCallbackPage.tsx index b32dd080..e606ac27 100644 --- a/frontend/src/pages/OAuthCallbackPage.tsx +++ b/frontend/src/pages/OAuthCallbackPage.tsx @@ -112,10 +112,10 @@ export function OAuthCallbackPage() { // Invitee path lands on the dashboard with the teammate-welcome // marker; new self-serve owners go to the welcome wizard; returning - // users to /. - let dest = '/' + // users to /home. + let dest = '/home' if (decoded?.accountInviteCode) { - dest = '/?welcome=teammate' + dest = '/home?welcome=teammate' } else if (result.is_new_user) { dest = '/welcome' } diff --git a/frontend/src/pages/PoliciesPage.tsx b/frontend/src/pages/PoliciesPage.tsx index eaabb472..1da097cd 100644 --- a/frontend/src/pages/PoliciesPage.tsx +++ b/frontend/src/pages/PoliciesPage.tsx @@ -7,7 +7,7 @@ export default function PoliciesPage() {

- ← Back to home + ← Back to home

Customer Policies

Last updated: May 7, 2026

Operator: ResolutionFlow, LLC (the “Company”), operator of ResolutionFlow (“Service”).

diff --git a/frontend/src/pages/PrivacyPage.tsx b/frontend/src/pages/PrivacyPage.tsx index 1478bbca..9a6fc5f3 100644 --- a/frontend/src/pages/PrivacyPage.tsx +++ b/frontend/src/pages/PrivacyPage.tsx @@ -7,7 +7,7 @@ export default function PrivacyPage() {
- ← Back to home + ← Back to home

Privacy Policy

Last updated: March 21, 2026

diff --git a/frontend/src/pages/PromotionsPage.tsx b/frontend/src/pages/PromotionsPage.tsx index 132ad10b..f495f1dd 100644 --- a/frontend/src/pages/PromotionsPage.tsx +++ b/frontend/src/pages/PromotionsPage.tsx @@ -7,7 +7,7 @@ export default function PromotionsPage() {
- ← Back to home + ← Back to home

Promotions

Last updated: May 7, 2026

diff --git a/frontend/src/pages/PublicTemplatesPage.tsx b/frontend/src/pages/PublicTemplatesPage.tsx index ec12e088..137aa4a0 100644 --- a/frontend/src/pages/PublicTemplatesPage.tsx +++ b/frontend/src/pages/PublicTemplatesPage.tsx @@ -168,7 +168,7 @@ export default function PublicTemplatesPage() { {/* Header */}
- + Resolution @@ -406,7 +406,7 @@ export default function PublicTemplatesPage() {
Powered by ResolutionFlow diff --git a/frontend/src/pages/SessionHistoryPage.tsx b/frontend/src/pages/SessionHistoryPage.tsx index 5823208d..1717eb3e 100644 --- a/frontend/src/pages/SessionHistoryPage.tsx +++ b/frontend/src/pages/SessionHistoryPage.tsx @@ -423,7 +423,7 @@ export default function SessionHistoryPage() { description="Start a FlowPilot or chat session to begin. All your sessions will appear here." action={ Start a Session diff --git a/frontend/src/pages/TermsPage.tsx b/frontend/src/pages/TermsPage.tsx index cd65338e..e7d31ce3 100644 --- a/frontend/src/pages/TermsPage.tsx +++ b/frontend/src/pages/TermsPage.tsx @@ -7,7 +7,7 @@ export default function TermsPage() {
- ← Back to home + ← Back to home

Terms of Service

Last updated: March 21, 2026

diff --git a/frontend/src/pages/VerifyEmailPage.tsx b/frontend/src/pages/VerifyEmailPage.tsx index da83ea83..12fb49b8 100644 --- a/frontend/src/pages/VerifyEmailPage.tsx +++ b/frontend/src/pages/VerifyEmailPage.tsx @@ -149,7 +149,7 @@ export function VerifyEmailPage() { action needed.

= 3 → / + * onboarding_dismissed === true → /home + * onboarding_step_completed >= 3 → /home * onboarding_step_completed === null/0 → /welcome/step-1 * onboarding_step_completed === 1 → /welcome/step-2 * onboarding_step_completed === 2 → /welcome/step-3 @@ -19,10 +19,10 @@ export function WelcomeRouter() { // the page loader rather than racing past the redirect. if (!user) return - if (user.onboarding_dismissed) return + if (user.onboarding_dismissed) return const completed = user.onboarding_step_completed ?? 0 - if (completed >= 3) return + if (completed >= 3) return if (completed === 2) return if (completed === 1) return return diff --git a/frontend/src/pages/welcome/WelcomeStep1.tsx b/frontend/src/pages/welcome/WelcomeStep1.tsx index 412217e9..60763b4d 100644 --- a/frontend/src/pages/welcome/WelcomeStep1.tsx +++ b/frontend/src/pages/welcome/WelcomeStep1.tsx @@ -85,7 +85,7 @@ export function WelcomeStep1() { try { await onboardingApi.dismissRest() await fetchUser() - navigate('/') + navigate('/home') } catch { setError('Could not save. Please try again.') setSubmitting(null) diff --git a/frontend/src/pages/welcome/WelcomeStep2.tsx b/frontend/src/pages/welcome/WelcomeStep2.tsx index c5f84805..bbc3acf8 100644 --- a/frontend/src/pages/welcome/WelcomeStep2.tsx +++ b/frontend/src/pages/welcome/WelcomeStep2.tsx @@ -90,7 +90,7 @@ export function WelcomeStep2() { try { await onboardingApi.dismissRest() await fetchUser() - navigate('/') + navigate('/home') } catch { setError('Could not save. Please try again.') setSubmitting(null) diff --git a/frontend/src/pages/welcome/WelcomeStep3.tsx b/frontend/src/pages/welcome/WelcomeStep3.tsx index d3659037..b345dfa3 100644 --- a/frontend/src/pages/welcome/WelcomeStep3.tsx +++ b/frontend/src/pages/welcome/WelcomeStep3.tsx @@ -191,7 +191,7 @@ export function WelcomeStep3() { try { await onboardingApi.dismissRest() await fetchUser() - navigate('/') + navigate('/home') } catch { setError('Could not save. Please try again.') setSubmitting(null) diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index cadd0032..805a9dda 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -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' const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV7(createBrowserRouter) import { @@ -118,10 +119,27 @@ function page(Component: React.LazyExoticComponent) { ) } +/** + * 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 + return page(LandingPage) +} + export const router = sentryCreateBrowserRouter([ + { + path: '/', + element: , + errorElement: , + }, + // Stale-bookmark redirect — keep one release, delete in a follow-up. { path: '/landing', - element: page(LandingPage), + element: , errorElement: , }, { @@ -229,7 +247,6 @@ export const router = sentryCreateBrowserRouter([ errorElement: , }, { - path: '/', element: ( @@ -237,56 +254,56 @@ export const router = sentryCreateBrowserRouter([ ), errorElement: , 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: }, - { path: 'assistant/:sessionId', element: }, - { 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: }, + { path: '/assistant/:sessionId', element: }, + { 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) }, // Admin routes { - path: 'admin', + path: '/admin', element: ( }> @@ -315,7 +332,7 @@ export const router = sentryCreateBrowserRouter([ }, // Account routes { - path: 'account', + path: '/account', element: ( }> From 13f527c4ad76e012d5939a80f201cd3c5c598640 Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Thu, 14 May 2026 17:35:44 -0400 Subject: [PATCH 2/4] test(e2e): align auth + public smoke tests with new / and /home routing Playwright specs still asserted the pre-refactor URLs and failed on CI: - auth.spec.ts expected post-login to land at `/`; now `/home`. - public.spec.ts expected unauth redirect to `/landing`; now `/`. - public.spec.ts's landing-loads test navigated to `/landing` (a stale- bookmark redirect); point it directly at `/`. Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/e2e/auth.spec.ts | 2 +- frontend/e2e/public.spec.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/e2e/auth.spec.ts b/frontend/e2e/auth.spec.ts index e5a261d9..a8212d38 100644 --- a/frontend/e2e/auth.spec.ts +++ b/frontend/e2e/auth.spec.ts @@ -7,7 +7,7 @@ test.describe('authentication smoke tests', () => { test('team admin can sign in through the login form', async ({ page }) => { await signIn(page) - await expect(page).toHaveURL(/\/$/) + await expect(page).toHaveURL(/\/home$/) await expect(page.getByTestId('app-shell')).toBeVisible() }) }) diff --git a/frontend/e2e/public.spec.ts b/frontend/e2e/public.spec.ts index 81fd469e..f78e3a39 100644 --- a/frontend/e2e/public.spec.ts +++ b/frontend/e2e/public.spec.ts @@ -4,7 +4,7 @@ test.use({ storageState: { cookies: [], origins: [] } }) test.describe('public route smoke tests', () => { test('landing page loads', async ({ page }) => { - await page.goto('/landing') + await page.goto('/') await expect( page.getByRole('link', { name: 'Start Free', exact: true }), @@ -17,7 +17,7 @@ test.describe('public route smoke tests', () => { test('protected routes redirect unauthenticated users to landing', async ({ page }) => { await page.goto('/sessions') - await expect(page).toHaveURL(/\/landing$/) + await expect(page).toHaveURL(/\/$/) await expect( page.getByRole('link', { name: 'Sign In' }), ).toBeVisible() From 86163a69aa63d3309bdd5424cdd6e8cc75fd27e2 Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Thu, 14 May 2026 19:25:50 -0400 Subject: [PATCH 3/4] test(welcome): align Router/Step1/Step2 stub routes with /home destination Post-refactor, WelcomeRouter and the Step1/Step2 "Skip-the-rest" handlers navigate to /home, but the MemoryRouter test stubs still mounted the "dashboard" marker at /. Update the stub routes (and matching it() titles) so the assertions resolve. Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/src/pages/welcome/__tests__/WelcomeRouter.test.tsx | 6 +++--- frontend/src/pages/welcome/__tests__/WelcomeStep1.test.tsx | 4 ++-- frontend/src/pages/welcome/__tests__/WelcomeStep2.test.tsx | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/pages/welcome/__tests__/WelcomeRouter.test.tsx b/frontend/src/pages/welcome/__tests__/WelcomeRouter.test.tsx index 94c81f30..47a45626 100644 --- a/frontend/src/pages/welcome/__tests__/WelcomeRouter.test.tsx +++ b/frontend/src/pages/welcome/__tests__/WelcomeRouter.test.tsx @@ -39,7 +39,7 @@ function renderRouter() { step-1
} /> step-2
} /> step-3
} /> - dashboard
} /> + dashboard
} /> , ) @@ -100,7 +100,7 @@ describe('WelcomeRouter', () => { }) }) - it('redirects to / when onboarding_step_completed >= 3', async () => { + it('redirects to /home when onboarding_step_completed >= 3', async () => { useAuthStore.setState({ user: makeUser({ onboarding_step_completed: 3 }), }) @@ -110,7 +110,7 @@ describe('WelcomeRouter', () => { }) }) - it('redirects to / when onboarding_dismissed is true', async () => { + it('redirects to /home when onboarding_dismissed is true', async () => { useAuthStore.setState({ user: makeUser({ onboarding_step_completed: 1, diff --git a/frontend/src/pages/welcome/__tests__/WelcomeStep1.test.tsx b/frontend/src/pages/welcome/__tests__/WelcomeStep1.test.tsx index 93483add..a067d171 100644 --- a/frontend/src/pages/welcome/__tests__/WelcomeStep1.test.tsx +++ b/frontend/src/pages/welcome/__tests__/WelcomeStep1.test.tsx @@ -65,7 +65,7 @@ function renderPage() { } /> step-2
} /> - dashboard
} /> + dashboard
} /> , ) @@ -148,7 +148,7 @@ describe('WelcomeStep1', () => { }) }) - it('Skip-the-rest dismisses and navigates to /', async () => { + it('Skip-the-rest dismisses and navigates to /home', async () => { const user = userEvent.setup() renderPage() diff --git a/frontend/src/pages/welcome/__tests__/WelcomeStep2.test.tsx b/frontend/src/pages/welcome/__tests__/WelcomeStep2.test.tsx index 57b2a9bd..3cb5c9ef 100644 --- a/frontend/src/pages/welcome/__tests__/WelcomeStep2.test.tsx +++ b/frontend/src/pages/welcome/__tests__/WelcomeStep2.test.tsx @@ -66,7 +66,7 @@ function renderPage() { } /> step-3
} /> integrations
} /> - dashboard
} /> + dashboard
} /> , ) @@ -158,7 +158,7 @@ describe('WelcomeStep2', () => { expect(screen.queryByTestId('welcome-step-2-connect-now')).not.toBeInTheDocument() }) - it('Skip-the-rest dismisses and navigates to /', async () => { + it('Skip-the-rest dismisses and navigates to /home', async () => { const user = userEvent.setup() renderPage() From f9f98b1a65c276140727a779d997ab0800fd71ba Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Fri, 15 May 2026 00:34:23 -0400 Subject: [PATCH 4/4] fix(routing): finish /home migration in WelcomeStep3 + VerifyEmailPage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original public-landing routing refactor migrated WelcomeRouter, WelcomeStep1, and WelcomeStep2 post-onboarding redirects to /home, but left four sites still pointing at the old / + query-string destinations: - WelcomeStep3 `completeWizardAndExit` (Send invites) - WelcomeStep3 `handleSkipStep` (Skip) - VerifyEmailPage post-verify auto-redirect (`setTimeout`) - VerifyEmailPage success-state "Go to dashboard" Link These all worked by accident because PublicLanding redirects authed users from / to /home — so users still landed on the dashboard, but through an unnecessary mount-and-redirect flicker, and the `?welcome=true` / `?verified=1` query markers got dropped on the way. Drop both query markers — neither is read anywhere in the codebase (grepped frontend/src; the dashboard's onboarding UX is driven by `getOnboardingStatus`, not URL state). Carrying dead URL params just invites future "is this load-bearing?" investigations. Test stubs in WelcomeStep3.test.tsx and VerifyEmailPage.test.tsx moved from `` to `` so the assertions verify the new destination instead of accidentally matching the old one (the previous stubs masked the partial migration). Out of scope: AcceptInvitePage and OAuthCallbackPage still use `?welcome=teammate`, but that one carries an explicit "decoded by the dashboard in Task 41" annotation and may be wired up later, so left untouched. Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/src/pages/VerifyEmailPage.tsx | 10 ++++------ frontend/src/pages/__tests__/VerifyEmailPage.test.tsx | 6 +++--- frontend/src/pages/welcome/WelcomeStep3.tsx | 6 +++--- .../src/pages/welcome/__tests__/WelcomeStep3.test.tsx | 2 +- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/frontend/src/pages/VerifyEmailPage.tsx b/frontend/src/pages/VerifyEmailPage.tsx index 12fb49b8..0ac355a0 100644 --- a/frontend/src/pages/VerifyEmailPage.tsx +++ b/frontend/src/pages/VerifyEmailPage.tsx @@ -20,8 +20,7 @@ const SUCCESS_REDIRECT_MS = 1200 * "Already verified" state. No API call. * - Else fire `POST /auth/email/verify` exactly once (a `useRef` guard keeps * React 19 strict-mode double-invoke from double-firing the call). On - * success, refresh the auth store and bounce to `/?verified=1` so the - * dashboard surfaces a toast. + * success, refresh the auth store and bounce to `/home`. * - On error, show "Invalid or expired token" + a "Resend" CTA that calls * `POST /auth/email/send-verification`. */ @@ -70,10 +69,9 @@ export function VerifyEmailPage() { if (cancelled) return setStatus('success') toast.success('Email verified') - // Brief success state, then redirect with a query flag so the - // dashboard can re-surface confirmation if it wants to. + // Brief success state, then redirect to the dashboard. window.setTimeout(() => { - navigate('/?verified=1', { replace: true }) + navigate('/home', { replace: true }) }, SUCCESS_REDIRECT_MS) }) .catch((err) => { @@ -126,7 +124,7 @@ export function VerifyEmailPage() { Redirecting you to the dashboard…

} /> - dashboard} /> + dashboard} /> , @@ -130,7 +130,7 @@ describe('VerifyEmailPage', () => { } /> - dashboard} /> + dashboard} /> , @@ -142,7 +142,7 @@ describe('VerifyEmailPage', () => { } /> - dashboard} /> + dashboard} /> , diff --git a/frontend/src/pages/welcome/WelcomeStep3.tsx b/frontend/src/pages/welcome/WelcomeStep3.tsx index b345dfa3..be2f6309 100644 --- a/frontend/src/pages/welcome/WelcomeStep3.tsx +++ b/frontend/src/pages/welcome/WelcomeStep3.tsx @@ -39,7 +39,7 @@ function makeEmptyRow(): InviteRow { * * 1. POST `/accounts/me/invites/bulk` with populated rows. * 2. PATCH `/users/me/onboarding-step` `{step: 3, action: "complete"}`. - * 3. Navigate to `/?welcome=true` and fire a "You're all set" toast. + * 3. Navigate to `/home` and fire a "You're all set" toast. * * Partial-failure UX: rows in `failed[]` keep their input and show an * inline error. The wizard does NOT auto-advance when there are failures — @@ -109,7 +109,7 @@ export function WelcomeStep3() { await onboardingApi.updateStep({ step: 3, action: 'complete' }) await fetchUser() toast.success("You're all set!") - navigate('/?welcome=true') + navigate('/home') } const handleSendInvites = async () => { @@ -177,7 +177,7 @@ export function WelcomeStep3() { await onboardingApi.updateStep({ step: 3, action: 'skip' }) await fetchUser() toast.success("You're all set!") - navigate('/?welcome=true') + navigate('/home') } catch { setError('Could not save. Please try again.') setSubmitting(null) diff --git a/frontend/src/pages/welcome/__tests__/WelcomeStep3.test.tsx b/frontend/src/pages/welcome/__tests__/WelcomeStep3.test.tsx index c90be2dd..b6d0788e 100644 --- a/frontend/src/pages/welcome/__tests__/WelcomeStep3.test.tsx +++ b/frontend/src/pages/welcome/__tests__/WelcomeStep3.test.tsx @@ -88,7 +88,7 @@ function renderPage() { } /> - dashboard} /> + dashboard} /> , )