diff --git a/static/index.html b/static/index.html index c42b23d..1593505 100644 --- a/static/index.html +++ b/static/index.html @@ -2826,8 +2826,9 @@ // BARCODE MODE // ═══════════════════════════════════════════════════════════════════════ function _barcodeHints() { - // All formats — 1D (Code 128, EAN, UPC, etc.) + 2D (QR, DataMatrix, - // PDF417, Aztec) for serial number stickers with 2D barcodes. + // Focused 1D formats for fast scanning on mobile. + // Removed TRY_HARDER (too slow on mobile) and 2D formats (QR, DataMatrix, + // PDF417, Aztec) which are rare for canteen machine barcodes. const hints = new Map(); const fmts = [ ZXing.BarcodeFormat.CODE_128, @@ -2836,31 +2837,41 @@ ZXing.BarcodeFormat.EAN_8, ZXing.BarcodeFormat.UPC_A, ZXing.BarcodeFormat.UPC_E, - ZXing.BarcodeFormat.CODE_93, ZXing.BarcodeFormat.ITF, - ZXing.BarcodeFormat.CODABAR, - ZXing.BarcodeFormat.QR_CODE, - ZXing.BarcodeFormat.DATA_MATRIX, - ZXing.BarcodeFormat.PDF_417, - ZXing.BarcodeFormat.AZTEC, ]; hints.set(ZXing.DecodeHintType.POSSIBLE_FORMATS, fmts); - hints.set(ZXing.DecodeHintType.TRY_HARDER, true); return hints; } function _barcodeOptions() { return { - delayBetweenScanAttempts: 300, // scan every 300ms (was 50ms — too fast - // for TRY_HARDER with 13 formats, caused - // decoder thrashing and missed barcodes) + delayBetweenScanAttempts: 300, delayBetweenScanSuccess: 2000, // 2s cooldown after a successful scan }; } + let zxingLoadAttempted = false; + + function checkZxingLoaded() { + if (typeof ZXing !== 'undefined') return true; + if (!zxingLoadAttempted) { + zxingLoadAttempted = true; + console.warn('ZXing library failed to load from CDN'); + } + return false; + } + async function startScanning() { if (addAssetMode !== 'barcode') return; if (scanningActive) return; + + // Check ZXing loaded from CDN + if (!checkZxingLoaded()) { + setScanStatus('Barcode library not loaded — enter ID manually below', 'error'); + showFallbackCameraError('Barcode scanner library failed to load.
Enter the ID manually instead.'); + return; + } + try { setScanStatus('Starting camera...', 'working'); @@ -2887,7 +2898,6 @@ deviceId = rear ? rear.deviceId : devices[0].deviceId; } } catch (e) { - // Fallback: pass null to let decodeFromVideoDevice pick default console.warn('listVideoInputDevices failed, using default camera:', e); } @@ -2897,11 +2907,9 @@ 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 + // Camera readiness check — give it 10s (was 5s, too tight for some mobile browsers) 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) { @@ -2909,15 +2917,9 @@ } handleCameraError(new Error('No camera stream — connect a camera or enter ID manually')); } - }, 5000); + }, 10000); - // decodeFromVideoDevice handles camera + scanning in one call. - // The callback fires on every video frame (~30fps). ZXing passes - // "No MultiFormat Readers..." as an error when no barcode is in view - // — that's NORMAL, not a hardware failure. Only treat real device - // errors (permission denied, camera not found, stream broken) as fatal. 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(); @@ -2925,7 +2927,6 @@ handleBarcode(val.trim()); } } else if (err && scanningActive) { - // Filter out "no barcode in frame" — that's normal, not a failure const msg = (err.message || err || '').toLowerCase(); if (!msg.includes('multiformat') && !msg.includes('unable to detect')) { console.error('ZXing camera error:', err); @@ -3036,14 +3037,13 @@ try { const asset = await api(`/api/assets/search?machine_id=${encodeURIComponent(machineId)}`); - // Asset exists — go directly to its detail view - showToast(`${esc(asset.name)} — showing details`, false); + // Asset exists — show result in-place with scan new button + showToast(`${esc(asset.name)} — found!`, false); // Auto-checkin if GPS available (background, non-blocking) if (AppState.gpsLat != null) { doAutoCheckin(asset.id); } - switchTab('tabAssets'); - viewAsset(asset.id); + showScannedAsset(asset); } catch (e) { if (e.message === 'Asset not found' || e.message.includes('404')) { showNewAssetForm(machineId); @@ -3073,6 +3073,7 @@ ${hasGps ? '✓ GPS captured' : '📍 GPS needed'} + `; @@ -3082,6 +3083,46 @@ } } + function resetBarcodeScanner() { + // Reset the scan result UI and restart barcode scanner + document.getElementById('scanResult').style.display = 'none'; + document.getElementById('scanResult').innerHTML = ''; + document.getElementById('checkinCard').style.display = 'none'; + setScanStatus('Ready — scan another barcode', 'success'); + stopScanning(); + // Re-show the camera placeholder + const ph = document.getElementById('cameraPlaceholder'); + if (ph) { + ph.style.display = 'block'; + ph.innerHTML = '' + gi('📷') + '
Tap to start camera
'; + } + document.getElementById('cameraVideo').style.display = 'none'; + const fallback = document.getElementById('manualSearchFallback'); + if (fallback) fallback.style.display = 'none'; + scanningActive = false; + currentScannedMachineId = null; + // Auto-start the camera + setTimeout(startScanning, 300); + } + + function resetOcrScanner() { + // Reset the OCR result UI and restart OCR camera + stopOcrCamera(); + document.getElementById('ocrResult').style.display = 'none'; + document.getElementById('ocrResult').innerHTML = ''; + document.getElementById('ocrGpsBadge').style.display = 'none'; + document.getElementById('ocrCameraArea').style.display = 'block'; + setOcrStatus('Point camera at the machine sticker and tap capture', 'success'); + const ph = document.getElementById('ocrPlaceholder'); + if (ph) { + ph.style.display = 'block'; + ph.innerHTML = '' + gi('📷') + '
Tap to start camera
'; + } + AppState.ocrGpsSaved = false; + // Auto-start the camera + setTimeout(startOcrCamera, 300); + } + async function doAutoCheckin(assetId) { try { await api('/api/checkins', { @@ -3318,6 +3359,25 @@ const el = document.getElementById('ocrResult'); el.style.display = 'block'; + // ── Helper: render matched_assets list ────────────────────────────── + const hasMatched = data.matched_assets && data.matched_assets.length > 0; + let matchedHtml = ''; + if (hasMatched) { + matchedHtml = '
' + + '
📋 DB matches found by serial/ID:
'; + data.matched_assets.forEach(function(m) { + matchedHtml += '
' + + '
' + esc(m.name || 'Unnamed') + '
' + + '
' + + 'ID: ' + esc(m.machine_id) + (m.serial_number ? ' · SN: ' + esc(m.serial_number) : '') + + ' · matched: ' + esc(m.matched_on) + '' + + '
' + + '' + + '
'; + }); + matchedHtml += '
'; + } + if (data.machine_id) { const methodBadge = data.method === 'client' ? '📡 Offline OCR' @@ -3328,12 +3388,21 @@
${esc(data.machine_id)}${methodBadge}
${confLabel}
${data.raw_text ? `
OCR text: ${esc(data.raw_text)}
` : ''} + ${matchedHtml}
🔍 Searching for asset...
`; setOcrStatus('Machine ID found! Searching...', 'working'); // Auto-search the asset lookupOcrAsset(data.machine_id); + } else if (hasMatched) { + // Machine ID not found, but we have DB matches from serial/ID cross-reference + el.innerHTML = ` +
✅ Asset found via serial/ID match
+ ${data.raw_text ? `
OCR text: ${esc(data.raw_text)}
` : ''} + ${matchedHtml} +
`; + setOcrStatus('Asset found via serial/ID match!', 'success'); } else { el.innerHTML = `
No machine ID detected
@@ -3414,6 +3483,7 @@ } el.innerHTML += `
+
`; } catch (e) {