Переглянути джерело

RHL-022 feat(explorer): enhance frontend documentation with Explorer v2 details and auth improvements

Code_Uwe 1 місяць тому
батько
коміт
6994c96fbd
1 змінених файлів з 315 додано та 286 видалено
  1. 315 286
      Docs/frontend-ui.md

+ 315 - 286
Docs/frontend-ui.md

@@ -8,50 +8,93 @@
 
 <!-- --------------------------------------------------------------------------- -->
 
-# Frontend UI: App Shell, Routing, Login Flow, and UI RBAC (RHL-019 / RHL-020 / RHL-021)
+# Frontend UI: App Shell, Routing, Auth/RBAC, and Explorer (RHL-019 / RHL-020 / RHL-021 / RHL-022)
 
-This document describes the **frontend routing scaffold** and the **application shell layout** for the RHL Lieferscheine app.
+This document describes the **frontend routing scaffold**, the **application shell layout**, and the **core navigation UI (Explorer)** for the RHL Lieferscheine app.
 
-It started as a pure scaffold in **RHL-019** (public vs protected routes + AppShell + placeholder pages), was extended in **RHL-020** with a **real login flow** and a **session guard** for protected routes, and was extended again in **RHL-021** with a **UI-side RBAC guard** plus consistent **Forbidden / NotFound UX**.
+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-021)
+### 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=<original-url>`.
+
+- **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)**:
 
-- Public `/login` route with a functional login form (shadcn/ui primitives).
+  - `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).
 
-- Protected application shell for all other routes.
+- **Route param validation** (syntactic):
 
-- Session guard for the protected area:
+  - `year`: `YYYY`
+  - `month`: `01–12`
+  - `day`: `01–31`
+  - Invalid params trigger `notFound()` early in layouts.
 
-  - checks session via `GET /api/auth/me`
-  - redirects to `/login?reason=expired&next=<original-url>` when unauthenticated
+- **Explorer v2** (Branch → Year → Month → Day → Files):
 
-- Logout button wired to `GET /api/auth/logout`.
+  - `/:branch` → years
+  - `/:branch/:year` → months
+  - `/:branch/:year/:month` → days
+  - `/:branch/:year/:month/:day` → files
 
-- Minimal `UserStatus` that displays session state (role + branch).
+- **Breadcrumb navigation**:
 
-- Centralized helper utilities for auth redirect behavior (`reason` / `next`) and error-to-message mapping.
+  - 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).
 
-**RHL-021 additions:**
+- **Consistent states** across Explorer levels:
 
-- UI-side RBAC guard for branch routes (`BranchGuard`).
-- Consistent **Forbidden** UX for branch mismatches.
-- Consistent **NotFound** UX for invalid route params.
-- Server-side param validation via nested layouts (`notFound()` early).
-- Optional branch existence validation for admin/dev users via `GET /api/branches`.
+  - 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
 
-- Explorer navigation UI (years/months/days lists in sidebar).
-- Search UI and filters.
-- PDF viewer / file open.
-- A UI branch selector for admin/dev users (Sidebar placeholder will later host this).
-- Centralized UI error boundary mapping for API-level errors (e.g. mapping `AUTH_FORBIDDEN_BRANCH` to Forbidden UX inside explorer/search components).
+- 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:
 
-> Note: Prior to RHL-021, “Full RBAC UI guard” was listed as out of scope. It is now implemented for branch routes.
+  - smoother navigation via client-side caching / prefetching
+  - skeleton/layout shift reduction
 
 ---
 
@@ -63,29 +106,29 @@ The app uses Next.js App Router **Route Groups** to separate public and protecte
 
 - **Public**: `app/(public)`
 
-  - Routes that do **not** show the authenticated app shell.
+  - Routes that do **not** show the authenticated AppShell.
   - Current route: `/login`
 
 - **Protected**: `app/(protected)`
 
-  - Routes that render inside the **AppShell**.
+  - Routes that render inside the AppShell.
   - Protected routes are guarded by:
 
-    1. session check (AuthProvider)
+    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 | Only rendered when authenticated                              |
-| `/:branch`                   | Branch placeholder          | Example: `/NL01`                                              |
-| `/:branch/:year`             | Year placeholder            | Example: `/NL01/2025`                                         |
-| `/:branch/:year/:month`      | Month placeholder           | Example: `/NL01/2025/12`                                      |
-| `/:branch/:year/:month/:day` | Day placeholder             | Example: `/NL01/2025/12/31`                                   |
-| `/:branch/search`            | Search placeholder          | Explicit segment so `search` is not interpreted as `:year`    |
-| `/forbidden`                 | Forbidden page              | Optional wrapper route; UI typically renders Forbidden inline |
+| 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:
 
@@ -114,21 +157,22 @@ Responsibilities:
 - Minimal centered layout for public routes.
 - Intended for `/login` (and potential future public routes).
 
-### 3.3 Protected layout (with session guard)
+### 3.3 Protected layout
 
 File: `app/(protected)/layout.jsx`
 
 Responsibilities:
 
-- Wraps all protected pages with the **AppShell**.
-- Wraps the auth provider in a **`<Suspense>` boundary**.
-- Adds the **session guard** via `components/auth/AuthProvider.jsx`.
+- Wrap all protected pages with:
 
-Why the Suspense boundary is required:
+  - `AuthProvider` (session check + redirect)
+  - `AppShell` (stable frame)
+  - `AuthGate` (renders auth loading/error/redirect UI inside the shell)
 
-- The session guard uses Next.js navigation hooks like `useSearchParams()`.
-- When a route is statically prerendered during production builds, `useSearchParams()` causes a CSR bailout unless wrapped by a Suspense boundary.
-- The Suspense fallback ensures the build stays valid while the client hydrates.
+UX rationale:
+
+- We keep the AppShell frame visible while auth/session checks run.
+- This avoids full-screen “blank spinners” on slow connections.
 
 ---
 
@@ -141,11 +185,10 @@ File: `components/auth/AuthProvider.jsx`
 Behavior:
 
 1. On mount, call `apiClient.getMe()`.
-
 2. If `{ user: { ... } }`:
 
    - set auth state to `authenticated`
-   - render the protected UI
+   - render protected UI
 
 3. If `{ user: null }`:
 
@@ -156,7 +199,17 @@ The `next` parameter:
 - includes the original `pathname` and query string
 - is sanitized to avoid open redirects (only internal paths are allowed)
 
-### 4.2 Login page (reason / next)
+### 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:
 
@@ -166,14 +219,12 @@ Files:
 Flow:
 
 1. Login page parses query params using `parseLoginParams(...)`.
-
 2. If `reason` is present:
 
-   - `expired` → show “Session expired” banner
-   - `logged-out` → show “Logged out” banner
+   - `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
@@ -181,17 +232,17 @@ Flow:
 
 5. On failure:
 
-   - show a safe, user-friendly error message
+   - show a safe, user-friendly error message (German)
 
 Username policy:
 
-- The backend stores usernames in lowercase and performs normalization during login.
-- The UI enforces this policy as well:
+- 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.3 Logout
+### 4.4 Logout
 
 File: `components/auth/LogoutButton.jsx`
 
@@ -200,7 +251,7 @@ Flow:
 1. Calls `apiClient.logout()`.
 2. Redirects to `/login?reason=logged-out`.
 
-### 4.4 User status
+### 4.5 User status
 
 Files:
 
@@ -209,365 +260,343 @@ Files:
 
 Behavior:
 
-- AuthProvider provides a minimal auth context (`status`, `user`).
-- `UserStatus` renders a short indicator:
+- AuthProvider provides a minimal auth context (`status`, `user`, `error`).
+- `UserStatus` renders a short indicator in the TopNav:
 
-  - loading → `Loading...`
-  - authenticated → `<role> (<branchId>)` when available
+  - loading → `Lädt…`
+  - authenticated → role + optional branchId
   - unauthenticated/error → fallback text
 
 ---
 
-## 5. Frontend helper modules
+## 5. UI RBAC, Forbidden, and NotFound (RHL-021)
 
-### 5.1 API client
+### 5.1 Goals
 
-File:
-
-- `lib/frontend/apiClient.js`
-
-Rules:
-
-- All UI code must call the backend through this client.
-
-- Defaults:
-
-  - `credentials: "include"`
-  - `cache: "no-store"`
+RHL-021 adds a friendly UI layer on top of backend RBAC:
 
-- Throws `ApiClientError` for standardized backend errors.
+- 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.
 
-- RHL-020 uses:
+Backend RBAC remains the source of truth. UI RBAC exists to:
 
-  - `login({ username, password })`
-  - `logout()`
-  - `getMe()`
+- prevent “obviously forbidden” navigation in the frontend
+- provide clear and consistent UX for end users
 
-### 5.2 Auth redirect helpers (`reason` / `next`)
+### 5.2 BranchGuard (UI-side RBAC)
 
-File: `lib/frontend/authRedirect.js`
+Files:
 
-Provides:
+- `components/auth/BranchGuard.jsx`
+- Pure logic:
 
-- `sanitizeNext(next)` to prevent open redirects.
-- `buildLoginUrl({ reason, next })`.
-- `parseLoginParams(searchParams)`.
+  - `lib/frontend/rbac/branchAccess.js`
+  - `lib/frontend/rbac/branchUiDecision.js`
 
-### 5.3 Auth message mapping
+Responsibilities:
 
-File: `lib/frontend/authMessages.js`
+- Read `user` and `status` from AuthContext.
+- Enforce branch rules:
 
-- Centralized mapping from error codes to user-facing strings.
-- Centralized banner copy for `reason=expired` and `reason=logged-out`.
+  - role `branch` → allowed only when `:branch === user.branchId`
+  - role `admin` / `dev` → allowed for any branch that exists
 
-### 5.4 Frontend route helpers
+Admin/dev branch existence validation:
 
-File: `lib/frontend/routes.js`
+- `BranchGuard` fetches `GET /api/branches` and verifies the route branch exists.
+- Fail-open policy:
 
-- Centralizes URL building.
-- Prevents scattered stringly-typed URLs.
-- Encodes dynamic segments defensively.
+  - If fetching the list fails, do not block rendering.
+  - Backend RBAC and subsequent API calls remain authoritative.
 
----
+### 5.3 Param validation (year/month/day)
 
-## 6. UI primitives (shadcn/ui)
+Files:
 
-The login UI uses shadcn/ui primitives from `components/ui/*`.
+- `lib/frontend/params.js`
 
-Required components for the current scope:
+Layout enforcement (server-side `notFound()`):
 
-- `card`
-- `input`
-- `label`
-- `alert`
+- `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`
 
-These are added to the repository via the shadcn CLI.
+### 5.4 Forbidden UX
 
----
+Files:
 
-## 7. File Naming Convention (.js vs .jsx)
+- `components/system/ForbiddenView.jsx`
+- Optional wrapper route: `app/(protected)/forbidden/page.jsx`
 
-To keep the project consistent and avoid tooling issues:
+Where Forbidden is shown:
 
-- Use **`.jsx`** for files that contain JSX:
+- BranchGuard renders ForbiddenView inline for branch mismatch.
 
-  - `app/**/page.jsx`, `app/**/layout.jsx`
-  - React components in `components/**`
+### 5.5 NotFound UX
 
-- Use **`.js`** for non-JSX files:
+Files:
 
-  - `lib/**` utilities and helpers
-  - `app/api/**/route.js`
-  - `models/**`
-  - tests that do not contain JSX
+- `components/system/NotFoundView.jsx`
+- Protected not-found entry: `app/(protected)/not-found.jsx`
 
-Note:
+Where NotFound is shown:
 
-- `components/auth/authContext.jsx` must be `.jsx` because it renders a JSX Provider.
+- invalid params in layouts via `notFound()`
+- admin/dev: non-existing branches via BranchGuard existence validation
 
 ---
 
-## 8. Tests
+## 6. Explorer v2 (RHL-022)
 
-### 8.1 Unit tests
+### 6.1 UI goal
 
-Existing (RHL-019 / RHL-020):
+Provide a simple “file explorer” drill-down:
 
-- `lib/frontend/routes.test.js` (route builder)
-- `lib/frontend/apiClient.test.js` (client defaults + error mapping)
-- `lib/frontend/authRedirect.test.js` (reason/next parsing + sanitization)
-- `lib/frontend/authMessages.test.js` (UI message mappings)
-- `components/app-shell/AppShell.test.js` (SSR smoke test)
+- Year → Month → Day → Files
 
-RHL-021 additions:
+### 6.2 Explorer pages
 
-- `lib/frontend/params.test.js` (year/month/day/branch param validation)
-- `lib/frontend/rbac/branchAccess.test.js` (pure RBAC decision helper)
-- `lib/frontend/rbac/branchUiDecision.test.js` (UI decision helper: allowed/forbidden/not-found)
+Routes and components:
 
-### 8.2 Running tests
+- `/: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`
 
-From the repo root:
+### 6.3 Data fetching strategy
 
-```bash
-npx vitest run
-```
+- All Explorer pages are **Client Components**.
+- All API calls go through `lib/frontend/apiClient.js`.
+- A small hook provides consistent query state:
 
-Optional build check:
+  - `lib/frontend/hooks/useExplorerQuery.js`
 
-```bash
-npm run build
-```
-
----
-
-## 9. Manual verification checklist
+Design:
 
-### 9.1 Local (Docker)
+- predictable states: `loading | success | error`
+- retry mechanism exposed to the UI
+- no routing side effects inside the hook (routing remains in the page components)
 
-Start:
-
-```bash
-docker compose -f docker-compose.yml -f docker-compose.local.yml up -d --build
-```
+### 6.4 Breadcrumbs (with dropdowns)
 
-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 “Invalid username or password.”
-
-- Valid login
-
-  - Expect redirect into the protected route
+Files:
 
-- Logout
+- UI component:
 
-  - Expect redirect to `/login?reason=logged-out`
+  - `components/explorer/breadcrumbs/ExplorerBreadcrumbs.jsx`
+  - `components/explorer/breadcrumbs/SegmentDropdown.jsx`
 
-RHL-021 checks:
+- Pure helpers:
 
-- Branch user:
+  - `lib/frontend/explorer/breadcrumbDropdowns.js`
+  - `lib/frontend/explorer/formatters.js` (German month labels)
+  - `lib/frontend/explorer/sorters.js`
 
-  - `/NL01/...` works (own branch)
-  - `/NL02/...` shows Forbidden (UI guard)
-  - Invalid params (e.g. `/NL01/abcd`, `/NL01/2024/99/01`) show NotFound
+Rules:
 
-- Admin/dev:
+- Breadcrumb shows the current path: branch → year → month → day.
+- Dropdowns appear only when options are available:
 
-  - Existing branches render
-  - Non-existing branch (e.g. `/NL9999`) shows NotFound (branch existence validation)
+  - years dropdown on month/day/files levels
+  - months dropdown on day/files levels
+  - days dropdown on files level
 
-> Note: Branch existence validation for admin/dev uses `GET /api/branches`. The backend branch listing is subject to a 60s server-side TTL micro-cache (storage module). New branch folders may appear with up to ~60s delay.
+Fail-open behavior:
 
-### 9.2 Server
+- If dropdown option queries fail, the breadcrumb still renders the current path.
 
-Deploy and verify on the server URL.
+### 6.5 Loading / empty / error states
 
-Important cookie note:
+Shared Explorer UI building blocks:
 
-- Browsers reject `Secure` cookies over HTTP.
-- Therefore the server `.env.server` must set:
+- `components/explorer/ExplorerPageShell.jsx`
+- `components/explorer/ExplorerSectionCard.jsx`
+- `components/explorer/states/*`
 
-```env
-SESSION_COOKIE_SECURE=false
-```
+Error mapping:
 
-Verify flows on the server URL:
+- `lib/frontend/explorer/errorMapping.js` maps API client errors to UX outcomes:
 
-- Unauthenticated redirect + `next`
-- Valid login sets cookie and redirects back to `next`
-- Logout clears session and shows `reason=logged-out`
+  - `AUTH_UNAUTHENTICATED` → redirect to login (expired)
+  - `AUTH_FORBIDDEN_BRANCH` → ForbiddenView
+  - `FS_NOT_FOUND` → ExplorerNotFound
+  - other errors → ExplorerError + retry
 
-RHL-021 checks on server:
+### 6.6 Files list
 
-- Branch-user forbidden routes show Forbidden UI.
-- Invalid params show NotFound.
-- Admin/dev branch existence validation matches real NAS branch folders.
+- Uses shadcn/ui `Table`.
+- Shows:
 
----
+  - file name
+  - relative path (desktop column + mobile secondary line)
 
-## 10. Planned follow-ups
+- Primary action:
 
-- HTTPS / reverse proxy deployment (separate ticket)
-- Replace placeholders with Explorer pages (years/months/days + files)
-- Add Search UI and filters
-- Add PDF open/view experience
-- Add admin/dev branch selector and navigation in the sidebar
-- Add centralized UI error mapping for API-level errors (Forbidden vs Session-expired)
+  - “Öffnen” button remains disabled until the PDF endpoint/viewer ticket (RHL-023).
 
 ---
 
-## 11. UI RBAC Guard & Forbidden/NotFound UX (RHL-021)
-
-### 11.1 Goals
+## 7. UI primitives (shadcn/ui)
 
-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.
-- Users should receive clear UX:
+The Explorer + auth UI uses shadcn/ui primitives from `components/ui/*`.
 
-  - Forbidden page for RBAC mismatches
-  - Consistent NotFound for invalid params and unknown branches
+Required components for the current scope:
 
-Backend RBAC remains the source of truth. UI RBAC exists to:
+- `card`
+- `input`
+- `label`
+- `alert`
+- `button`
+- `breadcrumb`
+- `dropdown-menu`
+- `skeleton`
+- `table`
 
-- prevent “obviously forbidden” navigation in the frontend
-- provide clearer, consistent UX for end users
+---
 
-### 11.2 BranchGuard (UI-side RBAC)
+## 8. File Naming Convention (.js vs .jsx)
 
-Files:
+To keep the project consistent:
 
-- `components/auth/BranchGuard.jsx`
-- Pure logic:
+- Use **`.jsx`** for files that contain JSX:
 
-  - `lib/frontend/rbac/branchAccess.js`
-  - `lib/frontend/rbac/branchUiDecision.js`
+  - `app/**/page.jsx`, `app/**/layout.jsx`
+  - React components in `components/**`
 
-Responsibilities:
+- Use **`.js`** for non-JSX files:
 
-- Read `user` and `status` from `AuthContext`.
-- Enforce branch rules:
+  - `lib/**` utilities and helpers
+  - `app/api/**/route.js`
+  - `models/**`
+  - tests that do not contain JSX
 
-  - role `branch` → allowed only when `:branch === user.branchId`
-  - role `admin` / `dev` → allowed for any branch that exists
+---
 
-Guard ordering:
+## 9. Tests
 
-1. **AuthProvider** runs first and ensures we have a valid session (or redirects to login).
-2. **BranchGuard** runs for all routes under `/:branch/...`.
+### 9.1 Unit tests
 
-### 11.3 Admin/dev branch existence validation
+Core tests:
 
-Problem:
+- `lib/frontend/routes.test.js`
+- `lib/frontend/apiClient.test.js`
+- `lib/frontend/authRedirect.test.js`
+- `lib/frontend/authMessages.test.js`
 
-- Without any existence check, an admin could navigate to a syntactically valid branch code that does not exist (e.g. `/NL200`) and still see a placeholder page.
+RBAC tests:
 
-Solution:
+- `lib/frontend/rbac/branchAccess.test.js`
+- `lib/frontend/rbac/branchUiDecision.test.js`
 
-- For `admin` and `dev` users, BranchGuard validates that the route branch exists by calling:
+Explorer helper tests:
 
-  - `GET /api/branches` via `apiClient.getBranches()`
+- `lib/frontend/explorer/breadcrumbDropdowns.test.js`
+- `lib/frontend/explorer/errorMapping.test.js`
+- `lib/frontend/explorer/formatters.test.js`
+- `lib/frontend/explorer/sorters.test.js`
 
-Behavior:
+Component SSR smoke test:
 
-- While the branch list is being fetched, BranchGuard shows a small loader:
+- `components/app-shell/AppShell.test.js`
 
-  - “Validating branch…”
+### 9.2 Running tests
 
-- If the requested `:branch` is not in the returned list:
+From the repo root:
 
-  - show **NotFound** UX
+```bash
+npx vitest run
+```
 
-Fail-open policy:
+Optional build check:
 
-- If fetching the branch list fails (network issues, temporary backend failure):
+```bash
+npm run build
+```
 
-  - BranchGuard does **not** block navigation permanently.
-  - It falls back to allowing the route to render.
-  - Backend RBAC and later API calls still enforce correctness.
+---
 
-Security note:
+## 10. Manual verification checklist
 
-- Branch users do **not** perform existence checks for other branches.
-- If a branch user navigates to a different branch, they see Forbidden regardless of whether the branch exists.
-- This avoids leaking branch existence through UI behavior.
+### 10.1 Local (Docker)
 
-### 11.4 Param validation (year/month/day)
+Start:
 
-Files:
+```bash
+docker compose -f docker-compose.yml -f docker-compose.local.yml up -d --build
+```
 
-- `lib/frontend/params.js`
-- Layout enforcement (server-side `notFound()`):
+Verify flows in the browser:
 
-  - `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`
+- Open a protected route while logged out (e.g. `/NL01/2025/12`)
 
-Rules (syntactic validation only):
+  - Expect redirect to `/login?reason=expired&next=/NL01/2025/12`
 
-- `year`: `YYYY` (4 digits)
-- `month`: `MM` (01–12)
-- `day`: `DD` (01–31)
+- Invalid login
 
-If a param is invalid:
+  - Expect a German error message (e.g. “Benutzername oder Passwort ist falsch.”)
 
-- Next’s `notFound()` is triggered immediately
-- The protected NotFound UI is shown
+- Valid login
 
-Notes:
+  - Expect redirect into the protected route
 
-- This ticket only covers obvious invalid params.
-- “Syntactically valid but missing on disk” (backend `FS_NOT_FOUND`) is handled later in Explorer/Search UI components.
+- Logout
 
-### 11.5 Forbidden UX
+  - Expect redirect to `/login?reason=logged-out`
 
-Files:
+RBAC checks:
 
-- Reusable UI: `components/system/ForbiddenView.jsx`
-- Optional wrapper route: `app/(protected)/forbidden/page.jsx`
+- Branch user:
 
-Where Forbidden is shown:
+  - `/NL01/...` works (own branch)
+  - `/NL02/...` shows Forbidden
+  - invalid params (e.g. `/NL01/abcd`, `/NL01/2024/99/01`) show NotFound
 
-- BranchGuard renders ForbiddenView inline for branch mismatch.
+Explorer checks:
 
-CTAs:
+- `/:branch` shows years
+- `/:branch/:year` shows months
+- `/:branch/:year/:month` shows days
+- `/:branch/:year/:month/:day` shows files
+- Breadcrumb dropdowns:
 
-- Branch users: “Go to my branch” (links to `/${user.branchId}`)
-- Admin/dev: “Go to home” (until a branch list/selector is available)
+  - year dropdown exists on month/day/files levels
+  - month dropdown exists on day/files levels
+  - day dropdown exists on files level
 
-### 11.6 NotFound UX
+### 10.2 Server
 
-Files:
+Deploy and verify on the server URL.
 
-- Reusable UI: `components/system/NotFoundView.jsx`
-- Protected not-found entry: `app/(protected)/not-found.jsx`
+Important cookie note:
 
-Where NotFound is shown:
+- Browsers reject `Secure` cookies over HTTP.
+- Therefore the server `.env.server` must set:
 
-- Invalid params in layouts via `notFound()`.
-- Admin/dev: non-existing branch codes via BranchGuard existence validation.
+```env
+SESSION_COOKIE_SECURE=false
+```
 
-### 11.7 Interaction with backend RBAC errors
+Verify flows:
 
-Even with UI-side RBAC, the backend remains authoritative.
+- Unauthenticated redirect + `next`
+- Valid login sets cookie and redirects back to `next`
+- Logout clears session and shows `reason=logged-out`
 
-Recommended policy for later UI tickets (Explorer/Search):
+Admin/dev checks:
 
-- `AUTH_UNAUTHENTICATED`:
+- existing branches render
+- non-existing branch (e.g. `/NL9999`) shows NotFound (existence validation)
 
-  - let the existing session flow handle it (redirect to `/login?reason=expired`)
+---
 
-- `AUTH_FORBIDDEN_BRANCH`:
+## 11. Planned follow-ups
 
-  - render ForbiddenView for that branch route
+- Search UI and filters (`/:branch/search`).
+- PDF open/view experience (RHL-023).
+- Admin/dev branch selector in the sidebar.
+- Smooth navigation / perceived performance improvements:
 
-This policy can be implemented as a small helper when Explorer/Search UI begins consuming the navigation endpoints.
+  - reduce skeleton/layout shift
+  - client-side caching / prefetching for Explorer drill-down