From 0f0aa1021a5e33847af556704098edda645b423c Mon Sep 17 00:00:00 2001 From: Shawn Date: Mon, 1 Jun 2026 22:11:12 -0400 Subject: [PATCH] Fix UX-015: ZXing async callback silently swallows camera errors - add handleCameraError(), 5s cameraFailTimer, error branch in decodeFromVideoDevice callback --- static/index.html | 65 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 18 deletions(-) diff --git a/static/index.html b/static/index.html index 4fb4db0..c4ed0eb 100644 --- a/static/index.html +++ b/static/index.html @@ -2893,35 +2893,64 @@ setScanStatus('Point camera at a barcode', 'success'); + // Camera readiness check: if the video never starts streaming + // (no media attached within 5s), treat as camera failure + const cameraFailTimer = setTimeout(() => { + if (video.readyState === 0 && scanningActive) { + console.warn('Camera stream never started — showing manual fallback'); + scanningActive = false; + video.style.display = 'none'; + if (codeReader) { + try { codeReader.reset(); } catch (e) { /* ignore */ } + } + handleCameraError(new Error('No camera stream — connect a camera or enter ID manually')); + } + }, 5000); + // decodeFromVideoDevice handles camera + scanning in one call codeReader.decodeFromVideoDevice(deviceId, video, (result, err) => { + // Clear the timeout once we get any callback (stream started or error) + clearTimeout(cameraFailTimer); if (result && scanningActive) { const val = result.getText(); if (val && val !== currentScannedMachineId) { handleBarcode(val.trim()); } + } else if (err && scanningActive) { + // ZXing reports camera errors through the callback (not as exceptions) + console.error('ZXing camera error:', err); + handleCameraError(new Error(err.message || err)); } }); } catch (e) { console.error('Camera error:', e); - setScanStatus('Camera failed: ' + e.message, 'error'); - const ph = document.getElementById('cameraPlaceholder'); - if (ph) ph.innerHTML = `${gi("📷")}
Camera: ${e.message}
`; - // Show manual search fallback - const fallback = document.getElementById('manualSearchFallback'); - if (fallback) { - fallback.style.display = 'block'; - fallback.innerHTML = ` -
- Camera unavailable
- ${getCameraErrorMessage(e)} -
-
Enter ID manually:
- - - - `; - } + handleCameraError(e); + } + } + + function handleCameraError(e) { + setScanStatus('Camera failed: ' + e.message, 'error'); + const ph = document.getElementById('cameraPlaceholder'); + if (ph) { + ph.style.display = 'block'; + ph.innerHTML = `${gi("📷")}
Camera: ${e.message}
`; + } + // Show manual search fallback + const fallback = document.getElementById('manualSearchFallback'); + if (fallback) { + fallback.style.display = 'block'; + fallback.innerHTML = ` +
+ Camera unavailable
+ ${getCameraErrorMessage(e)} +
+
Enter ID manually:
+ + + + `; + // Focus the manual input for convenience + setTimeout(() => document.getElementById('manualBarcodeInput')?.focus(), 100); } }