Просмотр исходного кода

refactor(docs): standardize folder naming and improve API documentation clarity

Code_Uwe 3 недель назад
Родитель
Сommit
c41bfb1905
3 измененных файлов с 353 добавлено и 238 удалено
  1. 38 40
      Docs/api.md
  2. 65 51
      Docs/frontend-api-usage.md
  3. 250 147
      Docs/frontend-ui.md

+ 38 - 40
Docs/api.md

@@ -1,20 +1,10 @@
 <!-- --------------------------------------------------------------------------- -->
 
-<!-- Ordner: Docs -->
+<!-- Ordner: docs -->
 
 <!-- Datei: api.md -->
 
-<!-- Relativer Pfad: Docs/api.md -->
-
-<!-- --------------------------------------------------------------------------- -->
-
-<!-- --------------------------------------------------------------------------- -->
-
-<!-- Folder: Docs -->
-
-<!-- File: api.md -->
-
-<!-- Relative Path: Docs/api.md -->
+<!-- Relativer Pfad: docs/api.md -->
 
 <!-- --------------------------------------------------------------------------- -->
 
@@ -24,7 +14,7 @@ This document describes the HTTP API exposed by the application using Next.js **
 
 All routes below are served under the `/api` prefix.
 
-> Frontend developers: For practical usage examples and the small `apiClient` helper layer, read **`Docs/frontend-api-usage.md`**. That document is the frontend-oriented single source of truth.
+> Frontend developers: For practical usage examples and the small `apiClient` helper layer, read **`docs/frontend-api-usage.md`**. That document is the frontend-oriented single source of truth.
 
 ---
 
@@ -134,37 +124,44 @@ The API uses these machine-readable codes (non-exhaustive list):
   - `AUTH_INVALID_CREDENTIALS`
   - `AUTH_FORBIDDEN_BRANCH`
 
-- Validation:
+- Validation (generic):
 
   - `VALIDATION_MISSING_PARAM`
-
   - `VALIDATION_MISSING_QUERY`
-
   - `VALIDATION_INVALID_JSON`
-
   - `VALIDATION_INVALID_BODY`
-
   - `VALIDATION_MISSING_FIELD`
 
-  - `VALIDATION_BRANCH`
+- Validation (filesystem route params):
 
+  - `VALIDATION_BRANCH`
   - `VALIDATION_YEAR`
-
   - `VALIDATION_MONTH`
-
   - `VALIDATION_DAY`
-
   - `VALIDATION_FILENAME`
-
   - `VALIDATION_FILE_EXTENSION`
-
   - `VALIDATION_PATH_TRAVERSAL`
 
+- Validation (search):
+
+  - `VALIDATION_SEARCH_SCOPE`
+  - `VALIDATION_SEARCH_BRANCH`
+  - `VALIDATION_SEARCH_BRANCHES`
+  - `VALIDATION_SEARCH_DATE`
+  - `VALIDATION_SEARCH_RANGE`
+  - `VALIDATION_SEARCH_LIMIT`
+  - `VALIDATION_SEARCH_CURSOR`
+  - `VALIDATION_SEARCH_MISSING_FILTER`
+
 - Storage:
 
   - `FS_NOT_FOUND`
   - `FS_STORAGE_ERROR`
 
+- Search (backend/provider):
+
+  - `SEARCH_BACKEND_UNAVAILABLE` (provider misconfiguration/unavailability)
+
 - Internal:
 
   - `INTERNAL_SERVER_ERROR`
@@ -596,7 +593,7 @@ Search delivery note content across PDFs.
 The endpoint returns _search hits_ with enough metadata to:
 
 - navigate to the correct Explorer day folder
-- open or download the PDF via the binary file endpoint
+- open the PDF via the binary file endpoint
 
 **Authentication**: required.
 
@@ -609,7 +606,7 @@ The endpoint returns _search hits_ with enough metadata to:
 
 - `admin`/`dev` role:
 
-  - can search across multiple branches (depending on `scope`)
+  - can search across branches using explicit scope parameters
 
 **Query params**
 
@@ -619,31 +616,32 @@ The endpoint returns _search hits_ with enough metadata to:
 
 - `scope` (optional)
 
-  - `branch` — only the current session branch
-  - `all` — all branches accessible to the current session
-  - `multi` — only branches listed in `branches`
-
-  Recommended: be explicit in the UI instead of relying on defaults.
+  - `branch` — single branch scope (admin/dev only; branch users are forced to their own branch)
+  - `all` — all branches (admin/dev only)
+  - `multi` — only the branches listed in `branches` (admin/dev only)
 
 - `branch` (optional)
 
-  - Search within a single branch (subject to RBAC).
+  - Single branch for `scope=branch`.
+  - Branch users may omit `branch` and are forced to their own branch.
 
 - `branches` (optional)
 
-  - Comma-separated branch list for `scope=multi` (subject to RBAC).
+  - Comma-separated branch list for `scope=multi`.
 
 - `from`, `to` (optional)
 
-  - Date filter in `YYYY-MM-DD` format.
+  - Inclusive date filter in `YYYY-MM-DD` format.
 
 - `limit` (optional)
 
   - Page size.
+  - Default: **100**
+  - Allowed: **50..200**
 
 - `cursor` (optional)
 
-  - Pagination cursor returned from the previous response.
+  - Pagination cursor returned by the previous response.
   - Treat as **opaque**.
 
 **Response 200 (example)**
@@ -662,7 +660,8 @@ The endpoint returns _search hits_ with enough metadata to:
 			"snippet": "..."
 		}
 	],
-	"nextCursor": "<opaque>"
+	"nextCursor": "<opaque>",
+	"total": 123
 }
 ```
 
@@ -670,19 +669,20 @@ Notes:
 
 - Result order is typically relevance-based.
 - `nextCursor` is `null` when there are no more results.
+- `total` is the total number of matching items for the current query. It may be `null` if the backend/provider cannot provide a reliable total.
 
 **Error responses**
 
 - `400` for missing/invalid parameters (returns a `VALIDATION_*` code)
 - `401` for unauthenticated access
 - `403` for branch violations (`AUTH_FORBIDDEN_BRANCH`)
-- `500` for internal search/index failures (`INTERNAL_SERVER_ERROR`)
+- `500` for internal search/index failures (`INTERNAL_SERVER_ERROR` or `SEARCH_BACKEND_UNAVAILABLE`)
 
 ---
 
 ## 5. API v1 freeze (RHL-008)
 
-The endpoints and response shapes documented here (and in `Docs/frontend-api-usage.md`) are considered **API v1** for the first frontend implementation.
+The endpoints and response shapes documented here (and in `docs/frontend-api-usage.md`) are considered **API v1** for the first frontend implementation.
 
 Rules:
 
@@ -703,5 +703,3 @@ When adding new endpoints:
 5. Use the standardized error contract (prefer `withErrorHandling` + `ApiError` helpers).
 6. Add route tests (Vitest).
 7. Update this document.
-
-<!-- --------------------------------------------------------------------------- -->

+ 65 - 51
Docs/frontend-api-usage.md

@@ -1,22 +1,10 @@
 <!-- --------------------------------------------------------------------------- -->
 
-<!-- --------------------------------------------------------------------------- -->
-
-<!-- Ordner: Docs -->
+<!-- Ordner: docs -->
 
 <!-- Datei: frontend-api-usage.md -->
 
-<!-- Relativer Pfad: Docs/frontend-api-usage.md -->
-
-<!-- --------------------------------------------------------------------------- -->
-
-<!-- --------------------------------------------------------------------------- -->
-
-<!-- Folder: Docs -->
-
-<!-- File: frontend-api-usage.md -->
-
-<!-- Relative Path: Docs/frontend-api-usage.md -->
+<!-- Relativer Pfad: docs/frontend-api-usage.md -->
 
 <!-- --------------------------------------------------------------------------- -->
 
@@ -28,10 +16,10 @@ Scope:
 
 - Stable **API v1** contracts (URLs, params, response shapes).
 - The minimal frontend `apiClient` helper layer (`lib/frontend/apiClient.js`).
-- Practical examples for building the first UI.
-- PDF streaming/opening behavior in the Explorer (RHL-023).
+- Practical examples for building the UI.
+- PDF streaming/opening behavior in the Explorer and in Search.
 
-> UI developers: For the app shell layout and frontend route scaffold (public vs protected routes, placeholder pages), see **`Docs/frontend-ui.md`**.
+> UI developers: For the app shell layout and frontend route scaffold (public vs protected routes, placeholder pages), see **`docs/frontend-ui.md`**.
 >
 > For UI navigation, prefer centralized route builders from `lib/frontend/routes.js` instead of hardcoding path strings.
 
@@ -43,6 +31,7 @@ Non-goals:
 Notes:
 
 - The backend provides a binary PDF stream/download endpoint (RHL-015). The Explorer integrates it for “Open PDF” (RHL-023).
+- The Search UI integrates the same “Open PDF” pattern (RHL-024).
 
 ---
 
@@ -61,7 +50,7 @@ The project provides a tiny wrapper to enforce those defaults: `lib/frontend/api
 
 ### 1.2 Happy-path navigation flow
 
-The core UI flow is a simple drill-down:
+The core Explorer UI flow is a simple drill-down:
 
 1. `login({ username, password })`
 2. `getMe()` (optional but recommended)
@@ -74,7 +63,7 @@ The core UI flow is a simple drill-down:
 
 Optional (admin/dev):
 
-- use the search endpoint (`GET /api/search`) to implement a global search UI (see section 4.4)
+- use the search endpoint (`GET /api/search`) to implement cross-branch search UI (see section 4.4)
 
 ### 1.3 Example usage (client-side)
 
@@ -198,6 +187,10 @@ Files:
 
 - `getFiles(branch, year, month, day)`
 
+Search:
+
+- `search({ q, branch, scope, branches, from, to, limit, cursor })`
+
 Low-level:
 
 - `apiFetch(path, options)`
@@ -393,7 +386,7 @@ Why it exists:
 
 ##### 4.3.2 Recommended UI usage pattern
 
-In the Explorer table, we open PDFs via navigation:
+In the Explorer (and Search results table), we open PDFs via navigation:
 
 - Use a normal anchor element with `target="_blank"`.
 - This avoids popup-blocker issues and is semantically correct.
@@ -437,7 +430,9 @@ const href = buildPdfDownloadUrl({ branch, year, month, day, filename });
 
 This is a **JSON** endpoint.
 
-You can call it via `apiFetch(...)` (or add a small convenience wrapper in `apiClient` when implementing the Search UI).
+In the frontend, prefer the dedicated wrapper:
+
+- `apiClient.search(...)`
 
 Query params:
 
@@ -449,7 +444,7 @@ Optional filters:
 - `branch`: single branch
 - `branches`: comma-separated branch list (for `scope=multi`)
 - `from`, `to`: `YYYY-MM-DD`
-- `limit`: page size
+- `limit`: page size (default `100`, allowed `50..200`)
 - `cursor`: pagination cursor returned by the previous response
 
 Response shape:
@@ -468,36 +463,44 @@ Response shape:
 			"snippet": "..."
 		}
 	],
-	"nextCursor": "<opaque>"
+	"nextCursor": "<opaque>",
+	"total": 123
 }
 ```
 
+Notes:
+
+- `nextCursor` is `null` when there are no more results.
+- `total` is the total number of matches for the current query and can be shown as “x of y loaded”.
+- `total` may be `null` if the provider cannot provide a reliable total.
+
 Recommended usage (client-side):
 
 ```js
-import { apiFetch } from "@/lib/frontend/apiClient";
-
-export async function searchDeliveryNotes({
-	q,
-	scope,
-	branch,
-	branches,
-	from,
-	to,
-	limit,
-	cursor,
-}) {
-	const params = new URLSearchParams();
-	params.set("q", q);
-	if (scope) params.set("scope", scope);
-	if (branch) params.set("branch", branch);
-	if (branches?.length) params.set("branches", branches.join(","));
-	if (from) params.set("from", from);
-	if (to) params.set("to", to);
-	if (limit) params.set("limit", String(limit));
-	if (cursor) params.set("cursor", cursor);
-
-	return apiFetch(`/api/search?${params.toString()}`);
+import { search, ApiClientError } from "@/lib/frontend/apiClient";
+
+export async function searchDeliveryNotesExample() {
+	try {
+		const res = await search({
+			q: "bridgestone",
+			branch: "NL20",
+			limit: 100,
+		});
+
+		return {
+			items: res.items,
+			nextCursor: res.nextCursor,
+			total: res.total,
+		};
+	} catch (err) {
+		if (err instanceof ApiClientError) {
+			// Example:
+			// - AUTH_UNAUTHENTICATED -> redirect to login
+			// - AUTH_FORBIDDEN_BRANCH -> show forbidden
+			// - VALIDATION_* -> show a friendly input message
+		}
+		throw err;
+	}
 }
 ```
 
@@ -556,6 +559,17 @@ Validation:
 - `VALIDATION_INVALID_BODY`
 - `VALIDATION_MISSING_FIELD`
 
+Search validation:
+
+- `VALIDATION_SEARCH_SCOPE`
+- `VALIDATION_SEARCH_BRANCH`
+- `VALIDATION_SEARCH_BRANCHES`
+- `VALIDATION_SEARCH_DATE`
+- `VALIDATION_SEARCH_RANGE`
+- `VALIDATION_SEARCH_LIMIT`
+- `VALIDATION_SEARCH_CURSOR`
+- `VALIDATION_SEARCH_MISSING_FILTER`
+
 Storage:
 
 - `FS_NOT_FOUND`
@@ -640,14 +654,14 @@ Rules:
 
 ## 9. Out of scope / planned additions
 
-- Search UI and filters (route exists as placeholder: `/:branch/search`).
+- Date range UI for Search (`from` / `to`) and URL sync (planned follow-up after Search UI v1).
 
-  - Backend support exists via `GET /api/search`.
-  - The UI is still considered a planned addition.
+- Optional Search UX improvements:
+
+  - grouping results by date / branch
+  - debounced search (optional; current v1 is explicit submit)
 
 - Optional Explorer UX polish:
 
   - add a dedicated “Herunterladen” UI action (download variant)
   - optional in-app PDF viewer experience (instead of a new tab)
-
-<!-- --------------------------------------------------------------------------- -->

+ 250 - 147
Docs/frontend-ui.md

@@ -1,16 +1,16 @@
 <!-- --------------------------------------------------------------------------- -->
 
-<!-- Folder: Docs -->
+<!-- Ordner: docs -->
 
-<!-- File: frontend-ui.md -->
+<!-- Datei: frontend-ui.md -->
 
-<!-- Relative Path: Docs/frontend-ui.md -->
+<!-- Relativer Pfad: docs/frontend-ui.md -->
 
 <!-- --------------------------------------------------------------------------- -->
 
-# Frontend UI: App Shell, Routing, Auth/RBAC, and Explorer (RHL-019 / RHL-020 / RHL-021 / RHL-022 / RHL-023)
+# Frontend UI: App Shell, Routing, Auth/RBAC, Explorer, and Search (RHL-019 / RHL-020 / RHL-021 / RHL-022 / RHL-023 / RHL-024)
 
-This document describes the **frontend routing scaffold**, the **application shell layout**, and the **core navigation UI (Explorer)** for the RHL Lieferscheine app.
+This document describes the **frontend routing scaffold**, the **application shell layout**, and the core UI modules for the RHL Lieferscheine app.
 
 Timeline:
 
@@ -19,6 +19,7 @@ Timeline:
 - **RHL-021**: UI-side RBAC guard + consistent Forbidden / NotFound UX.
 - **RHL-022**: Explorer v2 (Year → Month → Day → Files) + shadcn Breadcrumbs with dropdowns.
 - **RHL-023**: Explorer file action “Open PDF” using the binary PDF endpoint (opens in a new tab).
+- **RHL-024**: Search UI v1 (URL-driven `q`, scopes, cursor pagination, open PDF + jump to day).
 
 > **Language policy**
 >
@@ -30,7 +31,7 @@ Timeline:
 
 ## 1. Scope
 
-### 1.1 Implemented (as of RHL-023)
+### 1.1 Implemented (as of RHL-024)
 
 - **Public** `/login` route with a functional login form (shadcn/ui primitives).
 
@@ -63,6 +64,7 @@ Timeline:
 
 - **Route param validation** (syntactic):
 
+  - `branch`: `NL` + digits (syntactic validity; existence is validated by BranchGuard for admin/dev)
   - `year`: `YYYY`
   - `month`: `01–12`
   - `day`: `01–31`
@@ -85,27 +87,52 @@ Timeline:
   - Loading states (Skeleton)
   - Empty states
   - Error states with retry
-  - FS_NOT_FOUND mapped to an Explorer “path no longer exists” card
+  - `FS_NOT_FOUND` mapped to an Explorer “path no longer exists” card
 
 - **Explorer leaf action: Open PDF (RHL-023)**
 
   - The file list on `/:branch/:year/:month/:day` provides an **“Öffnen”** action.
-
   - Clicking “Öffnen” opens the selected PDF **in a new browser tab**.
+  - URL construction is centralized in `lib/frontend/explorer/pdfUrl.js`.
+
+- **Search UI v1 (RHL-024)**
+
+  - Route: `/:branch/search` (protected).
+
+  - URL-driven state for shareability:
+
+    - `q` (search query)
+
+    - scope params for admin/dev:
+
+      - single: `branch=NLxx`
+      - multi: `scope=multi&branches=NL06,NL20`
+      - all: `scope=all`
+
+    - `limit` (optional): `50 | 100 | 200` (default 100; only included in URL when non-default)
 
-  - URL construction is centralized in a pure helper:
+  - Cursor-based pagination (`nextCursor`) is **not stored in the URL**.
 
-    - `lib/frontend/explorer/pdfUrl.js` (`buildPdfUrl`, optional `buildPdfDownloadUrl`)
+  - Results support:
 
-  - The PDF endpoint is **binary** (`application/pdf`) and is opened via navigation (`<a target="_blank">`).
+    - Open PDF in a new tab (“Öffnen”)
+    - Navigate to the Explorer day route (“Zum Tag”)
 
-  - The frontend does **not** use `apiClient.apiFetch()` for PDF opening (JSON-centric).
+  - Search results UI includes:
+
+    - Empty/Loading/Error states
+    - Sorting dropdown (Relevanz / Datum / Dateiname)
+    - “Mehr laden” button (cursor pagination)
+    - “x von y” progress indicator when `total` is available
 
 ### 1.2 Still out of scope / planned
 
-- Search UI (route exists as placeholder: `/:branch/search`).
+- Date range UI for Search (`from` / `to`) and URL sync (planned follow-up after Search UI v1).
+
+- Optional Search UX improvements:
 
-- Admin/dev branch selector in the sidebar.
+  - grouping results by date and/or branch
+  - debounced “typeahead” search (current v1 is explicit submit)
 
 - Optional Explorer improvements:
 
@@ -148,7 +175,7 @@ The app uses Next.js App Router **Route Groups** to separate public and protecte
 | `/: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` |
+| `/:branch/search`            | Search UI (v1)              | Explicit segment so `search` is not interpreted as `:year` |
 | `/forbidden`                 | Forbidden page wrapper      | Optional wrapper; Forbidden is typically rendered inline   |
 
 Important:
@@ -197,9 +224,48 @@ UX rationale:
 
 ---
 
-## 4. Authentication UX (RHL-020)
+## 4. App Shell & Top Navigation
+
+### 4.1 AppShell framing
+
+File: `components/app-shell/AppShell.jsx`
+
+AppShell is the stable frame for all protected pages:
+
+- TopNav is always visible.
+- The sidebar is currently a placeholder (reserved for future navigation/filter UI).
+- Route pages render inside the main content area.
+
+### 4.2 Quick navigation (quality-of-life)
 
-### 4.1 Session check for protected routes
+File: `components/app-shell/QuickNav.jsx`
+
+Purpose:
+
+- Reduce manual URL typing during manual testing and daily usage.
+- Provide direct links to:
+
+  - Explorer (`/:branch`)
+  - Search (`/:branch/search`)
+
+Behavior:
+
+- Branch users: QuickNav uses the user’s `branchId`.
+- Admin/dev users:
+
+  - QuickNav loads branches via `GET /api/branches`.
+  - Provides a branch dropdown.
+  - Stores the last selected branch in `localStorage` for convenience.
+
+Responsive behavior:
+
+- QuickNav is hidden on small screens by default (`md` and up only) to keep the header compact.
+
+---
+
+## 5. Authentication UX (RHL-020)
+
+### 5.1 Session check for protected routes
 
 File: `components/auth/AuthProvider.jsx`
 
@@ -221,7 +287,7 @@ 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)
+### 5.2 AuthGate (in-shell gating)
 
 File: `components/auth/AuthGate.jsx`
 
@@ -231,7 +297,7 @@ Behavior:
 - 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)
+### 5.3 Login page (reason / next)
 
 Files:
 
@@ -266,7 +332,7 @@ Username policy:
   - username input is normalized to lowercase
   - `autoCapitalize="none"` to prevent mobile auto-caps
 
-### 4.4 Logout
+### 5.4 Logout
 
 File: `components/auth/LogoutButton.jsx`
 
@@ -275,27 +341,11 @@ 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)
+## 6. UI RBAC, Forbidden, and NotFound (RHL-021)
 
-### 5.1 Goals
+### 6.1 Goals
 
 RHL-021 adds a friendly UI layer on top of backend RBAC:
 
@@ -308,7 +358,7 @@ 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)
+### 6.2 BranchGuard (UI-side RBAC)
 
 Files:
 
@@ -334,7 +384,7 @@ Admin/dev branch existence validation:
   - 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.3 Param validation (year/month/day)
 
 Files:
 
@@ -347,40 +397,17 @@ Layout enforcement (server-side `notFound()`):
 - `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) + PDF Open (RHL-023)
+## 7. Explorer v2 (RHL-022) + PDF Open (RHL-023)
 
-### 6.1 UI goal
+### 7.1 UI goal
 
 Provide a simple “file explorer” drill-down:
 
 - Year → Month → Day → Files
 
-### 6.2 Explorer pages
+### 7.2 Explorer pages
 
 Routes and components:
 
@@ -389,7 +416,7 @@ Routes and components:
 - `/:branch/:year/:month` → `components/explorer/levels/DaysExplorer.jsx`
 - `/:branch/:year/:month/:day` → `components/explorer/levels/FilesExplorer.jsx`
 
-### 6.3 Data fetching strategy
+### 7.3 Data fetching strategy
 
 - All Explorer pages are **Client Components**.
 - All JSON API calls go through `lib/frontend/apiClient.js`.
@@ -397,13 +424,7 @@ Routes and components:
 
   - `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)
+### 7.4 Breadcrumbs (with dropdowns)
 
 Files:
 
@@ -418,37 +439,7 @@ Files:
   - `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 (leaf route) and “Open PDF”
+### 7.5 Files list (leaf route) and “Open PDF”
 
 Leaf route:
 
@@ -476,17 +467,119 @@ Implementation notes:
 
 - The PDF endpoint is binary (`application/pdf`). The UI uses navigation (`<a target="_blank">`) instead of `apiClient`.
 
-- The filename segment must be URL-encoded (handled by `buildPdfUrl(...)`).
+---
+
+## 8. Search UI v1 (RHL-024)
+
+### 8.1 Route and state model
+
+Route:
+
+- `/:branch/search`
+
+State policy:
+
+- Search state is **URL-driven** to support shareable links.
+- Cursor-based pagination state is **not shareable** and is kept in client state.
+
+Shareable params (first page identity):
+
+- `q` (string)
+
+- Scope params:
+
+  - single branch: `branch=NLxx`
+  - multi: `scope=multi&branches=NL06,NL20`
+  - all: `scope=all`
+
+- `limit`:
+
+  - allowed values: `50 | 100 | 200`
+  - default: `100`
+  - only written to URL when non-default to keep URLs readable
+
+Pagination:
+
+- `nextCursor` is kept in client state.
+- “Mehr laden” triggers a request with `cursor=nextCursor` and appends results.
+
+### 8.2 Roles and scope UI
+
+Branch users:
+
+- No scope selector.
+- The UI always searches within the current route branch.
+
+Admin/dev users:
+
+- Scope selector:
+
+  - “Diese Niederlassung” (single branch route context)
+  - “Mehrere Niederlassungen” (multi)
+  - “Alle Niederlassungen” (all)
+
+- Multi branch selection:
+
+  - simple checkbox list
+  - branch list is fetched from `GET /api/branches`
+  - fail-open behavior: if branch list cannot be loaded, the rest of the Search UI remains usable
+
+### 8.3 Result list and actions
+
+Search results show at least:
 
-Accessibility:
+- Branch (for multi/all)
+- Date (German formatted)
+- Filename
+- Relative path
+- Optional snippet (if returned by the provider)
 
-- The “Öffnen” action uses an `aria-label` like `PDF öffnen: <filename>`.
+Actions:
+
+- “Öffnen”
+
+  - uses `buildPdfUrl(...)`
+  - opens the binary endpoint in a new tab (`<a target="_blank" rel="noopener noreferrer">`)
+
+- “Zum Tag”
+
+  - navigates to `/:branch/:year/:month/:day` using `dayPath(...)`
+
+Sorting:
+
+- “Relevanz” (backend order)
+- “Datum (neueste zuerst)”
+- “Dateiname (A–Z)”
+
+Totals:
+
+- When the backend returns `total`, the UI shows “x von y Treffern geladen”.
+- If `total` is `null`, the UI falls back to showing only the loaded count.
+
+### 8.4 Error handling (Search)
+
+Search uses a dedicated error mapping helper:
+
+- `lib/frontend/search/errorMapping.js`
+
+Mapping principles:
+
+- Use stable error codes for UX decisions.
+- Do not leak raw backend messages to users.
+- Keep user-facing messages German.
+
+Key outcomes:
+
+- `AUTH_UNAUTHENTICATED` → redirect to login with `reason=expired&next=<current-url>`
+- `AUTH_FORBIDDEN_BRANCH` → show Forbidden UX
+- `VALIDATION_*` → show a user-friendly German validation message
+- network/unknown → generic German error + retry
 
 ---
 
-## 7. UI primitives (shadcn/ui)
+## 9. UI primitives (shadcn/ui)
 
-The Explorer + auth UI uses shadcn/ui primitives from `components/ui/*`.
+The Explorer + auth + search UI uses shadcn/ui primitives from `components/ui/*`.
 
 Required components for the current scope:
 
@@ -502,7 +595,7 @@ Required components for the current scope:
 
 ---
 
-## 8. File Naming Convention (.js vs .jsx)
+## 10. File Naming Convention (.js vs .jsx)
 
 To keep the project consistent:
 
@@ -520,9 +613,9 @@ To keep the project consistent:
 
 ---
 
-## 9. Tests
+## 11. Tests
 
-### 9.1 Unit tests
+### 11.1 Unit tests
 
 Core tests:
 
@@ -544,11 +637,19 @@ Explorer helper tests:
 - `lib/frontend/explorer/sorters.test.js`
 - `lib/frontend/explorer/pdfUrl.test.js` (RHL-023)
 
+Search helper tests:
+
+- `lib/frontend/search/urlState.test.js`
+- `lib/frontend/search/errorMapping.test.js`
+- `lib/frontend/search/normalizeState.test.js`
+- `lib/frontend/search/searchApiInput.test.js`
+- `lib/frontend/search/resultsSorting.test.js`
+
 Component SSR smoke test:
 
 - `components/app-shell/AppShell.test.js`
 
-### 9.2 Running tests
+### 11.2 Running tests
 
 From the repo root:
 
@@ -564,9 +665,9 @@ npm run build
 
 ---
 
-## 10. Manual verification checklist
+## 12. Manual verification checklist
 
-### 10.1 Local (Docker)
+### 12.1 Local (Docker)
 
 Start:
 
@@ -592,14 +693,6 @@ Verify flows in the browser:
 
   - 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
@@ -607,46 +700,56 @@ Explorer checks:
 - `/:branch/:year/:month` shows days
 - `/:branch/:year/:month/:day` shows files
 
-PDF open (RHL-023):
+PDF open:
 
-- On `/:branch/:year/:month/:day`, click **“Öffnen”** on multiple files
+- On `/:branch/:year/:month/:day`, click **“Öffnen”**
 
   - Expected: opens the PDF in a new tab
   - Expected: works for filenames with spaces and special characters (URL-encoding)
 
-### 10.2 Server
+Search checks:
 
-Deploy and verify on the server URL.
+- `/NL01/search`
 
-Verify flows:
+  - empty state
+  - submit triggers URL update and first-page fetch
 
-- Unauthenticated redirect + `next`
-- Valid login sets cookie and redirects back to `next`
-- Logout clears session and shows `reason=logged-out`
+- admin/dev:
 
-Admin/dev checks:
+  - scope switching (single/multi/all)
+  - multi selection (checkboxes)
+  - limit switching (50/100/200)
 
-- existing branches render
-- non-existing branch (e.g. `/NL9999`) shows NotFound (existence validation)
+- pagination:
 
-PDF open (RHL-023):
+  - “Mehr laden” appends results when `nextCursor` exists
 
-- Repeat the local PDF open checks against real NAS data
+### 12.2 Server
 
----
+Deploy and verify on the server URL.
 
-## 11. Planned follow-ups
+Verify:
 
-- Search UI and filters (`/:branch/search`).
+- Explorer navigation and PDF open
+- Search UI:
 
-- Optional Explorer UI enhancements:
+  - scopes
+  - limit selection
+  - total count (“x von y geladen”)
+  - open PDF / jump to day
+
+---
 
-  - add a secondary action “Herunterladen” (download variant)
-  - optional in-app PDF viewer experience (instead of a new tab)
+## 13. Planned follow-ups
 
-- Admin/dev branch selector in the sidebar.
+- Search date range UI (`from` / `to`) with shareable URL sync.
 
-- Smooth navigation / perceived performance improvements:
+- Optional Search UX improvements:
+
+  - grouping results by date / branch
+  - query presets
+
+- Optional Explorer improvements:
 
-  - reduce skeleton/layout shift
-  - client-side caching / prefetching for Explorer drill-down
+  - add “Herunterladen” action
+  - optional in-app PDF viewer