This document describes the frontend routing scaffold, the application shell layout, and the core navigation UI (Explorer) for the RHL Lieferscheine app.
Timeline:
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).
Public /login route with a functional login form (shadcn/ui primitives).
Protected application shell for all authenticated routes:
Session guard for protected routes:
GET /api/auth/me./login?reason=expired&next=<original-url>.In-shell auth gating (UX improvement):
Logout:
GET /api/auth/logout and redirects to /login?reason=logged-out.UI RBAC (branch-level):
BranchGuard prevents branch users from accessing other branches’ URLs.GET /api/branches.Route param validation (syntactic):
year: YYYYmonth: 01–12day: 01–31notFound() early in layouts.Explorer v2 (Branch → Year → Month → Day → Files):
/:branch → years/:branch/:year → months/:branch/:year/:month → days/:branch/:year/:month/:day → filesBreadcrumb navigation:
Breadcrumb + dropdowns for year/month/day when options are available.Consistent states across Explorer levels:
/:branch/search).Performance polish:
The app uses Next.js App Router Route Groups to separate public and protected UI.
Public: app/(public)
/loginProtected: app/(protected)
| 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:
/search route. Visiting /search matches /:branch with branch = "search".File: app/layout.jsx
Responsibilities:
app/globals.css).File: app/(public)/layout.jsx
Responsibilities:
/login (and potential future public routes).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:
File: components/auth/AuthProvider.jsx
Behavior:
apiClient.getMe().If { user: { ... } }:
authenticatedIf { user: null }:
/login?reason=expired&next=<current-url>The next parameter:
pathname and query stringFile: components/auth/AuthGate.jsx
Behavior:
Files:
app/(public)/login/page.jsx (Server Component)components/auth/LoginForm.jsx (Client Component)Flow:
parseLoginParams(...).If reason is present:
expired → show “session expired” banner (German)logged-out → show “logged out” banner (German)On submit, the form calls apiClient.login({ username, password }).
On success:
next if present/On failure:
Username policy:
UI enforces this policy as well:
autoCapitalize="none" to prevent mobile auto-capsFile: components/auth/LogoutButton.jsx
Flow:
apiClient.logout()./login?reason=logged-out.Files:
components/auth/authContext.jsxcomponents/app-shell/UserStatus.jsxBehavior:
status, user, error).UserStatus renders a short indicator in the TopNav:
Lädt…RHL-021 adds a friendly UI layer on top of backend RBAC:
Backend RBAC remains the source of truth. UI RBAC exists to:
Files:
components/auth/BranchGuard.jsxPure logic:
lib/frontend/rbac/branchAccess.jslib/frontend/rbac/branchUiDecision.jsResponsibilities:
user and status from AuthContext.Enforce branch rules:
branch → allowed only when :branch === user.branchIdadmin / dev → allowed for any branch that existsAdmin/dev branch existence validation:
BranchGuard fetches GET /api/branches and verifies the route branch exists.Fail-open policy:
Files:
lib/frontend/params.jsLayout enforcement (server-side notFound()):
app/(protected)/[branch]/layout.jsx (branch syntax)app/(protected)/[branch]/[year]/layout.jsxapp/(protected)/[branch]/[year]/[month]/layout.jsxapp/(protected)/[branch]/[year]/[month]/[day]/layout.jsxFiles:
components/system/ForbiddenView.jsxapp/(protected)/forbidden/page.jsxWhere Forbidden is shown:
Files:
components/system/NotFoundView.jsxapp/(protected)/not-found.jsxWhere NotFound is shown:
notFound()Provide a simple “file explorer” drill-down:
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.jsxlib/frontend/apiClient.js.A small hook provides consistent query state:
lib/frontend/hooks/useExplorerQuery.jsDesign:
loading | success | errorFiles:
UI component:
components/explorer/breadcrumbs/ExplorerBreadcrumbs.jsxcomponents/explorer/breadcrumbs/SegmentDropdown.jsxPure helpers:
lib/frontend/explorer/breadcrumbDropdowns.jslib/frontend/explorer/formatters.js (German month labels)lib/frontend/explorer/sorters.jsRules:
Dropdowns appear only when options are available:
Fail-open behavior:
Shared Explorer UI building blocks:
components/explorer/ExplorerPageShell.jsxcomponents/explorer/ExplorerSectionCard.jsxcomponents/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 → ForbiddenViewFS_NOT_FOUND → ExplorerNotFoundTable.Shows:
Primary action:
The Explorer + auth UI uses shadcn/ui primitives from components/ui/*.
Required components for the current scope:
cardinputlabelalertbuttonbreadcrumbdropdown-menuskeletontableTo keep the project consistent:
Use .jsx for files that contain JSX:
app/**/page.jsx, app/**/layout.jsxcomponents/**Use .js for non-JSX files:
lib/** utilities and helpersapp/api/**/route.jsmodels/**Core tests:
lib/frontend/routes.test.jslib/frontend/apiClient.test.jslib/frontend/authRedirect.test.jslib/frontend/authMessages.test.jsRBAC tests:
lib/frontend/rbac/branchAccess.test.jslib/frontend/rbac/branchUiDecision.test.jsExplorer helper tests:
lib/frontend/explorer/breadcrumbDropdowns.test.jslib/frontend/explorer/errorMapping.test.jslib/frontend/explorer/formatters.test.jslib/frontend/explorer/sorters.test.jsComponent SSR smoke test:
components/app-shell/AppShell.test.jsFrom the repo root:
npx vitest run
Optional build check:
npm run build
Start:
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)
/login?reason=expired&next=/NL01/2025/12Invalid login
Valid login
Logout
/login?reason=logged-outRBAC checks:
Branch user:
/NL01/... works (own branch)/NL02/... shows Forbidden/NL01/abcd, /NL01/2024/99/01) show NotFoundExplorer checks:
/:branch shows years/:branch/:year shows months/:branch/:year/:month shows days/:branch/:year/:month/:day shows filesBreadcrumb dropdowns:
Deploy and verify on the server URL.
Important cookie note:
Secure cookies over HTTP.Therefore the server .env.server must set:
SESSION_COOKIE_SECURE=false
Verify flows:
nextnextreason=logged-outAdmin/dev checks:
/NL9999) shows NotFound (existence validation)/:branch/search).Smooth navigation / perceived performance improvements: