This document describes the frontend routing scaffold, the application shell layout, and the core UI modules for the RHL Lieferscheine app.
Timeline:
q, scopes, cursor pagination, open PDF + jump to day).Language policy
- 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.User dropdown menu (RHL-032):
Profile password change (RHL-009):
/profile (protected)apiClient.changePassword({ currentPassword, newPassword }).Global toast notifications (Sonner):
lib/frontend/ui/toast.js) to keep copy and behavior consistent.UI RBAC (branch-level):
BranchGuard prevents branch users from accessing other branches’ URLs.GET /api/branches.Route param validation (syntactic):
branch: NL + digits (syntactic validity; existence is validated by BranchGuard for admin/dev)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:
FS_NOT_FOUND mapped to an Explorer “path no longer exists” cardExplorer leaf action: Open PDF (RHL-023)
/:branch/:year/:month/:day provides an “Öffnen” action.lib/frontend/explorer/pdfUrl.js.Search UI (RHL-024) + Scope UX improvements (RHL-037)
/:branch/search (protected).q, scope, branches, limit, from, to.nextCursor) is not stored in the URL.Search date range filter (RHL-025)
from and to.TopNav / navigation polish (RHL-032)
Email-based password reset/recovery:
Optional Search UX improvements:
from/to even when q is empty) if desired laterOptional Explorer improvements:
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 |
/profile |
Profile | Password change is implemented here (RHL-009) |
/: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 UI (v1) | 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:
AuthProvider (session check + redirect)AppShell (stable frame)AuthGate (renders auth loading/error/redirect UI inside the shell)UX rationale:
File: components/app-shell/AppShell.jsx
AppShell is the stable frame for all protected pages:
On very wide screens the UI should remain readable.
Current strategy (implemented in AppShell.jsx + TopNav.jsx):
2xl+).Below 2xl:
File: components/app-shell/TopNav.jsx
TopNav is a sticky header and is the primary navigation surface.
Layout groups (left → right):
/ (light + dark variants).Design note:
Asset convention:
public/brand/.dark: classes.File: components/app-shell/ThemeToggleButton.jsx
next-themes.File: components/app-shell/SessionIndicator.jsx
Goal:
Policy:
title attributes to avoid double tooltips.TooltipProvider is mounted at TopNav scope so all triggers share the same delay configuration.File: components/app-shell/UserStatus.jsx
Menu items (German):
/profile (account info + password change)mailto: link to info@attus.de.Support mailto guidelines:
mailto: query string with encodeURIComponent (not URLSearchParams) to avoid “+” rendering issues in some mail clients.File: components/app-shell/QuickNav.jsx
Purpose:
Provide direct links to:
/:branch)/:branch/search)For admin/dev: enable quick branch switching while preserving the current “context”.
Behavior:
Branch users:
branchId.Admin/dev users:
GET /api/branches.localStorage (rhl_last_branch) for convenience.selectedBranch stable and avoids update loops (guarded initialization).Branch switching rule (RHL-037):
Selecting a branch navigates to the same section while replacing the first path segment.
/NL32/2025/12/31 → /NL20/2025/12/31
Search route preserved and shareable params preserved:
/NL32/search?q=x&scope=multi&branches=NL06,NL20&limit=200&from=...&to=...
→ /NL20/search?q=x&scope=multi&branches=NL06,NL20&limit=200&from=...&to=...
Implementation notes:
lib/frontend/quickNav/branchSwitch.js.useSearchParams() inside QuickNav for “current query string” access.
window.location.search at click-time instead (client-only).Invalid branch routes (admin/dev) (RHL-032):
/NL200):
File: components/auth/AuthProvider.jsx
Behavior:
On mount, call apiClient.getMe().
If { user: { ... } }:
authenticatedIf { user: null }:
/login?reason=expired&next=<current-url>Notes:
GET /api/auth/me returns minimal identity data for the UI:
userId, role, branchId, and (optionally) email.The next parameter:
pathname and query stringFile: components/auth/AuthGate.jsx
Behavior:
RHL-032 note:
Files:
app/(public)/login/page.jsx (Server Component)components/auth/LoginForm.jsx (Client Component)Flow:
Login page parses query params using 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:
autoCapitalize="none" to prevent mobile auto-capsRHL-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.jsxlib/frontend/rbac/branchAccess.jslib/frontend/rbac/branchUiDecision.jsResponsibilities:
user and status from AuthContext.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.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.jsxProvide 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.lib/frontend/hooks/useExplorerQuery.jsFiles:
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.jsLeaf route:
/:branch/:year/:month/:dayFiles list behavior:
Table.Primary file action:
GET /api/files/:branch/:year/:month/:day/:filenameImplementation notes:
lib/frontend/explorer/pdfUrl.jsRoute:
/:branch/searchSingle Source of Truth rule (RHL-037):
/:branch/search is the source of truth for the current branch context.branch= for Single.URL-driven state policy:
Shareable params (first page identity):
q (string)
Scope params:
scope param required; route branch defines the branch.scope=multi&branches=NL06,NL20 (deterministic)scope=alllimit:
50 | 100 | 200100from/to (RHL-025):
YYYY-MM-DDGoal:
Approach:
useDebouncedVisibility(...) and centralized timing constants.Files:
Timing constants: lib/frontend/ui/uxTimings.js
LOADING_UI_DELAY_MSSESSION_INDICATOR_DELAY_MSSESSION_INDICATOR_MIN_VISIBLE_MSTOOLTIP_DELAY_MSDebounce hook: lib/frontend/hooks/useDebouncedVisibility.js
Applied to:
The project uses Sonner (shadcn/ui integration) for toast notifications.
app/layout.jsx (root layout) and respects the current theme.lib/frontend/ui/toast.js.Wrapper functions:
notifySuccess(...), notifyError(...), notifyInfo(...), notifyWarning(...), notifyLoading(...)notifyApiError(err, ...) for consistent mapping of ApiClientError to safe German copyRationale:
toast.* calls./profile (protected)components/profile/ProfilePage.jsx
Note:
Email changes are not supported in the UI; email is managed centrally (IT / developers).
components/profile/ChangePasswordCard.jsx
currentPasswordnewPasswordconfirmNewPassword
Uses inline validation for:
required fields
confirmation mismatch
new password equals current password
Calls apiClient.changePassword({ currentPassword, newPassword }).
Uses Sonner toasts for success/error feedback.
The backend enforces an explicit password policy (see docs/auth.md).
The frontend displays policy hints and maps VALIDATION_WEAK_PASSWORD details to user-friendly German hints.
Helper module:
lib/frontend/profile/passwordPolicyUi.jsThe Explorer + auth + search UI uses shadcn/ui primitives from components/ui/*.
Core primitives:
cardinputlabelalertbuttonbreadcrumbdropdown-menuskeletontableAdditional primitives used for Search scope UX:
popovercommandbadgecalendar (react-day-picker wrapper)tooltip (RHL-032)Radix integration note:
DropdownMenuTrigger, TooltipTrigger, …) require the trigger element to support ref forwarding.components/ui/button.jsx forwards refs to remain compatible with Radix asChild usage.To 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.jslib/frontend/explorer/pdfUrl.test.js (RHL-023)Search helper tests:
lib/frontend/search/urlState.test.jslib/frontend/search/errorMapping.test.jslib/frontend/search/normalizeState.test.jslib/frontend/search/searchApiInput.test.jslib/frontend/search/resultsSorting.test.jslib/frontend/search/dateRange.test.js (RHL-025)lib/frontend/search/datePresets.test.js (RHL-025)lib/frontend/search/dateRangePickerUtils.test.js (RHL-025)lib/frontend/search/searchDateValidation.test.js (RHL-025)lib/frontend/search/dateFilterValidation.test.js (RHL-025)QuickNav helper tests:
lib/frontend/quickNav/branchSwitch.test.jsComponent SSR smoke test:
components/app-shell/AppShell.test.jsPassword change / toast helpers:
lib/auth/passwordPolicy.test.jslib/frontend/profile/passwordPolicyUi.test.jslib/frontend/ui/toast.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/12Valid login
Logout
/login?reason=logged-outProfile checks (RHL-009):
Open /profile
Change password:
Navigation/TopNav checks (RHL-032):
/NL200) shows warning + recovery item.Explorer checks:
/:branch shows years/:branch/:year shows months/:branch/:year/:month shows days/:branch/:year/:month/:day shows filesSearch checks:
/NL01/search
admin/dev:
from/to in the URLDebounced loading UI (RHL-032):
Deploy and verify on the server URL.
Verify:
Explorer navigation and PDF open
Search UI:
Profile / password change:
Optional Search UX improvements:
Optional Explorer improvements:
Password reset / recovery: