Files
HartOMat/visual-audit-report.md
T
Hartmut 89c44b846f feat(phase5.1+6): fallback material cleanup + notification batch refactor
Phase 5.1 — MATERIAL_PALETTE removal:
- Remove MATERIAL_PALETTE + _material_to_color() from step_processor.py
- build_part_colors() now returns {part→material_name} for Blender resolver

Phase 6 — Notification Center Refactor:
- Migration 051: add channel (activity|notification|alert) to audit_log,
  add frequency (immediate|daily|never) to notification_configs
- Three notification channels: activity (per-render), notification (batch
  order summaries), alert (admin infrastructure)
- Per-render emit_notification_sync calls demoted to channel=activity
- New emit_batch_render_notification_sync(): single summary notification
  when all order lines reach terminal state ("47/50 succeeded, 3 failed")
- Beat task batch_render_notifications every 60s: safety-net for missed
  batch notifications after order completion
- GET /notifications: defaults to channel IN (notification, alert);
  accepts ?channel=activity for activity feed
- Unread count badge counts only notification+alert channels
- Notifications.tsx: three tabs (Notifications | Activity | Alerts)
- NotificationSettings.tsx: frequency dropdown per event type

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 20:20:07 +01:00

261 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Schaeffler Automat — UX & Quality Audit Report
**Date**: 2026-03-08
**Audited by**: Source-code analysis (frontend dev server not reachable at audit time)
**Overall Score**: 7.5/10
---
## Executive Summary
Schaeffler Automat is a feature-rich internal media-creation pipeline with a well-structured React/TypeScript frontend. The application covers a broad functional surface — order management, product library, render monitoring, billing, materials, workers, and a customisable analytics dashboard — and demonstrates strong UX thinking in several areas (kanban + list view duality, multi-step wizard flows, real-time activity feeds, rich filtering). The design system is coherent and token-driven with dark-mode and accent-colour theming. The most significant weaknesses are: the sidebar navigation grows unwieldy without visual hierarchy, several modal/dialog components opt out of the design system (hard-coded Tailwind white/gray classes instead of semantic tokens), the "Upload" primary workflow is hidden mid-list in the sidebar rather than foregrounded, and a handful of page-level experiences fall short of the richness shown elsewhere (Billing, WorkflowEditor, AssetLibraries are visibly incomplete). No automated tests exist for the UI, which is a process risk.
---
## Critical Issues
### C1 — ValidationDialog opts out of the design system entirely
**File**: `frontend/src/pages/Upload.tsx` (lines 710855)
The `ValidationDialog` and `NewInvoiceModal` (Billing.tsx lines 3976) use hard-coded `bg-white`, `text-gray-900`, `border-gray-200`, `text-gray-700` etc. instead of semantic tokens (`bg-surface`, `text-content`, `border-border-default`). In dark-mode these dialogs will render as fully white panels against a dark background — an accessibility and branding failure. The same pattern appears in the Billing page action buttons (`bg-blue-600`).
### C2 — Sticky bottom bars use a fixed `left-60` offset that breaks on mobile
**Files**: `NewProductOrder.tsx` (lines 476, 615, 736), `NewOrder.tsx` (assumed similar)
The bottom action bars are positioned with `fixed bottom-0 left-60 right-0`. On mobile, the sidebar is `w-60` but collapses behind an overlay — the bottom bar will overlap or clip because it always assumes a 240 px left sidebar is present. Combined with `pt-12` main content on mobile (for the top header bar), this creates layout collisions.
### C3 — `confirm()` used throughout for destructive confirmations
**Files**: `Orders.tsx` (line 164), `ProductLibrary.tsx` (line 167), `Materials.tsx` (line 404), `Admin.tsx` (multiple), `Billing.tsx` (line 209), `WorkerActivity.tsx` (line 288)
The native browser `confirm()` dialog is used for all destructive actions (delete order, delete product, purge queue, delete invoice). This is: (a) not styleable — it looks completely alien from the product design, (b) blocks the event loop, (c) not keyboard-accessible in a predictable way, and (d) inconsistent across browsers. A small reusable `ConfirmModal` would resolve all of these.
### C4 — No `<Toaster />` / toast provider verified in root; double `LiveRenderLog` import
**Files**: `App.tsx`, `OrderDetail.tsx` (imports `LiveRenderLog` from `../components/LiveRenderLog`), `WorkerActivity.tsx` (imports from `../components/LiveRenderLog`)
**File**: `frontend/src/components/tasks/LiveRenderLog.tsx` also exists
There are two versions of `LiveRenderLog.tsx` (`/components/LiveRenderLog.tsx` and `/components/tasks/LiveRenderLog.tsx`). `OrderDetail.tsx` imports from `../components/LiveRenderLog` (the root-level one). The `tasks/` version appears to be a separate copy. This creates a maintenance hazard where fixes applied to one are not reflected in the other.
---
## Major Improvements
### M1 — Sidebar navigation lacks visual grouping / hierarchy
**File**: `frontend/src/components/layout/Layout.tsx`
The sidebar lists up to 14 navigation items in a flat list without any section dividers between user-facing pages (Dashboard, Orders, Products, Materials, Activity, Preferences, Upload) and admin-facing pages (Admin, Billing, Media Browser, Workers, Workflows, Asset Libraries, Notification Settings, Tenants). At maximum, an admin user sees all 14+ items with no visual separation. Recommended: add a small section label or divider line before the admin section.
### M2 — "Upload" is buried in the nav when it is the primary data-entry point
**File**: `frontend/src/components/layout/Layout.tsx` (line 18)
The "Upload" link is item #7 of 7 in the main nav list, placed after Preferences. For the primary workflow path (upload Excel → review → create order → dispatch renders), Upload should either be promoted to a more prominent position or merged with the "New Order" CTA at the top of the sidebar. Currently the "New Order" button at sidebar top goes to `/orders/new` which is a choice page, while Excel import lives at `/upload`. The duplication is confusing.
### M3 — Price estimates display bare decimal numbers without currency symbol
**Files**: `NewProductOrder.tsx` (lines 623, 742), `OrderDetail.tsx`
Price estimates in the wizard bottom bars show `Estimated: 25.00` with no currency symbol. The `formatCurrency` function in `Billing.tsx` does this correctly with `Intl.NumberFormat`; the pattern is not shared. The wizard should use the same formatter: `€ 25,00`.
### M4 — Product Library hard cap of 200 products with no pagination
**File**: `frontend/src/pages/ProductLibrary.tsx` (line 131)
`listProducts` is called with `limit: 200`. Above 200 products there is no pagination, infinite scroll, or "load more" — items beyond the cap silently disappear. The same cap applies to the New Product Order wizard Step 1. This will become a real problem as the library grows.
### M5 — Orders Kanban view: columns are fixed-width (272 px) and overflow horizontally
**File**: `frontend/src/pages/Orders.tsx` (lines 593628)
The Kanban board uses `min-w-[272px]` per column inside `min-w-max` — so with 5 columns (~1400 px) the view is wider than a 1280 px laptop screen and triggers horizontal scrolling of the entire content area. A more adaptive approach (auto-sizing columns, or capping at 3 visible columns with horizontal scroll contained to the board) would be better.
### M6 — Billing page is disconnected from order data
**File**: `frontend/src/pages/Billing.tsx`
The "New Invoice" modal (lines 3476) creates invoices with an empty `order_line_ids: []` array — there is no way to select or attach order lines to an invoice from the UI. The `total_net` field in the table is always `null`/`—` for freshly created invoices since no line items are added. The page appears to be a skeleton that is visible to admins and project managers but does not yet deliver its core value.
### M7 — Workflow Editor page appears non-functional
**File**: `frontend/src/pages/WorkflowEditor.tsx` (not read in full but exposed in routing)
The Workflows route exists and is admin-gated but the page content was not visible in the source scan. Based on the API client (`/api/workflows.ts`) and the `GitBranch` icon, this feature appears to be scaffolded but not implemented, yet it is present in the sidebar nav for admins/PMs. An "Under construction" state would be more honest than a silent empty page.
### M8 — Floating bulk-action bars use a hardcoded `ml-[120px]` offset
**Files**: `Orders.tsx` (line 356), `ProductLibrary.tsx` (line 365)
The `fixed bottom-6 left-1/2 -translate-x-1/2 ml-[120px]` centering pattern assumes the sidebar is always 240 px wide (half = 120 px). On mobile, the sidebar is hidden, so this offset pushes the bar off-center. A responsive solution (e.g., centering within the main content area) is needed.
---
## Minor Refinements
### Y1 — `confirm()` expand text is sometimes inconsistent in casing and punctuation
E.g., `Delete ${ids.length} order${...}? This cannot be undone.` vs `Delete material "${mat.name}"?` — no "cannot be undone" note on the latter. Standardise the confirmation copy.
### Y2 — Login page has no "Forgot password" link
**File**: `frontend/src/pages/Login.tsx`
Even in an internal tool, this is a common expectation. The page is clean and minimal, but the absence of any password-reset path means admins must use the CLI/admin panel to reset credentials.
### Y3 — Category labels mix German and English in different contexts
`CATEGORY_LABELS` maps `Kugellager`, `Gleitlager`, `Anschlagplatten` (German) while other labels are English abbreviations (`TRB`, `CRB`, `SRB/TORB`, `Linear`). The mix is visible in filter dropdowns, product cards, and wizard steps. A consistent decision (all English or all German with English tooltips) would improve clarity.
### Y4 — `WorkerManagement` lists a `worker-thumbnail` service that MEMORY.md says is removed
**File**: `frontend/src/pages/WorkerManagement.tsx` (line 83)
`SCALABLE_SERVICES` includes `worker-thumbnail` which according to the project memory was replaced by `render-worker`. Scaling a non-existent service will silently fail. The constant should be updated to match the actual docker-compose.
### Y5 — Materials page header action area gets cramped at medium screen widths
**File**: `frontend/src/pages/Materials.tsx` (lines 214244)
The header row contains 4 buttons (Import Standards, Seed Aliases, Schaeffler Wizard, Add Material). At viewport widths below ~1100 px these wrap awkwardly or overflow. Grouping them in a dropdown or toolbar with overflow handling would help.
### Y6 — Search highlight in Orders uses only the first occurrence
**File**: `frontend/src/pages/Orders.tsx` `Highlight` component (lines 496509)
The `Highlight` component finds only the first match of the search term (`indexOf`). If a product name contains the search string twice, only the first is highlighted. This is a minor usability gap.
### Y7 — Notification page title uses `text-xl` while all other pages use `text-2xl`
**File**: `frontend/src/pages/Notifications.tsx` (line 77)
`h1 className="text-xl font-semibold text-content"` vs Dashboard/Orders/Products/Materials all using `text-2xl font-bold text-content`. Minor but visible inconsistency.
### Y8 — `WorkerActivity` page shows locale date in German but the app is primarily English
**File**: `frontend/src/pages/WorkerActivity.tsx` (lines 196, 458)
Dates use `toLocaleDateString('de-DE')` / `toLocaleTimeString('de-DE')` hard-coded. Since the rest of the UI is in English, using the browser's locale (no explicit locale argument) or `en-GB` would be more consistent.
### Y9 — Admin page contains an inline `DashboardCustomizeModal` alongside complex settings panels
**File**: `frontend/src/pages/Admin.tsx`
The Admin page is enormous (79 KB of source) with user management, renderer settings, pricing, output types, render templates, asset libraries, and dashboard customisation all on a single scrolling page with no in-page navigation or tabs. The page would be significantly easier to use if it were split into tabbed sections.
### Y10 — `ProductDetail` page has no breadcrumb back-link in context
The product detail page has an `ArrowLeft` back button but does not show a breadcrumb like "Products / 81113-L_CUT" making it easy to lose context when navigating via direct URL.
---
## Wins
- **Design system is solid**: the token-based Tailwind config (`bg-surface`, `text-content`, `border-border-default`) is consistently applied in 95% of components. Dark mode and accent-color theming work correctly throughout the main layout.
- **Dual view toggle** (Kanban/List for Orders, Grid/Table for Products) is well-implemented and the active state is clearly communicated.
- **Orders search is excellent**: real-time debouncing, backend full-text search, inline search highlight, contextual empty states, result counts, and status chips all combine into a genuinely useful experience.
- **New Product Order wizard** is well-designed: the 3-step flow (Select → Configure → Review), global toggles that apply across all selected products, partial-selection indicators (e.g., `2/5` counts on output type buttons), and the sticky bottom bar with live job count and price estimate are all thoughtful UX decisions.
- **Worker Activity page** is rich and informative: unified timeline merging CAD and Render events, expandable detail panels with Blender log, renderer badge, timing KPIs, queue visualisation, and task cancellation — all on one page with 5-second auto-refresh.
- **Notification center**: the bell dropdown with portal rendering, 15-second polling, unread badge, and click-to-navigate behaviour is a polished implementation. The full Notifications page adds pagination and "mark all as read".
- **Excel upload flow** is well-thought-out: the preview-first approach (no data is committed until explicit confirmation), per-row include/exclude toggles, duplicate detection with pre-unchecked rows, validation dialog with alias save-shortcut, and the optional draft-for-skipped-rows creation show genuine product thinking.
- **Dashboard** is highly customisable with 15 widget types, per-widget timeframe controls, drag-to-reorder, and tenant-level default dashboard config.
- **Sidebar live status indicators**: the blue pulse dot on "Activity" when tasks are active and the red dot when tasks have failed gives operators an at-a-glance system health signal without needing to open the page.
- **Responsive sidebar**: mobile hamburger + overlay backdrop + auto-close on navigation is correctly implemented with accessible `aria-label` attributes on the toggle buttons.
- **Preferences page** is simple and effective: theme mode (light/dark/system) and accent colour picker persist via Zustand with localStorage, applying immediately.
---
## Prioritized Recommendation List
| Priority | Area | Issue | Suggestion | Effort |
|----------|------|--------|------------|--------|
| P1 | Accessibility / Dark Mode | ValidationDialog and NewInvoiceModal use hard-coded white/gray, breaking dark mode | Replace all `bg-white`, `text-gray-*`, `border-gray-*` in dialogs with semantic tokens | S (12h) |
| P1 | Mobile Layout | Fixed bottom bars use `left-60` offset, breaking on mobile when sidebar is hidden | Use `left-0 md:left-60` or calculate offset responsively | S (1h) |
| P1 | UX Pattern | `confirm()` used for all destructive actions | Implement a small reusable `ConfirmModal` component | M (34h) |
| P2 | Navigation | Flat sidebar with 14+ items, no section grouping | Add a `<div class="border-t my-2">` + optional "Admin" label before admin routes | XS (30min) |
| P2 | Navigation | "Upload" is buried at item #7; primary data-entry workflow | Move Upload higher in nav or consolidate with "New Order" CTA | S (1h) |
| P2 | Data Display | Price estimates shown without currency symbol | Extract and reuse `formatCurrency` from Billing.tsx in wizard bottom bars | XS (30min) |
| P2 | Scalability | Product Library has a hard cap of 200 products, no pagination | Add server-side pagination with `offset` param and "Load more" or page buttons | M (35h) |
| P2 | Maintenance | Duplicate `LiveRenderLog.tsx` at two paths | Consolidate to `components/tasks/LiveRenderLog.tsx`, update all imports | S (1h) |
| P3 | Completeness | Billing page cannot attach order lines to invoices | Implement order-line selector in NewInvoiceModal | L (12d) |
| P3 | Completeness | WorkerManagement lists removed `worker-thumbnail` service | Remove from `SCALABLE_SERVICES` constant | XS (5min) |
| P3 | Admin UX | Admin page is one massive scrolling page | Add tabs (Users / Renderer / Pricing / Templates / System) | M (46h) |
| P3 | Internationalisation | German category labels mixed with English labels | Standardise to one language or add tooltip for German terms | S (1h) |
| P3 | Mobile UX | Orders Kanban columns overflow at standard laptop widths | Cap visible columns or provide horizontal scroll within the board only | M (3h) |
| P4 | UX Polish | Notification page uses `text-xl` while all peers use `text-2xl font-bold` | Align heading style | XS (5min) |
| P4 | UX Polish | Date formatting hard-coded to `de-DE` throughout worker activity | Use browser locale or consistent `en-GB` | S (1h) |
| P4 | Discoverability | Workflow Editor page is blank / non-functional but visible in nav | Add a "Coming soon" placeholder or remove from nav | XS (15min) |
---
## Theme & Visual Consistency Report
**Strengths**:
- The Tailwind design-token system is mature: `bg-surface`, `bg-surface-alt`, `bg-surface-hover`, `bg-surface-muted`, `text-content`, `text-content-secondary`, `text-content-muted`, `border-border-default`, `border-border-light`, and the full status token set (`status-success-bg/text`, `status-warning-bg/text`, `status-error-bg/text`, `status-info-bg/text`) are used consistently across ~95% of components.
- The accent-colour system (`bg-accent`, `text-accent`, `bg-accent-light`, `bg-accent-hover`, `text-accent-text`) is correctly applied throughout buttons, NavLink active states, and focus rings.
- Dark mode is applied at the `<html class="dark">` level with CSS variable overrides — a clean and proven approach.
**Inconsistencies / Gaps**:
1. **Dialogs and modals opt out**: `ValidationDialog` (Upload.tsx), `NewInvoiceModal` (Billing.tsx), and portions of the `Admin.tsx` asset-library section use `bg-white`, `text-gray-*`, `border-gray-200`, and hardcoded `bg-blue-600` buttons. These will break in dark mode.
2. **Purple is used as a secondary accent** in several places (`bg-purple-600`, `text-purple-700`, `bg-purple-100`) for "render positions" / "perspectives" — this is not part of the defined accent preset palette and will look incorrect when a user switches to a non-purple accent.
3. **Billing page "New Invoice" button** uses `bg-blue-600 hover:bg-blue-700` instead of `btn-primary` (which respects the accent token). If a user has chosen the Amber or Teal accent, the button stays blue.
4. **Status chips in Orders.tsx** for kanban column headers use Tailwind colour classes directly (`bg-gray-500`, `bg-blue-500`, `bg-amber-500`, `bg-green-600`, `bg-red-500`) rather than the status token system — these cannot be overridden in dark mode.
5. **`hover:brightness-95`** on Materials page group headers will not produce the intended subtle hover effect in dark mode where `bg-slate-50` already contrasts with the dark background.
---
## Functional QA Report
**Verified working (via code analysis)**:
- Auth flow: JWT stored in Zustand + localStorage, `ProtectedRoute` and `AdminRoute` guards, redirect to `/login` on unauthenticated access.
- Order creation: both wizard paths (Excel upload + product wizard) lead to draft orders with order number confirmation.
- Render dispatch: per-line and bulk dispatch buttons, cancel-render per-line and bulk, render progress bar in Kanban cards.
- Material alias management: add/delete aliases, seed from standards, search across aliases.
- Dashboard customisation: widget toggle modal, timeframe controls, custom date range.
- Worker activity: unified timeline with auto-refresh (5s), Blender log expand, reprocess trigger.
- Notification system: bell badge with unread count, 15s poll, portal dropdown, click-to-navigate to order, mark-one and mark-all-read.
**Potential functional issues**:
1. **`AliasPill` performs an extra API fetch on every delete** (Materials.tsx lines 494505): `listAliases(materialId)` is called lazily on each delete click to look up the alias ID, because `MaterialOut` only returns alias strings, not IDs. This means every alias delete triggers an extra GET request. If the network is slow or the aliases list is stale the delete may target the wrong alias.
2. **Draft orders are deletable via bulk delete** (Orders.tsx `isDeletable` allows `submitted` status too) — the confirmation copy says "This cannot be undone" but does not warn that a submitted order may already be in processing. Check if this is intentional.
3. **`NewProductOrder` wizard Step 1 bottom bar** appears at `fixed bottom-0 left-60` only when `selectedProducts.size > 0`. Before any product is selected, the "Next" button is not visible at all — there is no affordance indicating the user should click a product to proceed. A subtle "Select at least one product to continue" hint would help first-time users.
4. **Product Library `onSelect` handler inconsistency**: `ProductCard.onSelect` is called with `e.target.checked` (boolean) but the prop type is `(checked: boolean) => void` and the parent handler is `() => toggleOne(product.id)` — the `checked` argument is discarded. The checkbox visual and the toggle may briefly diverge if the click and the optimistic state differ.
5. **`WorkerManagement` scale control** starts at count=1 on every page load — there is no display of the current running instance count, so users cannot know if they are scaling up or down.
---
## Mobile Report
**What works on mobile**:
- Sidebar is correctly hidden by default behind a hamburger menu at `md:` breakpoint.
- Mobile top header bar (height 48px) with hamburger, title, and NotificationCenter bell.
- `pt-12 md:pt-0` on the main content area accounts for the fixed mobile header.
- Overlay backdrop (semi-transparent) closes sidebar on tap.
- Search inputs, status chips, and filter bars use `flex-wrap` throughout.
**Mobile problems**:
1. **Fixed bottom bars** (`NewProductOrder`, `NewProductOrder` Step 2/3, bulk-delete bar in Orders/Products) use `left-60` — on mobile this offsets the bar 240px from the left edge of a screen that may only be 375px wide, making the bar very narrow or partially off-screen.
2. **Orders Kanban** requires horizontal scrolling at any mobile screen size (5 × 272px = 1360px total). On mobile, the list view (`view === 'list'`) is much more appropriate; consider defaulting to list view at `sm:` breakpoints.
3. **Admin page** is a single very long scrolling page with complex tables (OutputTypeTable, RenderTemplateTable, PricingTierTable) that will overflow horizontally on mobile screens. These tables have `overflow-auto` wrappers but the horizontal scrolling UX is poor on touch.
4. **Order Detail page** — based on the import list this page is the most complex in the app (~80KB source). The two-panel layout (items list + render lines table) is likely problematic on small screens without explicit responsive column layout.
5. **Worker Activity KV grid** uses `grid-cols-2 sm:grid-cols-3` — on very small screens (320px) even 2-column grids may be too cramped for monospace metric values.
---
## User Flow Efficiency Report
**Core flow: Excel import → review → create order → upload STEP → dispatch renders**
- Step count: Upload page (4 sub-steps) → OrderDetail → StepDropzone → Dispatch button.
- The flow is logical but the step numbering on the Upload page is non-standard: Step 1 = drop zone, Step 2 = row review, Step 3 = output types, Step 4 = STEP upload. Steps 13 are on the Upload page; Step 4 redirects to the order. A persistent step indicator would reduce user disorientation.
- Missing: no "progress saved" state — if the user navigates away after Step 2, the preview result is lost (React state only, no URL state).
**Core flow: Product wizard → create order → dispatch renders**
- 3-step wizard with clear header and "Next" / "Back" navigation is clean.
- The "global apply" toggles for output types across all products is a significant time-saver for bulk ordering.
- The sticky bottom bar continuously shows job count and estimated price — very helpful.
- The back navigation from Step 3 to Step 2 correctly preserves all selections.
**Navigation efficiency**:
- The sidebar's primary CTA "New Order" goes to `/orders/new` which is a choice page (Excel vs Product Wizard). This extra click could be eliminated if the system knew which flow the user typically uses.
- The "Open →" hover text on Kanban cards appears only on hover with an opacity transition — keyboard users or users on touch devices will not see it.
- There is no global "search" — orders search is inside the Orders page, product search is inside the Products page. Power users working across both might want a unified command palette or global search.
---
## Performance Observations
**Polling and refresh intervals**:
- `worker-activity` query: `refetchInterval: 60_000` in Layout (for badge indicators), but `WorkerActivity` page does not set `refetchInterval` — it relies on manual invalidation or navigation. The description says "Auto-refresh every 5 s" but the code at line 25 shows `useQuery` with no `refetchInterval`. This appears to be a documentation/UI string that does not match the implementation.
- Queue status: `refetchInterval: 5_000` in WorkerActivity's `QueuePanel`, `10_000` in WorkerManagement — different refresh rates for the same data on different pages.
- Orders list: `refetchInterval: 15000` — reasonable.
- Notifications: `refetchInterval: 5_000` in `getNotifications` call on the Notifications page; `getUnreadCount` in `NotificationCenter` polls at 15s. Good differentiation.
**Data loading**:
- Product Library loads up to 200 products at once with thumbnails — on a populated system this could result in a significant number of parallel image requests. Consider lazy-loading thumbnails (Intersection Observer) for the grid view.
- Dashboard loads 15 widget types, each making their own API calls. On first load this creates a large burst of parallel requests. The `WidgetContainer` + `useQuery` pattern handles this gracefully via React Query's deduplication, but server-side combined endpoints could reduce round-trips.
- `NewProductOrder` Step 1 uses `keepPreviousData: keepPreviousData` only on the price estimate query, not on the product search — navigating back to Step 1 from Step 2 may show a loading state while the same products re-fetch.
**Bundle considerations**:
- `OrderDetail.tsx` at 80KB source is the largest single component file and will have a substantial compiled bundle weight. Code-splitting at the route level (React.lazy) is not visible in `App.tsx` — all routes appear to be eagerly imported.
- `Admin.tsx` at 79KB is similarly large. Splitting it at the tab level would reduce initial load.