fix(printer): surface real print errors and fix WebUSB connectivity - #535
Merged
Conversation
Print failures on /print-bill and /print-kot discarded the backend's already-computed diagnostic (offline, paper out, wrong queue name, etc.) and always showed a generic message, so every failure looked identical and undiagnosable (#532). The manual "add printer by name" flow also rejected common OS printer-name characters and had no way to match the exact OS queue name, guaranteeing dispatch failures for anyone who couldn't find their printer via auto-detect (#533). Separately, "Printer is not connected. Call connect() first." fired on every app reload/relaunch because WebUSB never silently re-attached to a previously-granted device, plus printTaxBill/printKot lacked the browser-print fallback printBill already had (#534). The WebUSB device picker also never worked in the packaged Electron app at all, since Electron's main process requires explicit session-level USB permission handlers that were never wired up. Fixes #532, #533, #534.
This was referenced Aug 27, 2026
Greptile SummaryThe PR improves printer diagnostics, manual printer-name handling, browser fallback behavior, and WebUSB reconnection and authorization.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| main/usb-device-permissions.ts | Adds trusted-origin, explicit per-device USB authorization and safely limits serial-less approvals to the running session. |
| frontend/src/lib/printer/PrinterService.ts | Adds serialized device opening, silent reconnection, connection switching cleanup, and reconnect coordination. |
| frontend/src/hooks/usePrinter.ts | Waits for startup reconnection before routing print jobs and provides browser fallback when no thermal transport is available. |
| main/index.ts | Registers Electron USB permission handlers once against the stable application origin and shared session. |
| main/routes/printers.ts | Returns detailed print failures and broadens printer-name validation while trimming accidental surrounding whitespace. |
| frontend/src/app/(dashboard)/settings/page.tsx | Surfaces backend printer errors and adds detected-name autocomplete with exact-match guidance. |
Sequence Diagram
sequenceDiagram
participant UI as Printer UI
participant PS as PrinterService
participant USB as WebUSB
participant Main as Electron permission handlers
UI->>PS: Startup status sync
PS->>USB: getDevices()
USB->>Main: Check device permission
Main-->>USB: Approve known device only
USB-->>PS: Previously granted devices
PS->>PS: Serialize openDevice()
PS-->>UI: Connected status
UI->>PS: Print
PS->>PS: awaitPendingReconnect()
alt Thermal transport connected
PS->>USB: Transfer ESC/POS bytes
else No thermal transport
PS-->>UI: Browser print fallback
end
Reviews (8): Last reviewed commit: "fix(printer): don't let serial-less USB ..." | Re-trigger Greptile
…nding reconnect before printing Addresses Greptile P1 findings on #535: - registerUsbDevicePermissions previously auto-approved USB device access for any origin. Scope select-usb-device and setDevicePermissionHandler to the app's own served origin, so a compromised dependency or stray navigation can't silently get hardware access. - The silent WebUSB reconnect kicked off at app startup is asynchronous, so a print triggered moments later (e.g. KOT auto-print on the very first order) could read isConnected as false and wrongly fall back to browser print. printBill, printTaxBill, and printKot now await any in-flight reconnect before checking connection state.
…ccess Addresses a follow-up Greptile P1 finding on #535: scoping USB grants to the trusted origin (previous commit) still auto-approved every matching device without any user-visible confirmation, unlike a real browser's device picker. Electron has no built-in picker UI, so show a native confirmation dialog naming the specific device the first time it's offered, and only remember that device as approved (for the rest of the running app session) once the user clicks Allow.
…econnect Addresses two further Greptile P1 findings on #535: - createWindow() can run more than once per app lifetime (renderer crash recovery, macOS 'activate'), but every window shares the same default session. Re-registering on each call was stacking duplicate select-usb-device listeners on that shared session, firing multiple confirmation dialogs for a single request. Register at most once. - connect() (user-initiated) and tryReconnect() (silent, at startup) could run concurrently and both mutate the same device/interface state; a failure in one would run openDevice()'s disconnect() cleanup and tear down a connection the other had just established. Both now serialize through a single async lock so only one device acquisition is ever in flight at a time.
…ser activation Addresses a further Greptile P1 finding on #535: wrapping the whole of connect() in the connectLock meant a click on Connect while tryReconnect() was still in flight would delay navigator.usb.requestDevice() until that reconnect settled. WebUSB requires requestDevice() to run within the click's transient activation window, so any meaningful delay makes the browser reject it with SecurityError instead of opening the picker. requestDevice() now always runs immediately relative to the user's click; only the openDevice() step that follows it (which is what actually mutates shared device/interface state) goes through the lock.
…picker cancel/error Addresses a further Greptile P1 finding on #535: since requestDevice() (per the previous fix) can now run concurrently with an in-flight tryReconnect(), that reconnect can succeed and set this.device while the picker is still open. Cancelling or failing the picker afterward unconditionally set status to 'disconnected'/'error', leaving this.device populated but isConnected reporting false — prints would then wrongly fall back or report the printer as disconnected despite a working connection. Both catch branches now only override status when there isn't already a connected device.
…rwriting an open device Addresses two further Greptile P1 findings on #535: - approvedDeviceIds was in-memory only, reset to empty on every app restart. setDevicePermissionHandler then rejected the previously approved printer during the silent startup getDevices() reconnect, so the user had to click Connect and approve again every launch — defeating the point of tryReconnect(). Approvals are now persisted to userData/usb-printer-approvals.json, keyed by the device's stable vendorId+productId+serialNumber identity (a deviceId isn't stable across sessions, so it can't be the persisted key). - openDevice() unconditionally overwrote this.device without releasing the previous device's interface claim first. If tryReconnect() succeeded while the user's picker was still open and they picked a different device, the original device's USB claim leaked. openDevice() now disconnects any existing different device first, and short-circuits if the "new" device is actually the same one already open.
…roval identity Addresses a further Greptile P1 security finding on #535: vendorId+productId alone is not a unique physical-device identity — many printers share generic VID/PID pairs from the same USB-to-serial chipset. Persisting approval under that bare pair meant a different physical device sharing it could silently inherit another device's standing trust after a restart, with no renewed confirmation. Only devices that report a serial number now get a persisted, cross-restart approval (keyed by vendorId+productId+serialNumber). A device without a serial number gets a session-scoped-only approval (keyed by Electron's own per-process deviceId) and must be re-confirmed via the dialog after every restart — mirrors how browsers themselves scope WebUSB's persisted-grant store.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the two most-reported printer complaints (generic "Print failed" / "Printer is not connected" errors) and the manual-add name-mismatch bug, per investigation in #532, #533, #534.
/print-billand/print-kotnow return the backend's already-computeddetail/failure_classinstead of a generic message; surfaced in the POS support-error panel, orders-page reprint toast, and settings save/test toasts.execFilearray args or an env var, never a shell), trimmed whitespace on save, added a detected-printer autocomplete + mismatch warning on the manual-add form, and documented the exact-match requirement indocs/printers.md.PrinterServicesilently re-attaches to a previously-granted WebUSB device on every app start instead of requiring a re-click;printTaxBill/printKotfall back to browser printing instead of throwing when no thermal transport is available; hoisted printer status sync to the dashboard layout to close a startup race. Also wired up the Electron main-process USB permission handlers (session.on('select-usb-device')+setDevicePermissionHandler) that were entirely missing — without them the WebUSB device picker could never resolve in the packaged desktop app.Test plan
npm run lint— 0 errors (pre-existing warnings only)npm run build— backend TypeScript compiles cleannpm run build:frontend— Next.js build + type-check cleannpm run i18n:check— translation parity across en/es/pt/fa passesnpm run test:printer,test:print-parity,test:issue-134-routing,test:printer-migrations,test:printer-width-refresh,test:printer-fallback-and-popup-verification— all pass🤖 Generated with Claude Code