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 layoutsidebar.tsx— role-gated nav (Personal items always · Lockers for ADMIN/COURIER · System for ADMIN)topbar.tsx— locale switcher + user menu + version badgeuser-menu.tsx— dropdown (profile, logout)version-badge.tsx— floating bottom-rightSmartBox vX.YZchip fromlib/version.ts
Auth¶
login-card.tsx— login form +session_expiredquery handling
Backgrounds¶
lava-background.tsx— 6 orbiting ErgoFlow bubbles, 30–44 s phase-offset cycles, used on login + dashboarddashboard-background.tsx— subtle dashboard backdrop
Shipments¶
shipments-table.tsx— server-paginated table; URL query params (?search&status&page&perPage) drive backendfindMany+count; status filter chips (CREATED / ASSIGNED / DEPOSITED / PICKED_UP / CANCELLED / EXPIRED),Active only/Allquick togglesshipment-detail.tsx— header (TrackingId + StatusBadge), timeline, people, placement, QR, metashipment-timeline.tsx— vertical timeline of state transitionsnew-shipment.tsx— dialog (recipient + size + notes)cancel-shipment.tsx— confirm dialogassign-locker-form.tsx— auto-pick first FREE locker (or specify lockerId / preferredSize)tracking-id.tsx— rendersSB-{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 +Stanoperational badge + system-info buttonmachine-status-dot.tsx— ONLINE / OFFLINE / MAINTENANCE colour dotregister-machine.tsx— register + show plain API key ONCEedit-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 chartlocker-grid.tsx— colour-coded grid (green=FREE, cyan=RESERVED, amber=OCCUPIED, rose=BROKEN), per-cell force-openlocker-config-grid.tsx— admin config grid forallowedFunctions+modbusAddresslocker-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.localaccounts (deep link to ErgoFlow integration page)new-user.tsx— modal — email + fullName + role; backend returns temp password ONCEuser-admin-panel.tsx— change role / active / reset-passworduser-credentials-list.tsx— list with per-row deactivate / deletenew-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 viaPATCH /integrations/ergoflow/configergoflow-sync-button.tsx— firesPOST /integrations/ergoflow/syncand shows a toast immediately (fire-and-forget)ergoflow-provision-progress.tsx— polls/configevery 2 s while mounted; shows indeterminate stripe during fetch phase, determinate progress bar (X / Y) during Keycloak provisioning phase, with phase labelergoflow-users-db-panel.tsx— paginated mirror users table; "Konto SmartBox" column links to the JIT-createdUserprofileergoflow-groups-db-panel.tsx— paginated mirror groups table + members drilldownergoflow-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 countupload-release.tsx— multipart upload (max 500 MB)assign-release.tsx— assign release to a machinerelease-status-card.tsx— 4 s polling — liveDOWNLOADING %/INSTALLING/INSTALLED/FAILEDwith cancel button
Audit¶
audit-table.tsx— filterable, expandable rows showing the redacted payload JSON
Primitives¶
dialog.tsx— modal wrapperqr-code.tsx— qrcode.react wrapperstatus-badge.tsx— shipment status coloursrole-badge.tsx— user role chiprelative-time.tsx— "2 hours ago" via date-fnslanguage-switcher.tsx+flag-pl.tsx+flag-gb.tsx— circle flag locale pickercoming-soon.tsx— placeholder for unimplemented routes
Auth flow¶
- Login —
POST /api/auth/loginproxies to Keycloakprotocol/openid-connect/token(grant_type=password, clientweb-panel). On success, sets two httpOnly cookies: sb_access— JWT, max-age =expires_in(~5 min)sb_refresh— refresh token, max-age =refresh_expires_in(~1 week)- Middleware —
src/middleware.tsruns on every/dashboard/**request: - Locale resolution (PL default, EN with explicit
/en/prefix) - Decodes
sb_accessJWT and checksexpwith 30 s leeway - If expired → internal redirect to
/api/auth/refresh?return=<original>which swaps the refresh token for a new pair and 302s back - If refresh fails → redirect to
/login?session_expired=1clearing both cookies - Server components — call
getMe()(cached per-request viaunstable_cache) which hitsGET /mewith the JWT. - Typed fetch —
api<T>(path, init)insrc/lib/backend.tsreads the cookie, sendsAuthorization: Bearer <jwt>, throws on non-2xx (with status + body in the error message).apiFormData<T>()for multipart uploads. - Logout —
GET /api/auth/logoutclears both cookies and redirects.
i18n¶
- Locales: PL (default) + EN, configured in
src/i18n.ts. - Messages:
messages/pl.jsonandmessages/en.json— nested keys (e.g.home.welcome,shipments.detail.timeline_title). - Routing:
[locale]segment withlocalePrefix: 'as-needed'— canonical PL URLs have no prefix, EN gets/en/.... - Server pages: call
setRequestLocale(locale)thengetTranslations(). - Client components:
useTranslations()+useLocale(). - Navigation: import
Linkandredirectfrom@/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:
LavaBackgroundorbiting bubbles on login + dashboard;DashboardBackgroundsubtle texture elsewhere. - Cards:
border-white/10 bg-white/[0.02] backdrop-bluris the recurring pattern. - Text scale:
text-white/text-white/65muted /text-white/45labels /text-white/30placeholders. - 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:
- deps —
npm ciagainstpackage-lock.json - build —
npm run build(Next compiles + emits.next/standalone+.next/static) - runtime — Node 22 alpine, non-root
nextjsuser, 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.30…v0.48(12 polish drops) - etap 4 →
v0.49…v0.55 - etap 5 (kiosk hardware integration) →
v0.78…v0.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). - Toasts —
sonner'stoast.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 rawnew 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 toopenapi-typescriptcodegen from the backend's Swagger doc. - Error handling —
api<T>()throws on non-2xx with the response body in the message; surface in toasts or inline error banners.