From 890cb80bef1b64c0309ab431d4b36e6e536b25d0 Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Fri, 29 May 2026 01:06:02 -0400 Subject: [PATCH] fix(l1): confine L1 techs to their surface + accessible rail nav labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two regressions surfaced by running the L1 e2e suite against current main (which carries PR #174's /home routing migration): 1. L1 post-login redirect keyed off `pathname === '/'`, but the authed index moved to /home in #174 — so L1 users landed on the engineer dashboard instead of /l1. Replace the ad-hoc '/' and /pilot|/assistant checks with a single allowlist: l1_tech users may only reach /l1*, /guides, /account, /change-password; everything else (incl. /home, /pilot, /trees/*, /escalations) bounces to /l1. Runs before the requiredRole check so L1 users never trip the engineer-route role logic. 2. Rail nav Links exposed only the truncated shortLabel as their accessible name (title= is not an accessible-name source when visible text exists), so the "L1 Workspace" coverage-engineer link was unreachable by role+name. Add aria-label={item.label} for an accurate accessible name on every rail link. Fixes all 3 failing cases in e2e/l1-workspace.spec.ts. tsc + eslint clean. Co-Authored-By: Claude Opus 4.7 --- .../src/components/layout/ProtectedRoute.tsx | 35 ++++++++++--------- frontend/src/components/layout/Sidebar.tsx | 1 + 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/frontend/src/components/layout/ProtectedRoute.tsx b/frontend/src/components/layout/ProtectedRoute.tsx index 184df619..8b364518 100644 --- a/frontend/src/components/layout/ProtectedRoute.tsx +++ b/frontend/src/components/layout/ProtectedRoute.tsx @@ -30,6 +30,24 @@ export function ProtectedRoute({ requiredRole, children }: ProtectedRouteProps) return } + // L1 techs are confined to their focused surface. The sidebar only exposes + // /l1*, /guides, and /account for them, so any other authed path (the engineer + // dashboard at /home, /pilot, /trees/*, /escalations, …) bounces to /l1. This + // also covers post-login landing: auth sends users to /home, which is not in + // the allowlist, so l1_tech users end up on /l1. Engineer-only AI surfaces + // (/pilot, /assistant) would 403 at POST /api/v1/ai-sessions anyway — this + // turns that backend error into a clean redirect. Runs before the requiredRole + // check so L1 users never trip the engineer-route role logic. + if (effectiveRole === 'l1_tech') { + const L1_ALLOWED_PREFIXES = ['/l1', '/guides', '/account', '/change-password'] + const allowed = L1_ALLOWED_PREFIXES.some( + (p) => location.pathname === p || location.pathname.startsWith(p + '/'), + ) + if (!allowed) { + return + } + } + if (requiredRole) { const ROLE_HIERARCHY: Record = { super_admin: 5, @@ -43,23 +61,6 @@ export function ProtectedRoute({ requiredRole, children }: ProtectedRouteProps) } } - // L1 users landing on / (e.g. post-login) get redirected to their workspace. - // Does not fire when already on /l1 or any other path, preventing loops. - if (effectiveRole === 'l1_tech' && location.pathname === '/') { - return - } - - // L1 users hitting engineer-only AI surfaces (Pilot / Assistant) get pushed - // back to /l1 — POST /api/v1/ai-sessions rejects them with 403 anyway, so - // this just turns a backend error into a clean route-level redirect. - if ( - effectiveRole === 'l1_tech' && - (location.pathname.startsWith('/pilot') || - location.pathname.startsWith('/assistant')) - ) { - return - } - return <>{children} } diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index d97830a8..aaca8061 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -256,6 +256,7 @@ export function Sidebar() { : 'text-text-rail-label hover:text-foreground' )} title={item.label} + aria-label={item.label} >