Etap 2 — Web operator panel¶
Etap 2 zbudował panel operatora w siedmiu chunk-ach + polish-ach (v0.21 → v0.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 | 547b833 … f2d2d14 |
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/65muted /text-white/45labels /text-white/30placeholders - 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ł
Stancolumn (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).