Fix UX-015: ZXing async callback silently swallows camera errors - add handleCameraError(), 5s cameraFailTimer, error branch in decodeFromVideoDevice callback

This commit is contained in:
2026-06-01 22:11:12 -04:00
parent d092abd91a
commit 0f0aa1021a
+47 -18
View File
@@ -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 = `<span class="cam-icon">${gi("📷")}</span><div class="cam-hint" style="margin-top:4px;">Camera: ${e.message}</div>`;
// Show manual search fallback
const fallback = document.getElementById('manualSearchFallback');
if (fallback) {
fallback.style.display = 'block';
fallback.innerHTML = `
<div style="background:var(--red-bg);color:var(--red);padding:10px;border-radius:8px;margin-bottom:10px;font-size:13px;">
<strong>Camera unavailable</strong><br>
${getCameraErrorMessage(e)}
</div>
<div style="font-size:12px;color:var(--text2);margin-bottom:6px;">Enter ID manually:</div>
<input type="text" id="manualBarcodeInput" class="input-field" placeholder="Connect ID or barcode..." onkeypress="if(event.key==='Enter')handleManualBarcode()">
<button class="btn btn-primary btn-sm" onclick="handleManualBarcode()" style="margin-top:6px;width:100%;">Search</button>
<button class="btn btn-outline btn-sm" onclick="startScanning()" style="margin-top:6px;width:100%;">Try camera again</button>
`;
}
handleCameraError(e);
}
}
function handleCameraError(e) {
setScanStatus('Camera failed: ' + e.message, 'error');
const ph = document.getElementById('cameraPlaceholder');
if (ph) {
ph.style.display = 'block';
ph.innerHTML = `<span class="cam-icon">${gi("📷")}</span><div class="cam-hint" style="margin-top:4px;">Camera: ${e.message}</div>`;
}
// Show manual search fallback
const fallback = document.getElementById('manualSearchFallback');
if (fallback) {
fallback.style.display = 'block';
fallback.innerHTML = `
<div style="background:var(--red-bg);color:var(--red);padding:10px;border-radius:8px;margin-bottom:10px;font-size:13px;">
<strong>Camera unavailable</strong><br>
${getCameraErrorMessage(e)}
</div>
<div style="font-size:12px;color:var(--text2);margin-bottom:6px;">Enter ID manually:</div>
<input type="text" id="manualBarcodeInput" class="input-field" placeholder="Connect ID or barcode..." onkeypress="if(event.key==='Enter')handleManualBarcode()">
<button class="btn btn-primary btn-sm" onclick="handleManualBarcode()" style="margin-top:6px;width:100%;">Search</button>
<button class="btn btn-outline btn-sm" onclick="startScanning()" style="margin-top:6px;width:100%;">Try camera again</button>
`;
// Focus the manual input for convenience
setTimeout(() => document.getElementById('manualBarcodeInput')?.focus(), 100);
}
}