diff --git a/frontend/src/components/layout/ProtectedRoute.tsx b/frontend/src/components/layout/ProtectedRoute.tsx index 808c331f..0b48c97c 100644 --- a/frontend/src/components/layout/ProtectedRoute.tsx +++ b/frontend/src/components/layout/ProtectedRoute.tsx @@ -43,6 +43,12 @@ 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 + } + return <>{children} } diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index f5404ab4..d97830a8 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -12,6 +12,7 @@ import { useUserPreferencesStore } from '@/store/userPreferencesStore' import { sidebarApi } from '@/api' import type { SidebarStatsResponse } from '@/api/sidebar' import { prefetchForRoute } from '@/lib/routePrefetch' +import { usePermissions } from '@/hooks/usePermissions' /* ── Types ──────────────────────────────────────────── */ @@ -37,6 +38,7 @@ export function Sidebar() { const location = useLocation() const sidebarPinned = useUserPreferencesStore(s => s.sidebarPinned) const toggleSidebarPinned = useUserPreferencesStore(s => s.toggleSidebarPinned) + const { isL1Tech, canCoverL1 } = usePermissions() const [stats, setStats] = useState(null) // Phase 6: pending-drafts badge on the Scripts nav. Fetched independently @@ -77,58 +79,74 @@ export function Sidebar() { * and pinned modes. Pin/unpin is a width/label affordance, not an * IA switch. A hairline divider separates the two groups; no labels. */ - const workItems: NavEntry[] = [ - { - href: '/', icon: LayoutGrid, label: 'Dashboard', shortLabel: 'Dash', - matchPaths: ['/'], - }, - { - href: '/tickets', icon: Ticket, label: 'Tickets', shortLabel: 'Tickets', - matchPaths: ['/tickets'], - }, - { - href: '/sessions', icon: Clock, label: 'Sessions', shortLabel: 'Sessions', - badge: stats?.active_count || undefined, - matchPaths: ['/sessions'], - }, - { - href: '/escalations', icon: AlertTriangle, label: 'Escalations', shortLabel: 'Escal', - badge: stats?.escalation_count || undefined, - matchPaths: ['/escalations'], - }, - ] + // L1 users get a focused sidebar with only their surfaces. + // Engineers/owners get the full sidebar; those with canCoverL1 also get + // an appended "L1 Workspace" entry in the library group. + const workItems: NavEntry[] = isL1Tech + ? [ + { href: '/l1', icon: LayoutGrid, label: 'Workspace', shortLabel: 'Work', matchPaths: ['/l1'] }, + { href: '/l1/tickets', icon: Ticket, label: 'Tickets', shortLabel: 'Tickets', matchPaths: ['/l1/tickets'] }, + { href: '/l1/drafts', icon: FileText, label: 'My Drafts', shortLabel: 'Drafts', matchPaths: ['/l1/drafts'] }, + ] + : [ + { + href: '/', icon: LayoutGrid, label: 'Dashboard', shortLabel: 'Dash', + matchPaths: ['/'], + }, + { + href: '/tickets', icon: Ticket, label: 'Tickets', shortLabel: 'Tickets', + matchPaths: ['/tickets'], + }, + { + href: '/sessions', icon: Clock, label: 'Sessions', shortLabel: 'Sessions', + badge: stats?.active_count || undefined, + matchPaths: ['/sessions'], + }, + { + href: '/escalations', icon: AlertTriangle, label: 'Escalations', shortLabel: 'Escal', + badge: stats?.escalation_count || undefined, + matchPaths: ['/escalations'], + }, + ] - const libraryItems: NavEntry[] = [ - { - href: '/trees', icon: GitBranch, label: 'Flows', shortLabel: 'Flows', - badge: stats?.tree_counts.total || undefined, - matchPaths: ['/trees', '/flows', '/my-trees', '/step-library', '/network-diagrams'], - children: [ - { href: '/trees?type=procedural', label: 'Projects', count: stats?.tree_counts.procedural || undefined }, - { href: '/step-library', label: 'Solutions Library' }, - { href: '/network-diagrams', label: 'Network Maps' }, - ], - }, - { - href: '/scripts', icon: FileText, label: 'Scripts', shortLabel: 'Scripts', - badge: pendingDraftCount || undefined, - matchPaths: ['/scripts', '/script-builder'], - children: [ - { href: '/script-builder', label: 'Script Builder' }, - ], - }, - { - href: '/review-queue', icon: ListChecks, label: 'Review Queue', shortLabel: 'Review', - matchPaths: ['/review-queue'], - }, - { - href: '/analytics', icon: BarChart3, label: 'Analytics', shortLabel: 'Stats', - matchPaths: ['/analytics', '/shares'], - children: [ - { href: '/shares', label: 'Exports' }, - ], - }, - ] + const libraryItems: NavEntry[] = isL1Tech + ? [] + : [ + { + href: '/trees', icon: GitBranch, label: 'Flows', shortLabel: 'Flows', + badge: stats?.tree_counts.total || undefined, + matchPaths: ['/trees', '/flows', '/my-trees', '/step-library', '/network-diagrams'], + children: [ + { href: '/trees?type=procedural', label: 'Projects', count: stats?.tree_counts.procedural || undefined }, + { href: '/step-library', label: 'Solutions Library' }, + { href: '/network-diagrams', label: 'Network Maps' }, + ], + }, + { + href: '/scripts', icon: FileText, label: 'Scripts', shortLabel: 'Scripts', + badge: pendingDraftCount || undefined, + matchPaths: ['/scripts', '/script-builder'], + children: [ + { href: '/script-builder', label: 'Script Builder' }, + ], + }, + { + href: '/review-queue', icon: ListChecks, label: 'Review Queue', shortLabel: 'Review', + matchPaths: ['/review-queue'], + }, + { + href: '/analytics', icon: BarChart3, label: 'Analytics', shortLabel: 'Stats', + matchPaths: ['/analytics', '/shares'], + children: [ + { href: '/shares', label: 'Exports' }, + ], + }, + // Engineers/owners with L1 coverage access also get the L1 Workspace entry + ...(canCoverL1 ? [{ + href: '/l1', icon: LayoutGrid, label: 'L1 Workspace', shortLabel: 'L1', + matchPaths: ['/l1'], + }] : []), + ] const footerItems: NavEntry[] = [ { href: '/guides', icon: BookOpen, label: 'Guides', shortLabel: 'Guides', matchPaths: ['/guides'] },