Przejdź do treści

Etap 2 — Web operator panel

Etap 2 zbudował panel operatora w siedmiu chunk-ach + polish-ach (v0.21v0.27). Po nim panel obejmował login, dashboard, shipments, machines, lockers, users, credentials, audit i self-service profile — wszystko w PL/EN, glass-morphism dark theme, role-aware menu.

Chunk Commit Co wleciało
2.1 547b833f2d2d14 login page · LavaBackground orbiting bubbles · i18n PL/EN · "powered by SDI Solution" footer
2.2 3db4dc0 · 7149453 dashboard shell · role-aware sidebar · token refresh · 4 placeholder pages
2.3 f4c4c06 shipment detail · new-shipment modal · assign-locker · cancel
2.4 05efe16 machines list · register-machine · locker grid · emergency open
2.5 b73f0b3 users CRUD · credentials management
2.6 85c967e · 65bb083 audit log — interceptor backend + admin viewer + floating version badge
2.7 c3050e1 · f7eee88 self-service profile · password change

Filozofia panelu

Server Components dla list i widoków szczegółowych (SSR z JWT z cookie), Client Components dla formularzy / modali / wykresów. Każda mutacja idzie przez API backendu — żaden write nie chodzi po prostu przez Server Actions Next-a (chcemy żeby wszystko trafiło do AuditLog).

Stack i decyzje

Technologia Decyzja
Next.js 15 App Router server components + middleware do auth + JWT z cookie
TypeScript 5 strict DTO ręcznie pisane w src/lib/types.ts (mirror backendu) — codegen z OpenAPI planowany na etap 8
Tailwind CSS 3.4 utility-first; brand zaszyty w tailwind.config.ts (ink palette + cyan/leaf accents + glass shadows)
next-intl 3.26 segment routing [locale] z localePrefix: 'as-needed' — kanoniczne URL-e PL bez prefixu, EN z /en/...
sonner toast-y
Recharts 2 wykres CPU/RAM/disk z etapu 3.5 polish 4
qrcode.react 4 QR shipmentu
date-fns RelativeTime

TanStack React Table — usunięte w etapie 6

Pierwotnie users-table i shipments-table używały TanStack React Table z client-side filterem na pełnej liście. Po przekroczeniu ~3700 wierszy strona hangowała na każdym keystroke. W etapie 6 oba przepisaliśmy na server-side pagination z <UrlPaginator> (URL state ?search&page&perPage&status...). Zostało recharts + qrcode.react + sonner + lucide-react + date-fns.

Auth flow + middleware

sequenceDiagram
    participant User
    participant Browser
    participant Next as Next.js middleware
    participant API as /api/auth/*
    participant KC as Keycloak
    participant Backend

    User->>Browser: GET /login
    Browser->>Next: middleware → public route, pass-through
    User->>Browser: submit email + password
    Browser->>API: POST /api/auth/login
    API->>KC: POST /protocol/openid-connect/token (grant_type=password)
    KC-->>API: { access_token, refresh_token, expires_in }
    API-->>Browser: Set-Cookie sb_access (HttpOnly, ~5min)<br/>Set-Cookie sb_refresh (HttpOnly, ~1week)
    Browser-->>User: redirect /dashboard

    Note over Browser,Backend: Każdy request /dashboard/**

    Browser->>Next: GET /dashboard/...
    Next->>Next: dekoduj sb_access JWT, sprawdź exp (30s leeway)
    alt token valid
      Next->>Backend: server fetch z Authorization: Bearer
      Backend-->>Browser: HTML
    else expired
      Next->>API: GET /api/auth/refresh?return=<oryg>
      API->>KC: refresh_token grant
      KC-->>API: nowa para
      API-->>Browser: Set-Cookie + 302 do oryginalnego URL
    else refresh fails
      Next->>Browser: 302 /login?session_expired=1<br/>+ clear obu cookies
    end

Cała logika w src/middleware.ts + 4 route handlers w src/app/api/auth/. Server components wołają getMe() (cached per-request przez unstable_cache) który hituje GET /me z JWT z cookie.

Routing i locale

next-intl z localePrefix: 'as-needed':

  • / → przekierowanie do /dashboard (PL bo default)
  • /dashboard/system/users — kanoniczna PL ścieżka
  • /en/dashboard/system/users — angielski wariant
  • /login (PL) i /en/login (EN) — flag picker w prawym górnym

Każda strona w App Routerze:

ts export default async function MyPage({ params, searchParams, }: { params: Promise<{ locale: string }>; searchParams: Promise<{ ... }>; }) { const { locale } = await params; setRequestLocale(locale); // pamiętaj const t = await getTranslations(); ... }

<Link> i redirect importujemy z @/lib/navigation (next-intl wrappers) — auto-injectują locale.

Role-aware menu

User.role z DB (nie z Keycloaka) decyduje co widać w sidebarze:

graph LR
    classDef any fill:#0AD6E8,stroke:#10F3FF,color:#06101A
    classDef oc fill:#5EE6A0,stroke:#5EE6A0,color:#06101A
    classDef admin fill:#F5C16C,stroke:#F5C16C,color:#06101A

    Personal["Personal<br/>(każdy)<br/>· Dashboard<br/>· My shipments<br/>· My credentials<br/>· Profile"]:::any
    Lockers["Lockers<br/>(ADMIN, COURIER)<br/>· Parcel shipments<br/>· Emergency open"]:::oc
    System["System<br/>(ADMIN)<br/>· Users<br/>· Machines<br/>· Releases<br/>· Audit<br/>· Integracje"]:::admin

COURIER ma read-only widok machines + cross-user shipment management, ale nie ma System. Wzorzec: każdy user widzi swoje rzeczy + (warunkowo) ekstra sekcje. Patrz role-aware-menu.md i frontend-web/README.md.

Brand — glass-morphism dark

Token Hex
ink-950 #06101A (najgłębszy backdrop)
ink-900 #0A1628
ink-800 #101F33
ink-700 #1A2C42
accent-cyan #0AD6E8 (interactive)
accent-neon #10F3FF (hover/glow)
accent-leaf #5EE6A0 (positive states)

Wzorce powtarzające się:

  • Cards: border-white/10 bg-white/[0.02] backdrop-blur
  • Glass shadows: shadow-glass (8 px blur + inset highlight), shadow-neon-cyan (0 px halo)
  • Backgrounds: LavaBackground (24 orbiting ErgoFlow bubbles, 30–44 s phase-offset cycles) na login + dashboard; DashboardBackground (subtelna tekstura) gdzie indziej
  • Text scale: text-white / text-white/65 muted / text-white/45 labels / text-white/30 placeholders
  • Footer: "powered by SDI Solution" w sidebarze (visible kiedy expanded)

Lava-lamp ergoflow bubbles

LavaBackground używa prawdziwych SVG-ek bubble-i z brandbook-a ErgoFlow (public/bubbles/*.svg). Każda baniaczka ma własny phase-offset i prędkość — dlatego nie widać pulsującego wzorca. Decyzja świadoma: pierwsza wersja miała 6 bubbles które chodziły synchronicznie, wyglądało jak slideshow.

Shipments — server-paginated table

Po refaktorze etap-6.5 architektura tabeli jest jednolita:

```ts // page.tsx (server component) const sp = await searchParams; const search = sp.search ?? ''; const statuses = parseStatuses(sp.status); const page = sp.page ? Math.max(0, Number(sp.page)) : 0; const perPage = clampPerPage(sp.perPage);

const qs = new URLSearchParams(); qs.set('scope', 'all'); if (search) qs.set('search', search); for (const s of statuses) qs.append('status', s); qs.set('page', String(page)); qs.set('perPage', String(perPage));

const resp = await api<{shipments, total, page, perPage}>(/shipments?${qs}); return ; ```

Tabela (client component) trzyma tylko stan input-a wyszukiwarki — wszystko inne pcha z powrotem do URL przez router.push('?'+next.toString()) w useTransition(). Browser back/forward działa, deep-linki działają, refresh nie traci stanu.

Machine detail page — <ReleaseStatusCard>

Polling co 4 s, pokazuje live DOWNLOADING X% / INSTALLING / INSTALLED / FAILED z cancel button. To powstało w etapie 3.2 razem z OTA pipeline (patrz Etap 3) ale UX został doszlifowany w etapie 2.6 polish.

Audit viewer

Filtry: actorType (USER/MACHINE/SYSTEM), action (z dropdown-em zasilanym GET /audit/actions), entityType, entityId. Każdy wiersz expandable — pokazuje redacted JSON payload requestu. Typowy use-case: debug "kto otworzył tę szafkę awaryjnie i z jakiego powodu" — entityType=Locker, entityId=<uuid> filtruje wszystkie operacje na tym schowku.

Versioning + version badge

src/lib/version.ts exports APP_VERSION. Floating chip w prawym dolnym rogu każdej /dashboard/** strony (<VersionBadge />). Bumpujemy w tym samym commicie co treść chunk-a — żeby co znajdziesz w produkcji od razu wiązało się z konkretnym chunkiem w git log.

Etap APP_VERSION
2.1 v0.21
2.2 v0.22
2.3 v0.23
2.4 v0.24
2.5 v0.25
2.6 v0.26
2.7 v0.27

Aktualny live: v0.68 (ostatni etap-6 chunk).

Co się zmieniło od etapu 2

  • Tabele — wszystkie krytyczne (system users, parcel shipments, my shipments) przepisane w etapie 6 na server-paginated. Patrz Etap 6.
  • Sekcja System → Integracje → ErgoFlow doszła w etapie 6 (cztery pod-strony: Synchronizacja, Użytkownicy DB, Grupy DB, Historia).
  • Kiosk OTA UI doszedł w etapie 3.2 (<UploadRelease>, <ReleasesTable>, <AssignRelease>, <ReleaseStatusCard> z 4-s pollingiem).
  • Profil maszyny w etapie 3.5 polish dostał Stan column (Pracuje / Pobiera X% / Instaluje / Wyłączona / Serwis), edit form (inactivity timings, SSH credentials), <MachineMetricsChart> na 6 h CPU/RAM/disk, <MachineSystemInfo> z IP (LAN + external) + admin login z eye toggle.
  • <ErgoflowSourceBadge> na users-table — chip linkujący do mirror users w integracji ErgoFlow dla każdego konta z emailem %@ergoflow.local (etap 6.5).

Pełna mapa stron + komponentów → Panel operatora (Next.js 15).