Spaces:
Sleeping
Sleeping
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Smart Attendance System</title> | |
<style> | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
} | |
body { | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
min-height: 100vh; | |
padding: 20px; | |
} | |
.container { | |
max-width: 1200px; | |
margin: 0 auto; | |
background: rgba(255, 255, 255, 0.95); | |
border-radius: 20px; | |
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); | |
overflow: hidden; | |
backdrop-filter: blur(10px); | |
} | |
.header { | |
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); | |
color: white; | |
padding: 30px; | |
text-align: center; | |
} | |
.header h1 { | |
font-size: 2.5rem; | |
margin-bottom: 10px; | |
font-weight: 600; | |
} | |
.header p { | |
font-size: 1.1rem; | |
opacity: 0.9; | |
} | |
.stats-grid { | |
display: grid; | |
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
gap: 20px; | |
padding: 30px; | |
background: #f8fafc; | |
} | |
.stat-card { | |
background: white; | |
padding: 25px; | |
border-radius: 15px; | |
text-align: center; | |
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); | |
transition: transform 0.3s ease, box-shadow 0.3s ease; | |
} | |
.stat-card:hover { | |
transform: translateY(-5px); | |
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); | |
} | |
.stat-number { | |
font-size: 2.5rem; | |
font-weight: bold; | |
margin-bottom: 10px; | |
} | |
.stat-label { | |
color: #64748b; | |
font-size: 1rem; | |
text-transform: uppercase; | |
letter-spacing: 0.5px; | |
} | |
.total { color: #3b82f6; } | |
.present { color: #10b981; } | |
.absent { color: #ef4444; } | |
.main-content { | |
display: grid; | |
grid-template-columns: 1fr 1fr; | |
gap: 30px; | |
padding: 30px; | |
} | |
.section { | |
background: white; | |
border-radius: 15px; | |
padding: 30px; | |
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); | |
} | |
.section h2 { | |
color: #1e293b; | |
margin-bottom: 20px; | |
font-size: 1.5rem; | |
font-weight: 600; | |
} | |
.camera-container { | |
position: relative; | |
margin-bottom: 20px; | |
} | |
#video { | |
width: 100%; | |
max-width: 400px; | |
border-radius: 10px; | |
border: 3px solid #e2e8f0; | |
} | |
.camera-overlay { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
width: 200px; | |
height: 200px; | |
border: 2px dashed #4f46e5; | |
border-radius: 50%; | |
pointer-events: none; | |
} | |
.controls { | |
display: flex; | |
gap: 10px; | |
margin-bottom: 20px; | |
flex-wrap: wrap; | |
} | |
.btn { | |
padding: 12px 24px; | |
border: none; | |
border-radius: 8px; | |
font-size: 1rem; | |
font-weight: 600; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
text-transform: uppercase; | |
letter-spacing: 0.5px; | |
} | |
.btn-primary { | |
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); | |
color: white; | |
} | |
.btn-success { | |
background: linear-gradient(135deg, #10b981 0%, #059669 100%); | |
color: white; | |
} | |
.btn-danger { | |
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); | |
color: white; | |
} | |
.btn:hover { | |
transform: translateY(-2px); | |
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.2); | |
} | |
.btn:disabled { | |
opacity: 0.6; | |
cursor: not-allowed; | |
transform: none; | |
} | |
.form-group { | |
margin-bottom: 20px; | |
} | |
.form-group label { | |
display: block; | |
margin-bottom: 8px; | |
color: #374151; | |
font-weight: 600; | |
} | |
.form-group input { | |
width: 100%; | |
padding: 12px; | |
border: 2px solid #e2e8f0; | |
border-radius: 8px; | |
font-size: 1rem; | |
transition: border-color 0.3s ease; | |
} | |
.form-group input:focus { | |
outline: none; | |
border-color: #4f46e5; | |
} | |
.status-message { | |
padding: 15px; | |
border-radius: 8px; | |
margin-bottom: 20px; | |
font-weight: 600; | |
text-align: center; | |
} | |
.status-success { | |
background: #dcfce7; | |
color: #166534; | |
border: 1px solid #bbf7d0; | |
} | |
.status-error { | |
background: #fef2f2; | |
color: #991b1b; | |
border: 1px solid #fecaca; | |
} | |
.attendance-list { | |
max-height: 400px; | |
overflow-y: auto; | |
} | |
.attendance-item { | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
padding: 15px; | |
border-bottom: 1px solid #e2e8f0; | |
transition: background-color 0.3s ease; | |
} | |
.attendance-item:hover { | |
background: #f8fafc; | |
} | |
.attendance-item:last-child { | |
border-bottom: none; | |
} | |
.attendance-name { | |
font-weight: 600; | |
color: #1e293b; | |
} | |
.attendance-time { | |
color: #64748b; | |
font-size: 0.9rem; | |
} | |
.time-display { | |
background: #f1f5f9; | |
padding: 10px; | |
border-radius: 8px; | |
text-align: center; | |
margin-bottom: 20px; | |
font-family: 'Courier New', monospace; | |
font-size: 1.1rem; | |
font-weight: bold; | |
color: #1e293b; | |
} | |
.loading { | |
text-align: center; | |
padding: 20px; | |
color: #64748b; | |
} | |
.spinner { | |
display: inline-block; | |
width: 20px; | |
height: 20px; | |
border: 3px solid #f3f3f3; | |
border-top: 3px solid #4f46e5; | |
border-radius: 50%; | |
animation: spin 1s linear infinite; | |
margin-right: 10px; | |
} | |
@keyframes spin { | |
0% { transform: rotate(0deg); } | |
100% { transform: rotate(360deg); } | |
} | |
@media (max-width: 768px) { | |
.main-content { | |
grid-template-columns: 1fr; | |
gap: 20px; | |
padding: 20px; | |
} | |
.stats-grid { | |
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); | |
gap: 15px; | |
padding: 20px; | |
} | |
.header h1 { | |
font-size: 2rem; | |
} | |
.controls { | |
flex-direction: column; | |
} | |
.btn { | |
width: 100%; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<div class="header"> | |
<h1>π― Smart Attendance System</h1> | |
<p>Advanced Face Recognition Attendance Management</p> | |
</div> | |
<div class="stats-grid"> | |
<div class="stat-card"> | |
<div class="stat-number total" id="totalUsers">{{ stats.total_users }}</div> | |
<div class="stat-label">Total Users</div> | |
</div> | |
<div class="stat-card"> | |
<div class="stat-number present" id="presentToday">{{ stats.present_today }}</div> | |
<div class="stat-label">Present Today</div> | |
</div> | |
<div class="stat-card"> | |
<div class="stat-number absent" id="absentToday">{{ stats.absent_today }}</div> | |
<div class="stat-label">Absent Today</div> | |
</div> | |
</div> | |
<div class="main-content"> | |
<div class="section"> | |
<h2>π· Face Recognition</h2> | |
<div class="time-display" id="currentTime"></div> | |
<div class="camera-container"> | |
<video id="video" autoplay muted></video> | |
<div class="camera-overlay"></div> | |
</div> | |
<div class="controls"> | |
<button class="btn btn-primary" id="startCamera">Start Camera</button> | |
<button class="btn btn-success" id="checkIn">Check In</button> | |
<button class="btn btn-danger" id="checkOut">Check Out</button> | |
</div> | |
<div id="recognitionStatus"></div> | |
</div> | |
<div class="section"> | |
<h2>π€ Add New User</h2> | |
<form id="addUserForm" enctype="multipart/form-data"> | |
<div class="form-group"> | |
<label for="userName">Full Name</label> | |
<input type="text" id="userName" name="name" required placeholder="Enter full name"> | |
</div> | |
<div class="form-group"> | |
<label for="faceImage">Face Photo</label> | |
<input type="file" id="faceImage" name="face_image" accept="image/*" required> | |
</div> | |
<button type="submit" class="btn btn-primary" style="width: 100%;">Add User</button> | |
</form> | |
<div id="addUserStatus"></div> | |
<h3 style="margin-top: 30px; margin-bottom: 15px; color: #1e293b;">π Today's Attendance</h3> | |
<div class="attendance-list" id="attendanceList"> | |
<div class="loading"> | |
<div class="spinner"></div> | |
Loading attendance... | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
let video; | |
let isRecognizing = false; | |
// Initialize the application | |
document.addEventListener('DOMContentLoaded', function() { | |
updateCurrentTime(); | |
setInterval(updateCurrentTime, 1000); | |
loadAttendanceData(); | |
setInterval(loadAttendanceData, 30000); // Refresh every 30 seconds | |
setupEventListeners(); | |
}); | |
function updateCurrentTime() { | |
const now = new Date(); | |
const options = { | |
timeZone: 'Asia/Kolkata', | |
weekday: 'long', | |
year: 'numeric', | |
month: 'long', | |
day: 'numeric', | |
hour: '2-digit', | |
minute: '2-digit', | |
second: '2-digit', | |
hour12: true | |
}; | |
document.getElementById('currentTime').textContent = now.toLocaleString('en-IN', options); | |
} | |
function setupEventListeners() { | |
document.getElementById('startCamera').addEventListener('click', startCamera); | |
document.getElementById('checkIn').addEventListener('click', () => recognizeAndMark('check_in')); | |
document.getElementById('checkOut').addEventListener('click', () => recognizeAndMark('check_out')); | |
document.getElementById('addUserForm').addEventListener('submit', addUser); | |
} | |
async function startCamera() { | |
try { | |
const stream = await navigator.mediaDevices.getUserMedia({ | |
video: { | |
width: { ideal: 640 }, | |
height: { ideal: 480 }, | |
facingMode: 'user' | |
} | |
}); | |
video = document.getElementById('video'); | |
video.srcObject = stream; | |
document.getElementById('startCamera').textContent = 'Camera Active'; | |
document.getElementById('startCamera').disabled = true; | |
showStatus('Camera started successfully!', 'success'); | |
} catch (error) { | |
console.error('Error accessing camera:', error); | |
showStatus('Error accessing camera. Please check permissions.', 'error'); | |
} | |
} | |
async function recognizeAndMark(type) { | |
if (!video || !video.srcObject || isRecognizing) { | |
showStatus('Please start the camera first or wait for current recognition to complete.', 'error'); | |
return; | |
} | |
isRecognizing = true; | |
const actionText = type === 'check_in' ? 'Checking In' : 'Checking Out'; | |
showStatus(`${actionText}... Please look at the camera.`, 'info'); | |
try { | |
// Capture frame from video | |
const canvas = document.createElement('canvas'); | |
canvas.width = video.videoWidth; | |
canvas.height = video.videoHeight; | |
const ctx = canvas.getContext('2d'); | |
ctx.drawImage(video, 0, 0); | |
const imageData = canvas.toDataURL('image/jpeg', 0.8); | |
const response = await fetch('/api/recognize', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify({ | |
image: imageData, | |
type: type | |
}) | |
}); | |
const result = await response.json(); | |
if (result.success) { | |
const actionPast = type === 'check_in' ? 'checked in' : 'checked out'; | |
showStatus(`β ${result.name} successfully ${actionPast} at ${result.time}! (Confidence: ${result.confidence}%)`, 'success'); | |
loadAttendanceData(); // Refresh attendance list | |
loadStats(); // Refresh statistics | |
} else { | |
showStatus(`β ${result.message}`, 'error'); | |
} | |
} catch (error) { | |
console.error('Recognition error:', error); | |
showStatus('Recognition failed. Please try again.', 'error'); | |
} finally { | |
isRecognizing = false; | |
} | |
} | |
async function addUser(event) { | |
event.preventDefault(); | |
const formData = new FormData(); | |
const nameInput = document.getElementById('userName'); | |
const imageInput = document.getElementById('faceImage'); | |
if (!nameInput.value.trim()) { | |
showUserStatus('Please enter a name.', 'error'); | |
return; | |
} | |
if (!imageInput.files[0]) { | |
showUserStatus('Please select a face image.', 'error'); | |
return; | |
} | |
formData.append('name', nameInput.value.trim()); | |
formData.append('face_image', imageInput.files[0]); | |
showUserStatus('Adding user... Please wait.', 'info'); | |
try { | |
const response = await fetch('/api/add_user', { | |
method: 'POST', | |
body: formData | |
}); | |
const result = await response.json(); | |
if (result.success) { | |
showUserStatus(`β ${result.message}`, 'success'); | |
document.getElementById('addUserForm').reset(); | |
loadStats(); // Refresh statistics | |
} else { | |
showUserStatus(`β ${result.message}`, 'error'); | |
} | |
} catch (error) { | |
console.error('Add user error:', error); | |
showUserStatus('Failed to add user. Please try again.', 'error'); | |
} | |
} | |
async function loadStats() { | |
try { | |
const response = await fetch('/api/stats'); | |
const stats = await response.json(); | |
document.getElementById('totalUsers').textContent = stats.total_users; | |
document.getElementById('presentToday').textContent = stats.present_today; | |
document.getElementById('absentToday').textContent = stats.absent_today; | |
} catch (error) { | |
console.error('Error loading stats:', error); | |
} | |
} | |
async function loadAttendanceData() { | |
try { | |
const response = await fetch('/api/stats'); | |
const data = await response.json(); | |
const attendanceList = document.getElementById('attendanceList'); | |
if (data.today_attendance && data.today_attendance.length > 0) { | |
attendanceList.innerHTML = data.today_attendance.map(record => ` | |
<div class="attendance-item"> | |
<div> | |
<div class="attendance-name">${record.name}</div> | |
<div class="attendance-time"> | |
${record.check_in ? `In: ${record.check_in}` : 'Not checked in'} | |
${record.check_out ? ` | Out: ${record.check_out}` : ''} | |
</div> | |
</div> | |
<div style="color: ${record.check_out ? '#10b981' : '#f59e0b'};"> | |
${record.check_out ? 'β Complete' : 'π Active'} | |
</div> | |
</div> | |
`).join(''); | |
} else { | |
attendanceList.innerHTML = ` | |
<div class="attendance-item" style="justify-content: center; color: #64748b;"> | |
π No attendance records for today | |
</div> | |
`; | |
} | |
// Update stats | |
document.getElementById('totalUsers').textContent = data.total_users; | |
document.getElementById('presentToday').textContent = data.present_today; | |
document.getElementById('absentToday').textContent = data.absent_today; | |
} catch (error) { | |
console.error('Error loading attendance:', error); | |
document.getElementById('attendanceList').innerHTML = ` | |
<div class="attendance-item" style="justify-content: center; color: #ef4444;"> | |
β Error loading attendance data | |
</div> | |
`; | |
} | |
} | |
function showStatus(message, type) { | |
const statusDiv = document.getElementById('recognitionStatus'); | |
statusDiv.innerHTML = `<div class="status-message status-${type === 'success' ? 'success' : 'error'}">${message}</div>`; | |
if (type === 'success') { | |
setTimeout(() => { | |
statusDiv.innerHTML = ''; | |
}, 5000); | |
} | |
} | |
function showUserStatus(message, type) { | |
const statusDiv = document.getElementById('addUserStatus'); | |
statusDiv.innerHTML = `<div class="status-message status-${type === 'success' ? 'success' : 'error'}">${message}</div>`; | |
if (type === 'success') { | |
setTimeout(() => { | |
statusDiv.innerHTML = ''; | |
}, 5000); | |
} | |
} | |
// Add visual feedback for camera status | |
document.getElementById('video').addEventListener('loadedmetadata', function() { | |
this.style.border = '3px solid #10b981'; | |
}); | |
// Handle page visibility changes to manage camera | |
document.addEventListener('visibilitychange', function() { | |
if (document.hidden && video && video.srcObject) { | |
// Optionally pause video when page is hidden | |
video.pause(); | |
} else if (!document.hidden && video && video.srcObject) { | |
video.play(); | |
} | |
}); | |
</script> | |
</body> | |
</html> |