Przejdź do treści

SmartBox Operator Panel (Next.js)

📖 Online: docs.smartbox.ergoflow.app/components/frontend-web/ · Live panel: smartbox.ergoflow.app

Web UI for the SmartBox locker system — login, shipments, machines, users, audit log, kiosk OTA management. Built on Next.js 15 App Router with role-aware navigation, glass-morphism dark theme, and next-intl PL/EN segment routing.

  • Public endpoint: https://smartbox.ergoflow.app
  • Stack: Next.js 15 · React 19 · TypeScript 5 · Tailwind CSS 3.4 · next-intl 3.26 · Recharts 2 · qrcode.react 4 · sonner · lucide-react · date-fns · Node 22+ (server-paginated tables — TanStack React Table was retired when row counts started hanging the page)
  • Auth: Keycloak — direct grant (today) → OIDC code flow + PKCE (etap 8)
  • Container: standalone Next output, multi-stage Dockerfile, port 3001

Layout

frontend-web/ ├── messages/ i18n bundles │ ├── pl.json ~28 kB — every UI string in Polish │ └── en.json ~27 kB — English mirror ├── public/ static assets (logos, ErgoFlow bubbles, flags) ├── src/ │ ├── middleware.ts auth gate + silent refresh + locale routing │ ├── i18n.ts next-intl request config │ ├── app/ │ │ ├── api/auth/ server route handlers (login / refresh / logout / callback) │ │ ├── [locale]/ │ │ │ ├── login/page.tsx public login screen │ │ │ └── dashboard/ │ │ │ ├── page.tsx home (welcome hero + stat cards + recent shipments) │ │ │ ├── profile/page.tsx identity + change-password │ │ │ ├── my-shipments/ personal shipments list + detail │ │ │ ├── my-credentials/ own PIN / RFID / MOBILE_TOKEN │ │ │ ├── lockers/ │ │ │ │ ├── parcel/shipments/ all shipments (ADMIN + COURIER) │ │ │ │ ├── parcel/emergency/ emergency-open with reason (ADMIN) │ │ │ │ ├── keys/ placeholder (ADMIN) │ │ │ │ └── documents/ placeholder (ADMIN) │ │ │ └── system/ │ │ │ ├── machines/ machines table + detail (ADMIN + COURIER read-only) │ │ │ ├── users/ user CRUD (ADMIN, server-paginated) │ │ │ ├── releases/ OTA upload + assignment (ADMIN) │ │ │ ├── integrations/ │ │ │ │ └── ergoflow/ ErgoFlow upstream — config + sync + db viewer + history (ADMIN) │ │ │ └── audit/ audit log viewer (ADMIN) │ ├── components/ ~44 React components — see "Components" below │ └── lib/ │ ├── backend.ts typed `api<T>(path)` + `getMe()` + cookie helpers │ ├── navigation.ts next-intl Link + redirect with auto-locale │ ├── types.ts hand-written DTO types (mirrors backend) │ └── version.ts APP_VERSION constant — bumped per chunk ├── Dockerfile multi-stage; runs as non-root `nextjs` user ├── next.config.ts `output: 'standalone'`, strict mode ├── tailwind.config.ts theme — ink palette + cyan/leaf accents + glass shadows └── package.json scripts: dev / build / start / lint

Pages

Public

Route Purpose
/[locale]/login login card (email + password → Keycloak direct grant); LavaBackground orbiting bubbles; PL/EN flag picker

Authenticated (any role)

Route Purpose
/[locale]/dashboard home — welcome hero, 3 stat cards, recent shipments list (5 pinned)
/[locale]/dashboard/profile identity card + change-password (verify-current-then-set via Keycloak direct-grant probe)
/[locale]/dashboard/my-shipments personal shipments table
/[locale]/dashboard/my-shipments/[id] personal shipment detail (timeline + QR + people)
/[locale]/dashboard/my-credentials own PIN / RFID / MOBILE_TOKEN (with one-time plain value on add)

Operator scope (ADMIN + COURIER)

Route Purpose Access
/[locale]/dashboard/lockers/parcel/shipments all shipments table + new-shipment dialog + assign-locker + cancel ADMIN, COURIER
/[locale]/dashboard/lockers/parcel/shipments/[id] shipment detail (timeline + QR + assign + cancel) ADMIN, COURIER
/[locale]/dashboard/lockers/parcel/emergency emergency open with reason (spawns OPEN_LOCKER task) ADMIN
/[locale]/dashboard/lockers/keys placeholder for KEY_RENTAL flow (etap 7) ADMIN
/[locale]/dashboard/lockers/documents placeholder for DOCUMENT flow (etap 7) ADMIN

System (ADMIN-only, COURIER gets read-only on machines)

Route Purpose
/[locale]/dashboard/system/users users table (role / active filters, server-paginated, ErgoFlow source badge for @ergoflow.local accounts), new-user modal
/[locale]/dashboard/system/users/[id] user detail — admin panel (role / active / reset-pw) + credentials list + recent shipments
/[locale]/dashboard/system/machines machines table with Stan column (Pracuje / Pobiera X% / Instaluje / Wyłączona / Serwis)
/[locale]/dashboard/system/machines/[id] machine detail — Edit form, locker grid, emergency open, system info card, telemetry chart, release status
/[locale]/dashboard/system/releases kiosk OTA — upload + list + per-machine assignment + delete
/[locale]/dashboard/system/integrations/ergoflow/sync ErgoFlow integration — config form, schedule toggle, manual "Sync now" with live progress bar + toast, last-run summary
/[locale]/dashboard/system/integrations/ergoflow/db-users mirror users table (server-paginated) with deep link to the SmartBox User profile when JIT-provisioned
/[locale]/dashboard/system/integrations/ergoflow/db-groups mirror groups table + members drilldown
/[locale]/dashboard/system/integrations/ergoflow/history sync run history (auto-refresh 5 s) — added/updated/skipped + duration
/[locale]/dashboard/system/audit audit log with filters (actor type / action / entity type / entity id) + expandable JSON payload

API route handlers

Route Purpose
POST /api/auth/login Keycloak password grant → set httpOnly sb_access + sb_refresh cookies
GET /api/auth/refresh swap sb_refresh for new tokens; called by middleware on access-token expiry
GET /api/auth/logout clear cookies, redirect /login
GET /api/auth/callback OIDC code-flow placeholder (etap 8)

Components

44 components in src/components/. The meaningful ones:

Layout & navigation

  • sidebar-shell.tsx — full-width topbar + collapsible sidebar layout
  • sidebar.tsx — role-gated nav (Personal items always · Lockers for ADMIN/COURIER · System for ADMIN)
  • topbar.tsx — locale switcher + user menu + version badge
  • user-menu.tsx — dropdown (profile, logout)
  • version-badge.tsx — floating bottom-right SmartBox vX.YZ chip from lib/version.ts

Auth

  • login-card.tsx — login form + session_expired query handling

Backgrounds

  • lava-background.tsx — 6 orbiting ErgoFlow bubbles, 30–44 s phase-offset cycles, used on login + dashboard
  • dashboard-background.tsx — subtle dashboard backdrop

Shipments

  • shipments-table.tsx — server-paginated table; URL query params (?search&status&page&perPage) drive backend findMany + count; status filter chips (CREATED / ASSIGNED / DEPOSITED / PICKED_UP / CANCELLED / EXPIRED), Active only / All quick toggles
  • shipment-detail.tsx — header (TrackingId + StatusBadge), timeline, people, placement, QR, meta
  • shipment-timeline.tsx — vertical timeline of state transitions
  • new-shipment.tsx — dialog (recipient + size + notes)
  • cancel-shipment.tsx — confirm dialog
  • assign-locker-form.tsx — auto-pick first FREE locker (or specify lockerId / preferredSize)
  • tracking-id.tsx — renders SB-{INSTANCE}-{12d} with the last 6 digits in bold (the suffix couriers type into the kiosk)

Lockers & machines

  • machines-table.tsx — list with status dot + Stan operational badge + system-info button
  • machine-status-dot.tsx — ONLINE / OFFLINE / MAINTENANCE colour dot
  • register-machine.tsx — register + show plain API key ONCE
  • edit-machine.tsx — edit name / location / inactivity timings / SSH credentials (admin-only with eye toggle)
  • machine-system-info.tsx — IP (LAN + external) + admin login / password (ADMIN-only, plain, eye toggle)
  • machine-metrics-chart.tsx — Recharts 6 h CPU/RAM/disk three-line chart
  • locker-grid.tsx — colour-coded grid (green=FREE, cyan=RESERVED, amber=OCCUPIED, rose=BROKEN), per-cell force-open
  • locker-config-grid.tsx — admin config grid for allowedFunctions + modbusAddress
  • locker-stats-banner.tsx — per-function / size / status occupancy summary

Users & credentials

  • users-table.tsx — server-paginated table with role / active filters and inline <ErgoflowSourceBadge> for @ergoflow.local accounts (deep link to ErgoFlow integration page)
  • new-user.tsx — modal — email + fullName + role; backend returns temp password ONCE
  • user-admin-panel.tsx — change role / active / reset-password
  • user-credentials-list.tsx — list with per-row deactivate / delete
  • new-credential.tsx — type chips (PIN / RFID / MOBILE_TOKEN), generate button, plain value shown ONCE

ErgoFlow integration

  • ergoflow-config-form.tsx — URL / email / password / enabled / intervalMinutes form; persists via PATCH /integrations/ergoflow/config
  • ergoflow-sync-button.tsx — fires POST /integrations/ergoflow/sync and shows a toast immediately (fire-and-forget)
  • ergoflow-provision-progress.tsx — polls /config every 2 s while mounted; shows indeterminate stripe during fetch phase, determinate progress bar (X / Y) during Keycloak provisioning phase, with phase label
  • ergoflow-users-db-panel.tsx — paginated mirror users table; "Konto SmartBox" column links to the JIT-created User profile
  • ergoflow-groups-db-panel.tsx — paginated mirror groups table + members drilldown
  • ergoflow-sync-history.tsx — auto-refreshing sync run history (added / updated / skipped / duration)
  • url-paginator.tsx — shared URL-driven paginator (per-page selector + first/prev/next/last + jump-to-page) used across users / shipments / ErgoFlow tables

Releases

  • releases-table.tsx — version + channel + uploader + assignment count
  • upload-release.tsx — multipart upload (max 500 MB)
  • assign-release.tsx — assign release to a machine
  • release-status-card.tsx — 4 s polling — live DOWNLOADING % / INSTALLING / INSTALLED / FAILED with cancel button

Audit

  • audit-table.tsx — filterable, expandable rows showing the redacted payload JSON

Primitives

  • dialog.tsx — modal wrapper
  • qr-code.tsx — qrcode.react wrapper
  • status-badge.tsx — shipment status colours
  • role-badge.tsx — user role chip
  • relative-time.tsx — "2 hours ago" via date-fns
  • language-switcher.tsx + flag-pl.tsx + flag-gb.tsx — circle flag locale picker
  • coming-soon.tsx — placeholder for unimplemented routes

Auth flow

  1. LoginPOST /api/auth/login proxies to Keycloak protocol/openid-connect/token (grant_type=password, client web-panel). On success, sets two httpOnly cookies:
  2. sb_access — JWT, max-age = expires_in (~5 min)
  3. sb_refresh — refresh token, max-age = refresh_expires_in (~1 week)
  4. Middlewaresrc/middleware.ts runs on every /dashboard/** request:
  5. Locale resolution (PL default, EN with explicit /en/ prefix)
  6. Decodes sb_access JWT and checks exp with 30 s leeway
  7. If expired → internal redirect to /api/auth/refresh?return=<original> which swaps the refresh token for a new pair and 302s back
  8. If refresh fails → redirect to /login?session_expired=1 clearing both cookies
  9. Server components — call getMe() (cached per-request via unstable_cache) which hits GET /me with the JWT.
  10. Typed fetchapi<T>(path, init) in src/lib/backend.ts reads the cookie, sends Authorization: Bearer <jwt>, throws on non-2xx (with status + body in the error message). apiFormData<T>() for multipart uploads.
  11. LogoutGET /api/auth/logout clears both cookies and redirects.

i18n

  • Locales: PL (default) + EN, configured in src/i18n.ts.
  • Messages: messages/pl.json and messages/en.json — nested keys (e.g. home.welcome, shipments.detail.timeline_title).
  • Routing: [locale] segment with localePrefix: 'as-needed' — canonical PL URLs have no prefix, EN gets /en/....
  • Server pages: call setRequestLocale(locale) then getTranslations().
  • Client components: useTranslations() + useLocale().
  • Navigation: import Link and redirect from @/lib/navigation (next-intl wrappers) — they auto-inject the current locale.

Theming

Glass-morphism dark theme, ErgoFlow brand identity:

  • Ink palette: ink-950 #06101A · ink-900 #0A1628 · ink-800 #101F33 · ink-700 #1A2C42.
  • Accents: cyan #0AD6E8 (interactive), accent-neon #10F3FF (hover/glow), leaf-green #5EE6A0 (positive states).
  • Glass shadows: shadow-glass (8 px blur + inset highlight), shadow-neon-cyan (0 px halo).
  • Backgrounds: LavaBackground orbiting bubbles on login + dashboard; DashboardBackground subtle texture elsewhere.
  • Cards: border-white/10 bg-white/[0.02] backdrop-blur is the recurring pattern.
  • Text scale: text-white / text-white/65 muted / text-white/45 labels / text-white/30 placeholders.
  • Footer: "powered by SDI Solution" in the sidebar (visible when expanded).

The full Tailwind theme — colours, keyframes (orbits, glow-pulse), shadows — is in tailwind.config.ts.

Environment

.env.example (this directory) is the source of truth.

Var Purpose
NODE_ENV development / production
PORT dev + prod port (default 3001)
NEXT_PUBLIC_API_BASE backend public URL (https://api.smartbox.ergoflow.app)
NEXT_PUBLIC_KC_ISSUER Keycloak realm issuer (https://auth.smartbox.ergoflow.app/realms/smartbox)
NEXT_PUBLIC_KC_CLIENT_ID Keycloak public client id (smartbox-web / web-panel)
NEXTAUTH_SECRET reserved for OIDC code-flow upgrade (etap 8)
NEXTAUTH_URL canonical URL for OAuth redirects

NEXT_PUBLIC_* vars are inlined at build time and visible in the client bundle — never put secrets there.

Build & deploy

```bash

local dev — http://localhost:3001

cp .env.example .env npm install npm run dev

production build

npm run build # next build → .next/standalone

Docker (matches infra/docker-compose.yml)

docker build -t smartbox-frontend-web . ```

The Dockerfile is multi-stage:

  1. depsnpm ci against package-lock.json
  2. buildnpm run build (Next compiles + emits .next/standalone + .next/static)
  3. runtime — Node 22 alpine, non-root nextjs user, tini for signal handling, only .next/standalone + .next/static + public/ copied in

Versioning

src/lib/version.ts exports APP_VERSION. Surfaced via the floating <VersionBadge /> bottom-right of every /dashboard/** page. Bumped in the same commit as the chunk content so the badge tracks production reality.

Convention:

  • etap 2 chunks → v0.21, v0.22, …, v0.27
  • etap 3 → v0.30v0.48 (12 polish drops)
  • etap 4 → v0.49v0.55
  • etap 5 (kiosk hardware integration) → v0.78v0.83
  • etap 6 (ErgoFlow integration) → v0.84

Conventions

  • Tracking IDs — render via <TrackingId value={s.trackingId} className="font-mono text-accent-cyan" />. The component bolds the last 6 characters (the suffix couriers type into the kiosk).
  • Toastssonner's toast.success() / toast.error() with i18n keys.
  • Tables — server-paginated; pages read URL query params (?search&status&page&perPage), forward them to the backend, and render the {rows, total, page, perPage} envelope. Pagination UI is the shared <UrlPaginator> and filter state lives in the URL so deep links + browser back/forward work.
  • Time — always render via <RelativeTime date={…} /> (date-fns), never raw new Date().toLocaleString() except in the meta block of detail pages where the absolute timestamp is the point.
  • Forms — server actions for mutations whenever possible; client components only for table state, dialogs, charts.
  • Types — hand-written in src/lib/types.ts (mirrors backend). Etap 8 will switch to openapi-typescript codegen from the backend's Swagger doc.
  • Error handlingapi<T>() throws on non-2xx with the response body in the message; surface in toasts or inline error banners.