Spaces:
Runtime error
Runtime error
| const video = document.getElementById('videoFeed'); | |
| const progressCircle = document.getElementById('progressCircle'); | |
| const statusText = document.getElementById('statusText'); | |
| const tabEmployee = document.getElementById('tabEmployee'); | |
| const tabManforce = document.getElementById('tabManforce'); | |
| const panelEmployee = document.getElementById('panelEmployee'); | |
| const panelManforce = document.getElementById('panelManforce'); | |
| const manforceAadhaar = document.getElementById('manforceAadhaar'); | |
| const manforceName = document.getElementById('manforceName'); | |
| const manforceMobile = document.getElementById('manforceMobile'); | |
| const btnRegisterManforce = document.getElementById('btnRegisterManforce'); | |
| const statusTextManforce = document.getElementById('statusTextManforce'); | |
| const employeeCode = document.getElementById('employeeCode'); | |
| const btnRegisterEmployee = document.getElementById('btnRegisterEmployee'); | |
| const statusTextEmployee = document.getElementById('statusTextEmployee'); | |
| const spoofToast = document.getElementById('spoofToast'); | |
| const spoofToastMessage = document.getElementById('spoofToastMessage'); | |
| if (!video) console.warn('Register: video element missing'); | |
| let activeTab = 'employee'; | |
| let manforceFrames = []; | |
| let employeeFrames = []; | |
| let isCapturing = false; | |
| let captureTarget = null; | |
| const REQUIRED_FRAMES = 5; | |
| const CAPTURE_INTERVAL_MS = 650; // 5 frames over ~3.2s so blink is likely | |
| const FACE_CHECK_POLL_MS = 500; | |
| const MAX_RETRIES_PER_SLOT = 2; // retry each slot up to 2 times if no face | |
| let toastDismissTimer = null; | |
| let faceCheckInterval = null; | |
| let faceReady = false; | |
| // Start camera | |
| navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user', width: 640, height: 480 } }) | |
| .then(function(stream) { if (video) video.srcObject = stream; }) | |
| .catch(function(err) { console.error(err); }); | |
| function switchTab(tab) { | |
| activeTab = tab; | |
| if (tab === 'employee') { | |
| if (tabEmployee) { tabEmployee.classList.add('active'); tabEmployee.setAttribute('aria-selected', 'true'); } | |
| if (tabManforce) { tabManforce.classList.remove('active'); tabManforce.setAttribute('aria-selected', 'false'); } | |
| if (panelEmployee) { panelEmployee.classList.add('active'); panelEmployee.removeAttribute('hidden'); } | |
| if (panelManforce) { panelManforce.classList.remove('active'); panelManforce.setAttribute('hidden', ''); } | |
| } else { | |
| if (tabManforce) { tabManforce.classList.add('active'); tabManforce.setAttribute('aria-selected', 'true'); } | |
| if (tabEmployee) { tabEmployee.classList.remove('active'); tabEmployee.setAttribute('aria-selected', 'false'); } | |
| if (panelManforce) { panelManforce.classList.add('active'); panelManforce.removeAttribute('hidden'); } | |
| if (panelEmployee) { panelEmployee.classList.remove('active'); panelEmployee.setAttribute('hidden', ''); } | |
| } | |
| updateVisibleButton(); | |
| } | |
| function getFramesFor(type) { | |
| return type === 'manforce' ? manforceFrames : employeeFrames; | |
| } | |
| function setFramesFor(type, frames) { | |
| if (type === 'manforce') manforceFrames = frames; else employeeFrames = frames; | |
| } | |
| function updateVisibleButton() { | |
| if (activeTab === 'manforce') updateManforceButton(); else updateEmployeeButton(); | |
| } | |
| function updateManforceButton() { | |
| if (!manforceAadhaar || !manforceName || !manforceMobile || !btnRegisterManforce) return; | |
| var valid = manforceAadhaar.value.trim().length > 0 && manforceName.value.trim().length > 0 && manforceMobile.value.trim().length > 0; | |
| if (isCapturing && captureTarget === 'manforce') { | |
| btnRegisterManforce.disabled = true; | |
| btnRegisterManforce.textContent = 'CAPTURING...'; | |
| } else if (manforceFrames.length === REQUIRED_FRAMES) { | |
| btnRegisterManforce.disabled = false; | |
| btnRegisterManforce.textContent = 'Register'; | |
| btnRegisterManforce.onclick = function() { submitRegistration('manforce'); }; | |
| } else if (valid && faceReady) { | |
| btnRegisterManforce.disabled = false; | |
| btnRegisterManforce.textContent = 'START CAPTURE'; | |
| btnRegisterManforce.onclick = function() { startCaptureProcess('manforce'); }; | |
| } else if (valid) { | |
| btnRegisterManforce.disabled = true; | |
| btnRegisterManforce.textContent = 'START CAPTURE'; | |
| btnRegisterManforce.onclick = null; | |
| } else { | |
| btnRegisterManforce.disabled = true; | |
| btnRegisterManforce.onclick = null; | |
| } | |
| } | |
| function updateEmployeeButton() { | |
| if (!employeeCode || !btnRegisterEmployee) return; | |
| var valid = employeeCode.value.trim().length > 0; | |
| if (isCapturing && captureTarget === 'employee') { | |
| btnRegisterEmployee.disabled = true; | |
| btnRegisterEmployee.textContent = 'CAPTURING...'; | |
| } else if (employeeFrames.length === REQUIRED_FRAMES) { | |
| btnRegisterEmployee.disabled = false; | |
| btnRegisterEmployee.textContent = 'Register'; | |
| btnRegisterEmployee.onclick = function() { submitRegistration('employee'); }; | |
| } else if (valid && faceReady) { | |
| btnRegisterEmployee.disabled = false; | |
| btnRegisterEmployee.textContent = 'START CAPTURE'; | |
| btnRegisterEmployee.onclick = function() { startCaptureProcess('employee'); }; | |
| } else if (valid) { | |
| btnRegisterEmployee.disabled = true; | |
| btnRegisterEmployee.textContent = 'START CAPTURE'; | |
| btnRegisterEmployee.onclick = null; | |
| } else { | |
| btnRegisterEmployee.disabled = true; | |
| btnRegisterEmployee.onclick = null; | |
| } | |
| } | |
| if (tabEmployee) tabEmployee.addEventListener('click', function() { switchTab('employee'); }); | |
| if (tabManforce) tabManforce.addEventListener('click', function() { switchTab('manforce'); }); | |
| // Swipe on tab bar to switch | |
| var tabBar = document.querySelector('.register-tabs'); | |
| if (tabBar) { | |
| var touchStartX = 0; | |
| tabBar.addEventListener('touchstart', function(e) { touchStartX = e.touches[0].clientX; }, { passive: true }); | |
| tabBar.addEventListener('touchend', function(e) { | |
| var dx = (e.changedTouches[0].clientX - touchStartX); | |
| if (Math.abs(dx) > 50) { | |
| if (dx > 0 && activeTab === 'manforce') switchTab('employee'); | |
| else if (dx < 0 && activeTab === 'employee') switchTab('manforce'); | |
| } | |
| }, { passive: true }); | |
| } | |
| [manforceAadhaar, manforceName, manforceMobile].forEach(function(input) { | |
| if (input) input.addEventListener('input', function() { updateManforceButton(); }); | |
| }); | |
| if (employeeCode) employeeCode.addEventListener('input', function() { updateEmployeeButton(); }); | |
| function updateProgress(percent) { | |
| if (progressCircle) { | |
| progressCircle.style.strokeDashoffset = 113 - (113 * percent); | |
| } | |
| } | |
| function captureFrameAsDataUrl() { | |
| if (!video || video.readyState < 2) return null; | |
| var canvas = document.createElement('canvas'); | |
| canvas.width = video.videoWidth; | |
| canvas.height = video.videoHeight; | |
| canvas.getContext('2d').drawImage(video, 0, 0); | |
| return canvas.toDataURL('image/jpeg', 0.8); | |
| } | |
| function runFaceCheck() { | |
| if (isCapturing || !video || video.readyState < 2) return; | |
| var frame = captureFrameAsDataUrl(); | |
| if (!frame) return; | |
| fetch('/api/face-check', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ frame: frame }) | |
| }) | |
| .then(function(r) { return r.json(); }) | |
| .then(function(data) { | |
| var wasReady = faceReady; | |
| faceReady = data.ready === true; | |
| if (faceReady && !wasReady) updateVisibleButton(); | |
| if (!faceReady && wasReady) updateVisibleButton(); | |
| var statusEl = activeTab === 'manforce' ? statusTextManforce : statusTextEmployee; | |
| var globalStatus = statusText; | |
| if (getFramesFor(activeTab).length === REQUIRED_FRAMES) return; | |
| if (data.ready) { | |
| if (statusEl) statusEl.textContent = 'Face detected β click START CAPTURE'; | |
| if (globalStatus) globalStatus.textContent = 'Face detected β click START CAPTURE'; | |
| } else if (data.face_detected) { | |
| if (statusEl) statusEl.textContent = 'Move closer and center your face'; | |
| if (globalStatus) globalStatus.textContent = 'Move closer and center your face'; | |
| } else { | |
| if (statusEl) statusEl.textContent = 'Position your face in the frame'; | |
| if (globalStatus) globalStatus.textContent = 'Position your face in the frame'; | |
| } | |
| }) | |
| .catch(function() {}); | |
| } | |
| function startFaceCheckPolling() { | |
| stopFaceCheckPolling(); | |
| faceCheckInterval = setInterval(runFaceCheck, FACE_CHECK_POLL_MS); | |
| runFaceCheck(); | |
| } | |
| function stopFaceCheckPolling() { | |
| if (faceCheckInterval) { | |
| clearInterval(faceCheckInterval); | |
| faceCheckInterval = null; | |
| } | |
| } | |
| function startCaptureProcess(target) { | |
| if (isCapturing) return; | |
| captureTarget = target; | |
| isCapturing = true; | |
| stopFaceCheckPolling(); | |
| setFramesFor(target, []); | |
| updateVisibleButton(); | |
| var statusEl = target === 'manforce' ? statusTextManforce : statusTextEmployee; | |
| if (statusEl) statusEl.textContent = 'Look at the camera and blink naturally...'; | |
| if (statusText) statusText.textContent = 'Look at the camera and blink naturally during capture.'; | |
| var count = 0; | |
| var slotRetries = 0; | |
| function trySlot() { | |
| if (count >= REQUIRED_FRAMES) { | |
| finishCapture(target); | |
| return; | |
| } | |
| var frame = captureFrameAsDataUrl(); | |
| if (!frame) { | |
| setTimeout(trySlot, CAPTURE_INTERVAL_MS); | |
| return; | |
| } | |
| fetch('/api/face-check', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ frame: frame }) | |
| }) | |
| .then(function(r) { return r.json(); }) | |
| .then(function(data) { | |
| if (data.face_detected) { | |
| var frames = getFramesFor(target); | |
| frames.push(frame); | |
| setFramesFor(target, frames); | |
| count++; | |
| slotRetries = 0; | |
| updateProgress(count / REQUIRED_FRAMES); | |
| var pct = Math.round((count / REQUIRED_FRAMES) * 100); | |
| if (statusEl) statusEl.textContent = 'Scanning... ' + pct + '%'; | |
| if (statusText) statusText.textContent = 'Scanning... ' + pct + '%'; | |
| setTimeout(trySlot, CAPTURE_INTERVAL_MS); | |
| } else if (slotRetries < MAX_RETRIES_PER_SLOT) { | |
| slotRetries++; | |
| if (statusEl) statusEl.textContent = 'Face not in frame β hold still...'; | |
| if (statusText) statusText.textContent = 'Face not in frame β hold still...'; | |
| setTimeout(trySlot, CAPTURE_INTERVAL_MS); | |
| } else { | |
| var frames = getFramesFor(target); | |
| frames.push(frame); | |
| setFramesFor(target, frames); | |
| count++; | |
| slotRetries = 0; | |
| updateProgress(count / REQUIRED_FRAMES); | |
| var pct = Math.round((count / REQUIRED_FRAMES) * 100); | |
| if (statusEl) statusEl.textContent = 'Scanning... ' + pct + '%'; | |
| if (statusText) statusText.textContent = 'Scanning... ' + pct + '%'; | |
| setTimeout(trySlot, CAPTURE_INTERVAL_MS); | |
| } | |
| }) | |
| .catch(function() { | |
| setTimeout(trySlot, CAPTURE_INTERVAL_MS); | |
| }); | |
| } | |
| setTimeout(trySlot, CAPTURE_INTERVAL_MS); | |
| } | |
| function finishCapture(target) { | |
| isCapturing = false; | |
| captureTarget = null; | |
| startFaceCheckPolling(); | |
| var statusEl = target === 'manforce' ? statusTextManforce : statusTextEmployee; | |
| if (statusEl) { | |
| statusEl.textContent = 'Face captured β'; | |
| statusEl.style.color = 'var(--color-primary)'; | |
| } | |
| if (statusText) { | |
| statusText.textContent = 'Face captured β'; | |
| statusText.style.color = 'var(--color-primary)'; | |
| } | |
| updateVisibleButton(); | |
| } | |
| async function submitRegistration(userType) { | |
| var isManforce = userType === 'manforce'; | |
| var btn = isManforce ? btnRegisterManforce : btnRegisterEmployee; | |
| var frames = getFramesFor(userType); | |
| if (!btn || !frames || frames.length === 0) return; | |
| btn.disabled = true; | |
| btn.textContent = 'REGISTERING...'; | |
| var payload; | |
| if (isManforce) { | |
| if (!manforceAadhaar || !manforceName || !manforceMobile) return; | |
| payload = { | |
| user_type: 'manforce', | |
| aadhaar: manforceAadhaar.value.trim(), | |
| name: manforceName.value.trim(), | |
| mobile: manforceMobile.value.trim(), | |
| frames: frames | |
| }; | |
| } else { | |
| if (!employeeCode) return; | |
| payload = { | |
| user_type: 'employee', | |
| employee_id: employeeCode.value.trim(), | |
| frames: frames | |
| }; | |
| } | |
| try { | |
| var res = await fetch('/api/register', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(payload) | |
| }); | |
| var data = await res.json(); | |
| if (data.status === 'registered') { | |
| document.getElementById('registerForm').style.display = 'none'; | |
| document.getElementById('successState').style.display = 'block'; | |
| var label = isManforce ? payload.name + ' β ' + payload.aadhaar : payload.employee_id; | |
| document.getElementById('successChip').textContent = label; | |
| } else if (data.status === 'spoof') { | |
| showSpoofToast(data); | |
| btn.disabled = false; | |
| btn.textContent = 'Register'; | |
| } else { | |
| var msg = data.message || 'Unknown error'; | |
| if (msg.indexOf('already registered') !== -1 || msg.indexOf('already registered to') !== -1) { | |
| if (typeof showSnackbar === 'function') showSnackbar('Duplicate registration β this face is already registered.', 'error'); | |
| } else if (msg.indexOf('No face detected') !== -1 || msg.indexOf('no face') !== -1) { | |
| if (typeof showSnackbar === 'function') showSnackbar('Clear photo β ensure your face is visible and well lit.', 'info'); | |
| } else { | |
| if (typeof showSnackbar === 'function') showSnackbar(msg, 'error'); | |
| } | |
| btn.disabled = false; | |
| btn.textContent = 'Register'; | |
| } | |
| } catch (e) { | |
| if (typeof showSnackbar === 'function') showSnackbar('Network error. Please try again.', 'error'); | |
| btn.disabled = false; | |
| btn.textContent = 'Register'; | |
| } | |
| } | |
| function showSpoofToast(data) { | |
| var msg = data.reason || data.message || 'Use a live face, not a photo or screen.'; | |
| if (spoofToastMessage) spoofToastMessage.textContent = msg; | |
| if (spoofToast) spoofToast.classList.add('show'); | |
| if (toastDismissTimer) clearTimeout(toastDismissTimer); | |
| toastDismissTimer = setTimeout(function() { | |
| if (spoofToast) spoofToast.classList.remove('show'); | |
| toastDismissTimer = null; | |
| }, 4500); | |
| } | |
| // Init: ensure Employee tab/panel active and start live face feedback | |
| switchTab('employee'); | |
| startFaceCheckPolling(); | |