---
convoy: scanner-desktop-layout
brief_number: 6
depends_on: [1, 2, 3, 4, 5]
recommended_model: composer-2.5-fast
model_tier: fast
files:
  - pages/scanner.js
  - components/scanner/ScannerCamera.js
  - lib/use-scanner-identification.js
  - lib/use-scanner-queue.js
  - test/pages/scanner.test.js
  - test/components/ScannerCamera.test.js
cross_brief_commitments:
  - brief: 1
    description: |
      Wires `camera.videoDevices`, `selectedDeviceId`, `setSelectedDeviceId`,
      and `devicePickerStatus` into the desk Camera `<select>` (md+ only).
  - brief: 2
    description: |
      Mounts `<ScannerTips />` in desktop page header (`hidden md:flex` row).
  - brief: 3
    description: |
      Mounts `<ScannerResultPanel />` with focus/advance/toast orchestration
      after `addSingleCardToOwned`.
  - brief: 4
    description: |
      Mounts `<ScannerHistoryStrip />` with `focusedCardId`, batch progress,
      and bulk commit handlers.
  - brief: 5
    description: |
      Batch Scan multi-file input calls `runSequentialGalleryIdentify` with
      `identification.identifyFromGalleryFile`.
---

# Brief 6: Desktop workstation page wiring

## Goal (1 sentence)

Wire `pages/scanner.js` into the md+ workstation grid (inspector + strip + desk controls) without regressing mobile immersive checkout.

## Files in scope (do not edit anything else)

- `pages/scanner.js`
- `components/scanner/ScannerCamera.js`
- `lib/use-scanner-identification.js`
- `lib/use-scanner-queue.js`
- `test/pages/scanner.test.js`
- `test/components/ScannerCamera.test.js`

## Conventions to follow

- **Layout chrome (boot finding):** `chrome="immersive"` already shows sidebar +
  TopSearchBar at `md+` per `Layout.js` (`max-md:hidden` only on mobile nav).
  Use explicit viewport split for semantics:

  ```js
  const [isDesktop, setIsDesktop] = useState(false);
  useEffect(() => {
    const mq = window.matchMedia('(min-width: 768px)');
    const sync = () => setIsDesktop(mq.matches);
    sync();
    mq.addEventListener('change', sync);
    return () => mq.removeEventListener('change', sync);
  }, []);
  // <Layout chrome={isDesktop ? 'default' : 'immersive'} />
  ```

  Smallest change — no `Layout.js` edits.

- **Mobile zero regression:** keep `ScannerCheckoutSheet`, overlay camera chrome,
  `ScannerCountPill`, scan peek — all `md:hidden` or `max-md:` as today.
- Remove desktop `ScannerReview` side panel (`hidden md:flex` block) — replaced by
  inspector + strip.
- **Pause coordination (boot finding):** `useScannerIdentification` currently has:

  ```js
  useEffect(() => {
    if (verificationPausedRef) {
      verificationPausedRef.current = Boolean(disambiguation);
    }
  }, [disambiguation, verificationPausedRef]);
  ```

  **Delete this effect** — page owns composite pause:

  ```js
  verificationPausedRef.current =
    mobileCheckoutPauses ||
    isListPickerOpen ||
    isAutoDetectPaused ||
    Boolean(identification.disambiguation);
  ```

  Desktop Auto-detect toggle sets `isAutoDetectPaused` (OFF = paused).

- **Focused card state:** `focusedCardId` + derived `focusedCard`. On new scan,
  auto-focus latest unprocessed (`scannedCards.find(c => !c.processed)`).
- **Inspector add success:** wrap `addSingleCardToOwned` — on success show
  `ScannerToast` (*Added to My Collection*), advance to next unprocessed or empty.
  Boot finding: current `addSingleCardToOwned` does not return boolean — check
  `markCardAsProcessed` by awaiting and verifying card.processed or catch errors;
  optionally extend queue method to `return true/false` in a follow-up if needed
  inside this brief only if `use-scanner-queue.js` is NOT in scope — use try/catch
  around API and inspect state after await.

  **Note:** To return success reliably, add minimal change to
  `lib/use-scanner-queue.js` `addSingleCardToOwned` → `return true` on success,
  `return false` on error — **only if needed**; prefer not to expand files list.
  Alternative: duplicate add call in page using `addScannedCardToOwned` from
  `scanner-route-api.js` — **rejected**; instead add `return true/false` to
  `use-scanner-queue.js` is out of files — use `addingCardIds` clearing + no error
  log as success heuristic OR add `use-scanner-queue.js` to this brief's files.

  **Architect lock:** add `lib/use-scanner-queue.js` to this brief for
  `addSingleCardToOwned` returning `boolean`.

- **List picker desktop:** `handleListPick` uses **focused card id** single selection
  (`queue.addSingleCardToCollection(focusedCard, collectionId)`), not bulk selected set.
- **Leave modal:** keep existing; trigger on back + unprocessed queue.
- **Batch Scan:** hidden `<input type="file" multiple accept="image/*" />` on desk bar;
  `runSequentialGalleryIdentify` with `cancelRef`; on `onFileError` enqueue synthetic
  failed row via `queue.handleCardScanned` with `identifyFailed: true` OR dedicated
  `enqueueFailedIdentify(file, error)` inline in page.
- **Rescan:** clear identify fields on focused card (`updateCardMetadata`), set
  `processed: false`; if `scanImageUrl` present fetch blob and
  `identifyFromGalleryFile`; else toast *Rescan from camera*.
- **ScannerCamera workstation:** add `variant="default" | "workstation"` (or `layoutMode`).

  At `md+` with `variant="workstation"`:
  - Container: framed panel not `min-h-[100dvh]` full-bleed — use `md:rounded-xl`,
    `md:aspect-video`, `md:min-h-0`, outer `GlassSurface` + ember rim.
  - Hide overlay top bar + bottom bar (`max-md:` keep current).
  - Hide `ScannerScanPeek`, mobile LIVE badge, `ScannerCountPill` at `md+`.
  - Show **Auto-detect badge** top-left in frame (ON/OFF from `!verificationPausedRef`
    composite for auto-detect slice only — pass `autoDetectOn` prop).
  - Badge dot pulse when ON; `prefers-reduced-motion: reduce` → static dot.
  - Accept optional `deskControls` slot rendered **below** frame by page OR
    `deskControlBar` prop — page renders Upload/Batch/Auto-detect/Camera select below.

- **Desk control bar (page, md+):** Camera `<select>` + Upload Image + Batch Scan +
  Auto-detect switch per Design direction. Upload uses existing single-file input path.
- **Tests:** `test/pages/scanner.test.js` — mock hooks; assert mobile checkout sheet
  present at narrow width; workstation elements (`ScannerResultPanel`, strip) at `md+`
  (use `window.matchMedia` mock). `test/components/ScannerCamera.test.js` — overlay
  chrome hidden at workstation variant.
- **Visual-diff:** `/scanner` is **not** in `tests/visual/` (only `home.png`) — no
  baseline update required.

## `use-scanner-queue.js` changes

- `addSingleCardToOwned` returns `true` on success, `false` on error (after `try/catch`).
- `addSingleCardToCollection` same boolean return for desktop list picker.

## Acceptance criteria

- [ ] `md+` workstation grid: header + camera column + inspector + bottom strip
- [ ] `max-md` immersive checkout sheet unchanged (regression test)
- [ ] `chrome` default on desktop, immersive on mobile
- [ ] Device picker + Batch Scan + Auto-detect on desk bar only at `md+`
- [ ] `ScannerTips` in desktop header
- [ ] Inspector advance + toast on successful add
- [ ] Batch sequential with cancel + per-file failure flags
- [ ] `verificationPausedRef` composite pause without identification hook overwrite
- [ ] No forbidden copy strings
- [ ] tests added; no visual-diff baseline change
- [ ] no scope expansion beyond listed files

## Rationale (≤3 sentences)

Only one brief can own `pages/scanner.js` composition. Camera workstation variant and pause fix are tightly coupled to page wiring. Briefs 1–5 ship mountable units; this brief integrates them without blocking parallel implementers.
