fix(l1): confine L1 techs to their surface + accessible rail nav labels
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 <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,24 @@ export function ProtectedRoute({ requiredRole, children }: ProtectedRouteProps)
|
|||||||
return <Navigate to="/change-password" replace />
|
return <Navigate to="/change-password" replace />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 <Navigate to="/l1" replace />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (requiredRole) {
|
if (requiredRole) {
|
||||||
const ROLE_HIERARCHY: Record<EffectiveRole, number> = {
|
const ROLE_HIERARCHY: Record<EffectiveRole, number> = {
|
||||||
super_admin: 5,
|
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 <Navigate to="/l1" replace />
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 <Navigate to="/l1" replace />
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>{children}</>
|
return <>{children}</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -256,6 +256,7 @@ export function Sidebar() {
|
|||||||
: 'text-text-rail-label hover:text-foreground'
|
: 'text-text-rail-label hover:text-foreground'
|
||||||
)}
|
)}
|
||||||
title={item.label}
|
title={item.label}
|
||||||
|
aria-label={item.label}
|
||||||
>
|
>
|
||||||
<span className="relative">
|
<span className="relative">
|
||||||
<Icon size={24} strokeWidth={1.6} className={active ? 'opacity-100' : 'opacity-60 group-hover:opacity-85'} />
|
<Icon size={24} strokeWidth={1.6} className={active ? 'opacity-100' : 'opacity-60 group-hover:opacity-85'} />
|
||||||
|
|||||||
Reference in New Issue
Block a user