Bug description
"Printer is not connected. Call connect() first." is tied for the #1 printer-related support complaint. It's thrown by the frontend WebUSB printer path any time a print is attempted while no WebUSB device handle is held — which is true after every app relaunch or page reload, and in several code paths where it fires even when a fully-configured backend (network/USB) printer exists.
Root cause
Thrown at frontend/src/lib/printer/PrinterService.ts:193:
if (!this.device || this._status !== 'connected') {
throw new Error('Printer is not connected. Call connect() first.');
}
There are three separate defects that each independently trigger this:
1. No silent reconnect on startup. connect() (PrinterService.ts:76-158) only calls navigator.usb.requestDevice(), which requires a live user gesture and shows a picker every time. Nothing calls the non-prompting navigator.usb.getDevices() to silently re-acquire a device the user already granted permission for in a previous session. So this.device is null after every relaunch/reload until the user manually clicks "Connect" again in PrinterStatus.tsx (frontend/src/components/pos/PrinterStatus.tsx:84-96, only rendered via PosTopbar.tsx:49).
2. Inconsistent fallback guards across print functions in frontend/src/hooks/usePrinter.ts:
| Function |
Behavior when no backend printer / not connected |
printBill (usePrinter.ts:107-216) |
Falls back to executeBrowserPrint() (:162-167) — does not throw. |
printTaxBill (usePrinter.ts:218-281) |
No hardware-printer branch at all. Whenever printMethod !== 'browser', unconditionally calls printerService.print(bytes) (:275) — even on stores with a configured backend network/USB printer, because no /printers/print-tax-bill backend route exists. |
printKot (usePrinter.ts:283-341) |
Has a backend-hardware branch (:296-305), but its fallthrough (:307-314) calls printerService.print(bytes) directly with no isConnected check and no browser fallback, unlike printBill. |
So printTaxBill and printKot throw this error in exactly the cases printBill handles gracefully.
3. A population race for hardwarePrinter. It's loaded only by refreshHardwarePrinter() (usePrinter.ts:80-92), invoked from usePrinterStatusSync() (usePrinter.ts:390-411), which only mounts on the POS page (via PrinterStatus.tsx) and Settings. It's explicitly excluded from persisted state (usePrinter.ts:375), so it's always null on fresh load and filled in asynchronously via GET /printers. Since KOT auto-print fires immediately on order placement (frontend/src/app/(dashboard)/pos/page.tsx:256-264, 530, 732, gated by autoPrintKot), a fast first order right after app start can race ahead of that fetch and hit the unguarded branch in defect #2.
Combined, this explains the complaint volume: it fires for (a) any WebUSB-only shop that reloaded/relaunched without re-clicking Connect, (b) every printTaxBill call on shops using tax/GST invoices regardless of printer type, and (c) intermittent KOT auto-print failures right after startup.
Proposed fix
- In
PrinterService.connect()/init, attempt a silent navigator.usb.getDevices() reconnect before falling back to the prompting requestDevice() flow, so a previously-granted device reattaches automatically on load.
- Add the same
hw-check + browser-fallback pattern that printBill already uses (usePrinter.ts:162-167) to printTaxBill and the printKot fallthrough branch, including adding a /printers/print-tax-bill backend route so tax invoices route through the backend printer path like bills do.
- Hoist
usePrinterStatusSync() to a layout-level provider (instead of only PrinterStatus.tsx) so hardwarePrinter is populated before the POS page can place an order, closing the startup race.
Environment
Affects WebUSB-configured printers on all platforms; also affects tax/GST-invoice printing on stores using backend network/USB printers.
Bug description
"Printer is not connected. Call connect() first." is tied for the #1 printer-related support complaint. It's thrown by the frontend WebUSB printer path any time a print is attempted while no WebUSB device handle is held — which is true after every app relaunch or page reload, and in several code paths where it fires even when a fully-configured backend (network/USB) printer exists.
Root cause
Thrown at
frontend/src/lib/printer/PrinterService.ts:193:There are three separate defects that each independently trigger this:
1. No silent reconnect on startup.
connect()(PrinterService.ts:76-158) only callsnavigator.usb.requestDevice(), which requires a live user gesture and shows a picker every time. Nothing calls the non-promptingnavigator.usb.getDevices()to silently re-acquire a device the user already granted permission for in a previous session. Sothis.deviceisnullafter every relaunch/reload until the user manually clicks "Connect" again inPrinterStatus.tsx(frontend/src/components/pos/PrinterStatus.tsx:84-96, only rendered viaPosTopbar.tsx:49).2. Inconsistent fallback guards across print functions in
frontend/src/hooks/usePrinter.ts:printBill(usePrinter.ts:107-216)executeBrowserPrint()(:162-167) — does not throw.printTaxBill(usePrinter.ts:218-281)printMethod !== 'browser', unconditionally callsprinterService.print(bytes)(:275) — even on stores with a configured backend network/USB printer, because no/printers/print-tax-billbackend route exists.printKot(usePrinter.ts:283-341):296-305), but its fallthrough (:307-314) callsprinterService.print(bytes)directly with noisConnectedcheck and no browser fallback, unlikeprintBill.So
printTaxBillandprintKotthrow this error in exactly the casesprintBillhandles gracefully.3. A population race for
hardwarePrinter. It's loaded only byrefreshHardwarePrinter()(usePrinter.ts:80-92), invoked fromusePrinterStatusSync()(usePrinter.ts:390-411), which only mounts on the POS page (viaPrinterStatus.tsx) and Settings. It's explicitly excluded from persisted state (usePrinter.ts:375), so it's alwaysnullon fresh load and filled in asynchronously viaGET /printers. Since KOT auto-print fires immediately on order placement (frontend/src/app/(dashboard)/pos/page.tsx:256-264, 530, 732, gated byautoPrintKot), a fast first order right after app start can race ahead of that fetch and hit the unguarded branch in defect #2.Combined, this explains the complaint volume: it fires for (a) any WebUSB-only shop that reloaded/relaunched without re-clicking Connect, (b) every
printTaxBillcall on shops using tax/GST invoices regardless of printer type, and (c) intermittent KOT auto-print failures right after startup.Proposed fix
PrinterService.connect()/init, attempt a silentnavigator.usb.getDevices()reconnect before falling back to the promptingrequestDevice()flow, so a previously-granted device reattaches automatically on load.hw-check + browser-fallback pattern thatprintBillalready uses (usePrinter.ts:162-167) toprintTaxBilland theprintKotfallthrough branch, including adding a/printers/print-tax-billbackend route so tax invoices route through the backend printer path like bills do.usePrinterStatusSync()to a layout-level provider (instead of onlyPrinterStatus.tsx) sohardwarePrinteris populated before the POS page can place an order, closing the startup race.Environment
Affects WebUSB-configured printers on all platforms; also affects tax/GST-invoice printing on stores using backend network/USB printers.