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