fix: OCR last-5-digits extraction, barcode 1D-only scanning, auto-checkin on create

- OCR: Extract only last 5 digits from Connect ID (XXXXX-XXXXXX → last 5)
- Barcode: Switch to decodeFromVideoDevice with 1D-only format hints
  (Code 128/39/EAN/UPC/ITF/Codabar), TRY_HARDER, 50ms scan interval
- Auto check-in: New assets auto-checkin with GPS on creation via
  all three entry modes (barcode, OCR, manual)
- Tests: Updated e2e tests for login wait and API flow
This commit is contained in:
2026-05-20 15:03:20 -04:00
parent 7da3f28c6a
commit b20998e8e3
5 changed files with 227 additions and 369 deletions
+75 -15
View File
@@ -2568,31 +2568,69 @@
// ═══════════════════════════════════════════════════════════════════════
// BARCODE MODE
// ═══════════════════════════════════════════════════════════════════════
function _barcodeHints() {
// Restrict to 1D barcode formats only — much faster and more accurate
// than scanning all formats (QR, DataMatrix, PDF417, Aztec, etc.)
const hints = new Map();
const fmts = [
ZXing.BarcodeFormat.CODE_128,
ZXing.BarcodeFormat.CODE_39,
ZXing.BarcodeFormat.EAN_13,
ZXing.BarcodeFormat.EAN_8,
ZXing.BarcodeFormat.UPC_A,
ZXing.BarcodeFormat.UPC_E,
ZXing.BarcodeFormat.CODE_93,
ZXing.BarcodeFormat.ITF,
ZXing.BarcodeFormat.CODABAR,
];
hints.set(ZXing.DecodeHintType.POSSIBLE_FORMATS, fmts);
hints.set(ZXing.DecodeHintType.TRY_HARDER, true);
return hints;
}
function _barcodeOptions() {
return {
delayBetweenScanAttempts: 50, // scan every 50ms (was default ~500ms)
delayBetweenScanSuccess: 2000, // 2s cooldown after a successful scan
};
}
async function startScanning() {
if (addAssetMode !== 'barcode') return;
if (scanningActive) return;
try {
setScanStatus('Starting camera...', 'working');
if (stream) {
stream.getTracks().forEach(t => t.stop());
stream = null;
// Stop any previous reader
if (codeReader) {
try { codeReader.reset(); } catch (e) { /* ignore */ }
codeReader = null;
}
stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: { ideal: 'environment' }, width: { ideal: 1920 }, height: { ideal: 1080 } },
audio: false,
});
const video = document.getElementById('cameraVideo');
if (!video) return;
video.srcObject = stream;
await video.play();
// Create reader with 1D-only hints for fast, accurate scanning
codeReader = new ZXing.BrowserMultiFormatReader(_barcodeHints(), _barcodeOptions());
document.getElementById('cameraPlaceholder').style.display = 'none';
video.style.display = 'block';
scanningActive = true;
if (!codeReader) codeReader = new ZXing.BrowserMultiFormatReader();
// Pick the rear-facing camera (environment-facing on phones)
const devices = await ZXing.BrowserMultiFormatReader.listVideoInputDevices();
let deviceId = null;
if (devices && devices.length > 0) {
const rear = devices.find(d =>
d.label && /back|rear|environment/i.test(d.label)
);
deviceId = rear ? rear.deviceId : devices[0].deviceId;
}
setScanStatus('Point camera at a barcode', 'success');
codeReader.decodeFromVideoElement(video, (result, err) => {
// decodeFromVideoDevice handles camera + scanning in one call
codeReader.decodeFromVideoDevice(deviceId, video, (result, err) => {
if (result && scanningActive) {
const val = result.getText();
if (val && val !== currentScannedMachineId) {
@@ -2612,10 +2650,7 @@
scanningActive = false;
if (codeReader) {
try { codeReader.reset(); } catch (e) { /* ignore */ }
}
if (stream) {
stream.getTracks().forEach(t => t.stop());
stream = null;
codeReader = null;
}
const video = document.getElementById('cameraVideo');
if (video) video.style.display = 'none';
@@ -2723,6 +2758,7 @@
showToast('Asset created!');
document.getElementById('newAssetCard').style.display = 'none';
AppState.currentAssetId = asset.id;
autoCheckin(asset.id);
showScannedAsset(asset);
} catch (e) {
showToast(e.message, true);
@@ -2756,6 +2792,28 @@
}
}
// ── Auto check-in on asset creation (GPS grab) ──────────────────────────
async function autoCheckin(assetId) {
if (!AppState.gpsLat) return; // No GPS — skip silently
try {
await api('/api/checkins', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
asset_id: assetId,
latitude: AppState.gpsLat,
longitude: AppState.gpsLng,
accuracy: AppState.gpsAcc,
notes: 'Auto check-in on creation',
}),
});
// Don't show toast — just silently grab the location
} catch (e) {
// GPS check-in is best-effort; never block creation for it
console.warn('Auto check-in failed:', e);
}
}
// ═══════════════════════════════════════════════════════════════════════
// OCR MODE
// ═══════════════════════════════════════════════════════════════════════
@@ -2870,6 +2928,7 @@
document.getElementById('ocrResult').style.display = 'none';
setOcrStatus('Ready — take another photo', 'success');
AppState.currentAssetId = asset.id;
autoCheckin(asset.id);
// Show check-in option
document.getElementById('checkinCard').style.display = 'block';
} catch (e) {
@@ -2997,6 +3056,7 @@
});
showToast('Asset created!');
AppState.currentAssetId = asset.id;
autoCheckin(asset.id);
if (addAnother) {
// Clear form