frontend-ui.md 17 KB

Frontend UI: App Shell, Routing, Auth/RBAC, and Explorer (RHL-019 / RHL-020 / RHL-021 / RHL-022 / RHL-023)

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.
  • RHL-023: Explorer file action “Open PDF” using the binary PDF endpoint (opens in a new tab).

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-023)

  • 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):

    • 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
  • 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 a pure helper:

    • lib/frontend/explorer/pdfUrl.js (buildPdfUrl, optional buildPdfDownloadUrl)

    • The PDF endpoint is binary (application/pdf) and is opened via navigation (<a target="_blank">).

    • The frontend does not use apiClient.apiFetch() for PDF opening (JSON-centric).

1.2 Still out of scope / planned

  • Search UI (route exists as placeholder: /:branch/search).

  • Admin/dev branch selector in the sidebar.

  • Optional Explorer improvements:

    • “Herunterladen” action (download variant) next to “Öffnen”
    • a dedicated in-app PDF viewer UI (instead of a new tab)
  • Perceived performance polish:

    • client-side caching/prefetch
    • 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=<current-url>

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) + PDF Open (RHL-023)

6.1 UI goal

Provide a simple “file explorer” drill-down:

  • Year → Month → Day → Files

6.2 Explorer pages

Routes and components:

  • /:branchcomponents/explorer/levels/YearsExplorer.jsx
  • /:branch/:yearcomponents/explorer/levels/MonthsExplorer.jsx
  • /:branch/:year/:monthcomponents/explorer/levels/DaysExplorer.jsx
  • /:branch/:year/:month/:daycomponents/explorer/levels/FilesExplorer.jsx

6.3 Data fetching strategy

  • All Explorer pages are Client Components.
  • All JSON 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 (leaf route) and “Open PDF”

Leaf route:

  • /:branch/:year/:month/:day

Files list behavior:

  • Uses shadcn/ui Table.
  • Shows:

    • file name
    • relative path (desktop column + mobile secondary line)

Primary file action:

  • “Öffnen” opens the PDF in a new browser tab via the binary PDF endpoint:

    • GET /api/files/:branch/:year/:month/:day/:filename

Implementation notes:

  • URL construction is centralized in:

    • lib/frontend/explorer/pdfUrl.js
  • 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(...)).

Accessibility:

  • The “Öffnen” action uses an aria-label like PDF öffnen: <filename>.

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
  • lib/frontend/explorer/pdfUrl.test.js (RHL-023)

Component SSR smoke test:

  • components/app-shell/AppShell.test.js

9.2 Running tests

From the repo root:

npx vitest run

Optional build check:

npm run build

10. Manual verification checklist

10.1 Local (Docker)

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)

    • 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

PDF open (RHL-023):

  • On /:branch/:year/:month/:day, click “Öffnen” on multiple files

    • Expected: opens the PDF in a new tab
    • Expected: works for filenames with spaces and special characters (URL-encoding)

10.2 Server

Deploy and verify on the server URL.

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)

PDF open (RHL-023):

  • Repeat the local PDF open checks against real NAS data

11. Planned follow-ups

  • Search UI and filters (/:branch/search).

  • Optional Explorer UI enhancements:

    • add a secondary action “Herunterladen” (download variant)
    • optional in-app PDF viewer experience (instead of a new tab)
  • Admin/dev branch selector in the sidebar.

  • Smooth navigation / perceived performance improvements:

    • reduce skeleton/layout shift
    • client-side caching / prefetching for Explorer drill-down