deckhearth/.convoys/scanner-desktop-layout/brief-6-page-workstation-wiring.md

172 lines
7.5 KiB
Markdown
Raw Permalink Normal View History

---
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 15 ship mountable units; this brief integrates them without blocking parallel implementers.