# UI/UX Audit — Nielsen's 10 Heuristics **Scope:** Live deployment at `http://100.108.208.56:8082/`, React frontend at `frontend/src/`, complementing the existing docs/repo reviews in this folder. **Method:** Playwright (system Chromium) navigated 20 routes/viewports; findings triangulated against source code with `file:line` references. **Screenshots:** `/tmp/opencode/mp-review/screenshots/` (20 PNGs referenced inline). **Severity scale:** **P0 (blocker)** — broken core flow · **P1 (major)** — wrong or misleading · **P2 (minor)** — polish/aa. --- ## Executive summary The app looks polished on the surface (Tailwind palette, clean cards, working toasts, working focus rings), but a live walkthrough surfaces **multiple silent failures and three outright broken data-rendering bugs**. The most damaging issues are not visual — they are *unmistakable data inconsistencies* the user is expected to read and act on (`$N/A per serving`, blank `lb Pork Chops` rows, hidden empty meal slots on mobile, snake_case aisle labels). They erode trust faster than a missing button. **Top 5 to fix first** (P0): 1. **Meal detail ingredients render without quantities** (field-name bug, `MealDetail.tsx:249-250`) — a core function of the page is unreadable. 2. **`$N/A per serving`** displayed literally (`MealDetail.tsx:191`). 3. **Recipe detail ingredients collapse unit and name** (`RecipeDetail.tsx:161` — `2 canBlack Beans`). 4. **`/recommended` returns a blank page** (missing route + no 404 catch-all in `App.tsx`). 5. **Mobile dashboard hides empty meal slots** (`Dashboard.tsx:164,219` — users on phones cannot *plan* meals, only view them). > **Sprint 1 status (commit `f3e4a44`, deployed by user 2026-06-02):** Items 1, 2, 3, 4, 5 all addressed in the frontend source. Live at `100.108.208.56:8082/`. Verification screenshots in `/tmp/opencode/mp-review/screenshots/fix-sprint1/`. > > **Sprint 2 status (commit `ccc70aa`, deploy helper `f5fb755`):** All six P1s plus the S3.3 mobile shopping-list stat-grid fix are addressed in source. > - **B6** Dashboard `MealCard` title: `truncate` → `line-clamp-2`; image shrinks to 40×40 on ` - **B7** `MealDetail` hero: title/description no longer overlap; description stripped of spoonacular SEO copy via `lib/utils.cleanDescription`; raw text moved to a "Notes from source" disclosure. > - **B8** `Pantry` aisle/unit: free-text → canonical `Select` from `PANTRY_AISLES` enum (`types/index.ts`). `Ingredient name` field now marked `*` required. Backend migration `0015_normalize_pantry_aisles.py` normalizes `ingredient.aisle` and `grocery_item.aisle` to canonical labels. Dry-run SQL helper at `backend/scripts/dry_run_aisle_migration.sql`. > - **B9** `ShoppingList` aisle section headers now human-readable via `AISLE_LABEL` map; falls back to raw key for unknown values. > - **B10** Mobile pantry table: right-edge white-to-transparent gradient overlay hints at horizontal overflow; container has `role="region"` + descriptive `aria-label`. > - **B11** Recipes filters: refactored to `pending`/`applied` state with explicit Apply / Reset buttons. `Filters` button shows active-count chip when filters are set. Wrapped in `role="region" aria-label="Filters"`. > - **S3.3** Shopping list stat cards: now `grid-cols-3` on all viewports with compact mobile sizing. > > Deployment commands (run on the deployment host — DB is in a container, no host psql needed): > ```bash > cd ~/MealPlanner > git pull > > # Optional: persistent backup of aisle values BEFORE the migration > docker compose exec -T db psql -U mealplanner -d mealplanner \ > -f /dev/stdin < backend/scripts/persist_aisle_backup.sql > > # Dry-run preview (no writes) > docker compose exec -T db psql -U mealplanner -d mealplanner \ > -f /dev/stdin < backend/scripts/dry_run_aisle_migration.sql > > # Apply the migration > docker compose exec backend alembic upgrade head > > # Rebuild & restart frontend > docker compose -f docker-compose.yml up -d --build frontend > ``` > **Sprint 3 status (commit pending, ready for deploy):** All P2s plus the a11y sweep. > - **B12** Native `confirm()` deleted for both delete sites. `lib/toast.tsx` (renamed from `.ts` for JSX) gains a new `showToast.undo(message, onUndo, ms=5000)` helper. `Dashboard.handleDelete` captures the full item, deletes, then surfaces an Undo toast that re-fires `generateItem(planId, dayOfWeek, mealType)` to refill the slot. `Pantry.handleRemove` is fully reversible: re-adds via `pantry.add` with the original `ingredient_id`/`quantity`/`unit`. Per-row loading state via new `removeId` state. > - **B13** `Navigation` link text gets `whitespace-nowrap`; padding reduced to `px-2 sm:px-3` so all 4 links fit on one line down to ~360 px. > - **S3.4** Confirmed `ErrorBoundary` is already mounted at `App.tsx:42` (verified `components/ErrorBoundary.tsx`). > - **S3.5** A11y sweep: `