feat(booth): open the pay/exit modal on a hardware scan regardless of focus

A barcode/QR scanner is an HID "keyboard wedge" — it types the id + Enter into
whatever holds focus. Previously that only worked while the ticket <input> was
focused; a scan with focus elsewhere (or nowhere) went nowhere.

New useScanner hook (apps/web/src/lib/use-scanner.ts): a document-level keydown
listener that detects the scanner's FAST keystroke burst ended by Enter and opens
the pay/exit modal via setActiveTicket — regardless of focus. A gap > 50ms resets
the buffer, so human-paced typing with nothing focused never registers as a scan
(min length 3 guards stray Enters). Keystrokes into an input/textarea/select/
contenteditable are ignored, so the manual ticket field still works by hand. The
hook is paused while a modal is already open — a scan must not abandon an
in-progress payment; the operator finishes/closes, then scans the next car.

Verified at runtime (Playwright): a fast burst with focus on BODY opens the modal;
a second scan while the modal is open is ignored; slow (120ms) human typing does
NOT open it; the manual input submit still opens it. build+lint 14/14.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
2026-06-21 15:03:01 +02:00
parent 35c10a7310
commit 3ed785c33e
4 changed files with 102 additions and 0 deletions
+8
View File
@@ -68,6 +68,14 @@ a right column with the **live event ticker**. Submitting/clicking a ticket open
modal** (entry/duration/total, tender, voucher checkbox, entry/exit snapshots). All live-refreshed via
the WS.
> **Focus-independent scan capture (2026-06-21).** A scan now opens the pay/exit modal **regardless of
> focus** — the operator needn't click the ticket field first. A document-level listener (`useScanner`,
> `apps/web/src/lib/use-scanner.ts`) detects the HID scanner's fast keystroke BURST ended by Enter (a
> gap > 50ms resets the buffer, so human-paced typing with nothing focused never triggers it) and fires
> the same `setActiveTicket`. It ignores keystrokes into an `<input>/<textarea>/select/contenteditable`
> so the manual ticket field still works by hand. It is **paused while a modal is already open** — a
> scan must not abandon an in-progress payment; the operator finishes/closes, then scans the next car.
### Explainable activity log (2026-06-19)
The ticker used to flag an **anomaly** with no explanation — a red row with just an id, "nobody knows
what happened." Now every event is **self-describing and clickable**:
+10
View File
@@ -1299,3 +1299,13 @@ viewport like /booth: a fixed title+filters, then a two-pane area (shift list |
pane scrolls independently (min-h-0/flex-1 + overflow-y-auto) instead of the whole page growing.
Verified at runtime (Playwright): /shift redirects, rows open the detail modal, layout fills height,
booth still works (0 console errors). build+lint 14/14.
## [2026-06-21] feat | Booth: focus-independent hardware-scan capture
A scan now opens the /booth pay/exit modal no matter what's focused (or if nothing is) — the operator
needn't click the ticket field first. New useScanner hook (apps/web/src/lib/use-scanner.ts): a
document-level keydown listener that detects the HID scanner's fast keystroke burst ended by Enter (gap
> 50ms resets the buffer, so human-paced typing never triggers it; min length 3) and fires
setActiveTicket. Ignores keystrokes into editable fields so the manual ticket input is unaffected;
paused while a modal is open so a scan can't abandon an in-progress payment. Verified at runtime
(Playwright): scan with focus on BODY opens the modal; second scan while open is ignored; slow typing
doesn't trigger; manual form submit still works. build+lint 14/14. See [[booth-console]].