/** * AttendAI Pro - Application Configuration * Production-ready configuration management */ const APP_CONFIG = { name: 'AttendAI Pro', version: '2.0.0', tagline: 'AI-Powered Anti-Proxy Attendance System', // Google OAuth - Replace with your actual Google OAuth Client ID // To get: console.cloud.google.com → Create Project → OAuth 2.0 Client IDs googleClientId: '578078243530-ridjf8bv9ie0cbustjmefvf56b77qfe5.apps.googleusercontent.com', // API Base URL (relative paths for the Table API) apiBase: 'tables', // Attendance Settings qrRotateInterval: 60, // seconds - QR code rotates every 60s geofenceRadius: 100, // meters - default geofence radius faceMatchThreshold: 0.6, // 0-1 face match confidence threshold attendanceCutoff: 75, // percent - attendance warning threshold lateThresholdMinutes: 10, // minutes after session start = late // Security sessionTimeout: 8 * 60 * 60 * 1000, // 8 hours in ms maxAttemptsPerSession: 1, // max attendance attempts per session fraudDetectionEnabled: true, // Allowed institutional email domains (configure per institution) // Leave empty to allow all emails (admin manages via database) allowedStudentDomains: [], // e.g., ['university.edu', 'college.ac.in'] // Supported institution types institutionTypes: ['University', 'College', 'High School', 'Corporate', 'Training Center'], // Routes routes: { landing: 'index.html', studentLogin: 'student-login.html', teacherLogin: 'teacher-login.html', studentDash: 'student-dashboard.html', teacherDash: 'teacher-dashboard.html', adminPanel: 'admin-panel.html', } }; /** * Auth State Manager */ const AuthState = { KEY: 'attendai_session', save(userData) { const session = { ...userData, loginTime: Date.now(), expires: Date.now() + APP_CONFIG.sessionTimeout }; sessionStorage.setItem(this.KEY, JSON.stringify(session)); localStorage.setItem(this.KEY + '_persist', JSON.stringify(session)); }, get() { try { const s = sessionStorage.getItem(this.KEY) || localStorage.getItem(this.KEY + '_persist'); if (!s) return null; const data = JSON.parse(s); if (Date.now() > data.expires) { this.clear(); return null; } return data; } catch { return null; } }, clear() { sessionStorage.removeItem(this.KEY); localStorage.removeItem(this.KEY + '_persist'); }, isLoggedIn() { return !!this.get(); }, getRole() { const s = this.get(); return s ? s.role : null; } }; /** * API Helper */ const API = { async get(table, params = {}) { const q = new URLSearchParams(params).toString(); const url = `${APP_CONFIG.apiBase}/${table}${q ? '?' + q : ''}`; const res = await fetch(url); if (!res.ok) throw new Error(`API Error: ${res.status}`); return res.json(); }, async getOne(table, id) { const res = await fetch(`${APP_CONFIG.apiBase}/${table}/${id}`); if (!res.ok) throw new Error(`API Error: ${res.status}`); return res.json(); }, async post(table, data) { const res = await fetch(`${APP_CONFIG.apiBase}/${table}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); if (!res.ok) throw new Error(`API Error: ${res.status}`); return res.json(); }, async put(table, id, data) { const res = await fetch(`${APP_CONFIG.apiBase}/${table}/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); if (!res.ok) throw new Error(`API Error: ${res.status}`); return res.json(); }, async patch(table, id, data) { const res = await fetch(`${APP_CONFIG.apiBase}/${table}/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); if (!res.ok) throw new Error(`API Error: ${res.status}`); return res.json(); }, async delete(table, id) { const res = await fetch(`${APP_CONFIG.apiBase}/${table}/${id}`, { method: 'DELETE' }); if (!res.ok) throw new Error(`API Error: ${res.status}`); return true; } }; /** * Utility Functions */ const Utils = { formatDate(ts) { if (!ts) return '—'; return new Date(ts).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); }, formatTime(ts) { if (!ts) return '—'; return new Date(ts).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); }, formatDateTime(ts) { if (!ts) return '—'; return new Date(ts).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); }, timeAgo(ts) { const s = Math.floor((Date.now() - ts) / 1000); if (s < 60) return `${s}s ago`; if (s < 3600) return `${Math.floor(s / 60)}m ago`; if (s < 86400) return `${Math.floor(s / 3600)}h ago`; return `${Math.floor(s / 86400)}d ago`; }, generateToken(len = 32) { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let token = ''; for (let i = 0; i < len; i++) token += chars[Math.floor(Math.random() * chars.length)]; return token; }, generateUUID() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = Math.random() * 16 | 0; return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); }); }, getDistance(lat1, lon1, lat2, lon2) { const R = 6371000; const dLat = (lat2 - lat1) * Math.PI / 180; const dLon = (lon2 - lon1) * Math.PI / 180; const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon / 2) ** 2; return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); }, async getGeoLocation() { return new Promise((resolve, reject) => { if (!navigator.geolocation) { reject(new Error('Geolocation not supported')); return; } navigator.geolocation.getCurrentPosition( pos => resolve({ lat: pos.coords.latitude, lng: pos.coords.longitude, accuracy: pos.coords.accuracy }), err => reject(new Error(err.message)), { enableHighAccuracy: true, timeout: 10000 } ); }); }, getDeviceFingerprint() { const data = `${navigator.userAgent}|${screen.width}x${screen.height}|${navigator.language}|${Intl.DateTimeFormat().resolvedOptions().timeZone}`; let hash = 0; for (let i = 0; i < data.length; i++) { hash = ((hash << 5) - hash) + data.charCodeAt(i); hash |= 0; } return Math.abs(hash).toString(16); }, debounce(fn, ms) { let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); }; }, percentColor(pct) { if (pct >= 75) return 'success'; if (pct >= 60) return 'warning'; return 'danger'; } }; /** * Toast Notification System */ const Toast = { container: null, init() { if (!this.container) { this.container = document.createElement('div'); this.container.className = 'toast-container'; document.body.appendChild(this.container); } }, show(message, type = 'info', duration = 4000) { this.init(); const icons = { success: '✅', error: '❌', warning: '⚠️', info: 'ℹ️' }; const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.innerHTML = ` ${icons[type] || icons.info}
${message}