// ───────────────────────────────────────────────────────────────────────────── // Field-targeted scanner overlay (Gemini single-value extraction). // Opt-in: pages include this when ui.page({ includeScanner: true }). // // Usage from page code: // scanner.open('serial_number', 'Numéro de série', function (value) { // // value is confirmed by the tech; write it into the target input. // }, { equipment_type, brand, model }) // // The page must also include
markup, which shell.js injects // automatically when includeScanner is on. // ───────────────────────────────────────────────────────────────────────────── (function () { var stream = null var field = '' var fieldLabel = '' var ctx = {} var callback = null function $ (id) { return document.getElementById(id) } function esc (s) { return window._esc(s) } function open (fieldKey, label, cb, context) { field = fieldKey; fieldLabel = label; callback = cb; ctx = context || {} $('fsLabel').textContent = 'Scanner : ' + label $('fsResult').innerHTML = '
Cadrez l\'étiquette puis appuyez sur Capturer
' $('fsOv').classList.add('open') startCam() } function close () { $('fsOv').classList.remove('open') stopCam() callback = null } function startCam () { var v = $('fsVid') if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { v.style.display = 'none' $('fsResult').innerHTML = '
Caméra indisponible
' return } navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment', width: { ideal: 1280 }, height: { ideal: 720 } } }).then(function (s) { stream = s v.srcObject = s v.style.display = 'block' }).catch(function (e) { $('fsResult').innerHTML = '
Caméra refusée: ' + (e.message || '') + '
' }) } function stopCam () { if (stream) { stream.getTracks().forEach(function (t) { t.stop() }); stream = null } var v = $('fsVid'); if (v) v.srcObject = null } function capture () { var v = $('fsVid'), c = $('fsCnv') if (!v.videoWidth) { window.toast('Caméra non prête', false); return } c.width = v.videoWidth; c.height = v.videoHeight var cctx = c.getContext('2d'); cctx.drawImage(v, 0, 0, c.width, c.height) var b64 = c.toDataURL('image/jpeg', 0.85) $('fsResult').innerHTML = '
🤖 Analyse par Gemini Vision…
' var payload = { image: b64, field: field } if (ctx.equipment_type) payload.equipment_type = ctx.equipment_type if (ctx.brand) payload.brand = ctx.brand if (ctx.model) payload.model = ctx.model if (ctx.hint) payload.hint = ctx.hint window.api.post('/field-scan', payload).then(function (r) { var d = r.data || {} if (!d.ok || !d.value) { $('fsResult').innerHTML = '
❌ Non détecté — rapprochez-vous et réessayez.
' return } var pct = Math.round((d.confidence || 0) * 100) var color = pct >= 70 ? '#22c55e' : pct >= 40 ? '#f59e0b' : '#ef4444' $('fsResult').innerHTML = '
' + '
' + esc(d.value) + '
' + '
Confiance : ' + pct + '%
' + '
' + '
' + '' + '' + '
' + '
' }).catch(function (e) { $('fsResult').innerHTML = '
Erreur réseau : ' + (e.message || '') + '
' }) } function confirm (value) { if (callback) callback(value) close() } // Bind close button + ESC window.addEventListener('load', function () { var x = document.querySelector('#fsOv .fs-close') if (x) x.addEventListener('click', close) var cap = document.querySelector('#fsOv .fs-capture') if (cap) cap.addEventListener('click', capture) var can = document.querySelector('#fsOv .fs-cancel') if (can) can.addEventListener('click', close) }) window.scanner = { open: open, close: close, capture: capture, confirm: confirm } })()