# Frontend UI: App Shell, Routing, Auth/RBAC, and Explorer (RHL-019 / RHL-020 / RHL-021 / RHL-022) This document describes the **frontend routing scaffold**, the **application shell layout**, and the **core navigation UI (Explorer)** for the RHL Lieferscheine app. Timeline: - **RHL-019**: Public vs protected route scaffold + AppShell + placeholder pages. - **RHL-020**: Real login flow + session handling and redirects. - **RHL-021**: UI-side RBAC guard + consistent Forbidden / NotFound UX. - **RHL-022**: Explorer v2 (Year → Month → Day → Files) + shadcn Breadcrumbs with dropdowns. > **Language policy** > > - Conversation and developer coordination can be German. > - **Code, comments, tests, and documentation are English.** > - **All user-facing UI strings are German** (labels, button text, alerts, hints). --- ## 1. Scope ### 1.1 Implemented (as of RHL-022) - **Public** `/login` route with a functional login form (shadcn/ui primitives). - **Protected** application shell for all authenticated routes: - Top navigation (brand, status, logout) - Sidebar placeholder area - Main content area - **Session guard** for protected routes: - Session identity is checked via `GET /api/auth/me`. - When unauthenticated: redirect to `/login?reason=expired&next=`. - **In-shell auth gating (UX improvement)**: - Auth loading/error/redirect states render **inside** the AppShell main content. - AppShell (TopNav + sidebar) remains stable; no “blank spinner screens”. - **Logout**: - Logout button calls `GET /api/auth/logout` and redirects to `/login?reason=logged-out`. - **UI RBAC (branch-level)**: - `BranchGuard` prevents branch users from accessing other branches’ URLs. - Admin/dev can access multiple branches. - Admin/dev branch existence validation uses `GET /api/branches`. - Fail-open policy on validation failures (do not lock the UI on temporary API errors). - **Route param validation** (syntactic): - `year`: `YYYY` - `month`: `01–12` - `day`: `01–31` - Invalid params trigger `notFound()` early in layouts. - **Explorer v2** (Branch → Year → Month → Day → Files): - `/:branch` → years - `/:branch/:year` → months - `/:branch/:year/:month` → days - `/:branch/:year/:month/:day` → files - **Breadcrumb navigation**: - shadcn/ui `Breadcrumb` + dropdowns for year/month/day when options are available. - Dropdown options are derived from real API results (only show segments that exist). - **Consistent states** across Explorer levels: - Loading states (Skeleton) - Empty states - Error states with retry - FS_NOT_FOUND mapped to an Explorer “path no longer exists” card ### 1.2 Still out of scope / planned - Search UI (route exists as placeholder: `/:branch/search`). - PDF viewer / streaming endpoint integration (planned as RHL-023). File “Open” stays disabled for now. - Admin/dev branch selector in the sidebar. - Performance polish: - smoother navigation via client-side caching / prefetching - skeleton/layout shift reduction --- ## 2. Route Groups & URL Structure The app uses Next.js App Router **Route Groups** to separate public and protected UI. ### 2.1 Route groups - **Public**: `app/(public)` - Routes that do **not** show the authenticated AppShell. - Current route: `/login` - **Protected**: `app/(protected)` - Routes that render inside the AppShell. - Protected routes are guarded by: 1. Session check (AuthProvider) 2. UI RBAC check for branch routes (BranchGuard) ### 2.2 URL map | URL | Purpose | Notes | | ---------------------------- | --------------------------- | ---------------------------------------------------------- | | `/login` | Login page | Supports `reason` and `next` query params | | `/` | Protected entry placeholder | Rendered only when authenticated | | `/:branch` | Explorer: years | Example: `/NL01` | | `/:branch/:year` | Explorer: months | Example: `/NL01/2025` | | `/:branch/:year/:month` | Explorer: days | Example: `/NL01/2025/12` | | `/:branch/:year/:month/:day` | Explorer: files | Example: `/NL01/2025/12/31` | | `/:branch/search` | Search placeholder | Explicit segment so `search` is not interpreted as `:year` | | `/forbidden` | Forbidden page wrapper | Optional wrapper; Forbidden is typically rendered inline | Important: - There is **no** standalone `/search` route. Visiting `/search` matches `/:branch` with `branch = "search"`. --- ## 3. Layouts ### 3.1 Root layout File: `app/layout.jsx` Responsibilities: - Global CSS imports (`app/globals.css`). - Theme provider setup (shadcn/ui + next-themes wrapper). - Base HTML/body structure. ### 3.2 Public layout File: `app/(public)/layout.jsx` Responsibilities: - Minimal centered layout for public routes. - Intended for `/login` (and potential future public routes). ### 3.3 Protected layout File: `app/(protected)/layout.jsx` Responsibilities: - Wrap all protected pages with: - `AuthProvider` (session check + redirect) - `AppShell` (stable frame) - `AuthGate` (renders auth loading/error/redirect UI inside the shell) UX rationale: - We keep the AppShell frame visible while auth/session checks run. - This avoids full-screen “blank spinners” on slow connections. --- ## 4. Authentication UX (RHL-020) ### 4.1 Session check for protected routes File: `components/auth/AuthProvider.jsx` Behavior: 1. On mount, call `apiClient.getMe()`. 2. If `{ user: { ... } }`: - set auth state to `authenticated` - render protected UI 3. If `{ user: null }`: - redirect to `/login?reason=expired&next=` The `next` parameter: - includes the original `pathname` and query string - is sanitized to avoid open redirects (only internal paths are allowed) ### 4.2 AuthGate (in-shell gating) File: `components/auth/AuthGate.jsx` Behavior: - While session is loading: show an in-shell loading card. - On auth errors: show an in-shell error card + retry. - On unauthenticated: show an in-shell “redirecting” message while redirect happens. ### 4.3 Login page (reason / next) Files: - `app/(public)/login/page.jsx` (Server Component) - `components/auth/LoginForm.jsx` (Client Component) Flow: 1. Login page parses query params using `parseLoginParams(...)`. 2. If `reason` is present: - `expired` → show “session expired” banner (German) - `logged-out` → show “logged out” banner (German) 3. On submit, the form calls `apiClient.login({ username, password })`. 4. On success: - redirect to `next` if present - otherwise redirect to `/` 5. On failure: - show a safe, user-friendly error message (German) Username policy: - Backend stores usernames in lowercase and performs normalization during login. - UI enforces this policy as well: - username input is normalized to lowercase - `autoCapitalize="none"` to prevent mobile auto-caps ### 4.4 Logout File: `components/auth/LogoutButton.jsx` Flow: 1. Calls `apiClient.logout()`. 2. Redirects to `/login?reason=logged-out`. ### 4.5 User status Files: - `components/auth/authContext.jsx` - `components/app-shell/UserStatus.jsx` Behavior: - AuthProvider provides a minimal auth context (`status`, `user`, `error`). - `UserStatus` renders a short indicator in the TopNav: - loading → `Lädt…` - authenticated → role + optional branchId - unauthenticated/error → fallback text --- ## 5. UI RBAC, Forbidden, and NotFound (RHL-021) ### 5.1 Goals RHL-021 adds a friendly UI layer on top of backend RBAC: - Branch users must not access other branches’ URLs. - Admin/dev users may access any existing branch. - Invalid route parameters (year/month/day) should surface as NotFound. Backend RBAC remains the source of truth. UI RBAC exists to: - prevent “obviously forbidden” navigation in the frontend - provide clear and consistent UX for end users ### 5.2 BranchGuard (UI-side RBAC) Files: - `components/auth/BranchGuard.jsx` - Pure logic: - `lib/frontend/rbac/branchAccess.js` - `lib/frontend/rbac/branchUiDecision.js` Responsibilities: - Read `user` and `status` from AuthContext. - Enforce branch rules: - role `branch` → allowed only when `:branch === user.branchId` - role `admin` / `dev` → allowed for any branch that exists Admin/dev branch existence validation: - `BranchGuard` fetches `GET /api/branches` and verifies the route branch exists. - Fail-open policy: - If fetching the list fails, do not block rendering. - Backend RBAC and subsequent API calls remain authoritative. ### 5.3 Param validation (year/month/day) Files: - `lib/frontend/params.js` Layout enforcement (server-side `notFound()`): - `app/(protected)/[branch]/layout.jsx` (branch syntax) - `app/(protected)/[branch]/[year]/layout.jsx` - `app/(protected)/[branch]/[year]/[month]/layout.jsx` - `app/(protected)/[branch]/[year]/[month]/[day]/layout.jsx` ### 5.4 Forbidden UX Files: - `components/system/ForbiddenView.jsx` - Optional wrapper route: `app/(protected)/forbidden/page.jsx` Where Forbidden is shown: - BranchGuard renders ForbiddenView inline for branch mismatch. ### 5.5 NotFound UX Files: - `components/system/NotFoundView.jsx` - Protected not-found entry: `app/(protected)/not-found.jsx` Where NotFound is shown: - invalid params in layouts via `notFound()` - admin/dev: non-existing branches via BranchGuard existence validation --- ## 6. Explorer v2 (RHL-022) ### 6.1 UI goal Provide a simple “file explorer” drill-down: - Year → Month → Day → Files ### 6.2 Explorer pages Routes and components: - `/:branch` → `components/explorer/levels/YearsExplorer.jsx` - `/:branch/:year` → `components/explorer/levels/MonthsExplorer.jsx` - `/:branch/:year/:month` → `components/explorer/levels/DaysExplorer.jsx` - `/:branch/:year/:month/:day` → `components/explorer/levels/FilesExplorer.jsx` ### 6.3 Data fetching strategy - All Explorer pages are **Client Components**. - All API calls go through `lib/frontend/apiClient.js`. - A small hook provides consistent query state: - `lib/frontend/hooks/useExplorerQuery.js` Design: - predictable states: `loading | success | error` - retry mechanism exposed to the UI - no routing side effects inside the hook (routing remains in the page components) ### 6.4 Breadcrumbs (with dropdowns) Files: - UI component: - `components/explorer/breadcrumbs/ExplorerBreadcrumbs.jsx` - `components/explorer/breadcrumbs/SegmentDropdown.jsx` - Pure helpers: - `lib/frontend/explorer/breadcrumbDropdowns.js` - `lib/frontend/explorer/formatters.js` (German month labels) - `lib/frontend/explorer/sorters.js` Rules: - Breadcrumb shows the current path: branch → year → month → day. - Dropdowns appear only when options are available: - years dropdown on month/day/files levels - months dropdown on day/files levels - days dropdown on files level Fail-open behavior: - If dropdown option queries fail, the breadcrumb still renders the current path. ### 6.5 Loading / empty / error states Shared Explorer UI building blocks: - `components/explorer/ExplorerPageShell.jsx` - `components/explorer/ExplorerSectionCard.jsx` - `components/explorer/states/*` Error mapping: - `lib/frontend/explorer/errorMapping.js` maps API client errors to UX outcomes: - `AUTH_UNAUTHENTICATED` → redirect to login (expired) - `AUTH_FORBIDDEN_BRANCH` → ForbiddenView - `FS_NOT_FOUND` → ExplorerNotFound - other errors → ExplorerError + retry ### 6.6 Files list - Uses shadcn/ui `Table`. - Shows: - file name - relative path (desktop column + mobile secondary line) - Primary action: - “Öffnen” button remains disabled until the PDF endpoint/viewer ticket (RHL-023). --- ## 7. UI primitives (shadcn/ui) The Explorer + auth UI uses shadcn/ui primitives from `components/ui/*`. Required components for the current scope: - `card` - `input` - `label` - `alert` - `button` - `breadcrumb` - `dropdown-menu` - `skeleton` - `table` --- ## 8. File Naming Convention (.js vs .jsx) To keep the project consistent: - Use **`.jsx`** for files that contain JSX: - `app/**/page.jsx`, `app/**/layout.jsx` - React components in `components/**` - Use **`.js`** for non-JSX files: - `lib/**` utilities and helpers - `app/api/**/route.js` - `models/**` - tests that do not contain JSX --- ## 9. Tests ### 9.1 Unit tests Core tests: - `lib/frontend/routes.test.js` - `lib/frontend/apiClient.test.js` - `lib/frontend/authRedirect.test.js` - `lib/frontend/authMessages.test.js` RBAC tests: - `lib/frontend/rbac/branchAccess.test.js` - `lib/frontend/rbac/branchUiDecision.test.js` Explorer helper tests: - `lib/frontend/explorer/breadcrumbDropdowns.test.js` - `lib/frontend/explorer/errorMapping.test.js` - `lib/frontend/explorer/formatters.test.js` - `lib/frontend/explorer/sorters.test.js` Component SSR smoke test: - `components/app-shell/AppShell.test.js` ### 9.2 Running tests From the repo root: ```bash npx vitest run ``` Optional build check: ```bash npm run build ``` --- ## 10. Manual verification checklist ### 10.1 Local (Docker) Start: ```bash docker compose -f docker-compose.yml -f docker-compose.local.yml up -d --build ``` Verify flows in the browser: - Open a protected route while logged out (e.g. `/NL01/2025/12`) - Expect redirect to `/login?reason=expired&next=/NL01/2025/12` - Invalid login - Expect a German error message (e.g. “Benutzername oder Passwort ist falsch.”) - Valid login - Expect redirect into the protected route - Logout - Expect redirect to `/login?reason=logged-out` RBAC checks: - Branch user: - `/NL01/...` works (own branch) - `/NL02/...` shows Forbidden - invalid params (e.g. `/NL01/abcd`, `/NL01/2024/99/01`) show NotFound Explorer checks: - `/:branch` shows years - `/:branch/:year` shows months - `/:branch/:year/:month` shows days - `/:branch/:year/:month/:day` shows files - Breadcrumb dropdowns: - year dropdown exists on month/day/files levels - month dropdown exists on day/files levels - day dropdown exists on files level ### 10.2 Server Deploy and verify on the server URL. Important cookie note: - Browsers reject `Secure` cookies over HTTP. - Therefore the server `.env.server` must set: ```env SESSION_COOKIE_SECURE=false ``` Verify flows: - Unauthenticated redirect + `next` - Valid login sets cookie and redirects back to `next` - Logout clears session and shows `reason=logged-out` Admin/dev checks: - existing branches render - non-existing branch (e.g. `/NL9999`) shows NotFound (existence validation) --- ## 11. Planned follow-ups - Search UI and filters (`/:branch/search`). - PDF open/view experience (RHL-023). - Admin/dev branch selector in the sidebar. - Smooth navigation / perceived performance improvements: - reduce skeleton/layout shift - client-side caching / prefetching for Explorer drill-down