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

@@ -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 (
<Helmet>
@@ -33,10 +39,11 @@ export function PageMeta({
<meta property="og:description" content={description} />
<meta property="og:type" content={ogType} />
<meta property="og:site_name" content={SITE_NAME} />
{resolvedUrl && <meta property="og:url" content={resolvedUrl} />}
{ogImage && <meta property="og:image" content={ogImage} />}
{/* Twitter */}
<meta name="twitter:card" content="summary" />
<meta name="twitter:card" content={twitterCard} />
<meta name="twitter:title" content={fullTitle} />
<meta name="twitter:description" content={description} />
{ogImage && <meta name="twitter:image" content={ogImage} />}

View File

@@ -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) {

View File

@@ -51,7 +51,7 @@ export function buildChecklistItems(
{
key: 'ran_session',
label: 'Run your first FlowPilot session',
path: '/',
path: '/home',
done: status.ran_session,
},
{

View File

@@ -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)' }}
>
<div className="flex h-14 items-center justify-between px-4" style={{ borderBottom: '1px solid var(--color-border-default)' }}>
<Link to="/" className="flex items-center gap-2.5">
<Link to="/home" className="flex items-center gap-2.5">
<BrandLogo size="sm" />
<span className="text-sm font-heading font-bold text-text-heading">ResolutionFlow</span>
</Link>

View File

@@ -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' },

View File

@@ -22,7 +22,7 @@ export function ProtectedRoute({ requiredRole, children }: ProtectedRouteProps)
}
if (!isAuthenticated) {
return <Navigate to="/landing" state={{ from: location }} replace />
return <Navigate to="/" state={{ from: location }} replace />
}
// Enforce must_change_password — redirect unless already on /change-password

View File

@@ -63,7 +63,7 @@ export function TopBar() {
>
{/* Logo area */}
<Link
to="/"
to="/home"
className="flex items-center gap-2.5 pr-4 transition-all duration-200"
>
<BrandLogo size="sm" />

View File

@@ -71,11 +71,11 @@ const FROZEN_NOW = new Date('2026-05-06T00:00:00Z')
function renderAppLayout() {
return render(
<MemoryRouter initialEntries={['/']}>
<MemoryRouter initialEntries={['/home']}>
<Routes>
<Route element={<AppLayout />}>
<Route
index
path="/home"
element={<div data-testid="child-route-content">child route</div>}
/>
</Route>

View File

@@ -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 (
<>
<div data-testid="probe-pathname">{loc.pathname}</div>
<div data-testid="probe-from">{from}</div>
</>
)
}
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(
<MemoryRouter initialEntries={['/home']}>
<Routes>
<Route
path="/home"
element={
<ProtectedRoute>
<div data-testid="home-content">home</div>
</ProtectedRoute>
}
/>
<Route path="/" element={<LocationProbe />} />
</Routes>
</MemoryRouter>,
)
// 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')
})
})